User tests: Successful: Unsuccessful:
This Pull requests allows to use to new interfaces which provide auto-wiring with constructor parameters or auto-wiring by providing an array ob required classes to be injected.
We introduce a new service provider factory which uses the newly introduced MVC autowire factory.
The new \Joomla\CMS\MVC\Factory\AutowireFactory does most of the new magic, it's a glue between the existing \DI\Container::buildObject() method and the component service provide. Instead of injecting the needed objects in the server provided we injected the full container into the new AutowireFactory and provide some workarounds for scalar values.
This PR is based on the https://github.com/joomla-framework/di/tree/4.x/autowire branch which allows to support scalar values in the container for autowiring, this is not intended to provide a scalar values or any other fixed values in the container, it's used in the local cloned container in the autowireFactory only.
The MVC base classes got a new method (if needed) to provide all currently injected dependencies, this allows us to use the AbstractAutowireInterface in CMS and 3rd Party.
As example I converted com_contact to use the AbstractAutowireInterface.
A bit tricky, for the core you can test com_contact in all variants (already done by the CI).
For own components You need to use the new Autowire Service Provider.
and implement the ConstructorAutowireInterface in your Controller/Model/View
If you extend the FormController it's working out of the box, if you need to extra dependencies you need to create a constructor with all needed parameters from the parent and the dependencies which should be provided to the constructor by the autowire factory.
If works
It still works but now cool with autowiring reducing maintainance costs a lot.
Please select:
Documentation link for guide.joomla.org:
No documentation changes for guide.joomla.org needed
Pull Request link for manual.joomla.org:
No documentation changes for manual.joomla.org needed
In the code are some todos we should discuss, for example legacy b/c code is included to load "old" objects which will do the same as the MVCFactory.
I know that API and frontend tests are failing, I will look at this later, for now it's ready for discussion.
| Status | New | ⇒ | Pending |
| Category | ⇒ | Administration com_contact Front End External Library Composer Change Libraries |
Why not letting the base classes implement the interface and use the trait? So we don't have to touch all the MVC classes in each component to support auto wiring. It will be used only when the AutowireMVCFactory is used.
I wanted to be safe and didn't thought deep enough about it. But yes the work is really annoying...
One of the issues could be that you can't remove the interface. So if you do it "right" in you extension and use the ConstructorAutowireInterface the buildObject does the work twice.
For example:
You have an CustomController, which extends the BaseController and implements the ConstructorAutowireInterface and have a constructor with __construct(DispatcherAwareInterface $dispatcher, LoggerAwareInterface $logger, CacheControllerFactoryAwareInterface $cacheFactory) everything is fine now.
If the BaseController implements the AbstractAutowireInterface the 3 interfaces will be set again after the construction. Not good performance wise, reflection is already expensive on it's own.
On the other side, it would allow us to add dependencies in the base classes at a later time without bothering extension developers to update the constructor...
I will test what will happen with the interface in the base classes.
IMHO, this is not a good approach, as it couples classes with the DI container through container-related interfaces. Consequently, service configuration becomes scattered across the codebase.
I would suggest defining autowiring configuration via simple arrays tagged with special "autowire" tags. For example:
// services/provider.php
$container->share('autowire_some_id', fn () => [
BaseController::class => [
'__construct' => true, // enables contructor autowiring
'setDispatcher' => DispatcherAwareInterface::class,
'setLogger' => LoggerAwareInterface::class,
'setCacheControllerFactory' => CacheControllerFactoryAwareInterface::class,
],
BaseDatabaseModel::class => [
'__construct' => true,
'setDispatcher' => DispatcherAwareInterface::class,
'setDatabase' => DatabaseAwareInterface::class,
'setMVCFactory' => MVCFactoryInterface::class,
'setCacheControllerFactory' => CacheControllerFactoryAwareInterface::class,
],
]);
$container->tag('autowire_some_id', 'autowire');On the first call, the AutowireFactory will collect all these arrays, merge them into one, and use it for wiring services.
IMHO, this is not a good approach, as it couples classes with the DI container through container-related interfaces. Consequently, service configuration becomes scattered across the codebase.
I would suggest defining autowiring configuration via simple arrays tagged with special "autowire" tags. For example:
// services/provider.php $container->share('autowire_some_id', fn () => [ BaseController::class => [ '__construct' => true, // enables contructor autowiring 'setDispatcher' => DispatcherAwareInterface::class, 'setLogger' => LoggerAwareInterface::class, 'setCacheControllerFactory' => CacheControllerFactoryAwareInterface::class, ], BaseDatabaseModel::class => [ '__construct' => true, 'setDispatcher' => DispatcherAwareInterface::class, 'setDatabase' => DatabaseAwareInterface::class, 'setMVCFactory' => MVCFactoryInterface::class, 'setCacheControllerFactory' => CacheControllerFactoryAwareInterface::class, ], ]); $container->tag('autowire_some_id', 'autowire');On the first call, the
AutowireFactorywill collect all these arrays, merge them into one, and use it for wiring services.
Sounds really wrong on several levels to me, you don't want to define class depending information in another file (in this case the service provider) also you don't want to inject magic names into the container (the scalar things is a not so nice workaround for our legacy code base, because our container is a service locator not a "information" storage).
I see no benefit from this, yes the Container provides 2 new Interfaces which tells you (the cms) that it supports autowiring (actually for the container only the AbstractAutowireInterface is relevant because it needs to inject the dependencies).
Normally (if we could do it right from the beginning) the way would be to use the constructor and you don't need to do any manual declaration but our codebase is old and should be move into a new direction without breaking everything.
Never the less, thanks you for your feedback.
| Labels |
Added:
Composer Dependency Changed
PR-6.2-dev
|
||
To some degree I share the concern of @voronkovich, that the dependency declaration functions are placed in the classes itself. What about using annotations for dependency declaration? Just a thought, not sure if it is of practical use.
I think annotations are not really helpful, you still need a method in the class to set the dependencies, mostly because a simple attribute has no "value" (for example holding the setter method). for this I think we need properties. makes it a bit more complex.
But maybe I'm wrong with how to use the annotations.
Beside that you need to rebuild the inheritance manually and reflect each parent class, which is expensive.
I would like to use more attributes or properties in joomla but it should make the code nicer
Also I might have missed the point where the class is no longer responsible for the needed dependencies.
ymmv
Why not letting the base classes implement the interface and use the trait? So we don't have to touch all the MVC classes in each component to support auto wiring. It will be used only when the AutowireMVCFactory is used.
I implemented the interface and I think it's a good idea and works as expected.
Why not letting the base classes implement the interface and use the trait? So we don't have to touch all the MVC classes in each component to support auto wiring. It will be used only when the AutowireMVCFactory is used.