问题
I have trouble with dependencies in my application in service layer.
I have following class:
<?php
class UserService{
private $userRepository;
private $vocationService;
private $roleService;
public function __construct(UserRepository $userRepository, VocationService $vocationService, RoleService $roleService)
{
$this->userRepository = $userRepository;
$this->vocationService = $vocationService;
$this->roleService = $roleService;
}
}
There are only three dependencies which I'm injecting. Assume, I want to add next dependency, for example: NextService. My constructor will grow again.
What if I wanted to pass more dependencies within constructor ?
Maybe should I solve this problem by passing IoC container and then get desirable class? Here is an example:
<?php
class UserService{
private $userRepository;
private $vocationService;
private $roleService;
public function __construct(ContainerInterface $container)
{
$this->userRepository = $container->get('userRepo');
$this->vocationService = $container->get('vocService');
$this->roleService = $container->get('roleService');
}
}
But now my UserService class depends on IoC container which I'm injecting.
How to solve a problem following good practices?
Regards, Adam
回答1:
Injecting the container as a dependency to your service is considered as a bad practice for multiple reasons. I think the main point here is to figure out why and then try to understand the problem that leads you to think about "injecting the container" as a possible solution and how to solve this problem.
In object oriented programming, it's important to clearly define the relations between objects. When you're looking at a given object dependencies, it should be intuitive to understand how the object behaves and what are the other objects it relies on by looking at its public API.
It's also a bad idea to let your object rely on a dependency resolver, In the example you shared your object can't live without the container
which is provided by the DI component.
If you want to use that object elsewhere, in an application that uses another framework for example, you'll then have to rethink the way your object get its dependencies and refactor it.
The main problem here is to understand why your service needs all these dependencies,
In object-oriented programming, the single responsibility principle states that every context (class, function, variable, etc.) should define a single responsibility, and that responsibility should be entirely encapsulated by the context. All its services should be narrowly aligned with that responsibility. Source: Wikipedia
Based on this definition, I think you should split your UserService
into services that handle only one responsability each.
- A service that fetch users and save them to your dababase for example
- Another service that manages roles for example
- ... and so on
回答2:
I agree that __construct
can grow fairly easy.
However, you have a Setter DI
at your disposal: http://symfony.com/doc/current/components/dependency_injection/types.html#setter-injection
Morover, there is a Property DI
, but I wouldn't recommed it as ti leaves your service wide-open to manipulation: http://symfony.com/doc/current/components/dependency_injection/types.html#property-injection
回答3:
You can abstract some of the commonly used services in one helper service and then just inject this helper into your other services. Also you can define some useful functions in this helper service. Something like this:
<?php
namespace Saman\Library\Service;
use Symfony\Bundle\FrameworkBundle\Routing\Router;
use Symfony\Component\Form\FormFactory;
use Symfony\Bundle\FrameworkBundle\Translation\Translator;
use Symfony\Bundle\TwigBundle\Debug\TimedTwigEngine;
use Symfony\Component\Security\Core\SecurityContext;
use Doctrine\ORM\EntityManager;
class HelperService
{
protected $translator;
protected $securityContext;
protected $router;
protected $templating;
protected $em;
public function __construct(
Translator $translator,
SecurityContext $securityContext,
Router $router,
TimedTwigEngine $templating,
EntityManager $em
)
{
$this->translator = $translator;
$this->securityContext = $securityContext;
$this->router = $router;
$this->templating = $templating;
$this->em = $em;
}
Getters ...
public function setParametrs($parameters)
{
if (null !== $parameters) {
$this->parameters = array_merge($this->parameters, $parameters);
}
return $this;
}
/**
* Get a parameter from $parameters array
*/
public function getParameter($parameterKey, $defaultValue = null)
{
if (array_key_exists($parameterKey, $this->parameters)) {
return $this->parameters[$parameterKey];
}
return $defaultValue;
}
}
Now imagine you have a UserService then you define it like this:
<?php
namespace Saman\UserBundle\Service;
use Saman\Library\Service\HelperService;
class UserService
{
protected $helper;
public function __construct(
Helper $helper,
$parameters
)
{
$this->helper = $helper;
$this->helper->setParametrs($parameters);
}
public function getUser($userId)
{
$em = $this->helper->getEntityManager();
$param1 = $this->helper->getParameter('param1');
...
}
回答4:
This example was created for Symfony 4 but the principle should work in older versions.
As others have mentioned, it's good to engineer your application to limit the functional scope of each service and reduce the number of injections on each consuming class.
The following approach will help if you truely need many injections, but it's also a nice tidy way to reduce boilerplate if you are injecting a service in many places.
Consider a service App\Services\MyService
that you wish to inject into App\Controller\MyController
:
Create an 'Injector' trait for your service.
<?php
// App\Services\MyService
namespace App\DependencyInjection;
use App\Services\MyService;
trait InjectsMyService
{
/** @var MyService */
protected $myService;
/**
* @param MyService $myService
* @required
*/
public function setMyService(MyService $myService): void
{
$this->myService = $myService;
}
}
Inside your controller:
<?php
namespace App\Controller;
class MyController
{
use App\DependencyInjection\InjectsMyService;
...
public myAction()
{
$this->myService->myServiceMethod();
...
}
...
}
In this way:
- a single line of code will make your service available in any container managed class which is super handy if you're using a service in many places
- it's easy to search for your injector class to find all usages of a service
- there are no magic methods involved
- your IDE will be able to auto-complete your protected service instance property and know it's type
- controller method signatures become simpler, containing only arguments
If you have many injections:
<?php
namespace App\Controller;
use App\DependencyInjection as DI;
class SomeOtherController
{
use DI\InjectsMyService;
use DI\InjectsMyOtherService;
...
use DI\InjectsMyOtherOtherService;
...
}
You can also create an injector for framework provided services, e.g. the doctrine entity manager:
<?php
namespace App\DependencyInjection;
use Doctrine\ORM\EntityManagerInterface;
trait InjectsEntityManager
{
/** @var EntityManagerInterface */
protected $em;
/**
* @param EntityManagerInterface $em
* @required
*/
public function setEm(EntityManagerInterface $em): void
{
$this->em = $em;
}
}
class MyClass
{
...
use App\DependencyInjection\InjectsEntityManager;
A final note: I personally wouldn't try to make these injectors any smarter than what I've outlined. Trying to make a single polymorphic injector will probably obfuscate your code and limit your IDE's ability to auto-complete and know what type your services are.
来源:https://stackoverflow.com/questions/26679524/many-dependencies-in-service