phpzend-framework2zend-framework-mvczend-framework3zf3

The valid pattern for ZF3 service DI for interconnected services


As far as I understand the valid pattern is:

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:


Solution

  • 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.