laravel-enhanced-container icon indicating copy to clipboard operation
laravel-enhanced-container copied to clipboard

Laravel Service Container on steroids.

Laravel Enhanced Container

Laravel Enhanced Container

Latest Version on Packagist Total Downloads Code Quality Code Coverage GitHub Tests Action Status PHPStan

This package provides enhanced contextual binding, method binding, method forwarding, and syntax sugar to operate on the Service Container.

The package requires PHP 8.x and Laravel 9.x.

#StandWithUkraine

SWUbanner

Contents

  • Installation
  • Usage
    • Basic binding
    • Binding instances
    • Extending bindings
    • Contextual binding
    • Contextual binding resolution outside of constructor
    • Method binding
    • Method forwarding
  • Testing

Installation

Install the package via composer:

composer require michael-rubel/laravel-enhanced-container

Usage

Basic binding

bind(ServiceInterface::class)->to(Service::class);
bind(Service::class)->itself();

As a singleton:

bind(ServiceInterface::class)->singleton(Service::class);
singleton(Service::class);

As scoped singleton:

bind(ServiceInterface::class)->scoped(Service::class);
scoped(Service::class);

Binding instances

bind(ServiceInterface::class)->instance(new Service);
instance(ServiceInterface::class, new Service)

Extending bindings

extend(ServiceInterface::class, function ($service) {
    $service->testProperty = true;

    return $service;
})

๐Ÿ” back to contents

Contextual binding

bind(ServiceInterface::class)
   ->contextual(Service::class)
   ->for(ClassWithTypeHintedInterface::class);

As a variadic dependency:

bind(ServiceInterface::class)
   ->contextual(
       fn ($app) => [
           $app->make(Service::class, ['param' => true]),
           $app->make(AnotherServiceSharingTheSameInterface::class),
       ]
   )
   ->for(ClassWithTypeHintedInterface::class);

As a primitive:

bind('$param')
   ->contextual(true)
   ->for(ClassWithTypeHintedPrimitive::class);

Contextual binding resolution outside of constructor

call(class: ServiceInterface::class, context: static::class);

// The call automatically resolves the implementation from an interface you passed.
// If you pass context, proxy tries to resolve contextual binding instead of global one first.
// Instead of static::class you may pass any class context for this particular abstract type.

๐Ÿ” back to contents

Method binding

Assuming that is your function in the service class:

class Service
{
    public function yourMethod(int $count): int
    {
        return $count;
    }
}

Bind the service to an interface:

bind(ServiceInterface::class)->to(Service::class);

Call your service method through container:

call(ServiceInterface::class)->yourMethod(100);

Override method behavior in any place of your app. You can add conditions in your method binding by catching parameters.

For example in tests:

bind(ApiGatewayContract::class)->to(InternalApiGateway::class);
bind(ApiGatewayContract::class)->method(
    'performRequest',
    fn () => true
);

$apiGateway = call(ApiGatewayContract::class);

$request = $apiGateway->performRequest();

$this->assertTrue($request);

Another example from the real-world app:

//
// ๐Ÿงช In tests:
//
function testData(array $params): Collection
{
    return collect([
        'object'      => 'payment_intent',
        'amount'      => $params['data']->money->getAmount(),
        'description' => $params['data']->description,
         ...
    ]);
}

bind(StripePaymentProvider::class)->method()->charge(
    fn ($service, $app, $params) => new Payment(
        tap(new PaymentIntent('test_id'), function ($intent) use ($params) {
            testData($params)->each(fn ($value, $key) => $intent->offsetSet($key, $value));
        })
    )
);

//
// โš™๏ธ In the service class:
//
$data = new StripePaymentData(
    // DTO parameters.
);

call(StripePaymentProvider::class)->charge($data);
// The data bound to the method from `testData` wrapped into PaymentIntent
// object with arguments you passed to the real function call. ๐Ÿ”ฅ

Remember that you need to use call() to method binding to work. It returns the instance of CallProxy. If you rely on interfaces, proxy will automatically resolve bound implementation for you.

Note for package creators

If you want to use method binding in your own package, you need to make sure the LecServiceProvider registered before you use this feature.

$this->app->register(LecServiceProvider::class);

๐Ÿ” back to contents

Method forwarding

This feature automatically forwards the method when it doesn't exist in your class to another one.

You can define forwarding in your ServiceProvider:

use MichaelRubel\EnhancedContainer\Core\Forwarding;

Forwarding::enable()
    ->from(Service::class)
    ->to(Repository::class);

You can as well use chained forwarding:

Forwarding::enable()
    ->from(Service::class)
    ->to(Repository::class)
    ->from(Repository::class)
    ->to(Model::class);

Important notes

  • Pay attention to which internal instance you're now working on in CallProxy when using forwarding. The instance may change without your awareness. If you interact with the same methods/properties on a different instance, the InstanceInteractionException will be thrown.
  • If you use PHPStan/Larastan you'll need to add the @method docblock to the service to make it static-analyzable, otherwise it will return an error that the method doesn't exist in the class.

๐Ÿ” back to contents

Testing

composer test

License

The MIT License (MIT). Please see License File for more information.