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

时间:2014-06-18 14:59:41

标签: php laravel orm laravel-4 repository-pattern

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

Customer.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->orders()->delivered())。我在这里看到的唯一解决方法是在Relation基类中使用预先生成的查询(类似于传递给模型中的作用域的内容)以某种方式实例化存储库。但这涉及到更改(并覆盖)大量代码和默认行为,并且似乎使事情变得更加耦合。

鉴于这种困境,这是滥用存储库吗?如果没有,我的解决方案是重新获得我想要的功能的唯一方法吗?或者是否在模型中的范围不够紧密耦合以证明这个额外的代码?如果范围不是紧密耦合,那么有没有办法同时使用存储库模式和范围,同时仍然是DRY?

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

1 个答案:

答案 0 :(得分:1)

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

(Hack-y)解决方案

存储库模式实现

应用/ LIB / PhpMyCoder /存储库/ Repository.php:

<?php namespace PhpMyCoder\Repository;

interface Repository {

    public function all();

    public function find($id);
}

应用/ LIB / PhpMyCoder /存储库/订购/ OrderRepository.php:

<?php namespace PhpMyCoder\Repository\Order;

interface OrderRepository extends PhpMyCoder\Repository\Repository {}

添加Eloquent Repositories(和hack)

应用/ LIB / PhpMyCoder /存储库/订购/ 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的详细信息。

应用/ LIB / PhpMyCoder /存储库/ 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会将我们的范围应用于模型创建的每个查询(如下所示)。

应用/ LIB / PhpMyCoder /存储库/ 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文件

应用/ LIB / PhpMyCoder /存储库/ 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);
        });
    }

}

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

'PhpMyCoder\Repository\RepositoryServiceProvider',

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

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

这需要composer.phar dump-autoload

模型

应用/模型/ Customer.php:

<?php

class Customer extends Eloquent {

    public function orders() {

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

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

应用/模型/ Order.php:

<?php

class Order extends Eloquent {

    public function customer() {

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

请注意范围不再存储在Order模型中。这具有更多结构意义,因为集合级别(存储库)应负责应用于所有订单的范围,而Order应仅关注特定于一个订单的详细信息。要使此演示生效,订单必须具有整数外键customer_idcustomers.id和布尔标志finished

控制器中的用法

应用/控制器/ 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模型一样。并且它们将所有范围添加到模型创建的每个查询中,以便在检索相关模型时可以访问它们。

此方法的问题

  • 许多功能很少的代码:可以说太多了,无法实现理想的结果
  • 这是hacky: Illuminate\Database\Eloquent\BuilderIlluminate\Database\Eloquent\ScopeInterface上的宏(与Illuminate\Database\Eloquent\Model::addGlobalScope一起使用)可能会以不应该的方式使用
  • 需要实例化存储库(MAJOR ISSUE):如果您在CustomerController内并且只有实例化的CustomerRepository,$this->customerRepository->find(1)->orders()->finished()->get()将无法按预期工作(除非您实例化finished()),否则Order宏/范围不会添加到每个OrderRepository查询中。

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

存储库模式的相关资源