Heim  >  Artikel  >  PHP-Framework  >  Erklären Sie im Detail, wozu das Repository-Muster (Repository) in Laravel dient.

Erklären Sie im Detail, wozu das Repository-Muster (Repository) in Laravel dient.

藏色散人
藏色散人nach vorne
2021-11-18 14:25:142049Durchsuche
Warum den Repository-Modus in Laravel verwenden?
  • In meinem
  • letzten Artikel
habe ich erklärt, was das Repository Pattern ist, wie es sich vom Active Record Pattern unterscheidet und wie man es in Laravel implementiert. Jetzt möchte ich näher darauf eingehen, warum Sie das Repository-Muster verwenden sollten.

Mir ist in den Kommentaren des vorherigen Beitrags aufgefallen, dass Repository-Muster ein kontroverses Thema in der Laravel-Community ist. Manche Leute sehen keinen Grund, es zu verwenden und bleiben beim integrierten Active Record-Modus. Andere bevorzugen andere Methoden, um den Datenzugriff von der logischen Domäne zu trennen. Bitte beachten Sie, dass ich diese Meinungen respektiere und diesem Thema einen kommenden Blogbeitrag widmen werde.

Mit diesem Haftungsausschluss wollen wir die Vorteile der Verwendung des Repository-Musters verstehen. [Verwandte Tutorial-Empfehlung: laravel]

Prinzip der Einzelverantwortung

Das Prinzip der Einzelverantwortung ist das Hauptunterscheidungsmerkmal zur Unterscheidung des Active Record-Modus und des Repository-Modus. Modellklassen enthalten bereits Daten und stellen Methoden für Domänenobjekte bereit. Bei Verwendung des Active Record-Musters ist der Datenzugriff eine zusätzliche Verantwortung, die eingeführt wird. Dies möchte ich an folgendem Beispiel veranschaulichen:
/**
 * @property string $first_name
 * @property int    $company_id
 */
class Employee extends Model {}

$jack = new Employee();
$jack->first_name = 'Jack';
$jack->company_id = $twitterId;
$jack->save();
Obwohl das Domänenmodell und die Datenzugriffstechnologie gemischte Verantwortlichkeiten haben, ergibt es intuitiv Sinn. In unserer Anwendung müssen die Mitarbeiter irgendwie in der Datenbank gespeichert werden. Warum also nicht save() für das Objekt aufrufen? Ein einzelnes Objekt wird in eine einzelne Datenzeile umgewandelt und gespeichert.

Aber gehen wir noch einen Schritt weiter und sehen, was wir sonst noch mit Mitarbeitern machen können:

$jack->where('first_name', 'John')->firstOrFail()->delete();
$competition = $jack->where('company_id', $facebookId)->get();

Jetzt wird es unintuitiv und widerspricht sogar unserem Domänenmodell. Warum sollte Jack plötzlich einen anderen Mitarbeiter löschen, der vielleicht sogar bei einem anderen Unternehmen arbeitet? Oder warum konnte er Facebook-Mitarbeiter herbeiholen? save()。单个对象被转化成单个数据行并存储。

但是,让我们更进一步,看看我们还能对员工做些什么:

class InvoiceController {

    public function index(): View {
        return view('invoices.index', [
            'invoices' => Invoice::where('overdue_since', '>=', Carbon::now())
                ->orderBy('overdue_since')
                ->paginate()
        ]);
    }}

现在,它变得不直观,甚至违背了我们的域模型。 为什么 Jack 会突然删除另一个甚至可能在不同公司工作的员工? 或者他为什么能把 Facebook 的员工拉过来?

当然,这个例子是人为设计的,但它仍然显示了 Active Record 模式如何不允许有意的域模型。 员工与所有员工列表之间的界限变得模糊。 您始终必须考虑该员工是被用作实际员工还是作为访问其他员工的机制。

仓库模式通过强制执行这个基本分区来解决这个问题。它的唯一用途是标识域对象的合集,而不是域对象的本身。

要点:

  • 通过将所有域对象的集合与单个域对象分离, 仓库模式体现了单一责任原则

不要重复自己 (DRY)

一些项目将数据库查询洒遍了整个项目。下面是一个例子,我们从数据库中获取列表,并在 Blade 视图中显示他们。

class InvoiceController {

    public __construct(private InvoiceRepository $repo) {}

    public function index(): View {
        return view('invoices.index', [
            'invoices' => $repo->paginateOverdueInvoices()
        ]);
    }}

当这样的查询遍得更加复杂并且在多个地方使用时,考虑将其提取到 Repository 方法中。

存储库模式通过将重复查询打包到表达方法中来帮助减少重复查询。如果必须调整查询,只需更改一次即可。

class InvoiceController {
    public function index(int $companyId): View {
        return view(
            'invoices.index',
            ['invoices' => Invoice::where('company_id', $companyId)->get()]
        );
    }}

现在查询只实现一次,可以单独测试并在其他地方使用。此外,单一责任原则再次发挥作用,因为控制器不负责获取数据,而只负责处理HTTP请求和返回响应。

Takeaway:

  • 存储库模式有助于减少重复查询

依赖反转

解释 Dependency Inversion Principle 值得发表自己的博客文章。我只是想说明存储库可以启用依赖项反转。

在对组件进行分层时,通常较高级别的组件依赖于较低级别的组件。 例如,控制器将依赖模型类从数据库中获取数据:

interface InvoiceRepository {
    public function findByCompanyId($companyId): Collection;}class InvoiceController {
    public function __construct(private InvoiceRepository $repo) {}

    public function index(int $companyId): View {
        return view(
            'invoices.index',
            ['invoices' => $this->repo->findByCompanyId($companyId)]
        );
    }}class EloquentInvoiceRepository implements InvoiceRepository {
    public function findByCompanyId($companyId): Collection {
        // 使用 Eloquent 查询构造器实现该方法
    }}

依赖关系是自上而下的,紧密耦合的。 InvoiceController 取决于具体的 Invoice

Natürlich ist dieses Beispiel gekünstelt, aber es zeigt dennoch, dass das Active Record-Muster keine absichtliche Domänenmodellierung zulässt. Die Grenze zwischen Mitarbeitern und der Liste aller Mitarbeiter verschwimmt. Es muss immer berücksichtigt werden, ob der Mitarbeiter als tatsächlicher Mitarbeiter oder als Mechanismus für den Zugriff auf andere Mitarbeiter genutzt wird.

Der Warehouse-Modus löst dieses Problem, indem er diese grundlegende Partitionierung erzwingt. Der einzige Zweck besteht darin, eine Sammlung von Domänenobjekten zu identifizieren, nicht die Domänenobjekte selbst.

Wichtige Punkte:

🎜🎜🎜Durch die Entkopplung einer Sammlung aller Domänenobjekte von einem einzelnen Domänenobjekt 🎜Das Warehouse-Muster verkörpert das Prinzip der Einzelverantwortung🎜. 🎜🎜🎜🎜🎜Wiederholen Sie sich nicht (DRY)🎜🎜Einige Projekte verteilen Datenbankabfragen über das gesamte Projekt. Unten sehen Sie ein Beispiel, in dem wir eine Liste aus der Datenbank abrufen und sie in einer Blade-Ansicht anzeigen. 🎜
interface InvoiceRepository {
    public function findById(int $id): Invoice;}class InvoiceCacheRepository implements InvoiceRepository {

    public function __construct(
        private InvoiceRepository $repo,
        private int $ttlSeconds
    ) {}

    public function findById(int $id): Invoice {
        return Cache::remember(
            "invoice.$id",
            $this->ttlSeconds,
            fn(): Invoice => $this->repo->findById($id)
        );
    }}class EloquentInvoiceRepository implements InvoiceRepository {

    public function findById(int $id): Invoice { /* 从数据库中取出 $id */ }}// --- 用法:$repo = new InvoiceCacheRepository(
    new EloquentInvoiceRepository(););
🎜Wenn eine Abfrage wie diese komplexer wird und an mehreren Stellen verwendet wird, sollten Sie erwägen, sie in eine Repository-Methode zu extrahieren. 🎜🎜Das Repository-Muster trägt dazu bei, doppelte Abfragen zu reduzieren, indem es sie in Ausdrucksmethoden verpackt. Wenn Sie die Abfrage anpassen müssen, müssen Sie diese nur einmal ändern. 🎜
class InMemoryInvoiceRepository implements InvoiceRepositoryInterface {

    private array $invoices;

    // implement the methods by accessing $this->invoices...
    }
    // --- Test Case:
    $repo = new InMemoryInvoiceRepository();
    $service = new InvoiceService($repo);
🎜Jetzt werden Abfragen nur einmal implementiert und können isoliert getestet und an anderer Stelle verwendet werden. Darüber hinaus kommt wieder das Single-Responsibility-Prinzip zum Tragen, da der Verantwortliche nicht für den Erhalt der Daten verantwortlich ist, sondern nur für die Verarbeitung der HTTP-Anfrage und die Rücksendung der Antwort. 🎜🎜🎜Takeaway: 🎜🎜🎜🎜Repository-Muster hilft, doppelte Abfragen zu reduzieren 🎜🎜🎜🎜🎜Abhängigkeitsinversion🎜🎜Die Erklärung des Abhängigkeitsinversionsprinzips verdient einen eigenen Blogbeitrag. Ich wollte nur veranschaulichen, dass ein Repository eine Abhängigkeitsumkehr ermöglichen kann. 🎜🎜Bei der Schichtung von Komponenten hängen Komponenten höherer Ebenen normalerweise von Komponenten niedrigerer Ebenen ab. Beispielsweise verlässt sich der Controller auf die Modellklasse, um Daten aus der Datenbank abzurufen: 🎜
$companyId = 42;

/** @var InvoiceRepository&MockObject */
$repo = $this->createMock(InvoiceRepository::class);

$repo->expects($this->once())
    ->method('findInvoicedToCompany')
    ->with($companyId)
    ->willReturn(collect([ /* invoices to return in the test case */ ]));

$service = new InvoiceService($repo);

$result = $service->calculateAvgInvoiceAmount($companyId);

$this->assertEquals(1337.42, $result);
🎜Die Abhängigkeit ist von oben nach unten und eng gekoppelt. InvoiceController hängt von der spezifischen Invoice-Klasse ab. Es ist schwierig, diese beiden Klassen zu entkoppeln, indem man sie beispielsweise separat testet oder den Speichermechanismus ersetzt. Durch die Einführung der Repository-Schnittstelle können wir eine Abhängigkeitsumkehr erreichen: 🎜rrreee🎜Controller hängt jetzt nur noch von der Repository-Schnittstelle ab, genau wie die Repository-Implementierung 🎜Diese beiden Klassen hängen jetzt nur noch von einer Abstraktion ab, wodurch die Kopplung reduziert wird im nächsten Abschnitt Dies bringt weitere Vorteile mit sich, wie in 🎜🎜🎜Takeaway:🎜🎜 erläutert
  • 存储库模式作为一种抽象类,支持依赖反转.

抽象类

存储库 提高了可读性 因为复杂的操作被具有表达性名称的高级方法隐藏了.

访问存储库的代码与底层数据访问技术分离. 如有必要,您可以切换实现,甚至可以省略实现,仅提供 Repository 接口。 这对于旨在与框架无关的库来说非常方便。

OAuth2 服务包 ——  league/oauth2-server 也用到这个抽象类机制。 Laravel Passport 也通过 实现这个库的接口 集成 league/oauth2-server 包。

正如 @bdelespierre 在 评论 里回应我之前的一篇博客文章时向我指出的那样,你不仅可以切换存储库实现,还可以将它们组合在一起。大致以他的示例为基础,您可以看到一个存储库如何包装另一个存储库以提供附加功能:

interface InvoiceRepository {
    public function findById(int $id): Invoice;}class InvoiceCacheRepository implements InvoiceRepository {

    public function __construct(
        private InvoiceRepository $repo,
        private int $ttlSeconds
    ) {}

    public function findById(int $id): Invoice {
        return Cache::remember(
            "invoice.$id",
            $this->ttlSeconds,
            fn(): Invoice => $this->repo->findById($id)
        );
    }}class EloquentInvoiceRepository implements InvoiceRepository {

    public function findById(int $id): Invoice { /* 从数据库中取出 $id */ }}// --- 用法:$repo = new InvoiceCacheRepository(
    new EloquentInvoiceRepository(););

要点:

  • 存储库模式抽象了有关数据访问的详细信息。
  • 存储库将客户端与数据访问技术分离
  • 这允许切换实现,提高可读性并实现可组合性。

可测试性

存储库模式提供的抽象也有助于测试。

如果你有一个 Repository 接口,你可以提供一个替代的测试实现。 您可以使用数组支持存储库,而不是访问数据库,将所有对象保存在数组中:

class InMemoryInvoiceRepository implements InvoiceRepositoryInterface {

    private array $invoices;

    // implement the methods by accessing $this->invoices...
    }
    // --- Test Case:
    $repo = new InMemoryInvoiceRepository();
    $service = new InvoiceService($repo);

通过这种方法,您将获得一个现实的实现,它速度很快并且在内存中运行。 但是您必须为测试提供正确的 Repository 实现,这 ** 本身可能需要大量工作**。 在我看来,这在两种情况下是合理的:

1.您正在开发一个(与框架无关的)库,它本身不提供存储库实现。

2.测试用例复杂,Repository 的状态很重要。

另一种方法是“模仿”,要使用这种技术,你不需要适当的接口。你可以模仿任何 non-final 类。
使用 PHPUnit API ,您可以明确规定如何调用存储库以及应该返回什么。

$companyId = 42;

/** @var InvoiceRepository&MockObject */
$repo = $this->createMock(InvoiceRepository::class);

$repo->expects($this->once())
    ->method('findInvoicedToCompany')
    ->with($companyId)
    ->willReturn(collect([ /* invoices to return in the test case */ ]));

$service = new InvoiceService($repo);

$result = $service->calculateAvgInvoiceAmount($companyId);

$this->assertEquals(1337.42, $result);

有了 mock,测试用例就是一个适当的单元测试。上面示例中测试的唯一代码是服务。没有数据库访问,这使得测试用例的设置和运行非常快速

另外:

  • 仓库模式允许进行适当的单元测试,这些单元测试运行快并且是隔离的

原文地址:https://dev.to/davidrjenni/why-use-the-repository-pattern-in-laravel-2j1m

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

Das obige ist der detaillierte Inhalt vonErklären Sie im Detail, wozu das Repository-Muster (Repository) in Laravel dient.. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Dieser Artikel ist reproduziert unter:learnku.com. Bei Verstößen wenden Sie sich bitte an admin@php.cn löschen