Test that all your interfaces are bound in Laravel

Tobias van Beek • October 10, 2026

If you have a lot of interfaces, it can be useful to test if the interfaces are bound in your Laravel application.

TLDR The complete test

If you only want the test, here is it.

<?php

declare(strict_types=1);

namespace Tests\AutoReview;

use Generator;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use Symfony\Component\Finder\Finder;
use Tests\TestCase;
use Throwable;

final class ContractBindingsTest extends TestCase
{
    // The path to the root of your project
    final public const PROJECT_DIR = __DIR__ . '/../../';
    
    final public const DEFAULT_PATHS = [
        __DIR__ . '/../../app',
        // other paths
    ];

    private static array $ignore = [
        // Add the interface that aren't bound by purpose
    ];
    
    #[Test]
    #[DataProvider('contractsDataProvider')]
    public function weCanGetTheImplementationFromTheContainer(string $contractName): void
    {
        // run
        try {
            $class = $this->app->make($contractName);

            // verify/assert
            $this->assertInstanceOf($contractName, $class);
        } catch (Throwable $throwable) {
            $this->fail('Failed to get an instance for "' . $contractName.'". The error is: ' . $throwable->getMessage());
        }
    }
    
    public static function contractsDataProvider(): Generator
    {
        $finder = Finder::create();

        $contractFiles = $finder->name('*Contract.php')->files()->in(self::DEFAULT_PATHS);
        foreach ($contractFiles as $file) {
            $fqdn = self::getFqdnForFilePath($file->getRealPath());

            if (in_array($fqdn, self::$ignore, true)) {
                continue;
            }
            yield $file->getRealPath() => [$fqdn];
        }
    }

    protected static function getFqdnForFilePath(string $filePath): string
    {
        $realProjectDir = realpath(self::PROJECT_DIR) . '/';

        // Any off the App classes
        if (str_starts_with($filePath, $realProjectDir . 'app/')) {
            // remove the app dir and the .php extension
            $className = substr(
                $filePath,
                strlen($realProjectDir . 'app/'),
                -4
            );
            // Replace the / with a \
            $className = str_replace('/', '\\', $className);
            // Add the app namespace
            return 'App\\' . $className;
        }

        // Possible add other namespace parts

        // Unknown
        self::fail('Impossible to match "' . $filePath . '" to FQDN');
    }
}

The working

The working has a couple of steps.

  1. First find the interface files in your project.
  2. Map the files to an FQDN
  3. Test if we can get an instance for the FQDN from the application.

Find your interfaces files

To find the interfaces in a project I use the Finder component from Symfony. On my current project all the interfaces have a suffix of Contract. With that I can search for all *Contract.php files to find the contracts. If you have any other standard you can change the finder calls to get the files with the interfaces.

The DEFAULT_PATHS is an array with paths in your project. Most applications will have only one path but my current project has many.
With the ignore array it is possible to skip an interface. This is for interfaces that aren't bound on purpose. For example if it defines some functionality.


    final public const DEFAULT_PATHS = [
        __DIR__ . '/../../app',
        // other paths
    ];

    private static array $ignore = [
        // Add the interface that aren't bound by purpose
    ];

    public static function contractsDataProvider(): Generator
    {
        $finder = Finder::create();
    
        $contractFiles = $finder->name('*Contract.php')->files()->in(self::DEFAULT_PATHS);
        foreach ($contractFiles as $file) {
            $fqdn = self::getFqdnForFilePath($file->getRealPath());
    
            if (in_array($fqdn, self::$ignore, true)) {
                continue;
            }
            yield $file->getRealPath() => [$fqdn];
        }
    }

Map the filepath to FQDN

After receiving the files we need to get the FQDN for a class. This says that the file app/Dummy/DummyContract.php has a class with the FQDN App\Dummy\DummyContract

If you have many namespaces or directories you can add more checks.

The last line say that we fail if it is impossible to match.

The result of this function is used to yield the FQDN from the contractsDataProvider method.

    // The path to the root of your project
    final public const PROJECT_DIR = __DIR__ . '/../../';
    
    protected static function getFqdnForFilePath(string $filePath): string
    {
        $realProjectDir = realpath(self::PROJECT_DIR) . '/';

        // Any off the App classes
        if (str_starts_with($filePath, $realProjectDir . 'app/')) {
            // remove the app dir and the .php extension
            $className = substr(
                $filePath,
                strlen($realProjectDir . 'app/'),
                -4
            );
            // Replace the / with a \
            $className = str_replace('/', '\\', $className);
            // Add the app namespace
            return 'App\\' . $className;
        }

        // Possible add other namespace parts

        // Unknown
        self::fail('Impossible to match "' . $filePath . '" to FQDN');
    }

The test function

And then the test itself. The previous written contractsDataProvider is the data provider for this test function. That means this test will run for each FQDN and gets the FQDN as parameter.

The function calls make on the app to get the instance. If we get an exception we let the test fail with the FQDN and the throwable message so we can see the result.

The test will also fail if we get an instance that doesn't implement the interface.


    #[Test]
    #[DataProvider('contractsDataProvider')]
    public function weCanGetTheImplementationFromTheContainer(string $contractName): void
    {
        // run
        try {
            $class = $this->app->make($contractName);

            // verify/assert
            $this->assertInstanceOf($contractName, $class);
        } catch (Throwable $throwable) {
            $this->fail('Failed to get an instance for "' . $contractName.'". The error is: ' . $throwable->getMessage());
        }
    }