4

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.
Jan M.
  • 127
  • 2
  • 14

2 Answers2

5

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.

Fge
  • 2,971
  • 4
  • 23
  • 37
  • OK, great, tnx :) That's smoothly solves efficiency problems with too many unnecessary dependencies. But is it valid pattern, that controller knows everything that logic layer (service) does? Does controller really have to know that service is going to use 4 more sub-services for it's operation and how it's gonna do that? – Jan M. Jul 07 '16 at 07:00
  • My thinking is that service is like some kind of API - you know where to make a call (API url or service function), but you don't always know what's happening behing the scenes. And for example, for big projects, multiple programmers - one programers codes controller and views, and knows how to use service but doesn't know logic behind it, while other programmer does only business logic in service, and won't go changing controller factory to inject more services. Is my thinking faulty? :) – Jan M. Jul 07 '16 at 07:00
  • You're right to assumethat the controller should not know what dependencies a service is having. Your controller will only receive the services you inject. it doesn't know how these are created and what dependencies these have in turn. That's the whole point of the Service Manager - hidding the facts of how are service (or any object) is created and what dependencies it has. The service manager takes care of that. The controller / service only receives already created instances. – Fge Jul 07 '16 at 07:19
  • Yes, but until now, ServiceLocator/ServiceManager (which are essentially the same: http://stackoverflow.com/questions/18682835/zf2-servicemanager-vs-servicelocator), was injected into each service, so services had access to all other services. And that is considered bad practise. As I gather everything should be injected in factories. Not just ControllerFactory, but also in ServiceFactory. All connected services must be injected, except ServiceLocator is "forbidden", because of invalid pattern. – Jan M. Jul 07 '16 at 08:49
  • Initial problem with that approach is efficiency (controller factory inject 2 services, each of that service factories injects 5 sub-services, each of them some more ...), but if LazyService works as it should, that should be solved. – Jan M. Jul 07 '16 at 08:49
  • Yes, that's what the lazy services are for. However I stronly recommend to not start with lazy services right away. Creating objects is quite cheap, even when creating lots of services. Only if those services are expensive to create (e.g. making connections to external services, read files ...) you should lazy create these (and only these). To great thing about these lazy services is, that you can make them lazy later on without changing your code - they are completly transparent for any service/controller using them. – Fge Jul 07 '16 at 09:57
1

You could compose a new service, say VehicleAuctionService and have both the AuctionService and VehicleService injected as dependancies using a factory.

This is object composition.

class VehicleAuctionService
{
    private $auctionService;
    private $vehicleService;

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

    public function getAuctionVehicles($auctionID)
    {
        $auction = $this->auctionService->getAuction($auctionID);

        if ($auction) {
            $params = [
                'foo' => 'bar',
            ];
            $this->vehicleService->getVehicles($params);
        }
        return false;
    }

}
AlexP
  • 9,906
  • 1
  • 24
  • 43
  • Where and how exactly would this service be injected into? If it's injected in controller it's just replacement for separate Auction and Vehicle services, but same problem still exists - in 95% of cases it wouldn't be used. And because this is part of business logic (auction business logic would be in Auction service), I don't want to presume in controller which sub-services Auction service will use. And if Auction service needs multiple sub-services, not just Vehicle, how would you combine all those services? By pairs or all potentially available services in single combined service? – Jan M. Jul 06 '16 at 18:36