Resolving a Contextual Binding for DI in Laravel

135 Views Asked by At

I am trying to learn the correct usage of the IoC in Laravel. In my experiment, I have an interface that has multiple implementations. I am trying to Bind these implementations using a Contextual Binding. What I have written, i thought would work however I am clearly misunderstanding something, or just doing it plain wrong...

In my AppServiceProvider.php I have defined my bindings as such

use ...;

// Bind the Document Writer Interface to its Implementations
$this->app->bind(DocumentWriter::class, function ($app, DocumentType $document_type) {
    return match ($document_type) {
        DocumentType::Pdf => PdfWriter::class,
        DocumentType::Doc => DocWriter::class,
        DocumentType::Xls => XlsWriter::class,
    };
});

$this->app->when(ManagementReport::class)
  ->needs(DocumentWriter::class)
  ->give(function ($app, DocumentType $document_type) {
      return match ($document_type) {
          DocumentType::Pdf => PdfWriter::class,
          DocumentType::Doc => DocWriter::class,
          DocumentType::Xls => XlsWriter::class,
      };
  });

I am trying to resolve the DocumentWriter based on a passed Enum.

In my command I am running this; where $management_report->getType() returns an Enum correctly.

// Initialise the Document Writer Service
use ...;

$document_writer = App::makeWith(
    DocumentWriter::class,
    ['document_type' => $management_report->getType()]
);

$management_report_service = App::makeWith(
    ManagementReport::class,
    ['document_type' => $management_report->getType()]
);

Both of these fail with the same error

AppServiceProvider: Argument #2 ($document_type) must be of type App\Enums\DocumentWriter\Type, array given

Am I misunderstanding the behavior here and thus doing something wrong? Is this even possible? I want the Resolving of this interface and Service to fail if the Document Type is not a valid value of the Enum.


I should also add that my ManagementReport Service is expecting an implementation of the DocumentWriter Interface.

My Enum

namespace App\Enums\DocumentWriter;

enum Type: string
{
    case Xls = 'xls';
    case Doc = 'doc';
    case Pdf = 'pdf';
}

My ManagementReport Service Class

namespace App\Services\Building;

use ...;

class ManagementReport
{
    public function __construct(protected DocumentWriter $writer) {}
}

My Interface

namespace App\Contracts;

interface DocumentWriter
{
    public function setTemplate();

    public function getTemplate();
}

And my 3 implementations of the Interface PdfWriter.php, DocWriter.php and XlsWriter.php

namespace App\Services\DocumentWriter;

use App\Contracts\DocumentWriter;

class PdfWriter implements DocumentWriter
{
    public function getTemplate()
    {
        // TODO: Implement getTemplate() method.
    }

    public function setTemplate()
    {
        // TODO: Implement setTemplate() method.
    }
}
1

There are 1 best solutions below

0
On

I was able to get this working as I desired with the following adjustment to my AppServiceProvider.

// App/Providers/AppServiceProvider.php

public function register(): void
{
    // Bind the Document Writer Interface to its Implementations
    $this->app->bind(DocumentWriter::class, function (Application $app, array $params) {
        if (isset($params['document_type']) && $params['document_type'] instanceof DocumentType) {
            return match ($params['document_type']) {
                DocumentType::Pdf => $app->make(PdfWriter::class),
                DocumentType::Doc => $app->make(DocWriter::class),
                DocumentType::Xls => $app->make(XlsWriter::class),
                Default => throw new Exception(
                    '"' . DocumentType::class . '" of "' . $params['document_type']->name . '" has not been implemented',
                    500,
                ),
            };
        }

        throw new Exception(
            'Expected a "document_type" array key with a value of "' . DocumentType::class . '"',
            500,
        );
    });

    // Bind the Management Report Generator Service to a Document Writer Implementation
    $this->app->bind(ManagementReport::class, function (Application $app, array $params) {
        return new ManagementReport($app->makeWith(DocumentWriter::class, $params));
    });
}

I further improved my solution by binding my service to the Contextual Binding of the Interface.

// Initialise the Management Report Service
$management_report_service = App::makeWith(
    ManagementReport::class,
    ['document_type' => $management_report->getType()],
);
dump($management_report_service);

Added a little error handling to give it some meaning and works well.

It should be noted that the makeWith function from the container sends the second parameter as is; directly to the Binding. makeWith does not work this way with the containers when()->needs() notation apparently.