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

前言:我试图在关系数据库的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 All fields (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()),这显然有主要的性能问题,如果你从数据库中提取。

帮助吗?

显然,在使用存储库时,我需要重新考虑一些事情。有谁能告诉我这个最好怎么处理吗?

88132 次浏览

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

我们如何处理收回我们不需要的田地?

根据我的经验,这真的归结为处理完整的实体与ad-hoc查询。

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

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

我更喜欢与完整的实体一起工作。

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

  1. 积极地缓存实体,这样您只需从数据库中支付一次读取代价。
  2. 花更多的时间建模你的实体,这样它们之间就有了很好的区别。(考虑将一个大实体分成两个小实体,等等)
  3. 考虑拥有多个版本的实体。你可以有一个User用于后端,也可以有一个UserSmall用于AJAX调用。一个可能有10个属性,一个有3个属性。

使用临时查询的缺点:

  1. 您最终在许多查询中得到本质上相同的数据。例如,使用User,你最终会为许多调用编写本质上相同的select *。一个调用将得到10个字段中的8个,一个将得到10个字段中的5个,一个将得到10个字段中的7个。为什么不把所有的都换成一个能打10分的电话呢?这很糟糕的原因是重构/测试/模拟是谋杀。
  2. 随着时间的推移,在高层次上对代码进行推理变得非常困难。而不是像“为什么User这么慢?”这样的语句,你最终会跟踪一次性查询,因此错误修复往往是小的和本地化的。
  3. 要取代底层技术真的很难。如果你现在把所有东西都存储在MySQL中,想要转移到MongoDB,替换100个临时调用要比替换几个实体困难得多。

我将有太多的方法在我的存储库。

答:我真的没有看到任何方法除了巩固调用。存储库中的方法调用实际映射到应用程序中的功能。特性越多,特定于数据的调用就越多。您可以向后推功能,并尝试将类似的调用合并为一个。

一天结束的时候,复杂性必须存在于某个地方。使用存储库模式,我们将其推入存储库接口,而不是制造一堆存储过程。

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

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

首先,我们定义诸如UserModel这样的模型,它使用ORM来创建UserEntity对象。当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创建__abc2。当OrderEntity调用PersistentEntity::getField时,实体指示它的模型将所有字段延迟加载到OrderEntity中。与一个UserEntity关联的所有__abc2都被视为一个结果集,并将立即加载。

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

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

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

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

我很抱歉回答这么复杂。我试着尽可能快速和清晰地总结我们的框架。如果你有任何其他问题,请随时问他们,我会更新我的答案。

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

这是我见过的一些不同的解决方案。每一种都有利弊,但这是由你来决定的。

问题#1:字段太多

这是一个重要的方面,特别是当你考虑到唯一索引扫描时。我认为有两种方法可以解决这个问题。您可以更新您的函数以接受一个可选的数组参数,该参数将包含要返回的列列表。如果此参数为空,则返回查询中的所有列。这可能有点奇怪;根据参数,可以检索对象或数组。还可以复制所有函数,这样就有两个运行相同查询的不同函数,但其中一个返回列数组,另一个返回对象。

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


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

问题2:方法太多

一年前,我曾短暂地使用过推动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]
}
}

我希望这能有所帮助。

我将在此补充一点,因为我目前正试图掌握所有这些内容。

#1和2

这是一个完美的地方,您的ORM做繁重的工作。如果你正在使用一个实现了某种ORM的模型,你可以使用它的方法来处理这些事情。如果需要,可以创建自己的orderBy函数来实现Eloquent方法。举个例子:

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


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

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

然而,如果您确实想避免ORM,那么您将不得不“自己滚动”以获得您想要的东西。

# 3

接口不应该是硬性要求。可以实现接口并向其添加内容。它不能做的是未能实现该接口所需的功能。你也可以像类一样扩展接口来保持DRY。

也就是说,我才刚刚开始理解,但这些认识对我很有帮助。

我想我应该尝试回答我自己的问题。以下只是解决我最初问题中的问题1-3的一种方法。

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

目标:

  • 创建一个用于查看和编辑Users的基本控制器的完整示例。
  • 所有代码必须是完全可测试的和可嘲弄的。
  • 控制器不应该知道数据存储在哪里(这意味着它可以被更改)。
  • 展示SQL实现的示例(最常见)。
  • 为了获得最大的性能,控制器应该只接收它们需要的数据——没有额外的字段。
  • 实现应该利用某种类型的数据映射器来简化开发。
  • 实现应该具有执行复杂数据查找的能力。

解决方案

我将我的持久存储(数据库)交互分为两类:R(读取)和反刍的食物(创建,更新,删除)。我的经验是,读取是真正导致应用程序变慢的原因。虽然数据操作(CUD)实际上更慢,但它发生的频率要低得多,因此不太值得关注。

反刍的食物(创建,更新,删除)很简单。这将涉及使用实际的模型,然后将其传递给我的Repositories进行持久化。注意,我的存储库仍将提供Read方法,但只是用于对象创建,而不是显示。稍后再详细介绍。

R (Read)不是那么容易。这里没有模型,只有值对象。使用数组如果你愿意。这些对象可以表示单个模型,也可以表示多个模型的混合,实际上什么都可以。它们本身并不是很有趣,但它们是如何产生的却很有趣。我使用的是Query Objects

代码:

用户模型

让我们从基本用户模型开始。注意,这里根本没有ORM扩展或数据库之类的东西。纯粹是模特的荣耀。添加getter, setter,验证等等。

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

查询对象接口

现在我们的存储库已经处理了反刍的食物(创建、更新、删除),我们可以专注于R(读取)。查询对象只是某种类型的数据查找逻辑的封装。它们是查询构建器。通过像我们的存储库一样抽象它,我们可以更容易地更改它的实现并测试它。查询对象的一个例子可能是AllUsersQueryAllActiveUsersQuery,甚至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,所以大多数情况下,我将在整个应用程序中直接引用这些模型。但是,在有更复杂查询的情况下,我将使用查询对象来提高这些查询的可重用性。我还应该指出,我总是将我的模型注入到我的方法中,使它们更容易在测试中模拟。

我使用以下接口:

  • Repository -加载,插入,更新和删除实体
  • Selector -在存储库中基于过滤器查找实体
  • Filter -封装过滤逻辑
我的Repository是数据库不可知论者;事实上,它没有指定任何持久性;它可以是任何东西:SQL数据库,xml文件,远程服务,来自外太空的外星人等等。 对于搜索功能,Repository构造了一个Selector,它可以被过滤、LIMIT-ed、排序和计数。最后,选择器从持久化中获取一个或多个Entities

下面是一些示例代码:

<?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,使用它们特定的InMemorySelectorFilterAdapterFilter转换为InMemoryFilter;因此,每个选择器实现都有自己的过滤器适配器。

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

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

附注:这是对我实际代码的简化

我建议https://packagist.org/packages/prettus/l5-repository作为供应商来实现存储库/标准等…在Laravel5:D

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

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

对于外部/端点使用,我非常喜欢GraphQL方法。

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

问题3:不可能匹配接口

我看到了为存储库使用接口的好处,所以我可以交换 输出我的实现(用于测试或其他目的)。我的 对接口的理解是它们定义了一个契约 执行必须紧随其后。这很好,直到你开始添加 您的存储库的附加方法,如findAllInCountry()。现在我 需要更新我的接口也有这个方法,否则,其他 实现可能没有它,这可能会破坏我的应用程序。 这感觉很疯狂…

.这是本末倒置的情况

我的直觉告诉我,这可能需要一个实现查询优化方法和泛型方法的接口。对性能敏感的查询应该有针对性的方法,而不频繁或轻量级的查询则由通用处理程序处理,这可能会使控制器做更多的杂耍。

泛型方法将允许实现任何查询,因此将防止在过渡期间发生破坏性更改。目标方法允许您在有意义时优化调用,并且它可以应用于多个服务提供者。

这种方法类似于硬件实现执行特定的优化任务,而软件实现只做简单的工作或灵活的实现。

我认为graphQL在这种情况下是一个很好的候选人,可以提供一个大规模的查询语言,而不会增加数据存储库的复杂性。

但是,如果您现在不想使用graphQL,还有另一种解决方案。通过使用DTO,其中一个对象用于在进程之间携带数据,在本例中是在服务/控制器和存储库之间。

上面已经提供了一个优雅的回答,但是我将尝试给出另一个例子,我认为它更简单,可以作为一个新项目的起点。

如代码所示,我们只需要4个方法进行CRUD操作。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);
   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();

所以我认为