mreschke/repository

Mreschke 仓库和实体管理器

5.3.12 2024-02-19 21:17 UTC

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 功能,它类似于应用级别的连接,可以在 lazyeager 模式下使用。由于可以跨仓库连接,因此 ->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:此部分尚未编写。目前,您可以查看FakeFake2文件夹,因为它们是用于测试的两个仓库。

事件

仓库触发许多事件。所有事件都是基于字符串的事件,而不是基于类的事件。它们是基于字符串的,因此可以根据实体进行分离,类似于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