DEV Community

tuandp
tuandp

Posted on

Interfaces in Laravel: Best Practices for Maintainable and Testable Code

Interfaces are a fundamental feature of object-oriented programming languages, including PHP. In Laravel, interfaces are used to define contracts that specify the methods that a class must implement. This allows you to create flexible and modular code that is easy to maintain and test.

In this article, we'll explore how to use interfaces in Laravel to create modular and flexible code, including tips on defining interfaces for services, using dependency injection, and binding services to the application container.

Defining Interfaces for Services

In Laravel, services are used to encapsulate complex business logic. By defining interfaces for your services, you can ensure that they can be easily swapped out or extended without affecting the rest of your code.

Here's an example of an interface for a payment gateway service:

interface PaymentGatewayInterface
{
    public function processPayment(int $amount);
    public function refundPayment(int $transactionId);
}
Enter fullscreen mode Exit fullscreen mode

In this example, we have defined an interface called PaymentGatewayInterface, which specifies two methods that any class implementing this interface must define: processPayment and refundPayment. By defining this interface, we can ensure that any class that needs to process payments or refunds implements these methods, regardless of the specific implementation details.

Using Dependency Injection

Laravel's powerful dependency injection system makes it easy to use interfaces in your code. By injecting interfaces into your classes, you can ensure that they only depend on the methods defined in the interface, rather than on specific implementations.

Here's an example of a class that uses the PaymentGatewayInterface to process payments:

class PaymentProcessor
{
    public function __construct(protected PaymentGatewayInterface $gateway): self
    {
    }

    public function process(int $amount)
    {
        $this->gateway->processPayment($amount);
    }

    public function refund(int $transactionId)
    {
        $this->gateway->refundPayment($transactionId);
    }
}
Enter fullscreen mode Exit fullscreen mode

Binding Services to the Application Container

To make our services available throughout our application, we can bind them to the Laravel application container using a service provider. Here's an example of a service provider that binds two payment gateway processors to the container:

class AppServiceProvider extends ServiceProvider
{
    public function register()
    {
        app()->bind(
            PaymentGatewayInterface::class,
            function ($app) {
                return collect([
                    'paypal' => app(PayPalGateway::class),
                    'stripe' => app(StripeGateway::class),
                ]);
            }
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, we are binding a collection of payment gateway processors to the container using the bind method. The key for this collection is PaymentGatewayInterface::class, and the value is a closure that returns a collection of payment gateway processors. The collection contains two payment gateway processors: paypal and stripe. Each of these is defined as a closure that returns an instance of the corresponding gateway class.

Using Services in Controllers

To use our payment gateway services in our controllers, we can simply instantiate a PaymentProcessor instance in our controller methods, and use the Laravel container to resolve the PaymentGatewayInterface from the container.

Here's an example of a controller that uses the PaymentProcessor to process or refund a payment:

class PaymentController extends Controller
{
    public function processPayment()
    {
        $gateway = app(PaymentGatewayInterface::class)->get('paypal');
        $processor = new PaymentProcessor($gateway);
        $processor->setGateway($gateway)->process(100);
    }

    public function refundPayment()
    {
        $gateway = app(PaymentGatewayInterface::class)->get('paypal');
        $processor = new PaymentProcessor($gateway);
        $processor->setGateway($gateway)->refund(12345);
    }
}
Enter fullscreen mode Exit fullscreen mode

Note that the value of parameter passed into processor collection can be swapped between stripe or paypal, it depends on our use cases.

Interface-Based Testing

Here's an example of a test that uses the PaymentGatewayInterface to test the PaymentProcessor class:

class PaymentProcessorTest extends TestCase
{
    public function testProcessMethodCallsProcessPaymentOnGateway()
    {
        $gateway = $this->createMock(PaymentGatewayInterface::class);
        $gateway->expects($this->once())
            ->method('processPayment')
            ->with($this->equalTo(100));

        $processor = new PaymentProcessor($gateway);
        $processor->process(100);
    }

    public function testProcessMethodHandlesPaymentGatewayExceptions()
    {
        $gateway = $this->createMock(PaymentGatewayInterface::class);
        $gateway->expects($this->once())
            ->method('processPayment')
            ->willThrowException(new PaymentGatewayException('Payment failed'));

        $processor = new PaymentProcessor($gateway);
        $this->expectException(PaymentProcessingException::class);
        $processor->process(100);
    }
}
Enter fullscreen mode Exit fullscreen mode

By using an interface to define the required behavior of the PaymentGateway, we can easily create mock implementations of that interface for testing purposes. This allows us to test different scenarios and edge cases without having to modify the PaymentProcessor class itself

Happy coding!

Top comments (2)

Collapse
 
yura712 profile image
Yura

Good explanation for beginners. But you have to update PaymentController, current state of PaymentProcessor doesn't have setGateway method, as you inject $gateway via DI. Another improvement for your post is to show somewhere that PayPalGateway and StripeGateway implement PaymentGatewayInterface, it can be unclear for beginners.

Collapse
 
thimytrang_nguyen_6290b profile image
Jenny Nguyen

Great article. It will be useful for my work! Thank you for sharing.