ttpn18121996 / simple-repository
快速简单地构建存储库和服务模式。
Requires
- php: ^8.2
- ext-json: *
- illuminate/collections: ^11.0
- illuminate/console: ^11.0
- illuminate/contracts: ^11.0
- illuminate/database: ^11.0
- illuminate/log: ^11.0
- illuminate/support: ^11.0
Requires (Dev)
- friendsofphp/php-cs-fixer: dev-master
- laravel/pint: dev-main
README
安装
使用composer安装
composer require ttpn18121996/simple-repository
接下来,使用simple-repository:install
命令发布SimpleRepository的资源
php artisan simple-repository:install
创建存储库
默认存储库使用Eloquent,运行命令make app/Repositories/Eloquent/UserRepository.php
文件和接口app/Repositories/Contracts/UserRepository.php
php artisan make:repository UserRepository
在创建过程中,您可以通过添加选项--model
或-m
来指定存储库所依赖的模型。
php artisan make:repository UserRepository --model=User
#OR
php artisan make:repository UserRepository -m User
在构建过程中使用另一个存储库,请添加--repo
或-r
选项。例如,如果您想使用Redis而不是Eloquent,现在存储库将在路径app/Repositories/Redis/UserRepository.php
中创建。
php artisan make:repository UserRepository -m User -r Redis
创建存储库后,请记得在app/Providers/RepositoryServiceProvider.php
中声明protected $repositories
(默认情况下将自动添加)
protected $repositories = [ ... \App\Repositories\Contracts\UserRepository::class => \App\Repositories\Eloquent\UserRepository::class, ]
示例显示了存储库模式的动态扩展。我们使用数据库中的省份数据。过了一段时间,我们意识到使用本地数据不再合适,我们想使用来自外部Web服务的数据源。编辑现有的Eloquent\ProvinceRepository
内容会导致错误或难以恢复到之前的状态。相反,我们将创建一个名为WebService\ProvinceRepository
的新存储库,同时确保其准确性,就像旧的存储库一样。
/app
├---/Providers
| ├---RepositoryServiceProvider.php
├---/Repositories
| ├---/Contracts
| | ├---ProvinceRepository.php
| ├---/Eloquent
| | ├---ProvinceRepository.php
| ├---/WebService
| | ├---ProvinceRepository.php
app/Repositories/Eloquent/ProvinceRepository.php
<?php namespace App\Repositories\Eloquent; use App\Models\Province; use App\Repositories\Contracts\ProvinceRepository as ProvinceRepositoryContract; class ProvinceRepository implements ProvinceRepositoryContract { public function getModelName(): string { return Province::class; } public function all() { return $this->model()->all(); } }
app/Repositories/WebService/ProvinceRepository.php
<?php namespace App\Repositories\WebService; use App\Repositories\Contracts\ProvinceRepository as ProvinceRepositoryContract; use Illuminate\Support\Facades\Http; class ProvinceRepository implements ProvinceRepositoryContract { public function getModelName(): string { return ''; } public function all() { $response = Http::get('https://api.domain.example/provinces'); return $response->success() ? $response->collection() : collect(); } }
最后,我们需要在RepositoryServiceProvider
中更改抽象和具体之间的绑定
protected $repositories = [ ... // \App\Repositories\Contracts\ProvinceRepository::class => \App\Repositories\Eloquent\ProvinceRepository::class, \App\Repositories\Contracts\ProvinceRepository::class => \App\Repositories\WebService\ProvinceRepository::class, ]
创建服务
运行make命令创建服务。例如:创建app/Services/UserService.php
文件。
php artisan make:service UserService
在创建过程中,您可以通过添加--model或-m选项来指定服务所依赖的模型。
php artisan make:service UserService --model=User --model=Role
#OR
php artisan make:service UserService -m User -m Role
您可以使用存储库而不是模型。在创建过程中,通过添加--repo或-r选项来指定服务所依赖的存储库。
php artisan make:service UserService --repo=UserRepository --repo=RoleRepository
#OR
php artisan make:service UserService -r UserRepository -r RoleRepository
自定义过滤器构建器
覆盖存储库类中的buildFilter
方法来自定义从请求中获取的buildFilter
。
protected function buildFilter(Builder $query, array $filters = []): Builder { return $query->orderBy('name') ->when(Arr::get($filters, 'name'), function (Builder $query, $name) { $query->where('name', 'like', "%{$name}%"); }); }
现在您只需要调用getAll
或getPagination
,查询将根据您传递的过滤器自动进行过滤。
自定义关系构建器
类似于过滤器构建器,您可以通过覆盖buildRelationships
方法来自定义关系查询处理。
protected function buildRelationships(): Builder { return $this->model()->with(['roles', 'permissions']); }
自定义查询构建器
如果您想自定义查询而不影响其他使用buildRelationships
和buildFilter
的方法,您可以通过覆盖getBuilder
方法。
protected function getBuilder(array $filters = []): Builder { return $this->model() ->with(['roles', 'permissions']) ->when(Arr::get($filters, 'name'), function (Builder $query, $name) { $query->where('name', 'like', "%{$name}%"); }) ->orderBy('name'); }
为服务设置已认证用户
使用已认证用户在服务中进行权限检查。
class UserController { public function index(Request $request) { $users = $this->userService ->useAuthUser($request->user()) ->getList($request->query()); ... } }
class UserService extends Service { public function getList(array $filters = []) { if ($this->authUser()->can('view_user')) { // Do something } ... } }
实现和扩展
简单存储库提供了两个特质类“HasFilter”和“Safetyable”,用于构建处理数据排序和过滤的查询(HasFilter)以及使用事务进行数据交互(Safetyable)。默认情况下,基础服务和基础存储库类扩展了这两个特质类。
HasFilter提供了一个buildFilter
方法。要使用此功能,我们需要以以下格式传递filters
参数:
$this->buildFilter(query: $query, filters: [ 'search' => [ // Relative search (operator "like") 'field_1' => 'value1', 'field_2' => 'value2', ], 'or_search' => [ // Relative search (operator "like"). Use the "orWhere" method 'field_1' => 'value1', 'field_2' => 'value2', ], 'filter' => [ // Absolute search (operator "=") 'field_1' => 'value1', 'field_2' => 'value2', ], 'or_filter' => [ // Absolute search (operator "="). Use the "orWhere" method 'field_1' => 'value1', 'field_2' => 'value2', ], 'sort' => [ // Sort data 'field' => 'field_name', 'direction' => 'asc' // asc | desc ], ]);
为了解决两个表有相同字段名的问题或您想更改URL上的字段以避免在数据库中暴露表和字段名,您可以在您的服务类中创建一个“transferredFields”属性。
例如:用户表和角色表都有一字段“name”。
namespace App\Services; class UserService extends Service { protected array $transferredFields = [ 'name' => 'users.name', 'role_name' => 'roles.name', ]; }
或者您也可以直接覆盖“getTransferredField”方法来传递字段名。
namespace App\Services; class UserService extends Service { protected function getTransferredField(string $field): string { return [ 'name' => 'users.name', 'role_name' => 'roles.name', ][$field] ?? $field; } }
ModelFactory 属性
当在服务中使用模型时,它看起来像这样
<?php namespace App\Services; use App\Models\User; class UserService extends Service { public function __construct( public User $user, ) { } public function getById($id) { return $this->user->find($id); } }
而不是使用服务时,依赖的模型将通过容器服务自动初始化和注入。现在,只有当您调用它们时才会初始化并存储。您可以通过 ModelFactory 属性像这样在服务中声明和使用模型
<?php namespace App\Services; use App\Models\User; use SimpleRepository\Attributes\ModelFactory; class UserService extends Service { #[ModelFactory(User::class)] public ?User $user = null; public function getById($id) { return $this->getModel('user')->find($id); } }
安全地使用数据库处理函数
而不是像这样
DB::beginTransaction(); try { // Do something DB::commit(); return $data; } catch (\Throwable $e) { DB::rollback(); logger()->error($e->getMessage()); return null; }
您可以使用 handleSafely() 方法代替。第一个参数是处理逻辑的回调,第二个参数是日志的标题。假设您遇到异常,它将呈现为“标题:{消息内容}”
use SimpleRepository\Traits\Safetyable; class MyClass { use Safetyable; public function doSomething($params) { return $this->handleSafely(function () { // Do something return $data; }, 'Do something'); } }
服务和仓库默认使用 Safetyable 特性。您可以直接在服务和仓库内部调用 handleSafely() 方法。
class UserService extends Service { #[ModelFactory(User::class)] protected ?User $user = null; public function create(array $data) { return $this->handleSafely(function () use ($data) { $user = $this->getModel('user', $data); $user->save(); return $user; }, 'Create user'); } }
在 config/simple-repository.php
文件中为 handleSafely() 方法设置日志通道,以便在出错时记录
<?php return [ ... 'log_channel' => 'stack', ];
技巧
在服务类内部,您可以在不导入和实例化的情况下调用同一命名空间中的其他服务。您可以通过 getService
方法(将服务名称作为参数值)来调用它们。例如,App\Services\UserService
想要使用 App\Services\RoleService
。
namespace App\Services\UserService; public function sampleMethod() { /** * @var \App\Services\RoleService */ $roleService = $this->getService('RoleService'); }
namespace App\Services\UserService; use App\Services\RoleService; use SimpleRepository\Attributes\ServiceFactory; class UserService extends Service { #[ServiceFactory(RoleService::class)] public ?RoleService $role = null; public function sampleMethod() { /** * @var \App\Services\RoleService */ $roleService = $this->getService('role'); } }