Laravel 中的存储库模式(Repository)

Laravel

在大多数 web 应用程序中,访问数据库占了代码库的很大一部分。为了避免在我们应用程序逻辑上掺杂 SQL 查询,我们依赖抽象,它隐藏了 PHP 方法背后的数据访问机制。

有几种模式可以结构化数据访问,“Active Record” 和 “Repository” 是最著名的两种。在这篇博文中,我将在 Laravel 框架 的背景下具体解释它们。关于使用 Repository 模式的优点和缺点的讨论将在单独的博客文章中进行。

活动记录

默认情况下,Laravel 使用 Active Record 模式。每个 Laravel 程序员都直观地使用它,因为它是在抽象的 Model 基类中实现的,而模型通常从它继承而来。让我们来看一个例子:

use Illuminate\Database\Eloquent\Model;

/**
 * @property int    $id
 * @property string $first_name
 * @property string $last_name
 */
class Person extends Model {
}

// --- 使用:

$person = new Person();
$person->first_name = 'Jack';
$person->last_name = 'Smith';
$person->save();

当然,您可以读写您在 Person 上创建的属性。 但是要保存模型,您也可以 直接在模型上调用方法。 不需要另一个对象——模型已经提供了访问相应数据库表的所有方法。

这意味着,域模型将您的自定义属性和方法与同一类中的所有数据访问方法相结合。 第二部分是通过继承 Model 来实现的。

要点:

  • 👉 Active Record 结合 域模型与数据访问功能。
  • 👉 Laravel 使用 Active Record 模式并通过 Model 类实现它。

Repository

Repository 模式是 Active Record 模式的替代方案。它还提供了处理数据访问的抽象。但更广泛地说,它可以被视为域对象的概念性存储库或集合。

与活动记录模式相反,存储模式将数据库访问与域模型分离。它提供了一个高级接口,你可以在其中创建、读取、更新和删除域模型,而不必考虑实际的底层数据存储。

底层的存储库可以通过构建和执行 SQL 查询访问数据库,通过 REST API 访问远程系统,或者仅仅管理包含所有域模型的内存数据结构。这对测试很有用。存储库模式关键部分是它为其余代码提供的高级接口。

要点:

  • 👉 存储库表示域对象的概念集合。
  • 👉 它只负责用高级接口封装数据访问。
  • 👉 Laravel 没有提供实现存储库模式的特定帮助程序

在 Laravel 中实现 Repository 模式时,我主要看到两种变体。

变体1:特定方法

在第一个变体中,存储库方法是重点和特定的。名称解释了调用者获得的内容,用于参数化底层查询的选项是有限的。

class InvoiceRepository {

    public function findAllOverdue(Carbon $since, int $limit = 10): Collection {
        return Invoice::where('overdue_since', '>=', $since)
            ->limit($limit)
            ->orderBy('overdue_since')
            ->get();
    }

    public function findInvoicedToCompany(string $companyId): Collection {
        return Invoice::where('company_id', $companyId)
            ->orderByDesc('created_at')
            ->get();
    }
}

这种方法的优势在于方法的表现力。阅读代码时,很清楚从方法中期望什么以及如何调用它们。这会导致更少的错误。 Repository 方法很容易测试,因为参数有限。

这种方法的一个缺点是,最终可能会在存储库中使用大量的方法。由于方法无法轻松重用,因此必须为新用例添加其他方法。

要点:

  • 👉 存储模式可以通过提供特定方法的类来实现
  • 👉 每个方法包装一个查询,只公开必要的参数
  • 👉 优点: 可读性和可测试性
  • 👉 缺点:  缺乏灵活性和较低的可重用性

变式2: 一般方法

另一方面的方法是提供一般的方法。这导致了方法的减少。但是这些方法有一个很大的 API 曲面,因为每个方法都可以使用不同的参数组合来调用。

其中的关键问题是参数表示。这种表示应该引导调用方理解方法签名并避免无效的输入。为此,您可以引入一个特殊的类,例如使用 Query Object 模式。

但是我在实践中经常看到的是标量参数和 PHP 数组的混合。调用方可以传递完全无效的输入,仅类型数据并不能说明要传递什么。但是如果使用得当,这种轻量级的方法可以避免更繁琐的抽象。

class InvoiceRepository {

    public function find(array $conditions, string $sortBy = 'id', string $sortOrder = 'asc', int $limit = 10): Collection {
        return Invoice::where($conditions)
            ->orderBy($sortBy, $sortOrder)
            ->limit($limit)
            ->get();
    }
}

// --- 使用:

$repo = new InvoiceRepository();
$repo->find(['overdue_since', '>=', $since], 'overdue_since', 'asc');
$repo->find(['company_id', '=', $companyId], 'created_at', 'asc', 100);

这种方法减轻了第一种方法的问题:你可以得到更少的 Repository 方法,这些方法更灵活,并且可以更频繁地重用。

从消极的方面看,Repository 变得更加难以测试,因为有更多的案例需要覆盖。方法签名更难理解,正因为如此,调用者可能会犯更多错误。此外,还将引入某种查询对象表示形式。无论它是显式的还是隐式的(比如数组),您的 Repository 实现及其调用者都将与它耦合。

要点:

  • 👉 存储库模式可以通过提供通用方法的类实现。
  • 👉 难点在于方法参数的表示。
  • 👉 优点: 更大的灵活性和更高的可复用性。
  • 👉 缺点: 更难测试,可读性差,与参数表示耦合。

当然,这两种方法可以结合起来使用。也许你想要一些特定的方法用于复杂的查询,而一些通用的方法用于简单的 where 查询。

实现

现在,我们来谈谈如何实现方法体。

在上面的例子中,我使用了 Model 类的方法来获得对 Eloquent 查询构造器的访问。所以 Repository 的实现实际上使用了 Active Record 模式作为实现。

你不需要这样做。你可以使用 DB facade 来获得一个查询构建器,同时避免使用 Model 类。或者你可以直接编写 SQL 查询:

class InvoiceRepository {

    public function findAllOverdue(Carbon $since, int $limit = 10): Collection {
        return DB::table('invoices')
            ->where('overdue_since', '>=', $since)
            ->limit($limit)
            ->orderBy('overdue_since')
            ->get();
    }

    public function findInvoicedToCompany(string $companyId): Collection {
        return DB::select('SELECT * FROM invoices
                           WHERE company_id = ?
                           ORDER BY created_at
                           LIMIT 100', [$companyId]);
    }
}

存储模式的优点是,实现可以是任何东西,只要它满足接口。你还可以管理内存中的对象或者包(和缓存)一个 API。

但是最常见的是,底层数据存储是一个 SQL 数据库。要访问它,你可以根据每个方法选择最佳实现。对于性能关键的或者复杂的查询,你可能希望直接使用 SQL 语句。更简单的查询可以使用 Eloquent 查询生成器。

当你没有使用 模型 类来实现你的 Repository ,你可能会考虑在模型中不继承它。但是这个方法违反了很多内置的 Laravel 魔术方法,在我看来并不是一个好的方法。

要点:

  • 👉 存储库模式很灵活,允许使用各种实现技术。
  • 👉 在 Laravel 中,当访问数据库时,Eloquent 查询构建器是一个实用的选择。

接口

你的另一个选择是,是否要引入一个接口。上面的例子可以用一个接口和一个或多个实现来分隔:

// --- 接口:

public interface InvoiceRepositoryInterface {

    public function findAllOverdue(Carbon $since, int $limit = 10): Collection;

    public function findInvoicedToCompany(string $companyId): Collection;
}

// --- 具体的类,实现了该接口

class InvoiceRepository implements InvoiceRepositoryInterface {

    public function findAllOverdue(Carbon $since, int $limit = 10): Collection {
        // 实现
    }

    public function findInvoicedToCompany(string $companyId): Collection {
        // 实现
    }
}

添加接口是一种额外的间接方法,并不一定是好的。如果您的应用程序是 Repository 的唯一用户,并且您不希望它有多个实现,那么我不认为引入接口有什么意义。对于测试,Repository 可以用 PHPUnit 模拟,只要它不被标记为 final

要点:

  • 👉仓库模式灵活,支持多种实现技术。
  • 👉 在Laravel中,Eloquent查询构建器是访问数据库时的一种实用选择。

接口

另一个选择是是否引入接口。以上示例可以分为一个或多个接口实现:

// --- Interface:

public interface InvoiceRepositoryInterface {

    public function findAllOverdue(Carbon $since, int $limit = 10): Collection;

    public function findInvoicedToCompany(string $companyId): Collection;
}

// --- Concrete class, implementing the interface:

class InvoiceRepository implements InvoiceRepositoryInterface {

    public function findAllOverdue(Carbon $since, int $limit = 10): Collection {
        // implementation
    }

    public function findInvoicedToCompany(string $companyId): Collection {
        // implementation
    }
}

添加接口是一种额外的间接方式,不一定很好。如果您的应用程序是存储库的唯一用户,并且您不希望有多个实现,那么我看不出引入接口有什么意义。为了测试,存储库可以用phpUnit映射, 只要它没有标记为final.

如果你知道你将有多个实现,你应该使用一个接口。如果你正在编写一个将在多个项目中使用的 。或者你想要测试一个特殊的 Repository 实现,那么可能会发生不同的实现。

为了从 Laravel 的依赖注入中获益,你必须将具体的实现绑定到接口上。这必须在服务提供者的注册方法中完成。

use Illuminate\Support\ServiceProvider;

class RepositoryServiceProvider extends ServiceProvider {

    public function register(): void {
        $this->app->bind(InvoiceRepositoryInterface::class, InvoiceRepository::class);
    }
}

要点:

  • 👉一个接口可以进一步 解耦 从代码的其余部分获取代码库。
  • 👉 当您期望有多个具体类实现它时,使用 Repository 接口。
  • 👉 在 Laravel 中,将具体类绑定到服务提供者中的接口。

👉 让我知道您将如何在 Laravel 实现存储库模式。

👉 下一篇文章中,我将探讨使用存储库模式的优点。

本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://dev.to/davidrjenni/repository-pa...

译文地址:https://learnku.com/laravel/t/62587

本文为协同翻译文章,如您发现瑕疵请点击「改进」按钮提交优化建议
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 4

最近很火哦?

2年前 评论

我觉得应该是interface→service实现interface并且调用Repository(数据仓库),数据仓库调用模型:
例如,定义查询用户的接口,我可以这样:

//用户查询的接口
Interface UserInfoInterface{
    public function getUserInfo($id);
}

//实现接口的service类:
class UserInfoService implements UserInfoInterface{
    protected $userInfoRepository;
    public function __construct(UserInfoRepository $userInfoRepository){
        $this->userInfoRepository = $userInfoRepository;
    }
    public function getUserInfo($id){
        return $this->userInfoRepository->getUserInfo($id);
    }
}

//数据仓库
use App\Models\UserInfo;

class UserInfoRepository{
    public function getUserInfo($id){
        return UserInfo::query()->find($id);
    }
}

//App\Providers\AppServiceProvider.php的register()中进行接口注入,这一步的作用可以省去在构造函数中依赖注入的方式引用service,在程序中任意地方都可以通过app(UserInfoService::class)的方式调用。

public function register()
{
  $this->app->bind(UserInfoInterface::class, UserInfoService::class);
}

//控制器调用service
class UserController extends BaseController
{
  private $userInfo;
  public function __construct()
  {
      //使用app()的方式,可以调用多个sevice,而不用在参数中进行注入
      $this->userInfo = app(UserInfoService::class);
  }
  public function getUserInfo($id){
      return->this->userInfo($id)
  }
}

2年前 评论

个人感觉,完全没必要。

2年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!