Eloquent 有用的存储库吗?

上周我写了 关于 Eloquent 实体的 Repository 模板无用的文章然而,他答应告诉我如何部分地利用它来为他谋取利益。 为此,我将尝试分析该模板通常如何在项目中使用。 存储库所需的最少方法集:

<?php
interface PostRepository
{
    public function getById($id): Post;
    public function save(Post $post);
    public function delete($id);
}

然而,在实际项目中,如果决定使用存储库,通常会在其中添加检索记录的方法:

<?php
interface PostRepository
{
    public function getById($id): Post;
    public function save(Post $post);
    public function delete($id);

    public function getLastPosts();
    public function getTopPosts();
    public function getUserPosts($userId);
}

这些方法可以通过 Eloquent 作用域来实现,但是重载实体类并承担获取自身的责任并不是最好的主意,将这种责任转移到存储库类似乎是合乎逻辑的。 是这样吗? 我特地直观地将这个界面分为两部分。 方法的第一部分将用于写操作。

标准的写操作是:

  • 新对象的构建和挑战 发布存储库::保存
  • PostRepository::getById、实体操纵和召唤 发布存储库::保存
  • 通话 发布存储库::删除

写操作不使用获取方法。 在读取操作中,仅使用 get* 方法。 如果您读到 接口隔离原则 (信 I в SOLID),那么很明显我们的接口太大并且至少执行两个不同的职责。 是时候将其除以二了。 方法 获取标识符 两者都是必要的,但随着应用程序变得更加复杂,其实现将会有所不同。 我们稍后会看到这一点。 我在上一篇文章中写过 write 部分的无用之处,所以在这篇文章中我将简单地忘记它。

在我看来,Read 部分并不是那么无用,因为即使对于 Eloquent 来说,这里也可能有多种实现。 给班级起什么名字? 能 阅读帖子存储库,但是对于模板 存储库 他已经没有什么相关性了。 你可以只 后置查询:

<?php
interface PostQueries
{
    public function getById($id): Post;
    public function getLastPosts();
    public function getTopPosts();
    public function getUserPosts($userId);
}

使用 Eloquent 实现它非常简单:

<?php
final class EloquentPostQueries implements PostQueries
{
    public function getById($id): Post
    {
        return Post::findOrFail($id);
    }

    /**
    * @return Post[] | Collection
    */
    public function getLastPosts()
    {
        return Post::orderBy('created_at', 'desc')
            ->limit(/*some limit*/)
            ->get();
    }
    /**
    * @return Post[] | Collection
    */
    public function getTopPosts()
    {
        return Post::orderBy('rating', 'desc')
            ->limit(/*some limit*/)
            ->get();
    }

    /**
    * @param int $userId
    * @return Post[] | Collection
    */
    public function getUserPosts($userId)
    {
        return Post::whereUserId($userId)
            ->orderBy('created_at', 'desc')
            ->get();
    }
}

接口必须与实现相关联,例如 应用服务提供者:

<?php
final class AppServiceProvider extends ServiceProvider 
{
    public function register()
    {
        $this->app->bind(PostQueries::class, 
            EloquentPostQueries::class);
    }
}

这个类已经很有用了。 他通过减轻控制器或实体类的负担来实现自己的责任。 在控制器中可以这样使用:

<?php
final class PostsController extends Controller
{
    public function lastPosts(PostQueries $postQueries)
    {
        return view('posts.last', [
            'posts' => $postQueries->getLastPosts(),
        ]);
    }
} 

方法 帖子控制器::lastPosts 只是要求一些实施 帖子查询 并与它一起工作。 在我们链接的提供者中 后置查询 与类 EloquentPost查询 这个类将被替换到控制器中。

让我们想象一下我们的应用程序已经变得非常流行。 每分钟有数千名用户打开包含最新出版物的页面。 最受欢迎的出版物也被经常阅读。 数据库不能很好地处理此类负载,因此它们使用标准解决方案 - 缓存。 除了数据库之外,某些数据快照还存储在针对某些操作优化的存储中 - memcached или Redis的.

缓存逻辑通常没有那么复杂,但是在 EloquentPostQueries 中实现它并不是很正确(如果只是因为 单一责任原则)。 使用模板更自然 装饰者 并实现缓存作为主要操作的装饰:

<?php
use IlluminateContractsCacheRepository;

final class CachedPostQueries implements PostQueries
{
    const LASTS_DURATION = 10;

    /** @var PostQueries */
    private $base;

    /** @var Repository */
    private $cache;

    public function __construct(
        PostQueries $base, Repository $cache) 
    {
        $this->base = $base;
        $this->cache = $cache;
    }

    /**
    * @return Post[] | Collection
    */
    public function getLastPosts()
    {
        return $this->cache->remember('last_posts', 
            self::LASTS_DURATION, 
            function(){
                return $this->base->getLastPosts();
            });
    }

    // другие методы практически такие же
}

忽略界面 存储库 在构造函数中。 由于某种未知的原因,他们决定以这种方式命名 Laravel 中的缓存接口。

缓存后查询 仅实现缓存。 $this->缓存->记住 检查该条目是否在缓存中,如果不在,则调用回调并将返回值写入缓存。 剩下的就是将此类实现到应用程序中。 我们需要应用程序中的所有类来请求接口的实现 后置查询 开始接收类的实例 缓存后查询。 然而他本人 缓存后查询 构造函数必须接收一个类作为参数 EloquentPost查询因为如果没有“真正的”实现它就无法工作。 我们改变 应用服务提供者:

<?php
final class AppServiceProvider extends ServiceProvider 
{
    public function register()
    {
        $this->app->bind(PostQueries::class, 
            CachedPostQueries::class);

        $this->app->when(CachedPostQueries::class)
            ->needs(PostQueries::class)
            ->give(EloquentPostQueries::class);
    }
}

我所有的愿望都在提供者中很自然地描述。 因此,我们仅通过编写一个类并更改容器配置来实现对请求的缓存。 应用程序代码的其余部分没有改变。

当然,要完全实现缓存,还需要实现失效,使被删除的文章不会在网站上挂一段时间,而是立即被删除。 但这些都是小事。

底线:我们使用的不是一个模板,而是两个模板。 样本 命令查询职责分离 (CQRS) 提出在接口级别完全分离读写操作。 我通过 接口隔离原则,这表明我熟练地操纵模式和原则,并从另一个推导出另一个作为定理:)当然,并不是每个项目都需要这样的抽象来选择实体,但我会与您分享技巧。开发时,只需创建一个类即可 后置查询 通过 Eloquent 进行通常的实现:

<?php
final class PostQueries
{
    public function getById($id): Post
    {
        return Post::findOrFail($id);
    }

    // другие методы
}

当需要缓存时,通过一个简单的操作,您可以创建一个接口(或抽象类)来代替此类 后置查询,将其实现复制到类中 EloquentPost查询 并转到我之前描述的方案。 其余的应用程序代码不需要更改。

所有这些关于类、接口的技巧, 依赖注入 и 连续QRS 中详细描述 我的书《复杂 Web 应用程序的架构》。 为什么本文示例中的所有类都被标记为最终类,这个谜题也有一个解决方案。

来源: habr.com

添加评论