Jan M. Jan M. - 4 months ago 106
PHP Question

The valid pattern for ZF3 service DI for interconnected services

As far as I understand the valid pattern is:


  • a FooControllerFactory that instantiates the needed service(s) (FooService)

  • a FooController with constructor __construct(FooService $fooService)

  • the Controller acquires some basic data and gets a result from the service

  • the Service contains all the required business logic
    This is a base service. Eventualy this service will need other services for various activities.
    For example CacheService, SomeOtherDataService.




The question is, what is a valid/appropriate pattern for
including/injecting those other interconnected services?


A reallife example of that we have currently, extremely simplified:

AuctionController

/**
* get vehicles for specific auction
*/
public function getVehiclesAction ()
{
$auctionService = $this->getAuctionService(); // via service locator
$auctionID = (int) $this->params('auction-id');
$auction = $auctionService->getAuctionVehicle($auctionID);
return $auction->getVehicles();
}


AuctionService

public function getAuctionVehicles($auctionID) {
$auction = $this->getAuction($auctionID);
// verify auction (active, permissions, ...)
if ($auction) {
$vehicleService = $this->getVehicleService(); // via service locator
$vehicleService->getVehicles($params); // $params = some various conditions or array of IDs
}
return false;
}


VehicleService

public function getVehicles($params) {
$cache = $this->getCache(); // via service locator
$vehicles = $cache->getItem($params);
if (!$vehicles) {
$vehicleDB = $this->getVehicleDB(); // via service locator
$vehicles = $vehicleDB->getVehicles($params);
}
return $vehicles;
}


Example of a suggested valid pattern

AuctionController

public function __construct(AuctionService $auctionService) {
$this->auctionService = $auctionService;
}

/**
* get vehicles for specific auction
*/
public function getVehiclesAction ()
{
$auctionID = (int) $this->params('auction-id');
$auction = $this->auctionService->getAuctionVehicle($auctionID);
return $auction->getVehicles();
}
**AuctionService**

public function getAuctionVehicles($auctionID) {
$auction = $this->getAuction($auctionID); // no problem, local function
// verify auction (active, permissions, ...)
if ($auction) {
$vehicleService = $this->getVehicleService(); // we don't have service locator
$vehicleService->getVehicles($params); // $params = some various conditions or array of IDs
}
return false;
}


VehicleService

public function getVehicles($params) {
$cache = $this->getCache(); // we don't have service locator, but cache is probably static?
$vehicles = $cache->getItem($params);
if (!$vehicles) {
$vehicleDB = $this->getVehicleDB(); // where and how do we get this service
$vehicles = $vehicleDB->getVehicles($params);
}
return $vehicles;
}


Some notes:


  • Services are interconnected only in some cases, in 95% they are standalone


    • Auction has a lot funcionality that does not need Vehicle

    • Vehicle has VehicleController and VehicleService that does only in some cases relate do Auction, it's a standalone module that has other functionalities

    • The Injection of every needed service in a controller would be a waste of resources, because they are not needed in every action (in the real-life application we have many more interconnected services, not just two)


  • Programming the same business logic in multiple services just to avoid the service locator is obviously an invalid pattern and not acceptable.


Fge Fge
Answer

If a controller requires too many different services, it usually is an indicator that the controller has too many responsibilities.

Following up on @AlexP's answer, this service then would be injected in your controller. Depending on your setup, this sure can result in dependecy injection cascades when a controller is created. This at least will limit the created services to those that are actually required by the controller (and those related transitively).

If some of these services are only required rarely and you are worried about creating them all on each request, the new Service Manager now supports lazy services too. Those still can be injected into a service / controller as a regular dependency (as above), but are only created when called for the first time.

Copying this from the documentation's example:

$serviceManager = new \Zend\ServiceManager\ServiceManager([
    'factories' => [
        Buzzer::class             => InvokableFactory::class,
    ],
    'lazy_services' => [
         // Mapping services to their class names is required
         // since the ServiceManager is not a declarative DIC.
         'class_map' => [
             Buzzer::class => Buzzer::class,
         ],
    ],
    'delegators' => [
        Buzzer::class => [
            LazyServiceFactory::class,
        ],
    ],
]);

When requesting the service, it does not get created right away:

$buzzer = $serviceManager->get(Buzzer::class);

But only when it is first used:

$buzzer->buz();

This way you can inject multiple dependencies into your controller and only the services actually required will be created. Of course this is true for any dependency, like Services required by other services and so on.

Comments