1

I have a controller that acquires data to pass to a view. Into this is injected (via a pimple container) a service which uses a number of domain models + business logic to create the data.

The service itself has a 'repository' class injected into it which has methods for creating data mappers and returning a domain model instance.

I'm aware that I might not have got my head around the repository concept as Martin Fowler puts it to "build another layer of abstraction over the mapping layer" & "A Repository mediates between the domain and data mapping layers, acting like an in-memory domain object collection." So I may be using this term erroneously.

service:

class InputService
    {
        private $repos;

        public function __construct($repo) {
            $this->repos = $repo;
        }

        public function getInitialData()
        {
            $product = $this->repo->getProduct();
            $country = $this->repo->getCountry();
            $spinalPoint = $this->repo->getPoint();

            /*business logic with model instances to produce data array*/

            return //array of data
        }
    }

repository:

class InputRepository
    {
        private $db;

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

        public function getCountry()
        {
            $mapper = new CountryMapper($this->db);
            $country = $mapper->fetch();
            return $country; //returns country object
        }
        // lots of other methods for returning different model objects
    }

mapper:

class CountryMapper
    {
        private $db;

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

        public function fetch()
        {
            $data = //code to grab data from db;

            $obj = new Country($data);
            return $obj;
        }
    }

As you can see, the mappers are tightly coupled to the repository class, however I can't see a way around it.

I was wondering if there is a way to implement this repository that provides looser coupling to the data mapper classes?

In the grand scheme of things this application is fairly small and so having to update code across both wouldn't be disastrous, but you never now when thing will grow!

GothicAnatomist
  • 146
  • 4
  • 15

2 Answers2

16
  • The db operations should be performed through adapters (MySqliAdapter, PdoAdapter, etc). So, the db connections are injected into adapters, not into the mappers. And certainly not in the repositories, because then the abstraction purpose of the repositories would be pointless.
  • A mapper receives adapter(s) as dependencies and can receive other mappers too.
  • The mappers are passed as dependencies to the repositories.
  • A repository name is semantically related to the domain layer names, not really to the ones of the service layer. E.g: "InputService": ok. "InputRepository": wrong. "CountryRepository": correct.
  • A service can receive more repositories. Or mappers, if you don't want to apply the extra layer of repositories.
  • In the code, the only tightly coupled structure is the Country object (entity or domain object) - dynamically created for each fetched table row. Even this could be avoided through the use of a domain objects factory, but I, personally, don't see it really necessary.

P.S: Sorry for not providing a more documented code.

Service

class InputService {

    private $countryRepository;
    private $productRepository;

    public function __construct(CountryRepositoryInterface $countryRepository, ProductRepositoryInterface $productRepository) {
        $this->countryRepository = $countryRepository;
        $this->productRepository = $productRepository;
    }

    public function getInitialData() {
        $products = $this->productRepository->findAll();
        $country = $this->countryRepository->findByName('England');

        //...

        return // resulted data
    }

}

Repository

class CountryRepository implements CountryRepositoryInterface {

    private $countryMapper;

    public function __construct(CountryMapperInterface $countryMapper) {
        $this->countryMapper = $countryMapper;
    }

    public function findByPrefix($prefix) {
        return $this->countryMapper->find(['prefix' => $prefix]);
    }

    public function findByName($name) {
        return $this->countryMapper->find(['name' => $name]);
    }

    public function findAll() {
        return $this->countryMapper->find();
    }

    public function store(CountryInterface $country) {
        return $this->countryMapper->save($country);
    }

    public function remove(CountryInterface $country) {
        return $this->countryMapper->delete($country);
    }

}

Data mapper

class CountryMapper implements CountryMapperInterface {

    private $adapter;
    private $countryCollection;

    public function __construct(AdapterInterface $adapter, CountryCollectionInterface $countryCollection) {
        $this->adapter = $adapter;
        $this->countryCollection = $countryCollection;
    }

    public function find(array $filter = [], $one = FALSE) {
        // If $one is TRUE then add limit to sql statement, or so...
        $rows = $this->adapter->find($sql, $bindings);

        // If $one is TRUE return a domain object, else a domain objects list.
        if ($one) {
            return $this->createCountry($row[0]);
        }

        return $this->createCountryCollection($rows);
    }

    public function save(CountryInterface $country) {
        if (NULL === $country->id) {
            // Build the INSERT statement and the bindings array...
            $this->adapter->insert($sql, $bindings);

            $lastInsertId = $this->adapter->getLastInsertId();

            return $this->find(['id' => $lastInsertId], true);
        }

        // Build the UPDATE statement and the bindings array...
        $this->adapter->update($sql, $bindings);

        return $this->find(['id' => $country->id], true);
    }

    public function delete(CountryInterface $country) {
        $sql = 'DELETE FROM countries WHERE id=:id';
        $bindings = [':id' => $country->id];

        $rowCount = $this->adapter->delete($sql, $bindings);

        return $rowCount > 0;
    }

    // Create a Country (domain object) from row.
    public function createCountry(array $row = []) {
        $country = new Country();

        /*
         * Iterate through the row items.
         * Assign a property to Country object for each item's name/value.
         */

        return $country;
    }

    // Create a Country[] list from rows list.
    public function createCountryCollection(array $rows) {
        /*
         * Iterate through rows.
         * Create a Country object for each row, with column names/values as properties.
         * Push Country object object to collection.
         * Return collection's content.
         */

        return $this->countryCollection->all();
    }

}

Db adapter

class PdoAdapter implements AdapterInterface {

    private $connection;

    public function __construct(PDO $connection) {
        $this->connection = $connection;
    }

    public function find(string $sql, array $bindings = [], int $fetchMode = PDO::FETCH_ASSOC, $fetchArgument = NULL, array $fetchConstructorArguments = []) {
        $statement = $this->connection->prepare($sql);
        $statement->execute($bindings);
        return $statement->fetchAll($fetchMode, $fetchArgument, $fetchConstructorArguments);
    }

    //...
}

Domain objects collection

class CountryCollection implements CountryCollectionInterface {

    private $countries = [];

    public function push(CountryInterface $country) {
        $this->countries[] = $country;
        return $this;
    }

    public function all() {
        return $this->countries;
    }

    public function getIterator() {
        return new ArrayIterator($this->countries);
    }

    //...
}

Domain object

class Country implements CountryInterface {
    // Business logic: properties and methods...
}
  • 1
    Thank you so much for your detailed response! I understand what you mean about the repository being linked to a single domain layer name - the reason I created the repo at all was to dynamically instantiate the requested object as otherwise it would involve injecting more than 10 mappers into the service where only 1 or 3 would be used before the next ajax request. So if I drop the repository and have the service deal solely with the mappers, should I just inject all 10 each time which themselves are each injected with the db adapter? – GothicAnatomist Feb 16 '18 at 09:40
  • 1
    @Gothic_Anatomist You are welcome. By naming the repos I wanted to point out the "direction" in which you should look for naming them. E.g., since they abstract the data mapper operations, they should be named by the mappers - so to say, not by the services which uses te repos. Because a repo can be used in multiple services... On the other hand, you could have a service dealing with a _SearchRepository_, respectively a _SearchMapper_ which could fetch a collection of _SearchModel_ entities. –  Feb 16 '18 at 13:10
  • 1
    @Gothic_Anatomist Each entity would hold the properties (product, country, etc.) with values from multiple tables, based on the selections, criteria you make on the client side. The SearchMapper would have the _find_ method receiving an array of criterias and returning the results in form of an array of SearchModel entities, each holding the values of a fetched record from db. –  Feb 16 '18 at 13:11
  • 1
    @Gothic_Anatomist So, in principle, when you populate the comboboxes, you use the InputService with the specific repos/mappers/entities. But when you post the criteria choosed by the user, then you are using another service, the SearchService with one repo/mapper/entity. –  Feb 16 '18 at 13:16
  • 1
    @Gothic_Anatomist [Here](https://stackoverflow.com/questions/5863870/how-should-a-model-be-structured-in-mvc/5864000#5864000) is the detailed perspective on the components. And read [the 4 or 5 articles](https://www.sitepoint.com/author/agervasio/page/2/) regarding domain objects, data mappers, repositories and services. Bye. –  Feb 16 '18 at 13:28
1

You could inject the class names OR instances in the constructor:

class InputRepository
{
    private $db;
    protected $mappers = array();

    public function __construct($db, array $mappers) {
        $this->db = $db;
        $this->mappers = $mappers;
    }

    public function getMapper($key) {
        if (!isset($this->mappers[$key]) {
           throw new Exception('Invalid mapper "'. $key .'"');
        }

        if (!$this->mappers[$key] instanceof MapperInterface) {
           $this->mappers[$key] = new $this->mappers[$key]($this->db);
        }

        return $this->mappers[$key];
    }

    public function getCountry()
    {
        $mapper = $this->getMapper('country');
        $country = $mapper->fetch();
        return $country; //returns country object
    }
    // lots of other methods for returning different model objects
}

You would probably want to make the interface checking a bit more robust, obviously.

prodigitalson
  • 60,050
  • 10
  • 100
  • 114