PHP中正确的存储库模式设计?

PHP中正确的存储库模式设计?,php,database,laravel,repository,repository-pattern,Php,Database,Laravel,Repository,Repository Pattern,前言:我试图在MVC体系结构中使用关系数据库的存储库模式 我最近开始学习PHP中的TDD,我意识到我的数据库与应用程序的其余部分耦合得太紧密了。我读过关于存储库和使用“注入”到控制器的文章。很酷的东西。但是现在有一些关于存储库设计的实际问题。考虑下面的例子。 根据我的经验,以下是对您的问题的一些回答: Q:我们如何处理带回不需要的字段 A:根据我的经验,这可以归结为处理完整实体而不是临时查询 完整的实体类似于用户对象。它具有属性和方法等。它是代码库中的一级公民 一个特殊的查询返回一些数据,但我们

前言:我试图在MVC体系结构中使用关系数据库的存储库模式

我最近开始学习PHP中的TDD,我意识到我的数据库与应用程序的其余部分耦合得太紧密了。我读过关于存储库和使用“注入”到控制器的文章。很酷的东西。但是现在有一些关于存储库设计的实际问题。考虑下面的例子。


根据我的经验,以下是对您的问题的一些回答:

Q:我们如何处理带回不需要的字段

A:根据我的经验,这可以归结为处理完整实体而不是临时查询

完整的实体类似于
用户
对象。它具有属性和方法等。它是代码库中的一级公民

一个特殊的查询返回一些数据,但我们不知道除此之外的任何事情。当数据在应用程序中传递时,它是在没有上下文的情况下完成的。它是
用户
?附加了一些
订单
信息的
用户
?我们真的不知道

我更喜欢使用完整的实体

您是对的,您经常会带回不使用的数据,但您可以通过多种方式解决这一问题:

  • 主动缓存实体,这样您只需支付一次从数据库读取的费用
  • 花更多的时间建模实体,使它们之间有很好的区别。(考虑将一个大型实体拆分为两个较小的实体等)
  • 考虑拥有实体的多个版本。后端可以有一个
    User
    ,AJAX调用可以有一个
    UserSmall
    。一个可能有10个属性,一个可能有3个属性
  • 使用临时查询的缺点是:

  • 在许多查询中,最终得到的数据基本相同。例如,对于一个
    用户
    ,您将为许多调用编写基本相同的
    select*
    。一个呼叫将得到10个字段中的8个,一个将得到10个字段中的5个,一个将得到10个字段中的7个。为什么不把所有的电话都换成10个电话中的10个呢?这不好的原因是,重新考虑/测试/模拟是谋杀
  • 随着时间的推移,很难对代码进行高层次的推理。而不是像“为什么<代码>用户这么慢?”这样的陈述,你最终会跟踪一次性的查询,因此错误修复往往是小的和本地化的
  • 很难替换底层技术。如果您现在将所有内容都存储在MySQL中,并希望迁移到MongoDB,那么替换100个临时调用要比替换少数实体困难得多
  • Q:我的存储库中将有太多方法

    A:除了整合通话之外,我还没有找到其他解决方法。存储库中的方法调用实际上映射到应用程序中的功能。功能越多,特定于数据的调用就越多。您可以推回功能并尝试将类似的调用合并为一个

    归根结底,复杂性肯定存在于某个地方。使用存储库模式,我们将其推入存储库界面,而不是制作一堆存储过程


    有时我不得不告诉自己,“好吧,它必须有所回报!没有银弹。”

    我只能评论我们(在我的公司)处理这件事的方式。首先,性能对我们来说不是太大的问题,但是拥有干净/正确的代码是非常重要的

    首先,我们定义了一些模型,比如使用ORM创建
    UserEntity
    对象的
    UserModel
    。从模型加载
    UserEntity
    时,将加载所有字段。对于引用外部实体的字段,我们使用适当的外部模型来创建相应的实体。对于这些实体,数据将按需加载。现在你的最初反应可能是。。。???。。。!!!让我给你举个例子,举个例子:

    class UserEntity extends PersistentEntity
    {
        public function getOrders()
        {
            $this->getField('orders'); //OrderModel creates OrderEntities with only the ID's set
        }
    }
    
    class UserModel {
        protected $orm;
    
        public function findUsers(IGetOptions $options = null)
        {
            return $orm->getAllEntities(/*...*/); // Orm creates a list of UserEntities
        }
    }
    
    class OrderEntity extends PersistentEntity {} // user your imagination
    class OrderModel
    {
        public function findOrdersById(array $ids, IGetOptions $options = null)
        {
            //...
        }
    }
    
    在我们的例子中,
    $db
    是一个能够加载实体的ORM。该模型指示ORM加载一组特定类型的实体。ORM包含一个映射,并使用该映射将该实体的所有字段注入到该实体中。但是,对于外部字段,仅加载这些对象的id。在这种情况下,
    OrderModel
    仅使用引用订单的id创建
    OrderEntity
    s。当
    OrderEntity
    调用
    PersistentEntity::getField
    时,该实体指示其模型将所有字段延迟加载到
    OrderEntity
    中。与一个UserEntity关联的所有
    OrderEntity
    s将被视为一个结果集,并将立即加载

    这里的神奇之处在于,我们的模型和ORM将所有数据注入到实体中,实体只为
    persistenentity
    提供的通用
    getField
    方法提供包装函数。总之,我们总是加载所有字段,但在必要时加载引用外部实体的字段。仅仅加载一堆字段并不是真正的性能问题。然而,加载所有可能的外部实体将导致性能大幅下降

    现在,根据where子句加载一组特定的用户。我们提供了一个面向对象的类包,允许您指定可以粘合在一起的简单表达式。在示例代码中,我将其命名为
    GetOptions
    。它是select查询所有可能选项的包装器。它包含where子句、group by子句和其他所有内容的集合。我们的where子句相当复杂,但您显然可以轻松地制作一个更简单的版本

    $objOptions->getConditionHolder()->addConditionBind(
        new ConditionBind(
            new Condition('orderProduct.product', ICondition::OPERATOR_IS, $argObjProduct)
        )
    );
    
    该系统的最简单版本是将查询的WHERE部分作为字符串直接传递给模型

    我很抱歉这个相当复杂的回答。我试图尽可能快而清晰地总结我们的框架。如果你
    public function findColumnsById($id, array $columns = array()){
        if (empty($columns)) {
            // use *
        }
    }
    
    public function findById($id) {
        $data = $this->findColumnsById($id);
    }
    
    public function __call($function, $arguments) {
        if (strpos($function, 'findBy') === 0) {
            $parameter = substr($function, 6, strlen($function));
            // SELECT * FROM $this->table_name WHERE $parameter = $arguments[0]
        }
    }
    
    class DbUserRepository implements UserRepositoryInterface
    {
        public function findAll()
        {
            return User::all();
        }
    
        public function get(Array $columns)
        {
           return User::select($columns);
        }
    
    class User
    {
        public $id;
        public $first_name;
        public $last_name;
        public $gender;
        public $email;
        public $password;
    }
    
    interface UserRepositoryInterface
    {
        public function find($id);
        public function save(User $user);
        public function remove(User $user);
    }
    
    class SQLUserRepository implements UserRepositoryInterface
    {
        protected $db;
    
        public function __construct(Database $db)
        {
            $this->db = $db;
        }
    
        public function find($id)
        {
            // Find a record with the id = $id
            // from the 'users' table
            // and return it as a User object
            return $this->db->find($id, 'users', 'User');
        }
    
        public function save(User $user)
        {
            // Insert or update the $user
            // in the 'users' table
            $this->db->save($user, 'users');
        }
    
        public function remove(User $user)
        {
            // Remove the $user
            // from the 'users' table
            $this->db->remove($user, 'users');
        }
    }
    
    interface AllUsersQueryInterface
    {
        public function fetch($fields);
    }
    
    class AllUsersQuery implements AllUsersQueryInterface
    {
        protected $db;
    
        public function __construct(Database $db)
        {
            $this->db = $db;
        }
    
        public function fetch($fields)
        {
            return $this->db->select($fields)->from('users')->orderBy('last_name, first_name')->rows();
        }
    }
    
    class AllOverdueAccountsQuery implements AllOverdueAccountsQueryInterface
    {
        protected $db;
    
        public function __construct(Database $db)
        {
            $this->db = $db;
        }
    
        public function fetch()
        {
            return $this->db->query($this->sql())->rows();
        }
    
        public function sql()
        {
            return "SELECT...";
        }
    }
    
    class UsersController
    {
        public function index(AllUsersQueryInterface $query)
        {
            // Fetch user data
            $users = $query->fetch(['first_name', 'last_name', 'email']);
    
            // Return view
            return Response::view('all_users.php', ['users' => $users]);
        }
    
        public function add()
        {
            return Response::view('add_user.php');
        }
    
        public function insert(UserRepositoryInterface $repository)
        {
            // Create new user model
            $user = new User;
            $user->first_name = $_POST['first_name'];
            $user->last_name = $_POST['last_name'];
            $user->gender = $_POST['gender'];
            $user->email = $_POST['email'];
    
            // Save the new user
            $repository->save($user);
    
            // Return the id
            return Response::json(['id' => $user->id]);
        }
    
        public function view(SpecificUserQueryInterface $query, $id)
        {
            // Load user data
            if (!$user = $query->fetch($id, ['first_name', 'last_name', 'gender', 'email'])) {
                return Response::notFound();
            }
    
            // Return view
            return Response::view('view_user.php', ['user' => $user]);
        }
    
        public function edit(SpecificUserQueryInterface $query, $id)
        {
            // Load user data
            if (!$user = $query->fetch($id, ['first_name', 'last_name', 'gender', 'email'])) {
                return Response::notFound();
            }
    
            // Return view
            return Response::view('edit_user.php', ['user' => $user]);
        }
    
        public function update(UserRepositoryInterface $repository)
        {
            // Load user model
            if (!$user = $repository->find($id)) {
                return Response::notFound();
            }
    
            // Update the user
            $user->first_name = $_POST['first_name'];
            $user->last_name = $_POST['last_name'];
            $user->gender = $_POST['gender'];
            $user->email = $_POST['email'];
    
            // Save the user
            $repository->save($user);
    
            // Return success
            return true;
        }
    
        public function delete(UserRepositoryInterface $repository)
        {
            // Load user model
            if (!$user = $repository->find($id)) {
                return Response::notFound();
            }
    
            // Delete the user
            $repository->delete($user);
    
            // Return success
            return true;
        }
    }
    
    class SqlEntityRepository
    {
        ...
        public function factoryEntitySelector()
        {
            return new SqlSelector($this);
        }
        ...
    }
    
    class SqlSelector implements Selector
    {
        ...
        private function adaptFilter(Filter $filter):SqlQueryFilter
        {
             return (new SqlSelectorFilterAdapter())->adaptFilter($filter);
        }
        ...
    }
    class SqlSelectorFilterAdapter
    {
        public function adaptFilter(Filter $filter):SqlQueryFilter
        {
            $concreteClass = (new StringRebaser(
                'Filter\\', 'SqlQueryFilter\\'))
                ->rebase(get_class($filter));
    
            return new $concreteClass($filter);
        }
    }
    
    /** @var Repository $repository*/
    $selector = $repository->factoryEntitySelector();
    $selector->filter(new AttributeEquals('activated', 1))->limit(2)->orderBy('username');
    $activatedUserCount = $selector->count(); // evaluates to 100, ignores the limit()
    $activatedUsers = $selector->fetchEntities();
    
    Model::where(['attr1' => 'val1'])->get();
    
    POST /api/graphql
    {
        query: {
            Model(attr1: 'val1') {
                attr2
                attr3
            }
        }
    }
    
    <?php
    
    interface SomeRepositoryInterface
    {
        public function create(SomeEnitityInterface $entityData): SomeEnitityInterface;
        public function update(SomeEnitityInterface $entityData): SomeEnitityInterface;
        public function delete(int $id): void;
    
        public function find(SomeEnitityQueryInterface $query): array;
    }
    
    class SomeRepository implements SomeRepositoryInterface
    {
        public function find(SomeQueryDto $query): array
        {
            $qb = $this->getQueryBuilder();
    
            foreach ($query->getSearchParameters() as $attribute) {
                $qb->where($attribute['field'], $attribute['operator'], $attribute['value']);
            }
    
            return $qb->get();
        }
    }
    
    /**
     * Provide query data to search for tickets.
     *
     * @method SomeQueryDto userId(int $id, string $operator = null)
     * @method SomeQueryDto categoryId(int $id, string $operator = null)
     * @method SomeQueryDto completedAt(string $date, string $operator = null)
     */
    class SomeQueryDto
    {
        /** @var array  */
        const QUERYABLE_FIELDS = [
            'id',
            'subject',
            'user_id',
            'category_id',
            'created_at',
        ];
    
        /** @var array  */
        const STRING_DB_OPERATORS = [
            'eq' => '=', // Equal to
            'gt' => '>', // Greater than
            'lt' => '<', // Less than
            'gte' => '>=', // Greater than or equal to
            'lte' => '<=', // Less than or equal to
            'ne' => '<>', // Not equal to
            'like' => 'like', // Search similar text
            'in' => 'in', // one of range of values
        ];
    
        /**
         * @var array
         */
        private $searchParameters = [];
    
        const DEFAULT_OPERATOR = 'eq';
    
        /**
         * Build this query object out of query string.
         * ex: id=gt:10&id=lte:20&category_id=in:1,2,3
         */
        public static function buildFromString(string $queryString): SomeQueryDto
        {
            $query = new self();
            parse_str($queryString, $queryFields);
    
            foreach ($queryFields as $field => $operatorAndValue) {
                [$operator, $value] = explode(':', $operatorAndValue);
                $query->addParameter($field, $operator, $value);
            }
    
            return $query;
        }
    
        public function addParameter(string $field, string $operator, $value): SomeQueryDto
        {
            if (!in_array($field, self::QUERYABLE_FIELDS)) {
                throw new \Exception("$field is invalid query field.");
            }
            if (!array_key_exists($operator, self::STRING_DB_OPERATORS)) {
                throw new \Exception("$operator is invalid query operator.");
            }
            if (!is_scalar($value)) {
                throw new \Exception("$value is invalid query value.");
            }
    
            array_push(
                $this->searchParameters,
                [
                    'field' => $field,
                    'operator' => self::STRING_DB_OPERATORS[$operator],
                    'value' => $value
                ]
            );
    
            return $this;
        }
    
        public function __call($name, $arguments)
        {
            // camelCase to snake_case
            $field = strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $name));
    
            if (in_array($field, self::QUERYABLE_FIELDS)) {
                return $this->addParameter($field, $arguments[1] ?? self::DEFAULT_OPERATOR, $arguments[0]);
            }
        }
    
        public function getSearchParameters()
        {
            return $this->searchParameters;
        }
    }
    
    $query = new SomeEnitityQuery();
    $query->userId(1)->categoryId(2, 'ne')->createdAt('2020-03-03', 'lte');
    $entities = $someRepository->find($query);
    
    // Or by passing the HTTP query string
    $query = SomeEnitityQuery::buildFromString('created_at=gte:2020-01-01&category_id=in:1,2,3');
    $entities = $someRepository->find($query);
    
       class Criteria {}
       class Select {}
       class Count {}
       class Delete {}
       class Update {}
       class FieldFilter {}
       class InArrayFilter {}
       // ...
    
       $crit = new Criteria();  
       $filter = new FieldFilter();
       $filter->set($criteria, $entity, $property, $value);
       $select = new Select($criteria);
       $count = new Count($criteria);
       $count->getRowCount();
       $select->fetchOne(); // fetchAll();