mblarsen/laravel-repository

增强查询构建器和仓库功能,以减少样板代码并使控制器保持精简。

0.13.1 2020-06-14 08:32 UTC

This package is auto-updated.

Last update: 2024-08-27 18:40:12 UTC


README

Latest Version on Packagist Build Status Code Coverage Quality Score Total Downloads

增强查询构建器和仓库功能,以减少样板代码并使控制器保持精简

特性

  • 零配置开始。 查看基本示例
  • 常用功能: allfindcreateupdatedestroy
  • 加上 allResourcesfindResource 等用于资源包装。
  • list() 函数对 <select> 和自动完成很有用。
  • 前端和用户驱动。请求是包含内容的上下文。
    • 在模型和关系上过滤查询
    • 包含关系(默认全部阻止)
    • 透明地处理分页
    • 按模型的属性或其关系的属性对模型进行排序(无需自定义 SQL)
  • 100% 测试覆盖率

此包的目标

  1. 避免分页、过滤和排序资源的样板代码
  2. 以安全的方式让客户端决定要获取的内容
  3. 将控制器逻辑与模型逻辑分离
  4. 让你能够控制特殊情况的查询

实际上,仓库类是查询构建器和仓库的结合,重点在检索上。

此包包括一个接口和三个类,供你构建

  • Repository 类,可以直接使用或扩展以满足你的模型需求。
  • ResourceContext 接口,为仓库提供数据。
  • RequestResourceContext,从传入的请求对象中提取数据。
  • ArrayResourceContext,你提供数据。适用于测试。

Contact me on Codementor

安装

您可以通过 composer 安装此包

composer require mblarsen/laravel-repository

仓库依赖于一个 ResourceContext 来提供必要的值,以便能够排序、过滤、分页等。这是自动处理的,但这意味着你应该让 Laravel 的依赖注入为你提供仓库。如果你扩展了基本仓库类,这将特别强大。

默认使用的上下文是 RequestResourceContext,它自动注入到仓库中。这特别适用于构建用于服务于前端的开源或私有 API。然而,还提供了一个允许你提供更高控制度的实现。这是 ArrayResourceContext。此实现适用于测试或当你构建使用 Blade 视图的传统 Laravel 应用程序时。

以下示例主要针对 RequestResourceContext 的使用。

基本示例

基本仓库对您的模型一无所知,因此除非您子类化仓库,否则您必须指定您正在查询的模型。

// Using Laravel's resolve() helper
$repository = resolve(Repository::class)->setModel(Post::class);
// Using static factory
$repository = Repository::for(Post::class);

在控制器中使用时,建议您让 Laravel 帮您处理

public function index(Repository $repository)
{
    return $repository->setModel(Post::class)->all();
}

... 或者在自定义仓库的情况下

public function index(PostRepository $repository)
{
    return $repository->all();
}

现在结果将根据请求自动 排序过滤分页

您可以提供的示例请求

/posts?sort_by=created_at&page=2&filters[title]=laravel

也就是说

  • /posts,请求帖子
  • sort_by=created_at,按 created_at 排序
  • sort_by=desc,按降序排序(默认:asc
  • page=2,分页结果,请求第 2 页(默认:null 表示不分页)
  • filters[title]=laravel 在帖子名称中搜索标题

您也可以通过关系进行过滤。例如 filters[address.zip]=1227

由于默认情况下不允许关系,所以没有请求要包含的关系将被忽略。但一旦我们设置好,你也将能够请求关系。

  • with[]=ads&with[]=comments,将包含关系adscomments

仓库功能

您可以使用链式API来定义仓库的行为。请确保查看下面的完整API

控制要包含哪些关系

查询:withwith[]

根据允许的内容,仓库将包含在with中指定的任何关系。这确保了客户端不能请求您未允许的数据。

$repository->setAllowedWith(['comments']);

有时您想默认包含某些关系。除了直接在模型上按照Laravel的方式操作外,您还可以在仓库上设置。这允许您控制控制器中每个动作的关系列表。

$relation->setDefaultWith(['comments']);

旁白:如果您正在构建公开API(用于您的SPA或第三方),建议您将结果包装在JsonResource中。这将让您控制公开哪些属性,并允许您进一步转换数据。请参阅资源部分

过滤

查询:filter[key]=value

此包通过其过滤器提供类似搜索的功能。底层使用LIKE,即%value%

如果您想要精确匹配,即=,则可以在键名末尾添加一个感叹号。

键不必是主模型的属性。它也可以是关系属性。以下是一些示例

// Search on model property
title=cra
// Search on model property exact match
code_name!=wowcrab
// Seacch on relation property
address.city=mass

您可以在搜索中组合属性

// Search in full name
first_name+middle_name+last_name=cra

最后,您可以选择在多个属性中搜索相同的值

// Search in both title, name, and email
title|name|email=cra

另一种过滤方式是向all()find()提供查询构建器。请参阅API中的示例。

转换RequestResourceContext

您不能直接修改请求上下文,但您可以选择将其转换为ArrayResourceContext

public function index(UserRepository $repository)
{
    $repository->setContext(
        ArrayResourceContext::create(
            $repository->getContext()->toArray()
        )
            ->merge([
                'filters' => ['status' => 'approved']
            ])
            ->exclude(['search_by'])
    );
}

自定义仓库

您的许多模型可能不需要自定义(子类化)仓库。但通常您的核心模型与它们相关的逻辑更多。在这种情况下,建议扩展基本仓库。

所有属性(除了model)都可以省略。好吧,您也可以省略模型,但这有点没有意义。

免责声明:示例的目的是演示灵活性,而不是非常现实。

class PostRepository extends Repository
{
    // We serve you Posts
    protected $model = Post::class;

    // The client can request to include the following relations
    protected $allowed_with = ['ads', 'comments'];

    // However, we will include these automatically
    protected $default_with = ['comments'];

    // We change the default sort key to created_at ...
    protected $default_sort_by = 'created_at';

    // ... in descending order
    protected $default_sort_order = 'desc';

    // We override modelQuery to ensure only
    // published posts will be returned
    protected function modelQuery($query = null)
    {
        // It is perfectly okay to not invoke the parent.
        // It simly defaults to an empty query of the current
        // model. These are identical:
        //
        // $query = parent::modelQuery($query);
        // $query = $query ?: Post::query()

        $query = parent::modelQuery($query);
        $query->whereNotNul('published_at');

        return $query;
    }
}

您可以用基本仓库实现相同的功能,但当然,每次都需要重复设置。

public function index(Repository $repository)
{
    $only_published = Post::query()->whereNotNul('published_at');

    $repository
        ->setModel(Post::class)
        ->setDefaulSort('created_at', 'desc')
        ->setAllowedWith(['ads', 'comments'])
        ->setDefaultWith(['comments'])
        ;

    return $repository->all($only_published);
}

与...

public function index(PostRepository $repository)
{
    return $repository->all();
}

资源

如果您正在构建公开API(用于您的SPA或第三方),建议您将结果包装在JsonResource中。这将让您控制公开哪些属性,并允许您进一步转换数据。

做起来非常简单。

// using base repository
$repository->setResource(UserResource::class);

// or extending
protected $resource = UserResource::class;

然后您只需使用任何API方法,如这里所示,在前面加上ResourceResources

$repository->allResources();
$repository->findResource(3);
$repository->createResource(['name' => 'foo']);
$repository->updateResource($user, ['name' => 'foo']);

如果您实现了一个集合资源,可以将它作为setResource方法调用中的第二个参数

$repository->setResource(UserResource::class, UserResourceCollection::class);

// or in class
protected $resource_collection = UserResourceCollection::class

注意:list()不支持资源。这对我来说没有意义。欢迎改变我的看法。

自定义RequestResourceContext

RequestResourceContext在传入请求中默认寻找的值是

  • 过滤器
  • 页面
  • 每页
  • 排序
  • 排序顺序
  • 包含

如果您不喜欢这些或由于某些原因无法使用它们,您可以选择使用上下文中的mapKeys()提供自己的键。 mapKeys允许您将一个或多个键映射到您首选的方案。

到目前为止,最简单的方法是在您的AppServiceProvider中添加以下代码片段。

public function register()
{
    $this->app->resolving(RequestResourceContext::class, function ($context) {
        $context->mapKeys([
            'filters' => 'filter'
            'sort_by' => 'order_by',
            'sort_order' => 'direction',
            'per_page' => 'take',
        ]);

        // or if you simply prefer camelCase

        $context->mapKeys([
            'sort_by' => 'sortBy',
            'sort_order' => 'sortOrder',
            'per_page' => 'perPage',
        ]);
    });
}

所以这将是旧的方式

/posts?sort_by=title&per_page=10&with=comments

您的客户端现在应使用此方案

/posts?sortBy=title&perPage=10&with=comments

测试

在测试 ArrayResourceContext 时,它会派上用场。它允许您直接传递上下文值。

$repository = Repository::for(User::class, ArrayResourceContext::create([
    'filters' => [
        'status' => 'approved',
    ]
]));

// or shorter

$repository = Repository::for(User::class, [
    'filters' => [
        'status' => 'approved',
    ]
]);

API

当扩展基本仓库时,您可能希望查看这些附加功能

all($query = null)

allQuery($query = null)

allResources($query = null)

根据当前资源上下文返回所有模型。

public function index(UserRepository $user_repository)
{
    // To get only users with status 'pending'
    $query = User::where('status', 'pending');
    $users = $user_repository->all($query);

    // Of course in this simple case we could have achived the same using a
    // filter: GET /users?filters[status]=pending
}

使用 Query 后缀作为查询构建器返回。

使用 Resources 后缀作为资源集合返回。

list(string|callable $column = null, $query = null)

listQuery(string|callable $column = null, $query = null)

产生适合选择、列表和自动完成的结果。所有条目都包含 'value' 和 'label' 键。

如果省略 $column,则使用默认排序。在许多情况下,它们可能是一样的。

使用 Query 后缀作为查询构建器返回。

注意:如果使用可调用函数,映射将在内存中执行,而字符串将在数据库层执行。另外,请注意,在使用 listQuery 变体时,可调用函数不会反映。

find($id, $query = null)

findQuery($id, $query = null)

findResource($id, $query = null)

获取单个模型。您可以通过提供一个起始查询来进一步缩小结果。请参阅 all() 中的示例。

使用 Query 后缀作为查询构建器返回。

使用 Resource 后缀作为资源返回。

create(array $data)

createResource(array $data)

典型的 crUd。使用 Resource 后缀作为资源返回。

update(Model $model, array $data)

典型的 crUd。

使用 Resource 后缀作为资源返回。

destroy(Model $model)

典型的 cruD。

setContext(ResourceContext|array $resource_context)

此方法允许您在创建仓库后设置或更改上下文。

setModel(string $model)

请参阅示例代码。

setAllowedWith(array $allowed)

请参阅示例代码。

setDefaultSort(string $by, string $order = 'asc')

setDefaultWith(array $with)

请参阅示例代码。

modelQuery($query = null)

请参阅示例代码。

register()

在创建仓库时调用。这对于为子类仓库设置此 default_list_column 函数很有用。

protected function register()
{
    $this->default_list_column = function ($model) {
        return $model->full_name;
    };
}

shouldAuthorize(bool $value = true)

当设置为 true 时,在执行任何查询之前执行授权。

示例

Repository::for(Post::class)
    ->shouldAuthorize()
    ->update($post, ['author' => 'me');

interface ResourceContext

请参阅 ResourceContext 实现。

变更日志

请参阅 CHANGELOG 了解最近更改的更多信息。

安全

如果您发现任何与安全相关的问题,请通过电子邮件 m19n@pm.me 而不是使用问题跟踪器。

先前工作

致谢

许可协议

MIT 许可协议 (MIT)。有关更多信息,请参阅 许可文件

Laravel 包模板

此包是使用 Laravel 包模板 生成的。

贡献

请参阅 CONTRIBUTING 了解详细信息。

请参阅 DEVELOPING 了解入门帮助。

贡献者 ✨

感谢以下这些出色的人们 (emoji key)

此项目遵循 all-contributors 规范。欢迎任何形式的贡献!