cakephp
cakephp copied to clipboard
Support the use of dependency injection in Components
Description
Currently dependency injection is supported in Controllers and Commands (reference). It's also fairly easy to set up in Listeners since the end user has control over the creation and registration of these during the bootstrap processes.
However, it is not currently possible to use DI in Components owing to the following code in ComponentRegistry.php, which constructs instances of Components:
protected function _create($class, string $alias, array $config): Component
{
/** @var \Cake\Controller\Component $instance */
$instance = new $class($this, $config);
$enable = $config['enabled'] ?? true;
if ($enable) {
$this->getEventManager()->on($instance);
}
return $instance;
}
For us this has proven to be a major issue.
Owing to this limitation, using dependency injection throughout our application would require a massive refactoring exercise touching hundreds of files, since all code that we have in Components which requires any dependencies would need moving out into separate service classes (which would not be callable from the Components).
Please consider supporting the use of dependency injection in Components.
CakePHP Version
4.4
Can you please give an example of where you need to use a component and can't just use a service class instead directly in your action?
@ADmad - If our project were a greenfield project, it would probably be possible to write it in such a way that the relevant service classes were used directly from controller actions (albeit this would not necessarily be consistent with the "thin controller, fat model" principle).
However, our project is decidedly not a greenfield project (with about 50+ man years invested in its development) and we have many tens of components that contain significant amounts of logic and cannot easily be changed into injectable services because of their reliance on having access to the request/response/controller/other components. It is simply not feasible for us to refactor these to get round the dependency injection problem. We would need to allocate months of effort to do the refactoring and then re-test the entire application.
Effectively we bought into the idea that "[c]omponents are packages of logic that are shared between controllers" at quite an early stage in the project's development and are now in the position where it prevents us from making proper usage of dependency injection and modern best practices (e.g. regarding testing) owing to CakePHP's design.
If I were starting a CakePHP new CakePHP project now, this limitation would effectively lead me to conclude that there were very few use cases in which using a component should actually be regarded as appropriate.
Finally, I should note that the main practical reason we want to use dependency injection in our components is to allow certain services that speak to the outside world to be mocked out during PHPUnit integration tests.
Hi @zejji!
I can definitely understand your frustration because I myself used to do the same with the shared controller logic in the past.
Unfortunately, there is no straight forward way to get access to the DI inside Components unless you want to inject your services in the controller action and pass them on to the component call like so
public function myAction(MyService $service)
{
$this->MyComponent->something($service);
}
But (and you may excuse me for my obnoxiously dirty hack here) you could also do that:
// src/Application.php
class Application extends BaseApplication
{
public static ContainerInterface $myDI;
public function bootstrap(): void
{
// usual content of the bootstrap() method
self::$myDI = $this->getContainer();
}
}
Then you can do
class SearchComponent extends Component
{
public function myMethod()
{
$diContainer = \App\Application::$myDI;
$myService = $diContainer->get(\App\Service\MyService::class);
}
}
Which will work with the usually $this->mockService()
functionality in PHPUnit as described in the docs.
How you call your static property is of course up to you.
Controller Components were present long before the DI container was introduced in CakePHP 4.2 Therefore, they have a different meaning and purpose right now than when they were first introduced.
See e.g. https://book.cakephp.org/3/en/development/testing.html#testing-components on how Component testing was done in CakePHP 3
But again this hack is not really how the DI container should be used (or made publicly accessible via this hack) because we don't want users to just freely have access to it wherever they like (like in templates or models)
@LordSimal - Thanks for this - I appreciate the suggestions.
To be honest, I think CakePHP components should come with a health warning in the documentation if it is not possible to make them work with the dependency injection container. This is because it becomes a severe impediment to writing tests, which are a requirement in modern software development.
However, in an ideal world a solution would be found that would allow injected dependencies to be passed to new component instances along with the component registry and config, so I think it makes sense for this issue to be kept open and ideally prioritized, since it is a major blocker to best practices.
But wouldnt other specific CakePHP elements like Helpers also need the same adjustments? Only adjusting one element seems a bit inconsistent.
Given their purposes, Helpers don't tend to need either of the following, so from my perspective this is less of an issue:
- injection of alternative implementations; or
- mocking out during tests.
Generally I tend to need the ability to mock services during tests where they involve:
- access to other systems (i.e. any communication via network calls or queues);
- sending of emails or other notifications to users (e.g. SMS); or
- writing of files.
To support mocking e.g. writing files we would have to re-add the File and Folder Utility which we recently just removed.
As I see it CakePHP tries to give you as much as possbile but not too much. Most of the e.g. file and folder features can be achieved by doing native PHP calls so there is little need to wrap them in a Utility class just so its easier to mock them.
If you need such mocking functionality it is encouraged to write a service yourself yourself exactly to the specification you need instead of the framework having to provide a wrapper which fits 80% of people but 20% need additional hooks/adjustments/functionality in their specific case which just makes them more complicated in the long run.
Mocking network calls via the CakePHP HTTP Client is already a thing: https://book.cakephp.org/4/en/core-libraries/httpclient.html#namespace-Cake\Http\TestSuite
Dependency injection should be integrated with every main part of the framework, such as controllers, commands, components, helpers, behaviors, ecc, like other frameworks do.
in addition a global access to the container should be available if you want to create an object instance on the fly or to get a global singleton object, based on the DI configuration. Using the global container instance will be possible to instantiate an object using the DI itself and integrate DI when not possible yet like custom service classes
Well what I described above is already what you want: A globally accessible DI container via a static property on the Application class. But again, this is heavily not recommended.
The problem is just the fact, that with great power comes great responsibility. And giving access to the DI Container in a globally accessible way would allow people to shoot themselves in the foot in so many creative and absolutely horrible ways. Imagine using the DI Container inside templates to load data. The whole concept of MVC is out of the window with that.
Thats why we as the CakePHP Core team will not provide a default provided way to access the DI Container from anywhere.
in addition a global access to the container should be available if you want to create an object instance on the fly or to get a global singleton object, based on the DI configuration.
This is a service locator, and global variables with a lot of extra work. If an application absolutely needs this, I'm confident that this could be built in application code, but it isn't something we want to provide as part of the framework as it encourages practices that make code hard to maintain
Dependency injection should be integrated with every main part of the framework, such as controllers, commands, components, helpers, behaviors, ecc, like other frameworks do.
This I can get behind. When we added the DI container we took a cautious approach and started with building limited objects from DI. From your list we already have commands and controller actions taken care of. Having more of the framework building blocks come from DI is something that can be done in minor releases. Doing these changes in a backwards compatible way could be tricky but it sounds doable.
A global DIC is an antipattern, let's not go that way.
@zejji I had similar use case a few years back. AFAIR I worked around this with my custom AppController and ComponentRegistry classes which I made DIC aware.
This issue is stale because it has been open for 120 days with no activity. Remove the stale
label or comment or this will be closed in 15 days