-
-
Notifications
You must be signed in to change notification settings - Fork 312
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
Contextual Bindings / Strategy Pattern Proof of Concept #809
Comments
I think I have questions that your code (at least the tests around it) does not immediately answer: You have three classes implementing the Your tests do not answer how many instances are being created in the process. Most of the time they are testing for the color string, alternatively they are testing It appears to me that anything that isn't a builtin type now has to be defined using The overall question: Does this allow doing something that is impossible without it, and how much hassle is the old method compared to the new one? I currently believe I would be able to change any of your example definitions above into some form of |
Hi @SvenRtbg, I appreciate your reply. I'll try to respond to everything I can here. First, here are the classes being used in the tests.
Correct, we would then be coupling concrete dependencies to classes, with a pattern like the strategy pattern and essentially the design by contract philosophy, this is the opposite of what we're trying to achieve by being able to switch out a dependency without modifying our classes and adding "new" work instead of editing existing work. We would never have to touch the target class, but instead modify the factory to provide a different or new implementation. I coupled the tests by name for clarity, but you can imagine that
The The power of programming to an interface here, is that I can implement a whole brand new crazy way to implement that interface, and that often means the constructor might need an additional dependency or two, which I believe could be be easier (see below).
Yes, but I lose autowiring as far as I understand. I want to allow a developer to add a whole bunch of concrete injections without worrying it will break.
In these tests, we're essentially creating one per request. The color string is just a string that matches the class from which it comes from, e.g. green. I agree, I could just return the instance instead and test that for consistency, but it's essentially the same thing.
Allows me contextually bind a concrete to an interface, while keep autowiring for the object, and allowing the constructor order/variable names to be whatever they want to be without fear of breaking the implementation. In addition, allowing me to adjust the strategy of which concrete instance gets injected by only modifying the "service provider" or definer. This also allows providing a different instance for a concrete class, tied to the specific parent that would be accepting the dependency.
That's currently correct, but I guess it doesn't have to be that way. What do you mean by "old way", via
They currently don't fail for builtins, they would throw the same exceptions PHP-DI does now if you pass a
If you'd be able to show me how to write an inline factory like this while fully maintaining autowiring and not returning an actual The biggest benefit here is maintaining autowiring, so a class that implements an interface can have its constructor parameters change quite easily (if I wanted to add another concrete, where PHP-DI would just know to inject that). I'm sure an example would help best here of what I'm trying to avoid/improve. Let's say we have this class: class ColorManager {
private ColorInterface $color;
public function __construct( ColorInterface $color ) {
$this->color = $color;
}
} Right now, we can use a factory like this in a definer: ColorManager::class => static fn ( ContainerInterface $c ) => new ColorManager( $c->get( Red::class ) ), Or we can globally bind a single concrete to a single interface, and we wouldn't need the above factory at all, but now we can't switch the strategy at runtime: ColorInterface::class => static fn ( ContainerInterface $c ) => $c->get( Red::class ), For the sake of argument, let's say we need add a few more concrete dependencies to the ColorManager constructor, all that can be autowired: class ColorManager {
private ColorInterface $color;
private ColorModifier $modifier;
private ColorSharpener $sharpener;
public function __construct( ColorInterface $color, ColorModifier $modifier, ColorSharpener $sharpener ) {
$this->color = $color;
$this->modifier = $modifier;
$this->sharpener = $sharpener;
}
} We have to now modify our factory in our definer: ColorManager::class => static fn ( ContainerInterface $c ) => new ColorManager( $c->get( Red::class ), $c->get( ColorModifier::class ), $c->get( ColorSharpener::class ) ); With the change I'm suggesting, it would lessen the amount of work, because the following definition would work for all the above situations, before and after the class changes, and without "globally" binding the interface to a single concrete class: ColorManager::class => Container\autowire()->contextualParameter( Color::class, Red::class ), Of course, the real power is the strategy configuration at runtime, where some other object determines the strategy, for example:
As mentioned before, this brings similar functionality like Laravel's contextual bindings. In their documentation, I hope this helps and let me know if I can provide anything else. |
Could you detail why this doesn't solve your problem: return [
ColorManager::class => autowire()->constructorParameter('color', factory(function () {
return new Red();
})),
]; |
I do agree with most of your statements, specifically that the Still I think your main argument is based on "runtime based configuration", with giving examples that all rely for the DI container definition to fetch some information that already requires the DI container to exist, then feed that information back into the container to proceed in the code path, influencing what really gets created. I believe that this is explicitly not a design goal anymore for PHP-DI. It is designed to deliver statically definable object trees of stateless objects - that's what all that ability to compile the container is about: We'd have to have a final configuration at some point, after that the compilation process has all the information to put into faster build instructions. While it still allows to influence it's configuration or objects after the container is established, that isn't really a feature that implementors should rely on. So when looking at the Laravel example:
this is in fact a nice way to write a static definition, but you'd still be able to do it with PHP-DI right now, albeit having to write more "custom" closures. Implementing any example like this would aim to improve the syntax of PHP-DI required for such a use case. However, anything that uses "a users stored settings" or "user roles" is out of scope by definition. Surely getting hold of these settings would require to use objects, be it database access, reading cookies, querying the authentication subsystem or doing whatever is necessary. So the DI container is already established in order to get the info, which should not be looped back into the container to influence future object construction - and one of the reasons is that you'd have to very properly build the container to avoid using objects that are supposed to change based on the info put back into the container. Yes, having another container just as the second-step container sounds like the next best plan, but in fact I'd rather go with strategy factories instead. They can easily be injected statically, and decide which implementation to provide based on however the information to make the decision is passed to them.
The closure doesn't do anything fancy. I'd assume that you would simply be able to just call I'm thinking about the one statement that you made about
(seeing that @mnapoli commented while I was writing my comment, I'd thing that you don't even need the closure) In general, influencing the way autowiring works currently requires stating code elements like parameter variable names or method names as strings. That's not desirable very much because of tight coupling with the code, affecting the ability to refactor at will. The same is not true for class names as long as they are written as |
|
@SvenRtbg keep i mind with Laravel example you can do all of the logic I've detailed here, including utilizing the already defined or autowired definitions, looping back into the container, e.g. $this->app->when([VideoController::class, UploadController::class])
->needs(Filesystem::class)
->give(function () {
return $this->app->make( Storage::class )->disk('s3');
});
// or
$this->app->when([VideoController::class, UploadController::class])
->needs(Filesystem::class)
->give( static function ( Application $app ) {
return $app->make( Storage::class )->disk( 'local' );
});
// or an inline factory/strategy like example
$this->app->when([VideoController::class, UploadController::class])
->needs(Filesystem::class)
->give( static function ( Application $app ) {
$environment = $app->environment();
switch ( $environment ) {
case 'production':
case 'staging':
$storageStrategy = 's3';
default:
$storageStrategy = 'local';
}
return $app->make( Storage::class )->disk( $storageStrategy );
}); |
So the problem is that you define the injection based on a variable name? Since PHP now supports named arguments, argument names are now part of the API of a method. Furthermore, variables can be renamed and parameter types can be changed in the same way, I don't see one as more "robust" than the other.
The return [
ColorManager::class => autowire()->constructorParameter('color', get(Red::class)),
]; Does that answer this point? On a side note, TBH the discussion is extremely long and complex to follow. I'd love one concrete example from your project that fits in a few lines (if you have that). |
@defunctl Out of curiosity, why not this? return [
ClassA::class => autowire()->constructor(get(Red::class)),
ClassB::class => autowire()->constructor(get(Green::class)),
]; where class Green implements ColorInterface and consuming class is public function __construct(ColorInterface $c) In example of Laravel and Symfony, both are not "true" strategy as they still define concrete implementation in the config of service locator. If we are in situation where we don't know what concrete argument value until we actually run the code, I think a standalone factory class would be a better choice as this way your code is more separated plus you can easily unit test it. |
Hello!
There have a been a few requests for this functionality in the past:
I took a stab at coding this functionality using a custom Definition Helper based on the AutoWireDefinitionHelper here: https://github.com/moderntribe/tribe-libs/pull/110/files
I'd be curious if you'd be open to getting similar functionality directly in PHP-DI, instead?
Other containers do support some form of this functionality:
Please see the preliminary tests for how these definitions could be used to...
The text was updated successfully, but these errors were encountered: