Skip to content

[12.x] feat: Add Contextual Implementation/Interface Binding via PHP8 Attribute #55904

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Conversation

yitzwillroth
Copy link
Contributor

Summary

This PR introduces a new Provide attribute that enables contextual dependency binding using PHP 8 attributes, offering a more declarative and intuitive alternative to traditional Service Provider registration for dependency injection.

Problem

Currently, Laravel developers must register contextual bindings in Service Providers, which can become verbose and disconnected from the actual usage context:

// Traditional approach - in ServiceProvider
$this->app->when(UserController::class)
          ->needs(UserRepositoryInterface::class)
          ->give(DatabaseUserRepository::class);

This approach has several limitations:

  • Binding logic is separated from where dependencies are actually used
  • Requires boilerplate code in Service Providers
  • Makes it harder to understand dependencies at a glance
  • Can lead to cluttered Service Providers in large applications

Solution

The Provide attribute allows developers to specify concrete implementations directly at the point of injection:

class UserController extends Controller
{
    public function __construct(
        #[Provide(DatabaseUserRepository::class)]
        private UserRepositoryInterface $userRepository
    ) {}
}

Key Features

  • Declarative: Dependencies are declared where they're used
  • Type-safe: Leverages PHP 8's attribute system with generic type hints
  • Flexible: Supports constructor parameters for complex instantiation
  • Compatible: Works alongside existing binding mechanisms

Usage Examples

Basic Usage

class OrderService
{
    public function __construct(
        #[Provide(StripePaymentProcessor::class)]
        private PaymentProcessorInterface $processor
    ) {}
}

With Constructor Parameters

class NotificationService
{
    public function __construct(
        #[Provide(EmailService::class, ['template' => 'welcome','from' => 'noreply@app.com'])]
        private EmailServiceInterface $emailService
    ) {}
}

Method Injection

class ReportController extends Controller
{
    public function generate(
        #[Provide(PdfReportGenerator::class)]
        ReportGeneratorInterface $generator
    ) {
        return $generator->generate();
    }
}

Implementation Details

The Provide attribute implements Laravel's ContextualAttribute interface, ensuring seamless integration with the existing container resolution system. The attribute:

  1. Accepts a concrete class name and optional constructor parameters
  2. Validates that only concrete classes are provided (not interfaces or abstract classes)
  3. Uses the container's make() method to resolve dependencies with proper parameter injection
  4. Includes comprehensive PHPDoc with examples and type hints

Benefits

  • Improved Readability: Dependencies are visible at the injection point
  • Reduced Boilerplate: No need for Service Provider registration in many cases
  • Better Developer Experience: IDE support for navigation and refactoring
  • Maintainability: Changes to dependencies are localized to their usage context

Backward Compatibility

This enhancement is fully backward compatible:

  • Existing Service Provider bindings continue to work unchanged
  • No breaking changes to existing APIs
  • The attribute is optional and doesn't affect existing dependency injection patterns

Requirements

  • PHP 8.0+ (for attribute support)
  • Concrete class implementations only (interfaces and abstract classes will throw binding resolution errors)

Related Issues

This enhancement addresses common developer pain points around:

  • Verbose Service Provider configurations
  • Disconnected binding definitions
  • Difficulty in tracking dependency relationships

Note: I vacillated a lot on naming... Provides, Autowire, Autowired, Inject, Make, Resolve, ResolveTo, ResolvesTo, Bind, Give, Service, Implementation, Concrete, etc. also would work, of course.

@yitzwillroth yitzwillroth changed the title [12.x] feat: Add Contextual Implementation/Interface bBnding via PHP8 Attribute [12.x] feat: Add Contextual Implementation/Interface Binding via PHP8 Attribute Jun 1, 2025
@devajmeireles
Copy link
Contributor

I would say it is contextually duplicated of #55645, am I wrong?

@yitzwillroth
Copy link
Contributor Author

This PR doesn't introduce proxies at all, rather simply allows you to specify the contextual binding via attribute instead of in a service provider, reducing boilerplate and making the relationship more explicit. It uses the existing Container resolution mechanisms 100%.

@devajmeireles
Copy link
Contributor

This PR doesn't introduce proxies at all, rather simply allows you to specify the contextual binding via attribute instead of in a service provider, reducing boilerplate and making the relationship more explicit. It uses the existing Container resolution mechanisms 100%.

I mentioned it as a duplicate of the other PR based on the logic, I might be wrong. Related to this PR itself, I think it's an overkill. We can resolve what this PR proposes using a simple app()->make(). When compared with the other PR, the other one makes more sense in general.

@shaedrich
Copy link
Contributor

Wouldn't it make more sense to approach this the other way round?

  • #[Provide] has to be written for each usage
  • #[Bind] (or whatever we would call it) would be used about the class that will be bound to the container once for n usages

Also: It looks a little weird to type a method argument as an interface and then directly above restrict this to a concrete class. If I want to use this class always and know this right there, where's the benefit of the interface then?

@yitzwillroth
Copy link
Contributor Author

yitzwillroth commented Jun 1, 2025

@shaedrich

  1. The goal is to make the relationship more visible and to replace contextual binding hiding in a service provider. Think of it as an override.

  2. A common use case would be inheritance, where the base class specifies the interface and different subclasses require different injected dependencies (as with contextual binding in a service provider which is already available).

The pull request doesn't provide any functionality that isn't already available, simply specifies the relationship between contextual implementation and interface closer to the code:

Screenshot_20250602_062538_Chrome.jpg

@taylorotwell
Copy link
Member

Renamed to Give just to have consistency with existing give method. Thanks. 👍

@taylorotwell taylorotwell merged commit f66e812 into laravel:12.x Jun 2, 2025
1 check failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants