mreschke / repository
Mreschke 仓库和实体管理器
README
介绍
这是一个基于 Laravel 的活动记录风格的实体映射仓库系统,适用于所有后端。
本库的主要目的是
- 提供美观的纯 PHP 实体转储。你曾经转储过 eloquent 模型吗?又丑又大。
- 提供实体列映射(将数据库中的
some_ugly_old_column
映射到 PHP 中的->perfectColumn
) - 提供完整的 eloquent 风格查询构建器,但基于
->where('perfectColumn')
而不是 eloquent 中的some_ugly_old_column
,实体映射无处不在! - 提供关系和
->with()
功能,具有单次懒加载或批量预加载的能力,甚至跨越仓库和数据中心。 - 提供活动记录的
->save()
和->delete()
语法,但仍然在优雅转储的纯 PHP 对象之上(等等!?是的,基于 IoC 的单例缓存) - 提供通过
$myApp->connection('otherBackend')
动态交换仓库后端的能力 - 提供仓库级别的格式化和约束,如大写、小写、ucwords、trim、必需、默认值,以及数据类型大小溢出检测。
- 提供版本化实体映射。你曾经使用 https://github.com/thephpleague/fractal 作为模型 JSON API 转换器吗?由于本质,这个实体映射器已经做了这个转换,而且还允许你为这个原因版本化实体映射!已经映射了一次,为什么还要在 REST API 中再次映射呢?
- 允许跨仓库 JOIN,即使仓库位于不同的服务器或不同的网络/数据中心。
- 提供完全功能的属性系统。你曾经想要存储额外的列或有关行的元数据,但不想添加另一个列?每个实体行都可以有任意多的附加属性。你甚至可以使用
->whereAttribute()
基于属性查找实体! - 提供完全功能的对象存储系统。类似于属性(存储在实体上的任意值),但能够处理巨大大小(文件、笔记、json 块)。属性用于小型键/值项,对象用于其他一切。
基本使用和语法
在我的示例中,我有一个名为 VFI 的应用程序,该应用程序有用户、经销商、ros、项目。这个虚拟实体将通过 $this->vfi
在这些示例中访问。
获取记录
注意: ->all()
和 ->get()
是相同的,可以互换使用
<?php // Single record based on primary key $user = $this->vfi->user->find(1234); // Single record based on where statement $user = $this->vfi->user->where('email', 'mail@mreschke.com')->first(); // Multi record and wheres $users = $this->vfi->user->all(); //or ->get() also works $users = $this->vfi->user->where('disabled', false)->get(); $users = $this->vfi->user->where('state', 'TX')->where('disabled', false)->get(); // Where In $users = $this->vfi->user->where('id', 'in', [1,2,3,4,5])->get(); // Where NOT In $users = $this->vfi->user->where('id', '!in', [1,2,3,4,5])->get(); // Like $users = $this->vfi->user->where('name', 'like', 'bob%')->get(); // NOT Like $users = $this->vfi->user->where('name', '!like', 'bob%')->get(); // Where null or not null $users = $this->vfi->user->where('completed', 'null', true) $users = $this->vfi->user->where('completed', 'null', false) // Selects and pluck $users = $this->vfi->user->select('id', 'name')->where('disabled', false)->all(); $users = $this->vfi->user->select('id', 'name')->pluck('name', 'id'); // Ordering records $users = $this->vfi->user->where('disabled', false)->orderBy('name')->all(); // Counting records at a db level (not result level) $users = $this->vfi->user->count(); $users = $this->vfi->user->where('disabled', false)->count(); // Limiting records $users = $this->vfi->user->limit(0, 10)->get();
所有可能的 WHERE 操作符
注意 != 和 <> 是相同的
$operators = [
'=', '!=', '<>',
'>', '<', '>=', '<=',
'like', '!like', 'not like',
'in', '!in', 'not in',
'null'
];
连接
<?php
关系
关系涉及子实体。即,与其他实体相关联的实体。此仓库系统可以通过几种方式访问子实体。一种是使用 ->join
功能。由于不支持 *,->join
功能不能跨仓库连接,并且你必须始终 ->select('specific', 'columns')
。另一种是 ->with
功能,它类似于应用级别的连接,可以在 lazy
和 eager
模式下使用。由于可以跨仓库连接,因此 ->with
功能很有用。
要访问关系(子实体),您可以使用->with()
关键字或使用自定义构建实体方法。在链中适当的位置使用->with()
关键字将导致懒加载或预加载。懒加载将为每个实体运行一个关系,如果处理不当,可能会非常低效。预加载允许您预加载所有查询实体的所有关系。预加载实际上将运行2个查询。一个用于主实体,另一个基于IN语句的关系。然后,在PHP中将这些结果合并。
您还可以使用点号->select()
选择子实体,例如:->select('id', 'name', 'address.state')
,其中address是子实体。
<?php // Lazy loaded using ->with() method $user = $this->vfi->user->find(1234)->with('address'); //lazy query happens here echo $user->address->state; // Lazy loaded using automatic method $user = $this->vfi->user->find(1234); echo $user->address->state; //lazy query happens here // Lazy loaded in a loop...careful, this is where it gets inefficient as its one query per entity $users = $this->vfi->user->all(); foreach ($users as $user) { echo $user->address->state; //lazy query happens here } // Eager loaded using ->with(). Far more efficient if you need it on multiple objects. // Notice ->with() is BEFORE ->all() or ->get() $users = $this->vfi->user->with('address')->all(); // Complex ->with() based query $query = $this->vfi->client->with('address'); $totalRecords = $query->count(false); //count total before a filter #$query->select('id', 'name', 'address.city, 'address.state'); // this works, so does select * which is default $query->where('disabled', false); $query->where('address.state', 'CO'); $query->limit(0, 10); $results = $query->get(); // Complex ->join() based query $query = $this->vfi->client->joinAddress(); $totalRecords = $query->count(false); //count total before a filter $query->select('id', 'name', 'address.city, 'address.state'); //required for join
属性系统
您可以在系统中存储关于任何实体的额外属性(或元数据)。默认情况下,所有实体的此功能都是关闭的。有关如何设置的详细信息,请参阅入门。
属性可以是懒加载和预加载的。在此处请务必小心,因为不当使用懒加载非常低效。
注意:属性旨在是小型键/值对,而不是大型百万行字符串。当您加载任何属性时,该实体的所有属性也将同时加载到相同的->attributes
属性中。因此,建议每个实体不要有数百个小属性。然而,->object()
系统是为无限和大规模任意大小对象设计的。
<?php // Lazy loaded attributes using ->with() method $user = $this->vfi->user->find(1234)->with('attributes'); //lazy query happens here echo $user->attributes('some-attribute'); // Lazy loaded attributes using automatic method $user = $this->vfi->user->find(1234); echo $user->attributes('some-attribute'); //lazy query happens here // Lazy loaded in a loop...careful, this is where it gets inefficient as its one query per entity $users = $this->vfi->user->all(); foreach ($users as $user) { echo $user->attributes('some-attribute'); //lazy query happens here } // Eager loaded using ->with(). Far more efficient if you need it on multiple objects. // Notice ->with() is BEFORE ->all() or ->get() $users = $this->vfi->user->with('attributes')->all(); // Find entity records based on an attribute value $users = $this->vfi->user->whereAttribute('some-attribute', 'someValue')->get(); // Find entity records that even have this attribute (value is not important) $users = $this->vfi->user->whereAttribute('some-attribute')->get(); // Setting attributes $this->vfi->user->find(1234)->attributes('new-attribute', 'new value here'); // Delete attribute (single) $this->vfi->user->find(1234)->forgetAttribute('some-attribute'); // Delete all attributes // There is no way to delete all attributes other than looping them all manually $user = $this->vfi->user->find(1234)->with('attributes'); foreach ($user->attributes as $attribute) { $user->forgetAttribute($attribute) }
实体格式化
在将新实体插入数据库之前,您可以通过实体格式化器运行您的数组。这将根据存储映射部分格式化每个列。这允许您在插入之前进行大写、小写、截断等操作。如果值超过定义的size
属性,将触发一个overflow
事件,有关详细信息请参阅事件。
<?php $newEntities = 'this is an array of your items you want to insert'; foreach ($newEntities as $entity) { // Format entities first $entity->format(); // Save to backend $entity->save(); }
删除记录
<?php // Single entity delete $this->vfi->roItem->find(1234)->delete(); $this->vfi->roItem->where('roNum', 222258)->first()->delete(); // Bulk query builder based delete (most efficient) // Results in query: DELETE FROM table WHERE techNum = 842 $this->vfi->roItem->where('techNum', 903)->delete(); // Multiple collection of entities // Results in query: DELETE FROM table WHERE IN (1,2,3,...) $ros = $this->vfi->roItem->where('techNum', 903)->get(); $this->vfi->roItem->delete($ros); // Multiple array of entities // Results in query: DELETE FROM table WHERE IN (1,2,3,...) $ros = $this->vfi->roItem->where('techNum', 903)->get(); $ros = $ros->toArray(); $this->vfi->roItem->delete($ros); // Multiple array of arrays // Results in query: DELETE FROM table WHERE IN (1,2,3,...) $ros = $this->vfi->roItem->where('techNum', 903)->get(); $tmp = []; foreach ($ros as $ro) { $tmp[] = (array) $ro; } $this->vfi->roItem->delete($ros); // Multiple array of arrays manually // Results in query: DELETE FROM table WHERE IN (1,2,3,...) $this->vfi->roItem->delete([ ['id' => 1], ['id' => 2], ['id' => 3] ]); // Trying to delete * should fail in case we messed up the query builder // To delete * use ->truncate instead $this->vfi->roItem->delete();
更新记录
<?php // Update single entity using ->save $client = $this->vfi->client->find(5975); $client->name = "New Name"; $client->save(); // Update single entity using ->update and passing back the object $client = $this->vfi->client->find(5975); $client->name = "New Name"; $this->vfi->client->update($client); // Update same column(s) on bulk records based on ->where // This is a query builder level update and the most efficient! $clients = $this->vfi->client ->where('disabled', true) ->update(['name' => 'DISABLED', 'date' => date()]);
入门
本节解释了如何设置mreschke/repository
。您可以如何使用此系统为自己的实体。
FIXME:此部分尚未编写。目前,您可以查看Fake
和Fake2
文件夹,因为它们是用于测试的两个仓库。
事件
仓库触发许多事件。所有事件都是基于字符串的事件,而不是基于类的事件。它们是基于字符串的,因此可以根据实体进行分离,类似于Eloquent根据模型名称触发事件,以便您可以监听单个模型而不是所有模型。
所有事件都以前缀repository.yourrepo.yourentity
开头。
在customer
实体上的dynatron/vfi事件示例。
实体级别事件
repository.dynatron.vfi.customer.overflow
repository.dynatron.vfi.customer.attributes.saving
repository.dynatron.vfi.customer.attributes.saved
repository.dynatron.vfi.customer.attributes.deleting
repository.dynatron.vfi.customer.attributes.deleted
repository.dynatron.vfi.customer.attributes.saving
存储级别事件
repository.dynatron.vfi.customer.saving
repository.dynatron.vfi.customer.saved
repository.dynatron.vfi.customer.creating
repository.dynatron.vfi.customer.created
repository.dynatron.vfi.customer.updating
repository.dynatron.vfi.customer.updated
repository.dynatron.vfi.customer.deleting
repository.dynatron.vfi.customer.deleted
repository.dynatron.vfi.customer.truncating
repository.dynatron.vfi.customer.truncated
监听器
Laravel可以监听通配符事件
<?php $dispatcher->listen('repository.dynatron.vfi.*.overflow', 'Dynatron\Vfi\Listeners\RepositoryEventSubscription@overflowHandler');
测试
这是一个mrcore模块,因此需要mrcore5才能运行测试。
我知道,永远不要针对实际数据库进行测试。我不知道如何做到这一点。我的测试是集成测试,而不是单元测试。我使用test.sqlite数据库,并需要一个本地的mongo安装。我在运行时自动添加正确的数据库连接信息,因此无需调整config/database.php。sqlite数据库已从git中排除,mongo很便宜,如果需要,您可以删除fake*-repository数据库。
一次播种,随时测试
Fake/Database/create
./test
为什么写这个
FIXME:这还不完整
在2015年,仓库在Laravel社区中很流行。它们的优点很明显,但它们的实现是冗长、笨拙,并且不像Eloquent那样容易进行“查询构建”。
大多数人喜欢有一个仓库层,以防他们将来决定更换后端。对大多数人来说,为后端未来的变化进行工程往往是浪费时间,被认为是过度设计。对我来说并不成立,因为我当时正在将所有的软件从MSSQL重写为MySQL和MongoDB。这是一场五年的棋局,需要一些应用在过渡阶段同时运行在旧MSSQL后端和新MySQL后端。这些后端是临时的,并且在此期间每月迁移。这意味着我被迫思考“先API”。换句话说,我无需关心我的数据目前在哪里以及最终会去哪里。我只需要考虑在理想世界中如何与我想要交互的数据。仓库模式允许我在数据中构建出漂亮的API,尽管后端是MSSQL垃圾。与像users_first_name
这样的旧列相比,我可以将其映射为简单的$user->name
,这是eloquent所不具备的。
所以如果你的数据库看起来像这样
Table: tblContacts
---------------
contact_id
first_name
last_name
email
Table: tblAddresses
-------------------
address_id
address
zip_code
你可以构建完美映射(翻译)的实体,如下所示
// We don't want to use the word contacts or tblContacts, we want to use 'users'
// And we don't want first_name, we want firstName...thus the entity mapper
var_dump( $this->vfi->user->find(1)->with('address') )
Mreschke\Vfi\User {
id: 1
firstName: "Matthew"
lastName: "Reschke"
email: "mail@mreschke.com"
address: {
id: "3212"
address: "Some address"
zip: 75067
}
}
现在这些完美映射的实体也像普通的PHP对象一样操作。这意味着你可以在PHP中使用var_dump()或dd()或dump()来处理它们,并获得非常漂亮的结果,而不是像Eloquent或Query builder那样,你会得到数百万其他Laravel属性。 这为你提供了干净的输出,这是一大优势!
我们的实体不仅现在完美且一致地映射用于输出,而且还可以在这些完美映射的列上进行可靠的查询!
<?php // So we can now use firstName not first_name everywhere, like so $user = $this->vfi->user->where('firstName', 'Matthew')->first()
当然,由于你的实体只是普通的PHP对象,你可以添加任何你选择的辅助方法或属性。例如,如果你想要一个byName
辅助函数,只需将其添加到你的实体中
开发笔记
HTTP存储
如果我构建一个HTTP JSON存储,那么在完全使用查询构建器的情况下,URL会是什么样子?
由于这是一个公开的HTTP API,我需要始终了解上下文,例如查询API的登录用户是谁。因为如果他们调用users/179,我需要知道调用该API的$user实际上有访问user 179的权限等等。在基于PHP的库中这不是问题,因为我是调用API的人。但是如果是HTTP,那么任何人都可以调用它。
all() http://iam/user http://iam/user/byUser(179) http://iam/user/managersByDealer(5975)
where() http://iam/user/where('disabled',true) http://iam/user/where('disabled',true)/where('id','>',100)
order() http://iam/user/orderBy('id') http://iam/user/where('disabled',true)/orderBy('id')
find() http://iam/user/179 http://iam/client/byExtract(4345) http://iam/client/whereHostname('bgmo')/orderBy('id')
方法 http://iam/user/179/types http://iam/user/179/apps http://iam/user/179/hasPerm('admin') http://iam/user/179/isEmployee http://iam/client/accessibleBy(179)
关系 http://iam/client/179/with('host','address') http://iam/client/179/host http://iam/client/179/address