Skip to content

[12.x] Introduce #[Bind] attribute #56383

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

Merged
merged 12 commits into from
Jul 23, 2025
Merged

[12.x] Introduce #[Bind] attribute #56383

merged 12 commits into from
Jul 23, 2025

Conversation

cosmastech
Copy link
Contributor

@cosmastech cosmastech commented Jul 23, 2025

Similar to #56334, now a developer can bind a specific concrete implementation to an interface via an attribute.

#[Bind(TwChatService::class, environments: ['staging', 'production'])]
#[Bind(FakeTwChatService::class, environments: ['*'])]
interface ChatServiceInterface
{
    public function getMessages($user): array;

    public function sendMessage($user, $message): Response;
}

In the above scenario, if the environment is staging or production, then calling app(ChatServiceInterface::class) will return TwChatService. For all other environments (eg: dev or testing), FakeTwChatService would be returned.

Why?

Our biggest use of interfaces is to provide a production version of the code, and then inside of tests, we swap out with a test implementation. All of our code type-hints the interface, but in order to figure out which concrete implementation it is, we have to jump back to the service provider to see where it's bound. This keeps it tidy and in one place.

Technical Decisions

We are keeping a cache of class strings which we have already checked for the Bind attribute. This cuts down on repeated reflection if we already know the class does not have the attribute. While it will increase the memory footprint of the Container class slightly, it should improve performance on repeated instantiation of objects. Maybe this should be a static property instead? 🤔

If using the container outside of the Laravel Application, then you must manually set the environment resolver. If none is set, then the attributes will be ignored.

Wildcards

We are using fuzzy-matching (via the Str::is() method). If all environments are passed, then we will still compare all of the attributes to see if there is a more specific match.

#[Bind(MyDummyClass::class, ['*'])]
#[Bind(MyProdClass::class, ['production'])]
interface MyInterface { }

For the above, app(MyInterface::class) will return MyProdClass if the environment is production, but MyDummyClass for all other environments.

Improvements

The scoped/singleton property could possibly be shoe-horned in here.

@taylorotwell
Copy link
Member

@cosmastech how hard would it be to add the environment stuff in? I think it's pretty key to using this feature.

@cosmastech
Copy link
Contributor Author

cosmastech commented Jul 23, 2025

@cosmastech how hard would it be to add the environment stuff in? I think it's pretty key to using this feature.

Not hard, but tell me which you prefer:

Only allow the attribute once

#[Bind([
    ProductionTwChatService::class => ['staging', 'production'],
    StubTwChatService::class => ['local', 'testing'],
])]
interface ChatServiceInterface
{
    // ...
}

vs
Allow the attribute to be repeated

#[Bind(ProductionTwChatService::class, environments: ['staging', 'production'])]
#[Bind(StubTwChatService::class, environments: ['local', 'testing'])]
interface ChatServiceInterface
{
    // ...
}

Additionally:
What do we use to signify it's for all environments? environments: ['*']?

How do we signify the default fall-through if the environment is not specified?

Another question is: since the Container class doesn't have access to the environment, it would have to be handled by the Application class instead. What do we do in that case?

@taylorotwell
Copy link
Member

taylorotwell commented Jul 23, 2025

@cosmastech I think ['*'] is fine for all environments. If environments are defined but no matching environment is found, I would expect it to be as if the interface wasn't bound at all. I would maybe inject some environment resolver closure into the container via a setter like resolveEnvironmentUsing(fn). If that is not set then this feature would not work, but when using the framework we would set it for you.

@cosmastech
Copy link
Contributor Author

I'll mark this as draft and get to to work on it. 🙇 Thanks @taylorotwell

@cosmastech cosmastech marked this pull request as draft July 23, 2025 15:02
@cosmastech cosmastech marked this pull request as ready for review July 23, 2025 18:29
@taylorotwell taylorotwell merged commit 9eded7e into laravel:12.x Jul 23, 2025
60 checks passed
@taylorotwell
Copy link
Member

Thanks - added enum support to environments 👍

@cosmastech
Copy link
Contributor Author

Thanks - added enum support to environments 👍

Me--of all people--not thinking of adding enum support 😂

Let me know if you want me to take a tumble at adding docs for this.

Thanks @taylorotwell 👋 See you at Laracon!

@cosmastech cosmastech deleted the bind branch July 23, 2025 23:24
@AhmedAlaa4611
Copy link
Contributor

I think this PR adds another way to do something that already works well, which could introduce unnecessary complexity. Supporting multiple ways to accomplish the same task can make the framework harder to refactor, maintain, and extend over time.

As Taylor Otwell has often emphasized:

To preserve our ability to adequately maintain the framework, we need to be very careful regarding the amount of code we include.

If applicable, please consider releasing your code as a package so that the community can still take advantage of your contributions!"

I think we need to revert this one.

AhmedAlaa4611 referenced this pull request in laravel/docs Aug 7, 2025
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.

3 participants