Implement the Strategy pattern with Symfony's tagged services for runtime algorithm selection and extensibility
This skill inherits all available tools. When active, it can use any tool Claude has access to.
Strategy allows selecting an algorithm at runtime. In Symfony, use tagged services for clean implementation.
<?php
// src/Payment/PaymentProcessorInterface.php
namespace App\Payment;
interface PaymentProcessorInterface
{
public function supports(string $method): bool;
public function process(Payment $payment): PaymentResult;
public function refund(Payment $payment, int $amount): RefundResult;
}
<?php
// src/Payment/Processor/StripeProcessor.php
namespace App\Payment\Processor;
use App\Payment\PaymentProcessorInterface;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
#[AutoconfigureTag('app.payment_processor')]
class StripeProcessor implements PaymentProcessorInterface
{
public function __construct(
private StripeClient $stripe,
) {}
public function supports(string $method): bool
{
return in_array($method, ['card', 'stripe'], true);
}
public function process(Payment $payment): PaymentResult
{
$charge = $this->stripe->charges->create([
'amount' => $payment->getAmount(),
'currency' => $payment->getCurrency(),
'source' => $payment->getToken(),
]);
return new PaymentResult(
success: $charge->status === 'succeeded',
transactionId: $charge->id,
);
}
public function refund(Payment $payment, int $amount): RefundResult
{
// Stripe refund implementation
}
}
// src/Payment/Processor/PayPalProcessor.php
#[AutoconfigureTag('app.payment_processor')]
class PayPalProcessor implements PaymentProcessorInterface
{
public function supports(string $method): bool
{
return $method === 'paypal';
}
public function process(Payment $payment): PaymentResult
{
// PayPal implementation
}
public function refund(Payment $payment, int $amount): RefundResult
{
// PayPal refund implementation
}
}
// src/Payment/Processor/BankTransferProcessor.php
#[AutoconfigureTag('app.payment_processor')]
class BankTransferProcessor implements PaymentProcessorInterface
{
public function supports(string $method): bool
{
return $method === 'bank_transfer';
}
public function process(Payment $payment): PaymentResult
{
// Bank transfer - create pending payment
return new PaymentResult(
success: true,
transactionId: uniqid('bt_'),
pending: true,
);
}
public function refund(Payment $payment, int $amount): RefundResult
{
// Bank transfer refund
}
}
<?php
// src/Payment/PaymentService.php
namespace App\Payment;
use Symfony\Component\DependencyInjection\Attribute\AutowireIterator;
class PaymentService
{
/**
* @param iterable<PaymentProcessorInterface> $processors
*/
public function __construct(
#[AutowireIterator('app.payment_processor')]
private iterable $processors,
) {}
public function process(Payment $payment, string $method): PaymentResult
{
$processor = $this->getProcessor($method);
return $processor->process($payment);
}
public function refund(Payment $payment, int $amount): RefundResult
{
$processor = $this->getProcessor($payment->getMethod());
return $processor->refund($payment, $amount);
}
public function getSupportedMethods(): array
{
$methods = [];
foreach ($this->processors as $processor) {
// Each processor reports what it supports
}
return $methods;
}
private function getProcessor(string $method): PaymentProcessorInterface
{
foreach ($this->processors as $processor) {
if ($processor->supports($method)) {
return $processor;
}
}
throw new UnsupportedPaymentMethodException($method);
}
}
<?php
// src/Export/ExporterInterface.php
namespace App\Export;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
#[AutoconfigureTag('app.exporter')]
interface ExporterInterface
{
public static function getFormat(): string;
public function export(array $data): string;
public function getContentType(): string;
public function getFileExtension(): string;
}
// src/Export/CsvExporter.php
class CsvExporter implements ExporterInterface
{
public static function getFormat(): string
{
return 'csv';
}
public function export(array $data): string
{
$output = fopen('php://temp', 'r+');
if (!empty($data)) {
fputcsv($output, array_keys($data[0]));
foreach ($data as $row) {
fputcsv($output, $row);
}
}
rewind($output);
return stream_get_contents($output);
}
public function getContentType(): string
{
return 'text/csv';
}
public function getFileExtension(): string
{
return 'csv';
}
}
// src/Export/JsonExporter.php
class JsonExporter implements ExporterInterface
{
public static function getFormat(): string
{
return 'json';
}
public function export(array $data): string
{
return json_encode($data, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR);
}
public function getContentType(): string
{
return 'application/json';
}
public function getFileExtension(): string
{
return 'json';
}
}
// src/Export/XlsxExporter.php
class XlsxExporter implements ExporterInterface
{
public static function getFormat(): string
{
return 'xlsx';
}
public function export(array $data): string
{
// PhpSpreadsheet implementation
}
public function getContentType(): string
{
return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
}
public function getFileExtension(): string
{
return 'xlsx';
}
}
<?php
// src/Export/ExportService.php
namespace App\Export;
use Symfony\Component\DependencyInjection\Attribute\TaggedLocator;
use Symfony\Component\DependencyInjection\ServiceLocator;
class ExportService
{
public function __construct(
#[TaggedLocator('app.exporter', defaultIndexMethod: 'getFormat')]
private ServiceLocator $exporters,
) {}
public function export(array $data, string $format): ExportResult
{
if (!$this->exporters->has($format)) {
throw new UnsupportedFormatException($format);
}
/** @var ExporterInterface $exporter */
$exporter = $this->exporters->get($format);
return new ExportResult(
content: $exporter->export($data),
contentType: $exporter->getContentType(),
filename: 'export.' . $exporter->getFileExtension(),
);
}
public function getAvailableFormats(): array
{
return array_keys($this->exporters->getProvidedServices());
}
}
#[AutoconfigureTag('app.payment_processor', ['priority' => 10])]
class StripeProcessor implements PaymentProcessorInterface
{
// Higher priority = checked first
}
#[AutoconfigureTag('app.payment_processor', ['priority' => 0])]
class FallbackProcessor implements PaymentProcessorInterface
{
// Lower priority = fallback
}
class PaymentServiceTest extends TestCase
{
public function testSelectsCorrectProcessor(): void
{
$stripe = $this->createMock(PaymentProcessorInterface::class);
$stripe->method('supports')->willReturnCallback(
fn($m) => $m === 'card'
);
$paypal = $this->createMock(PaymentProcessorInterface::class);
$paypal->method('supports')->willReturnCallback(
fn($m) => $m === 'paypal'
);
$service = new PaymentService([$stripe, $paypal]);
// Verify correct processor is selected
$stripe->expects($this->once())->method('process');
$service->process($payment, 'card');
}
}