ttpn18121996/simple-repository

快速简单地构建存储库和服务模式。

v3.1.3 2024-09-08 17:07 UTC

README

PHP v8.2

Laravel v11.x

安装

使用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}%");
        });
}

现在您只需要调用getAllgetPagination,查询将根据您传递的过滤器自动进行过滤。

自定义关系构建器

类似于过滤器构建器,您可以通过覆盖buildRelationships方法来自定义关系查询处理。

protected function buildRelationships(): Builder
{
    return $this->model()->with(['roles', 'permissions']);
}

自定义查询构建器

如果您想自定义查询而不影响其他使用buildRelationshipsbuildFilter的方法,您可以通过覆盖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');
    }
}