Search code examples
phpmodel-view-controllerdependency-injectiondependencies

How to avoid implicit dependencies between Service class and Model class in PHP


If I have a site design which follows a dependency flow of:

Handler Class <-> Service Class <-> Mapper Class <-> Model Class

Handler receives the request, Service contains the logic, Mapper performs the DB queries and uses the Model to work on the data, while the Model represents a single record in the DB.

Ideally, you would have explicitly defined dependencies on/from the neighboring layers. So if I change a particular layer, only the two immediately neighboring layers might be affected.

However, I realized I may be using some bad practices. In particular, I have the Service class call a method on the Mapper class. The Mapper class would return an instance of the Model class. But if I then call a method of the Model class, from within the Service class, there is now a direct and implicit dependency between the Service class and the Model class.

I have a few thoughts on how to address this, which I'll outline below. But I feel there has got to be a better way. So I'm curious what the general consensus is on avoid these implicit dependency situations while avoiding unnecessary processing.

  1. Inject an empty instance of the Model into the Service class to make the dependency explicit. But this seems clunky and doesn't account for multi-record result sets.
  2. Specify the Model interface as the return type in the Mapper Interface for that method. To me, this seems like the preferred method. However, it gets complicated when you have to return multi-record result sets. I would rather not unnecessarily iterate through the entire result set within the Mapper class, before I even begin to use the results for the actual application logic. At a minimum, that would be O(2n) time complexity for nearly every query. So I've just been returning an object which is an \Iterable and iterating over that within the Service class (so O(n) is the floor instead of O(2n)). But this is the same situation where there is an implicit dependency on the Model class.

I've been trying to find out if it's possible to specify the type of object the \Iterable will iterate over, like you can with arrays (IE: int[] is an array of integers). The best I could come up with was to create a custom Iterator class specific to the Model class. Is this the preferred method of eliminating these implicit dependencies, while avoiding unnecessary loops?

Much appreciation!

Edit based on feedback:

I don't have any code which fails, and this question isn't meant to be specific to any real code. This question is solely about an abstract problem. That is why no code was originally included. But based on feedback I've drafted up the following to help make the purposed situation more clear.

Just ignore the Handler class, it's not needed to illustrate the concept, I was just trying to describe the overall structure with it to make it easier to identify the design pattern.

The Service class:

namespace App\Service;

class Service implements ServiceInterface 
{
    protected mapperInterface $mapper;

    public function __construct(mapperInterface $mapper){
        $this->mapper = $mapper;
    }

    public getAllRows() {
       $iterator = $this->mapper->getAllRows();
       while($iterator->valid()){
           $current = $iterator->current();
           // This line creates an implicit dependency on the Model class
           $name = $current->getName();
           $iterator->next();
       }
    }
}

The Mapper Class: namespace App\Mapper;

class Mapper implements MapperInterface 
{
    protected modelInterface $modelPrototype;

    public function __construct(modelInterface $model){
        $this->modelPrototype = $model;
    }

    public getAllRows() : Iterator {
       // Execute SQL then hydrate into an iterator over multiple "Model" objects into a variable ($resultSet in this case)

       return $resultSet; // This is an Iterator instance of which each iteration contains a "Model" object.
    }
}

The Model class:

class Model implements ModelInterface 
{
    protected string $name;

    public function getName() : string {
        return $this->name;
    }

    public function setName(string $name) : void {
        $this->name = $name;
    }
}

It is specifically this line in the Service class which creates an implicit dependency between the Service class and the Model class (bypassing the transitive dependency through the Mapper class):

$name = $current->getName();

There are two ways to correct this, either make the dependency explicit, or eliminate the dependency in the first place. I've listed my ideas on how to achieve those two options. But I am curious what the most common solution to this situation is. Is there some other option or method of addressing this which I have not thought of? Or is one of my proposed ideas the generally preferred method.


Solution

  • Technically i also don't see a dependency. The Service ask it's neighbour Mapper for a collection of Model and it provides one. The Mapper uses the Model and hydrates the data from whereever. So communication-wise everything is fine.

    If you would access data in your Service with

    $this->mapper->model->someModelMethod();
    

    instead of

    $this->mapper->getAllRows();
    

    only then you would violate something at least in the sense of the law of demeter.

    The dependency on the level you might think having an issue is probably some kind of tight coupling inside your layers, which is generally okay for this use case.

    If you wanna decouple it between layers/modules/units consider introducing another kind of data container to pass around.

    For example using some kind of Transfer Object and map/hydrate the data from your model to it. This transfer object can be used to pass around e.g. on the communication layer like Controllers, Services etc. You only rely on the transfer objects properties but not on the model itself. Only your mapper has to be adapted if something changed on both the Model and the Transfer Object