Search code examples
phpdatabaselaravelmodel-view-controllerrepository

Using models in a controller alongside with repository


As I understand using repositories restricts controller from accessing database layer, and all queries goes through repository. But can controller use model (laravel can inject model instead of ID in a controller) to pass it to repository or service - for example to make a transaction between users? Or better to send IDs to repository, to find users and apply business logic (do user have money, or is he banned).

And more generic question, can you use models outside of the repository, because if you change some tables from postgres or mysql to something else your models will change also. And this means your repository should have get method to send back some DTO object?


Solution

  • Note: This is a general perspective on the matter, appliable to any application based on MVC, not only to Laravel.

    An application based on the MVC pattern should be composed of three parts:

    • delivery mechanism: UI logic (user request handling and server response creation),
    • service layer: application logic,
    • domain model: business logic.

    Here are some graphical representations (of my own making):

    MVC (general)

    MVC (detail 1)

    MVC (detail 2)

    As shown above (and described in detail in the resources below), the controllers and the views are part of the delivery mechanism. They should interact with the domain model only through the service layer objects (services). Consequently, they should have no knowledge of the domain model components (entities - also known as domain objects, data mappers, repositories, etc). More of it, the controllers should have only one responsibility: to pass the values of the user request to the service layer, in order for it to update the model.

    So, to answer your first question: No, controllers should not be able to create any instances of elements of the domain model (so instances of what you're calling "models" - in respect of Laravel's Active Record), or even to pass such objects to other components (like repositories, services, etc). Instead, the controllers should just pass the values of the request (the user id, for example) to the corresponding services. These services will then create the proper domain model objects and use the proper repositories, data mappers, etc, in order to save/fetch to/from database.

    As for the second question (if I understood it correctly): The repositories are to be seen as collections of entities - which are domain model components. As such, elements (e.g. entity instances) can be fetched, stored, altered, or removed to/from them. So, by definition, the entities must be defined/used separately from the repositories. In regard of Laravel, the same should apply: The "models" should be defined/used separately from the repositories.

    A "general" MVC implementation (for more clarity):

    Controller:

    <?php
    
    namespace MyApp\UI\Web\Controller\Users;
    
    use MyApp\Domain\Service\Users;
    use Psr\Http\Message\ServerRequestInterface;
    
    /**
     * Add a user.
     */
    class AddUser {
    
        /**
         * User service.
         * 
         * @var Users
         */
        private $userService;
    
        /**
         * 
         * @param Users $userService User service.
         */
        public function __construct(Users $userService) {
            $this->userService = $userService;
        }
    
        /**
         * Invoke.
         * 
         * @param ServerRequestInterface $request Request.
         * @return void
         */
        public function __invoke(ServerRequestInterface $request) {
            // Read request values.
            $username = $request->getParsedBody()['username'];
    
            // Call the corresponding service.
            $this->userService->addUser($username);
        }
    
    }
    

    Service:

    <?php
    
    namespace MyApp\Domain\Service;
    
    use MyApp\Domain\Model\User\User;
    use MyApp\Domain\Model\User\UserCollection;
    use MyApp\Domain\Service\Exception\UserExists;
    
    /**
     * Service for handling the users.
     */
    class Users {
    
        /**
         * User collection (a repository).
         * 
         * @var UserCollection
         */
        private $userCollection;
    
        /**
         * 
         * @param UserCollection $userCollection User collection.
         */
        public function __construct(UserCollection $userCollection) {
            $this->userCollection = $userCollection;
        }
    
        /**
         * Find a user by id.
         * 
         * @param int $id User id.
         * @return User|null User.
         */
        public function findUserById(int $id) {
            return $this->userCollection->findUserById($id);
        }
    
        /**
         * Find all users.
         * 
         * @return User[] User list.
         */
        public function findAllUsers() {
            return $this->userCollection->findAllUsers();
        }
    
        /**
         * Add a user.
         * 
         * @param string $username Username.
         * @return User User.
         */
        public function addUser(string $username) {
            $user = $this->createUser($username);
    
            return $this->storeUser($user);
        }
    
        /**
         * Create a user.
         * 
         * @param string $username Username.
         * @return User User.
         */
        private function createUser(string $username) {
            $user = new User();
    
            $user->setUsername($username);
    
            return $user;
        }
    
        /**
         * Store a user.
         * 
         * @param User $user User.
         * @return User User.
         */
        private function storeUser(User $user) {
            if ($this->userCollection->userExists($user)) {
                throw new UserExists('Username "' . $user->getUsername() . '" already used');
            }
    
            return $this->userCollection->storeUser($user);
        }
    
    }
    

    Repository:

    <?php
    
    namespace MyApp\Domain\Infrastructure\Repository\User;
    
    use MyApp\Domain\Model\User\User;
    use MyApp\Domain\Infrastructure\Mapper\User\UserMapper;
    use MyApp\Domain\Model\User\UserCollection as UserCollectionInterface;
    
    /**
     * User collection.
     */
    class UserCollection implements UserCollectionInterface {
    
        /**
         * User mapper (a data mapper).
         * 
         * @var UserMapper
         */
        private $userMapper;
    
        /**
         * 
         * @param UserMapper $userMapper User mapper.
         */
        public function __construct(UserMapper $userMapper) {
            $this->userMapper = $userMapper;
        }
    
        /**
         * @inheritDoc
         */
        public function findUserById(int $id) {
            return $this->userMapper->fetchUserById($id);
        }
    
        /**
         * @inheritDoc
         */
        public function findAllUsers() {
            return $this->userMapper->fetchAllUsers();
        }
    
        /**
         * @inheritDoc
         */
        public function storeUser(User $user) {
            return $this->userMapper->saveUser($user);
        }
        
        /**
         * @inheritDoc
         */
        public function userExists(User $user) {
            return $this->userMapper->userExists($user);
        }
    
    }
    

    Repository interface:

    <?php
    
    namespace MyApp\Domain\Model\User;
    
    use MyApp\Domain\Model\User\User;
    
    /**
     * An interface to a collection of users.
     */
    interface UserCollection {
    
        /**
         * Find a user by id.
         * 
         * @param int $id User id.
         * @return User|null User.
         */
        public function findUserById(int $id);
    
        /**
         * Find all users.
         * 
         * @return User[] User list.
         */
        public function findAllUsers();
    
        /**
         * Store a user.
         * 
         * @param User $user User.
         * @return User User.
         */
        public function storeUser(User $user);
    
        /**
         * Check if the given user exists.
         * 
         * @param User $user User.
         * @return bool True if user exists, false otherwise.
         */
        public function userExists(User $user);
        
    }
    

    Entity:

    <?php
    
    namespace MyApp\Domain\Model\User;
    
    /**
     * User.
     */
    class User {
    
        /**
         * Id.
         * 
         * @var int
         */
        private $id;
    
        /**
         * Username.
         * 
         * @var string
         */
        private $username;
    
        /**
         * Get id.
         * 
         * @return int
         */
        public function getId() {
            return $this->id;
        }
    
        /**
         * Set id.
         * 
         * @param int $id Id.
         * @return $this
         */
        public function setId(int $id) {
            $this->id = $id;
            return $this;
        }
    
        /**
         * Get username.
         * 
         * @return string
         */
        public function getUsername() {
            return $this->username;
        }
    
        /**
         * Set username.
         * 
         * @param string $username Username.
         * @return $this
         */
        public function setUsername(string $username) {
            $this->username = $username;
            return $this;
        }
    
    }
    

    Data mapper:

    <?php
    
    namespace MyApp\Domain\Infrastructure\Mapper\User;
    
    use PDO;
    use MyApp\Domain\Model\User\User;
    use MyApp\Domain\Infrastructure\Mapper\User\UserMapper;
    
    /**
     * PDO user mapper.
     */
    class PdoUserMapper implements UserMapper {
    
        /**
         * Database connection.
         * 
         * @var PDO
         */
        private $connection;
    
        /**
         * 
         * @param PDO $connection Database connection.
         */
        public function __construct(PDO $connection) {
            $this->connection = $connection;
        }
    
        /**
         * Fetch a user by id.
         * 
         * Note: PDOStatement::fetch returns FALSE if no record is found.
         * 
         * @param int $id User id.
         * @return User|null User.
         */
        public function fetchUserById(int $id) {
            $sql = 'SELECT * FROM users WHERE id = :id LIMIT 1';
    
            $statement = $this->connection->prepare($sql);
            $statement->execute([
                'id' => $id,
            ]);
    
            $data = $statement->fetch(PDO::FETCH_ASSOC);
    
            return ($data === false) ? null : $this->convertDataToUser($data);
        }
    
        /**
         * Fetch all users.
         * 
         * @return User[] User list.
         */
        public function fetchAllUsers() {
            $sql = 'SELECT * FROM users';
    
            $statement = $this->connection->prepare($sql);
            $statement->execute();
    
            $data = $statement->fetchAll(PDO::FETCH_ASSOC);
    
            return $this->convertDataToUserList($data);
        }
    
        /**
         * Check if a user exists.
         * 
         * Note: PDOStatement::fetch returns FALSE if no record is found.
         * 
         * @param User $user User.
         * @return bool True if the user exists, false otherwise.
         */
        public function userExists(User $user) {
            $sql = 'SELECT COUNT(*) as cnt FROM users WHERE username = :username';
    
            $statement = $this->connection->prepare($sql);
            $statement->execute([
                ':username' => $user->getUsername(),
            ]);
    
            $data = $statement->fetch(PDO::FETCH_ASSOC);
    
            return ($data['cnt'] > 0) ? true : false;
        }
        
        /**
         * Save a user.
         * 
         * @param User $user User.
         * @return User User.
         */
        public function saveUser(User $user) {
            return $this->insertUser($user);
        }
        
        /**
         * Insert a user.
         * 
         * @param User $user User.
         * @return User User.
         */
        private function insertUser(User $user) {
            $sql = 'INSERT INTO users (username) VALUES (:username)';
    
            $statement = $this->connection->prepare($sql);
            $statement->execute([
                ':username' => $user->getUsername(),
            ]);
    
            $user->setId($this->connection->lastInsertId());
    
            return $user;
        }
        
        /**
         * Update a user.
         * 
         * @param User $user User.
         * @return User User.
         */
        private function updateUser(User $user) {
            $sql = 'UPDATE users SET username = :username WHERE id = :id';
    
            $statement = $this->connection->prepare($sql);
            $statement->execute([
                ':username' => $user->getUsername(),
                ':id' => $user->getId(),
            ]);
    
            return $user;
        }
    
        /**
         * Convert the given data to a user.
         * 
         * @param array $data Data.
         * @return User User.
         */
        private function convertDataToUser(array $data) {
            $user = new User();
    
            $user
                    ->setId($data['id'])
                    ->setUsername($data['username'])
            ;
    
            return $user;
        }
    
        /**
         * Convert the given data to a list of users.
         * 
         * @param array $data Data.
         * @return User[] User list.
         */
        private function convertDataToUserList(array $data) {
            $userList = [];
    
            foreach ($data as $item) {
                $userList[] = $this->convertDataToUser($item);
            }
    
            return $userList;
        }
    
    }
    

    View:

    <?php
    
    namespace MyApp\UI\Web\View\Users;
    
    use MyApp\UI\Web\View\View;
    use MyApp\Domain\Service\Users;
    use MyLib\Template\TemplateInterface;
    use Psr\Http\Message\ResponseInterface;
    use Psr\Http\Message\ResponseFactoryInterface;
    
    /**
     * Add a user.
     */
    class AddUser extends View {
    
        /**
         * User service.
         * 
         * @var Users
         */
        private $userService;
    
        /**
         * 
         * @param ResponseFactoryInterface $responseFactory Response factory.
         * @param TemplateInterface $template Template.
         * @param Users $userService User service.
         */
        public function __construct(ResponseFactoryInterface $responseFactory, TemplateInterface $template, Users $userService) {
            parent::__construct($responseFactory, $template);
    
            $this->userService = $userService;
        }
        
        /**
         * Display a form for adding a user.
         * 
         * @return ResponseInterface Response.
         */
        public function index() {
            $body = $this->template->render('@Template/Users/add-user.html.twig', [
                'activeMainMenuItem' => 'addUser',
                'action' => '',
            ]);
            
            $response = $this->responseFactory->createResponse();
            $response->getBody()->write($body);
    
            return $response;
        }
    
        /**
         * Add a user.
         * 
         * @return ResponseInterface Response.
         */
        public function addUser() {
            $body = $this->template->render('@Template/Users/add-user.html.twig', [
                'activeMainMenuItem' => 'addUser',
                'message' => 'User successfully added.',
            ]);
    
            $response = $this->responseFactory->createResponse();
            $response->getBody()->write($body);
    
            return $response;
        }
    
    }
    

    Resources: