Interface implementation based on env variable in Symfony

Setup

We have interface:


<?php

namespace App\Payment;

interface PaymentProcessorInterface
{
    public function processPayment(float $amount, string $currency): bool;
}

Implementation 1:

<?php
// src/Payment/PaypalPaymentProcessor.php

namespace App\Payment;

class PaypalPaymentProcessor implements PaymentProcessorInterface
{
    public function processPayment(float $amount, string $currency): bool
    {
        echo "Processing \${$amount} {$currency} payment through PayPal.\n";
        return true;
    }
}

Implementation 2:

<?php
// src/Payment/StripePaymentProcessor.php

namespace App\Payment;

class StripePaymentProcessor implements PaymentProcessorInterface
{
    public function processPayment(float $amount, string $currency): bool
    {
        echo "Processing \${$amount} {$currency} payment through Stripe.\n";
        return true;
    }
}

Factory:

<?php
// src/Payment/PaymentProcessorFactory.php

namespace App\Payment;

use InvalidArgumentException;

class PaymentProcessorFactory
{
    public function __construct(
       private PaypalPaymentProcessor $paypalProcessor,
        private StripePaymentProcessor $stripeProcessor
    ) {
    }

    public function create(string $paymentType): PaymentProcessorInterface
    {
        switch ($paymentType) {
            case 'paypal':
                return $this->paypalProcessor;
            case 'stripe':
                return $this->stripeProcessor;
            default:
                throw new InvalidArgumentException("Invalid payment processor type: {$paymentType}");
        }
    }
}

In .env we have the variable for the default payment type.

PAYMENT_TYPE=stripe

Solution

Wire this in services.yaml like this (presuming you are using default autowire: true, autoconfigure: true):

App\Payment\PaymentProcessorInterface:
  factory: ['@App\PaymentProcessorFactory', "create"]
  arguments:
    $dataProviderType: "%env(PAYMENT_TYPE)%"
  1. First we create a service id for configuring interface object creation. We are using fully qualified class name.

  2. Because interface doesn’t have constructor we need to tell symfony how to create an object. For that there is factory definition key which tells what specific class and method is used to create object, in this case interface.

  3. Fill out factory parameters: [$service, $method].

    • Because we are using non static factory we need to use registered service instead of class. And because we have autowiring true our factory is already registered as service and we can refer to it with @ like this: @App\PaymentProcessorFactory. If the factory is using static method we would only need to write: App\PaymentProcessorFactory.

    • for $method we use our method create

  4. To pass arguments to the factory method we use arguments key and then explicitly define variable name and value which is used in factory method which is $paymentType.

  5. Payment type we get from environment variable PAYMENT_TYPE which we can use using symfony yaml syntax %env(PAYMENT_TYPE)%.

Complete service.yaml now looks like this:

# This file is the entry point to configure your own services.
# Files in the packages/ subdirectory configure your dependencies.

# Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
parameters:

services:
  # default configuration for services in *this* file
  _defaults:
    autowire: true # Automatically injects dependencies in your services.
    autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.

  # makes classes in src/ available to be used as services
  # this creates a service per class whose id is the fully-qualified class name
  App\:
    resource: "../src/"
    exclude:
      - "../src/DependencyInjection/"
      - "../src/Entity/"
      - "../src/Kernel.php"
  # add more service definitions when explicit configuration is needed
  # please note that last definitions always *replace* previous ones

  App\Payment\PaymentProcessorInterface:
    factory: ['@App\PaymentProcessorFactory', "create"]
    arguments:
      $dataProviderType: "%env(PAYMENT_TYPE)%"

Reference