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


291

前言:我正在尝试在MVC体系结构和关系数据库中使用存储库模式。

我最近开始用PHP学习TDD,并且意识到我的数据库与我的其余应用程序紧密结合。我已经阅读了有关存储库和使用IoC容器的信息将其“注入”到控制器中的信息。很酷的东西。但是现在有一些关于存储库设计的实际问题。考虑以下示例。

<?php

class DbUserRepository implements UserRepositoryInterface
{
    protected $db;

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

    public function findAll()
    {
    }

    public function findById($id)
    {
    }

    public function findByName($name)
    {
    }

    public function create($user)
    {
    }

    public function remove($user)
    {
    }

    public function update($user)
    {
    }
}

问题1:字段过多

所有这些查找方法都使用全选字段(SELECT *)方法。但是,在我的应用程序中,我总是试图限制获得的字段数,因为这通常会增加开销并减慢运行速度。对于那些使用这种模式的人,您如何处理呢?

问题2:方法太多

尽管该类现在看起来不错,但我知道在现实世界中的应用程序中,我需要更多的方法。例如:

  • findAllByNameAndStatus
  • findAllInCountry
  • findAllWithEmailAddressSet
  • findAllByAgeAndGender
  • findAllByAgeAndGenderOrderByAge
  • 等等。

如您所见,可能的方法列表可能非常长。然后,如果您添加上面的字段选择问题,则问题会恶化。过去,我通常只是将所有这些逻辑放在我的控制器中:

<?php

class MyController
{
    public function users()
    {
        $users = User::select('name, email, status')
            ->byCountry('Canada')->orderBy('name')->rows();

        return View::make('users', array('users' => $users));
    }
}

对于我的存储库方法,我不想结束于此:

<?php

class MyController
{
    public function users()
    {
        $users = $this->repo->get_first_name_last_name_email_username_status_by_country_order_by_name('Canada');

        return View::make('users', array('users' => $users))
    }

}

问题3:无法匹配接口

我看到了使用存储库接口的好处,因此我可以换出我的实现(出于测试目的或其他目的)。我对接口的理解是,它们定义了实现必须遵循的契约。直到您开始向存储库中添加其他方法(例如,findAllInCountry()。现在,我需要更新我的界面,使其也具有此方法,否则,其他实现可能没有它,这可能会破坏我的应用程序。这样感觉很疯狂……尾巴摇晃着狗。

规格模式?

这使我相信,库应该只有方法固定数量(如save()remove()find()findAll(),等)。但是,我该如何运行特定的查找?我听说过规范模式,但是在我看来,这只会减少整个记录集(通过IsSatisfiedBy()),如果从数据库中提取数据,显然会遇到主要的性能问题。

帮帮我?

显然,在使用存储库时,我需要重新考虑一下。任何人都可以对如何最好地处理开悟吗?

Answers:


208

我以为我会努力回答自己的问题。接下来的内容只是解决我原始问题1-3中的一种方法。

免责声明:在描述模式或技术时,我可能并不总是使用正确的术语。抱歉

目标:

  • 创建一个用于查看和编辑的基本控制器的完整示例Users
  • 所有代码都必须是完全可测试和可模拟的。
  • 控制器不应该知道数据的存储位置(意味着可以更改)。
  • 显示SQL实现的示例(最常见)。
  • 为了获得最佳性能,控制器应该只接收所需的数据,而不能接收其他字段。
  • 为了简化开发,实现应利用某种类型的数据映射器。
  • 实现应具有执行复杂数据查找的能力。

解决方案

我将持久性存储(数据库)交互分为两类:R(读取)和CUD(创建,更新,删除)。我的经验是,读取确实是导致应用程序运行缓慢的原因。而且,尽管数据处理(CUD)实际上较慢,但它发生的频率却要低得多,因此也就少了很多问题。

CUD(创建,更新,删除)很容易。这将涉及使用实际模型,然后将其传递给我Repositories以保持持久性。请注意,我的存储库仍将提供Read方法,但仅用于创建对象而不显示。以后再说。

R(读取)不是那么容易。这里没有模型,只是重视对象如果愿意,请使用数组。这些对象可能代表单个模型或许多模型的混合,实际上是任何东西。它们本身并不是很有趣,但是它们是如何产生的。我正在使用我所说的话Query Objects

编码:

用户模型

让我们从基本用户模型开始简单。请注意,根本没有ORM扩展或数据库内容。只是纯粹的模特荣耀。添加您的getter,setter,validation等。

class User
{
    public $id;
    public $first_name;
    public $last_name;
    public $gender;
    public $email;
    public $password;
}

储存库介面

在创建用户存储库之前,我想创建我的存储库界面。这将定义存储库必须遵循的“合同”,以供我的控制器使用。记住,我的控制器将不知道数据实际存储在哪里。

请注意,我的存储库将只包含这三种方法。该save()方法仅根据用户对象是否设置了ID来负责创建和更新用户。

interface UserRepositoryInterface
{
    public function find($id);
    public function save(User $user);
    public function remove(User $user);
}

SQL存储库实现

现在创建我的接口实现。如前所述,我的示例将使用SQL数据库。请注意,使用数据映射器可以避免编写重复的SQL查询。

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');
    }
}

查询对象接口

现在,我们的存储库负责处理CUD(创建,更新,删除),我们可以专注于R(读取)。查询对象只是某种类型的数据查找逻辑的封装。他们不是查询生成器。通过像存储库一样抽象它,我们可以更改它的实现并对其进行测试。查询对象的示例可能是AllUsersQueryor AllActiveUsersQuery或什至MostCommonUserFirstNames

您可能会想:“我是否不能仅在存储库中为这些查询创建方法?” 是的,但是这就是为什么我不这样做的原因:

  • 我的存储库用于处理模型对象。在现实世界中的应用中,为什么我需要获得password如果我要列出所有用户字段?
  • 存储库通常是特定于模型的,但是查询通常涉及多个模型。那么,您将方法放入哪个存储库?
  • 这使我的存储库非常简单-而不是庞大的方法类。
  • 现在,所有查询都组织到各自的类中。
  • 确实,在这一点上,存在存储库只是为了抽象我的数据库层。

对于我的示例,我将创建一个查询对象以查找“ AllUsers”。这是界面:

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();
    }
}

在继续介绍控制器之前,我想展示另一个例子来说明它的功能。也许我有一个报告引擎,需要为创建报告AllOverdueAccounts。对于我的数据映射器,这可能很棘手,SQL在这种情况下,我可能想写一些实际的东西。没问题,这是该查询对象的外观:

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;
    }
}

最后的想法:

这里要注意的重要事情是,当我修改(创建,更新或删除)实体时,我正在使用真实的模型对象,并通过我的存储库执行持久性。

但是,当我显示(选择数据并将其发送到视图)时,我不是在使用模型对象,而是使用普通的旧值对象。我只选择我需要的字段,并且它是经过设计的,以便可以最大程度地提高数据查找性能。

我的存储库保持非常干净,而是将此“混乱”组织到我的模型查询中。

我使用数据映射器来帮助开发,因为为常见任务编写重复的SQL太荒谬了。但是,您绝对可以在需要的地方编写SQL(复杂的查询,报告等)。当您这样做时,它会很好地藏在一个适当命名的类中。

我很想听听您对我的做法的看法!


2015年7月更新:

我在评论中被问到我所有这些都以什么结尾。好吧,实际上相差不远。说实话,我还是不太喜欢存储库。我发现它们对于基本的查询(特别是如果您已经在使用ORM)来说是多余的,并且在处理更复杂的查询时会显得凌乱。

我通常使用ActiveRecord风格的ORM,因此大多数情况下,我只会在整个应用程序中直接引用这些模型。但是,在查询比较复杂的情况下,我将使用查询对象使这些查询更可重用。我还应注意,我总是将模型注入方法中,从而使它们在测试中更易于模拟。


4
@PeeHaa同样,这是为了保持示例简单。如果代码段与当前主题不相关,那么通常将它们留在示例中。实际上,我会传递我的依赖关系。
乔纳森

4
有趣的是,您从阅读中拆分了“创建”,“更新”和“删除”。认为值得一提的是“命令查询责任隔离”(CQRS)正式做到这一点。martinfowler.com/bliki/CQRS.html
亚当

2
@Jonathan回答您自己的问题已经一年半了。我想知道您是否对您的答案仍然满意,这是否现在是您大多数项目的主要解决方案?最近几周,我一直在阅读关于存储库的分配,并且我看到人们对分配应该如何实施有自己的解释。您称其为查询对象,但这是现有模式吗?我想我已经看到它被其他语言使用了。
Boedy 2014年

1
@Jonathan:如何处理那些不应以“ ID”而是用户名(例如“用户名”)或者甚至是多个条件组成的更复杂查询的查询?
2014年

1
@Gizzmo使用查询对象,您可以传递其他参数来帮助您进行更复杂的查询。例如,您可以在构造函数中执行此操作:new Query\ComplexUserLookup($username, $anotherCondition)。或者,通过setter方法执行此操作$query->setUsername($username);。您可以真正设计它,但是这对于您的特定应用程序来说很有意义,我认为查询对象在这里留下了很多灵活性。
乔纳森(Jonathan)2015年

48

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

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

答:根据我的经验,这实际上可以归结为处理完整的实体而不是即席查询。

一个完整的实体就像一个 User对象。它具有属性和方法等。它是您代码库中的一等公民。

临时查询返回一些数据,但除此之外我们什么都不知道。当数据在应用程序中传递时,没有上下文。是User吗?一个User附带一些Order信息?我们真的不知道。

我更喜欢与完整实体合作。

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

  1. 积极地缓存实体,因此您只需支付一次从数据库读取的价格。
  2. 花更多的时间为您的实体建模,以便它们之间有很好的区别。(考虑将一个大实体拆分为两个较小的实体,依此类推)
  3. 考虑拥有多个版本的实体。您可以User为后端设置一个,也可以UserSmall为AJAX调用设置一个。一个可能具有10个属性,而一个可能具有3个属性。

使用即席查询的缺点:

  1. 您最终在许多查询中得到的数据基本相同。例如,使用User,您最终select *将为许多呼叫编写相同的内容。一个电话将获得10个字段中的8个,一个将获得10个字段中的5个,一个将获得10个字段中的7个。为什么不将一个呼叫替换为10个字段中的10个呢?不好的原因是重构/测试/模拟是谋杀。
  2. 随着时间的流逝,很难对代码进行高层次的推理。而不是像“为什么User这么慢?”这样的语句。您最终会跟踪一次性查询,因此错误修复往往很小且已本地化。
  3. 替换基础技术真的很难。如果您现在将所有内容存储在MySQL中并想移至MongoDB,则替换100个即席调用要比少数实体难得多。

问:我的存储库中有太多方法。

答:除了合并电话以外,我还没有真正的解决方法。存储库中的方法调用实际上映射到应用程序中的功能。功能越多,特定于数据的调用就越多。您可以推迟使用功能,然后尝试将类似的调用合并为一个。

最终,复杂性必须存在于某个地方。使用存储库模式,我们将其推入存储库接口,而不是制作一堆存储过程。

有时我不得不告诉自己,“好吧,它必须放在某个地方!没有银弹。”


感谢您的彻底答复。你现在让我思考。我在这里最担心的是,我读到的所有内容都不会SELECT *,只选择您需要的字段即可。例如,请参阅此问题。至于您提到的所有这些广告查询,我当然知道您来自哪里。我现在有一个非常大的应用程序,其中有很多应用程序。那就是我的“好吧,它必须给某个地方!” 此刻,我选择了最佳性能。但是,现在我正在处理很多不同查询的商品。
乔纳森

1
一个跟进的想法。我已经看到使用R-CUD方法的建议。由于reads通常会出现性能问题,因此您可以为它们使用更自定义的查询方法,这些方法不会转换为实际的业务对象。然后,对createupdate并且delete,使用ORM,这与整个对象的作品。对这种方法有什么想法吗?
乔纳森

1
作为使用“选择*”的注意事项。我过去做过,而且效果很好-直到我们点击varchar(max)字段为止。那些杀死了我们的疑问。因此,如果您的表带有整数,小的文本字段等,那还不错。感觉不自然,但是软件就是这样。不好的是突然好,反之亦然。
ryan1234

1
R-CUD方法实际上是CQRS
MikeSW

2
@ ryan1234“一天结束时的复杂性必须存在于某个地方。” 这次真是万分感谢。让我感觉好些。
约翰尼

20

我使用以下接口:

  • Repository -加载,插入,更新和删除实体
  • Selector -在存储库中基于过滤器查找实体
  • Filter -封装过滤逻辑

Repository与数据库无关;实际上,它没有指定任何持久性;它可以是任何东西:SQL数据库,xml文件,远程服务,来自外太空的外星人等。对于搜索功能,可以过滤,-ed,排序和计数的Repository构造。最后,选择器从持久性中获取一个或多个。SelectorLIMITEntities

这是一些示例代码:

<?php
interface Repository
{
    public function addEntity(Entity $entity);

    public function updateEntity(Entity $entity);

    public function removeEntity(Entity $entity);

    /**
     * @return Entity
     */
    public function loadEntity($entityId);

    public function factoryEntitySelector():Selector
}


interface Selector extends \Countable
{
    public function count();

    /**
     * @return Entity[]
     */
    public function fetchEntities();

    /**
     * @return Entity
     */
    public function fetchEntity();
    public function limit(...$limit);
    public function filter(Filter $filter);
    public function orderBy($column, $ascending = true);
    public function removeFilter($filterName);
}

interface Filter
{
    public function getFilterName();
}

然后,一个实现:

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);
    }
}

思想是通用Selector使用Filter而实现SqlSelector使用SqlFilter; 使SqlSelectorFilterAdapter泛型适应Filter具体SqlFilter

客户端代码创建Filter对象(它们是通用过滤器),但是在选择器的具体实现中,这些过滤器在SQL过滤器中转换。

其他选择器实现,例如InMemorySelector从转换FilterInMemoryFilter使用其特定的InMemorySelectorFilterAdapter;因此,每个选择器实现都带有自己的过滤器适配器。

使用这种策略,我的客户代码(在bussines层中)并不关心特定的存储库或选择器实现。

/** @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();

PS这是我的真实代码的简化


“存储库-加载,插入,更新和删除实体”这就是“服务层”,“ DAO”,“ BLL”可以做到的
Yousha Aleayoub

5

我将在此方面加一点,因为我目前正试图自己掌握所有这些。

#1和2

这是您的ORM进行繁重工作的理想场所。如果您使用的是实现某种ORM的模型,则可以使用它的方法来处理这些事情。根据需要制作实现Eloquent方法的自己的orderBy函数。例如,使用Eloquent:

class DbUserRepository implements UserRepositoryInterface
{
    public function findAll()
    {
        return User::all();
    }

    public function get(Array $columns)
    {
       return User::select($columns);
    }

您似乎正在寻找的是ORM。没有理由您的存储库不能基于一个。这将需要用户扩展才能,但是我个人认为这不是问题。

但是,如果您确实想避免使用ORM,则必须“自己动手”以获取所需的内容。

#3

接口不是硬性要求和快速要求。可以实现某个接口并将其添加到其中。它不能做的是无法实现该接口的必需功能。您还可以扩展类之类的接口,以保持事物干燥。

就是说,我才刚刚开始了解,但是这些认识帮助了我。


1
我对此方法不满意的是,如果您有MongoUserRepository,则该方法和您的DbUserRepository将返回不同的对象。Db返回了Eloquent \ Model,而Mongo则返回了自己的东西。当然,更好的实现是使两个存储库都返回单独的Entity \ User类的实例/集合。这样,当您切换到使用MongoRepository时,您不会错误地依赖Eloquent \ Model的DB方法
danharper 2013年

1
我绝对同意你的观点。为了避免这种情况,我可能永远不会在Eloquent需求类之外使用这些方法。因此,正如您所指出的,get函数可能应该是私有的,并且只能在类中使用,因为您会指出,它将返回其他存储库无法返回的内容。
威尔

3

我只能评论我们(在我公司)处理此问题的方式。首先,对于我们来说,性能并不是什么大问题,但是拥有干净/适当的代码才是问题。

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

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。在这种情况下,OrderModelcreates OrderEntity仅带有参考订单的I​​D。当PersistentEntity::getFieldOrderEntity实体调用时,它会指示其模型将所有字段延迟加载到OrderEntitys中。所有的OrderEntity与一个UserEntity关联的都被视为一个结果集,并将被立即加载。

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

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

$objOptions->getConditionHolder()->addConditionBind(
    new ConditionBind(
        new Condition('orderProduct.product', ICondition::OPERATOR_IS, $argObjProduct)
    )
);

该系统的最简单版本是将查询的WHERE部分作为字符串直接传递给模型。

对于如此复杂的回复,我感到抱歉。我试图尽快总结我们的框架。如果您还有其他疑问,请随时提出,我将更新答案。

编辑:此外,如果您真的不想立即加载某些字段,则可以在ORM映射中指定延迟加载选项。由于所有字段最终都是通过该getField方法加载的,因此您可以在调用该方法时在最后一分钟加载某些字段。在PHP中这不是一个很大的问题,但是我不建议在其他系统上使用。


3

这些是我见过的一些不同的解决方案。每个都有优点和缺点,但这取决于您自己决定。

问题1:字段过多

这是一个重要方面,尤其是当您考虑“ 仅索引扫描”时。我看到了两种解决此问题的方法。您可以更新函数以采用可选的数组参数,该参数将包含要返回的列的列表。如果此参数为空,则将返回查询中的所有列。这可能有点不可思议。根据参数,您可以检索对象或数组。您还可以复制所有函数,以便有两个不同的函数运行相同的查询,但是一个函数返回一个列数组,另一个函数返回一个对象。

public function findColumnsById($id, array $columns = array()){
    if (empty($columns)) {
        // use *
    }
}

public function findById($id) {
    $data = $this->findColumnsById($id);
}

问题2:方法太多

一年前,我曾与Propel ORM短暂合作过,这是基于我从那次经历中所能记住的。Propel可以选择根据现有数据库架构生成其类结构。它为每个表创建两个对象。第一个对象是一长串访问函数,类似于您当前列出的访问函数;findByAttribute($attribute_value)。下一个对象继承自第一个对象。您可以更新此子对象以构建更复杂的getter函数。

另一个解决方案是使用 __call()将未定义的函数映射到可操作的对象。您的__call方法将能够将findById和findByName解析为不同的查询。

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]
    }
}

我希望这至少可以有所帮助。



0

我同意@ ryan1234的观点,即应该在代码中传递完整的对象,并应使用通用查询方法来获取这些对象。

Model::where(['attr1' => 'val1'])->get();

对于外部/端点使用,我真的很喜欢GraphQL方法。

POST /api/graphql
{
    query: {
        Model(attr1: 'val1') {
            attr2
            attr3
        }
    }
}

0

问题3:无法匹配接口

我看到了将接口用于存储库的好处,因此我可以换出我的实现(出于测试目的或其他目的)。我对接口的理解是,它们定义了实现必须遵循的契约。除非您开始向存储库中添加其他方法(如findAllInCountry()),否则这很好。现在,我需要更新我的界面,使其也具有此方法,否则,其他实现可能没有它,这可能会破坏我的应用程序。这样感觉很疯狂……尾巴摇晃着狗。

我的直觉告诉我,这可能需要一个与通用方法一起实现查询优化方法的接口。对性能敏感的查询应具有针对性的方法,而不常见或轻量级的查询则由通用处理程序处理,这可能是控制器进行更多处理的开销。

通用方法将允许执行任何查询,因此将防止在过渡期间中断更改。目标方法使您可以在合理的情况下优化呼叫,并且可以将其应用于多个服务提供商。

这种方法类似于执行特定优化任务的硬件实现,而软件实现则可以轻松完成工作或灵活实现。


0

我认为在不增加数据存储库复杂性的情况下,graphQL是提供大规模查询语言的理想选择。

但是,如果您现在不想使用graphQL,则还有另一种解决方案。通过使用DTO,在该DTO中,对象用于在流程之间(在这种情况下,在服务/控制器和存储库之间)保存数据。

优雅的答案已经在上面提供的,但是我会尽力举个例子,我认为这是简单,可作为一个新项目的起点。

如代码所示,CRUD操作只需要4种方法。该find方法将通过传递对象参数用于列出和读取。后端服务可以基于URL查询字符串或基于特定参数来构建定义的查询对象。

SomeQueryDto如果需要,查询对象()也可以实现特定的接口。并且很容易在以后扩展而不增加复杂性。

<?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);
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.