将存储库模式(和查询范围)与关系一起使用

Posted

技术标签:

【中文标题】将存储库模式(和查询范围)与关系一起使用【英文标题】:Using the Repository Pattern (and Query Scopes) with Relations 【发布时间】:2014-08-08 22:42:32 【问题描述】:

在 Laravel 4 中,查询范围可用于所有查询(包括由关系查询生成的查询)。这意味着对于以下(示例)模型:

客户.php:

<?php
class Customer extends Eloquent 
    public function order()  return $this->hasMany('Order'); 

Order.php:

<?php
class Order extends Eloquent 
   public function scopeDelivered($query)  return $query->where('delivered', '=', true); 
   public function customer()  return $this->belongsTo('Customer'); 

以下两项工作:

var_dump(Order::delivered()->get()); // All delivered orders
var_dump(Customer::find(1)->orders()->delivered()->get()); // only orders by customer #1 that are delivered

这在控制器内很有用,因为不必重复查找已交付订单的查询逻辑。

不过,最近,我确信存储库模式不仅适用于关注点分离,而且适用于 ORM/DB 切换的可能性或添加缓存等中间件的必要性。存储库感觉很自然,因为现在不是让范围膨胀我的模型,而是关联的查询是存储库的一部分(这更有意义,因为这自然是集合的方法而不是项目)。

例如,

<?php
class EloquentOrderRepository 
    protected $order;

    public function __construct(Order $order)  $this->order = $order; 
    public function find($id)  /* ... */ 
    /* etc... */
    public function allDelievered()  return $this->order->where('delivered', '=', true)->get(); 

但是,现在我重复了交付的范围,因此为了避免违反 DRY,我将其从模型中删除(根据上述理由,这似乎是合乎逻辑的)。但是现在,我不能再在关系上使用范围(例如$customer-&gt;orders()-&gt;delivered())。我在这里看到的唯一解决方法是以某种方式使用Relation 基类中的预制查询(类似于传递给模型中范围的内容)来实例化存储库。但这涉及更改(和覆盖)大量代码和默认行为,并且似乎使事情变得比应有的更加耦合。

考虑到这种困境,这是对存储库的滥用吗?如果不是,我的解决方案是恢复我想要的功能的唯一方法吗?还是模型中的范围不够紧密耦合来证明这个额外的代码是合理的?如果作用域不是紧密耦合的,那么有没有办法在保持 DRY 的同时使用存储库模式和作用域?

注意:我知道 some similar questions 在类似的 topics 上,但它们都没有解决这里提出的问题,由不依赖于存储库的关系生成的查询。

【问题讨论】:

我绝不是 PHP 架构专家,但我认为您非常接近。您是否尝试在存储库的构造函数中注入模型?那么public function allDelievered() return $this-&gt;order-&gt;where('delivered', '=', true)-&gt;get(); 应该适合你。 @user3158900 你是对的。在我设计的示例中,我错误地实现了存储库模式。我已经更新了我的帖子以反映您的正确修正。谢谢!不幸的是,这仍然会给不使用我的存储库的关系返回的查询留下问题。 做了一些研究,这可能对你来说是完美的。 culttt.com/2014/03/17/eloquent-tricks-better-repositories @user3158900 有趣的是那篇文章说服我尝试存储库模式。不幸的是,它也不能解决 Repository 方法在 Eloquent 关系返回的查询上不可用的问题。为了帮助该主题的任何未来读者,我将包含一些我发现的与该主题相关的阅读材料。 【参考方案1】:

我设法找到了解决方案。它相当老套,我不确定我是否认为它可以接受(它以可能不打算使用的方式使用了很多东西)。总而言之,该解决方案允许您将范围移动到存储库。每个存储库(在实例化时)都会启动一次,在此过程中,所有作用域方法都被提取出来,并通过 Illuminate\Database\Eloquent\ScopeInterface 的方式添加到由 eloquent 模型(通过宏)创建的每个查询中。

(Hack-y)解决方案

存储库模式实现

app/lib/PhpMyCoder/Repository/Repository.php:

<?php namespace PhpMyCoder\Repository;

interface Repository 

    public function all();

    public function find($id);

app/lib/PhpMyCoder/Repository/Order/OrderRepository.php:

<?php namespace PhpMyCoder\Repository\Order;

interface OrderRepository extends PhpMyCoder\Repository\Repository 

添加 Eloquent 存储库(和 hack)

app/lib/PhpMyCoder/Repository/Order/EloquentOrderRepository.php:

<?php namespace PhpMyCoder\Repository\Order;

use PhpMyCoder\Repository\EloquentBaseRepository;

class EloquentOrderRepository extends EloquentBaseRepository implements OrderRepository 

    public function __construct(\Order $model) 

        parent::__construct($model);
    

    public function finished() 

        return $this->model->finished()->get();
    

    public function scopeFinished($query) 

        return $query->where('finished', '=', true);
    

注意存储库如何包含通常存储在Order 模型类中的范围。在数据库中(对于本示例),Order 需要有一个布尔列 finished。我们将在下面介绍EloquentBaseRepository 的详细信息。

app/lib/PhpMyCoder/Repository/EloquentBaseRepository.php:

<?php namespace PhpMyCoder\Repository;

use Illuminate\Database\Eloquent\Model;

abstract class EloquentBaseRepository implements Repository 

    protected $model;

    // Stores which repositories have already been booted
    protected static $booted = array();

    public function __construct(Model $model) 

        $this->model = $model;

        $this->bootIfNotBooted();
    

    protected function bootIfNotBooted() 

        // Boot once per repository class, because we only need to
        // add the scopes to the model once
        if(!isset(static::$booted[get_class($this)])) 

            static::$booted[get_class($this)] = true;
            $this->boot();
        
    

    protected function boot() 

        $modelScope = new ModelScope();  // covered below
        $selfReflection = new \ReflectionObject($this);

        foreach (get_class_methods($this) as $method) 

            // Find all scope methods in the repository class
            if (preg_match('/^scope(.+)$/', $method, $matches)) 

                $scopeName = lcfirst($matches[1]);
                // Get a closure for the scope method
                $scopeMethod = $selfReflection->getMethod($method)->getClosure($this)->bindTo(null);

                $modelScope->addScope($scopeName, $scopeMethod);
            
        

        // Attach our special ModelScope to the Model class
        call_user_func([get_class($this->model), 'addGlobalScope'], $modelScope);
    

    public function __call($method, $arguments) 

        // Handle calls to scopes on the repository similarly to
        // how they are handled on Eloquent models
        if(method_exists($this, 'scope' . ucfirst($method))) 

            return call_user_func_array([$this->model, $method], $arguments)->get();
        
    

    /* From PhpMyCoder\Repository\Order\OrderRepository (inherited from PhpMyCoder\Repository\Repository) */
    public function all() 

        return $this->model->all();
    

    public function find($id) 

        return $this->model->find($id);
    

每次第一次实例化存储库类的实例时,我们都会启动存储库。这涉及将存储库上的所有“范围”方法聚合到 ModelScope 对象中,然后将其应用于模型。 ModelScope 会将我们的作用域应用于模型创建的每个查询(如下所示)。

app/lib/PhpMyCoder/Repository/ModelScope.php:

<?php namespace PhpMyCoder\Repository;

use Illuminate\Database\Eloquent\ScopeInterface;
use Illuminate\Database\Eloquent\Builder;

class ModelScope implements ScopeInterface 

    protected $scopes = array(); // scopes we need to apply to each query

    public function apply(Builder $builder) 

        foreach($this->scopes as $name => $scope) 

            // Add scope to the builder as a macro (hack-y)
            // this mimics the behavior and return value of Builder::callScope()
            $builder->macro($name, function() use($builder, $scope) 

                $arguments = func_get_args();

                array_unshift($arguments, $builder->getQuery());

                return call_user_func_array($scope, $arguments) ?: $builder->getQuery();
            );
        
    

    public function remove(Builder $builder) 

        // Removing is not really possible (no Builder::removeMacro),
        // so we'll just overwrite the method with one that throws a
        // BadMethodCallException

        foreach($this->scopes as $name => $scope) 

            $builder->macro($name, function() use($name) 

                $className = get_class($this);
                throw new \BadMethodCallException("Call to undefined method $className::$name()");
            );
        
    

    public function addScope($name, \Closure $scope) 

        $this->scopes[$name] = $scope;
    

ServiceProvider 和 Composer 文件

app/lib/PhpMyCoder/Repository/RepositoryServiceProvider.php:

<?php namespace PhpMyCoder\Repository;

use Illuminate\Support\ServiceProvider;
use PhpMyCoder\Repository\Order\EloquentOrderRepository;

class RepositoryServiceProvider extends ServiceProvider 

    public function register() 

        // Bind the repository interface to the eloquent repository class
        $this->app->bind('PhpMyCoder\Repository\Order\OrderRepository', function() 

            return new EloquentOrderRepository(new \Order);
        );
    


请务必将此服务提供者添加到app.php 配置中的providers 数组中:

'PhpMyCoder\Repository\RepositoryServiceProvider',

然后将app/lib 添加到作曲家的自动加载中

"autoload": 
    "psr-0": 
        "PhpMyCoder\\": "app/lib" 
    ,
    /* etc... */
,

这需要composer.phar dump-autoload

模型

app/models/Customer.php:

<?php

class Customer extends Eloquent 

    public function orders() 

        return $this->hasMany('Order');
    

请注意,为简洁起见,我已排除为 Customer 编写存储库,但在实际应用程序中您应该这样做。

app/model/Order.php:

<?php

class Order extends Eloquent 

    public function customer() 

        return $this->belongsTo('Customer');
    

注意示波器不再存储在Order 模型中。这更具有结构意义,因为集合级别(存储库)应该负责适用于所有订单的范围,而 Order 应该只关注特定于一个订单的细节。为了让这个演示工作,订单必须有一个整数外键 customer_idcustomers.id 和一个布尔标志 finished

在控制器中的使用

app/controllers/OrderController.php:

<?php

// IoC will handle passing our controller the proper instance
use PhpMyCoder\Repository\Order\OrderRepository;

class OrderController extends BaseController 

    protected $orderRepository;

    public function __construct(OrderRepository $orderRepository) 

        $this->orderRepository = $orderRepository;
    

    public function test() 

        $allOrders = $this->orderRepository->all();

        // Our repository can handle scope calls similarly to how
        // Eloquent models handle them
        $finishedOrders = $this->orderRepository->finished();

        // If we had made one, we would instead use a customer repository
        // Notice though how the relation query also has order scopes
        $finishedOrdersForCustomer = Customer::find(1)->orders()->finished();
    

我们的存储库不仅包含子模型的范围,更多的是SOLID。它们还能够像真正的 Eloquent 模型一样处理对范围的调用。他们将所有范围添加到模型创建的每个查询中,以便您在检索相关模型时可以访问它们。

这种方法的问题

大量代码用于小功能:可以说太多,无法实现预期结果 很老套:Illuminate\Database\Eloquent\BuilderIlluminate\Database\Eloquent\ScopeInterface(与 Illuminate\Database\Eloquent\Model::addGlobalScope 结合使用)上的宏可能以不符合预期的方式使用 它需要存储库的实例化(主要问题):如果您在 CustomerController 中并且您只有实例化的 CustomerRepository,$this-&gt;customerRepository-&gt;find(1)-&gt;orders()-&gt;finished()-&gt;get() 将无法按预期工作(finished() 宏/除非您实例化 OrderRepository,否则不会将范围添加到每个 Order 查询中。

我会调查是否有更优雅的解决方案(可以解决上面列出的问题),但这是迄今为止我能找到的最佳解决方案。

存储库模式的相关资源

Creating flexible Controllers in Laravel 4 using Repositories Eloquent tricks for better Repositories

【讨论】:

以上是关于将存储库模式(和查询范围)与关系一起使用的主要内容,如果未能解决你的问题,请参考以下文章

将存储库模式与文档数据库一起使用是不是有意义?

将具有单独日期范围的表格组合在一起而不重叠数据 - Access

将多个范围与 spotipy 一起使用

我可以将 Parallel.For 与 sql 命令一起使用吗?

如何将范围与变量一起使用 - 1004 错误不断出现

存储库模式 - 如何理解它以及它如何与“复杂”实体一起使用?