Skip to content
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

[11.x] Allow for contextual bindings using attributes #51115

Closed
wants to merge 1 commit into from

Conversation

ollieread
Copy link
Contributor

@ollieread ollieread commented Apr 18, 2024

Note

I will update this with tests and some default implementations before marking for review. I've created it as a draft PR in case anyone wishes to feedback.

This PR adds functionality to allow users to register PHP 8 attributes and associated handlers with the container to add in resolving dependencies. All attributes must implement the Illuminate\Contracts\Container\ContextualAttribute marker interface. The attributes should be added to parameters, and will trump normal resolution.

The primary use of this sort of functionality would be to have dependency injection work with auth guards, connections, and other things where multiple instances exist with no difference from the perspective of the container.

Let's say for example that you have a platform that uses two guards, and different classes depend on different ones. Currently, the only solution to this is to manually get the guard, or create a contextual binding for each class. Instead, you could achieve it like so.

First, you create the attribute.

#[Attribute(Attribute::TARGET_PARAMETER)]
class AuthGuard implements ContextualAttribute
{
	public function __construct(
		public readonly string $name
	){}
}

Then you register it with the container.

Container::getInstance()->whenHas(AuthGuard::class, function (AuthGuard $attribute) {
	return Auth::guard($attribute->name);
});

Once this is done, you can use the attribute with dependency injection.

class MyService {
	private Guard $guard;
	
	public function __construct(#[AuthGuard('api')] Guard $guard) {
		$this->guard = $guard;
	}
}

$service = Container::getInstance()->make(MyService::class);

Copy link

Thanks for submitting a PR!

Note that draft PR's are not reviewed. If you would like a review, please mark your pull request as ready for review in the GitHub user interface.

Pull requests that are abandoned in draft may be closed due to inactivity.

@driesvints driesvints changed the title Allow for contextual bindings using attributes [11.x] Allow for contextual bindings using attributes Apr 18, 2024
@innocenzi
Copy link
Contributor

The primary use of this sort of functionality would be to have dependency injection work with auth guards, connections, and other things where multiple instances exist with no difference from the perspective of the container.

This is great, there's currently no way to do this, even by hijacking debug_stacktrace.

It would be great to also add a "after" hook, so we can act on a dependency that just got resolved from the container. This would be very useful in many scenarios, not just for framework stuff, but for business logic as well:

public function __construct(
    #[OnTenant(Tenant::FLY7)]
    private readonly Connector $connector
) {}
Container::getInstance()->afterResolvingAttribute(function (OnTenant $attribute, Connector $connector) {
    $connector->onTenant($attribute->tenant);
});

@driesvints
Copy link
Member

@ollieread are you still working on this one?

@innocenzi
Copy link
Contributor

I'm willing to take over the pull request if necessary. 👍

@ollieread
Copy link
Contributor Author

@ollieread are you still working on this one?

I intend to, but work has currently taken over. It's on a post-it on my monitor, I think I just need to write the tests, though if you can provide feedback ahead of that it would be appreciated.

@innocenzi
Copy link
Contributor

@ollieread
though if you can provide feedback ahead of that it would be appreciated.

I think in its current state, the feature would not be flexible enough. Having the ability to act on an already-resolved dependency instead of having to resolve it manually would offer a few more possibilities.

I wouldn't be able to implement OnTenant in the example above with this implementation (or you would have to replace $this->resolve with manually calling $handler).

@ollieread
Copy link
Contributor Author

@ollieread
though if you can provide feedback ahead of that it would be appreciated.

I think in its current state, the feature would not be flexible enough. Having the ability to act on an already-resolved dependency instead of having to resolve it manually would offer a few more possibilities.

I wouldn't be able to implement OnTenant in the example above with this implementation (or you would have to replace $this->resolve with manually calling $handler).

What you're requesting would remove all the flexibility from this solution. Only acting on resolved dependencies removes the majority of the flexibility and provides no benefit, as this is already possible.

In your example, you're hardcoding the reference to a tenant, so this solution wouldn't be applicable to you anyway, as you're looking for a dynamic solution to a static problem. If you have code that only needs to work when a particular tenant is resolved, don't call it unless that tenant is resolved.

@innocenzi
Copy link
Contributor

I'm suggesting an addition to your solution, not a replacement. 👍

In your example, you're hardcoding the reference to a tenant, so this solution wouldn't be applicable to you anyway, as you're looking for a dynamic solution to a static problem.

Not sure what you mean here, sorry.

I tried your PR locally and I couldn't find an application to my business logic use cases, unfortunately. All of them would require to modify an instance of an injected dependency, which itself would need to be resolved from the container due to their own dependencies.

I'll just attempt another PR myself after, it's fine. 👍

@ollieread
Copy link
Contributor Author

In your example, you're hardcoding the reference to a tenant, so this solution wouldn't be applicable to you anyway, as you're looking for a dynamic solution to a static problem.

Not sure what you mean here, sorry.

In the example that you gave, the usage of an attribute is completely redundant and over-engineered. You want to call onTenant() with a hardcoded constant. Just call it. The following is a way more efficient and way simpler solution to the problem you posed.

public function __construct(
    private readonly Connector $connector
) {
    $this->connector->onTenant(Tenant::FLY7);
}

I tried your PR locally and I couldn't find an application to my business logic use cases, unfortunately. All of them would require to modify an instance of an injected dependency, which itself would need to be resolved from the container due to their own dependencies.

I'll just attempt another PR myself after, it's fine. 👍

Everything you would like to do can already be done, and you can still use the container to resolve an instance and modify it.

@innocenzi
Copy link
Contributor

innocenzi commented Jun 25, 2024

@ollieread
In the example that you gave, the usage of an attribute is completely redundant and over-engineered.

I disagree with it being over-engineered, it's practical DX when this is used in many different places. This is not the subject of this PR though.

@ollieread
The following is a way more efficient and way simpler solution to the problem you posed.

Your suggestion is what I'm doing already, and something I'd rather do through an attribute.

@ollieread
You want to call onTenant() with a hardcoded constant.

It's the same as hardcoding api in your own example.

@ollieread
Everything you would like to do can already be done, and you can still use the container to resolve an instance and modify it.

As I said, your PR won't work for this—I tested it—unless you replace $this->resolve with manually calling $handler.

Let's just agree to disagree, go ahead with the PR 👍

@driesvints
Copy link
Member

Thanks @ollieread. Feel free to resubmit this then when you're ready.

@innocenzi feel free to attempt your own PR if you want to 👍

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