Implementing the Factory Method Design Pattern in Symfony

rahul chavan
3 min readOct 16, 2023

Design patterns are a fundamental part of object-oriented programming, providing a blueprint for solving recurring problems in a structured and maintainable way. Among these patterns, the Factory Method design pattern is a powerful tool for creating objects while allowing subclasses to determine the exact type of objects to be created. In this article, I’ll explore one of the ways to implement the Factory Method pattern in Symfony.

Factory Method Pattern Overview

The Factory Method pattern is a creational design pattern that addresses the problem of object creation. It defines an interface for creating an object but delegates the responsibility of instantiating the object to its subclasses. In essence, it lets a class decide what to instantiate based on its specific requirements.

The Factory Method pattern typically involves the following key components:

  • Creator: This is an abstract class or interface that declares the Factory Method. The Creator provides a method for creating objects but does not specify their concrete classes.
  • Concrete Creator: Subclasses of the Creator implement the Factory Method to produce instances of specific objects. Each Concrete Creator is responsible for creating a particular type of object.
  • Product: The Product is an interface or an abstract class defining the common interface for objects that the Factory Method creates.
  • Concrete Product: Subclasses of the Product provide concrete implementations of the objects. Each Concrete Product corresponds to a specific object type.

Now, let’s dive into a practical example.

Payment System Example

Suppose you are building a payment processing system in Symfony, and you need to support multiple payment gateways such as RazorPay. To achieve this, you can use the Factory Method pattern. Here’s how it works:

The PaymentFactory

namespace App\Services\Payment;

use App\Services\Exception\ServiceException;
use App\Services\Exception\ServiceExceptionData;

final readonly class PaymentFactory
{
public function __construct(
#[AutowireIterator('payment_provider')]
private iterable $paymentProviders
) {
}

public function getPaymentProvider(string $paymentName): PaymentInterface
{
/** @var PaymentInterface $paymentProvider */
foreach ($this->paymentProviders as $paymentProvider) {
if ($paymentProvider->support($paymentName)) {
return $paymentProvider;
}
}

throw new PaymentException(new PaymentExceptionData(type: 'Payment gateway not supported'));
}
}

The PaymentFactory class acts as a Service Locator pattern implementation. It provides a central location to retrieve the appropriate payment provider based on the requested payment method. The use of an #[AutowireIterator] suggests that it’s using a dependency injection framework to easily manage these payment providers. So, in simpler terms, PaymentFactory helps your code find and use the right payment service, and it’s using a attribute to make that process smoother.

The PaymentInterface

namespace App\Services\Payment;

use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;

#[AutoconfigureTag(name: "payment_provider")]
interface PaymentInterface
{
public function support(string $paymentName): bool;
public function pay(): bool;
}

The PaymentInterface represents the Product. It defines the common interface for all payment providers, specifying the methods support to check if a payment method is supported and pay to initiate the payment.

RazorPay Payment Provider

namespace App\Services\Payment;

class RazorPay implements PaymentInterface
{
const PAYMENT_NAME = 'razor';

public function support(string $paymentName): bool
{
return $paymentName === self::PAYMENT_NAME;
}

public function pay(): bool
{
// Implementation specific to RazorPay
}
}

The RazorPay class is a Concrete Product. It implements the PaymentInterface, providing specific implementations for support and pay methods tailored for the RazorPay payment gateway.

Using the Factory Method in a Controller

Now, let’s see how this Factory Method is used in a Symfony controller:

use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;

#[Route(path: "/make/payment", name: "make_payment", methods: ["POST"])]
public function makePayment(Request $request, PaymentFactory $PaymentFactory): JsonResponse
{
$paymentMethod = $request->get('payment-method');

if (!$paymentMethod) {
// Handle the absence of a payment method and possibly throw an exception
}

$paymentProvider = $PaymentFactory->getPaymentProvider($paymentMethod);

if (!$paymentProvider->pay()) {
// Handle payment failure and possibly throw an exception
}

return new JsonResponse(data: "Show success message");
}

In the controller, we inject the PaymentFactory and use it to create and process payments based on the specified payment method.

Conclusion

The Factory Method design pattern is a versatile and valuable tool in object-oriented programming. It allows for flexibility and extensibility in object creation, making it well-suited for scenarios where you need to create objects with varying implementations based on specific requirements. In the Symfony-based payment system example, the Factory Method pattern helps manage different payment gateways efficiently and maintainably. Understanding and implementing this pattern can significantly improve the structure and maintainability of your software applications.

--

--