dernier articleDans mon
- Modèle de référentiel dans Laravel à utiliser ?
, j'ai expliqué ce qu'est le modèle de référentiel, en quoi il diffère du modèle d'enregistrement actif et comment l'implémenter dans Laravel. Je souhaite maintenant examiner de plus près pourquoi vous devriez utiliser le modèle de référentiel. J'ai remarqué dans les commentaires du post précédent que le
Repository pattern est un sujet controversé dans la communauté Laravel. Certaines personnes ne voient aucune raison de l’utiliser et s’en tiennent au mode Active Record intégré. D'autres préfèrent utiliser d'autres méthodes pour séparer l'accès aux données du domaine logique. Veuillez noter que je respecte ces avis et consacrerai un prochain article de blog à ce sujet. Avec cet avertissement, comprenons les avantages de l'utilisation du modèle de référentiel. [Recommandation du didacticiel connexe :
laravel/** * @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();
Bien que le modèle de domaine et la technologie d'accès aux données aient des responsabilités mixtes, cela a intuitivement du sens. Dans notre application, les employés doivent être stockés dans la base de données d'une manière ou d'une autre, alors pourquoi ne pas appeler save()
sur l'objet. Un seul objet est converti en une seule ligne de données et stocké.
Mais allons plus loin et voyons ce que nous pouvons faire d'autre avec les employés : save()
。单个对象被转化成单个数据行并存储。
但是,让我们更进一步,看看我们还能对员工做些什么:
$jack->where('first_name', 'John')->firstOrFail()->delete(); $competition = $jack->where('company_id', $facebookId)->get();
现在,它变得不直观,甚至违背了我们的域模型。 为什么 Jack 会突然删除另一个甚至可能在不同公司工作的员工? 或者他为什么能把 Facebook 的员工拉过来?
当然,这个例子是人为设计的,但它仍然显示了 Active Record 模式如何不允许有意的域模型。 员工与所有员工列表之间的界限变得模糊。 您始终必须考虑该员工是被用作实际员工还是作为访问其他员工的机制。
仓库模式通过强制执行这个基本分区来解决这个问题。它的唯一用途是标识域对象的合集,而不是域对象的本身。
要点:
一些项目将数据库查询洒遍了整个项目。下面是一个例子,我们从数据库中获取列表,并在 Blade 视图中显示他们。
class InvoiceController { public function index(): View { return view('invoices.index', [ 'invoices' => Invoice::where('overdue_since', '>=', Carbon::now()) ->orderBy('overdue_since') ->paginate() ]); }}
当这样的查询遍得更加复杂并且在多个地方使用时,考虑将其提取到 Repository 方法中。
存储库模式通过将重复查询打包到表达方法中来帮助减少重复查询。如果必须调整查询,只需更改一次即可。
class InvoiceController { public __construct(private InvoiceRepository $repo) {} public function index(): View { return view('invoices.index', [ 'invoices' => $repo->paginateOverdueInvoices() ]); }}
现在查询只实现一次,可以单独测试并在其他地方使用。此外,单一责任原则再次发挥作用,因为控制器不负责获取数据,而只负责处理HTTP请求和返回响应。
Takeaway:
解释 Dependency Inversion Principle 值得发表自己的博客文章。我只是想说明存储库可以启用依赖项反转。
在对组件进行分层时,通常较高级别的组件依赖于较低级别的组件。 例如,控制器将依赖模型类从数据库中获取数据:
class InvoiceController { public function index(int $companyId): View { return view( 'invoices.index', ['invoices' => Invoice::where('company_id', $companyId)->get()] ); }}
依赖关系是自上而下的,紧密耦合的。 InvoiceController
取决于具体的 Invoice
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 查询构造器实现该方法 }}Maintenant, cela devient peu intuitif et va même à l'encontre de notre modèle de domaine. Pourquoi Jack supprimerait-il soudainement un autre employé qui pourrait même travailler dans une autre entreprise ? Ou pourquoi a-t-il pu faire venir des employés de Facebook ?
Bien sûr, cet exemple est artificiel, mais il montre quand même comment le modèle Active Record ne permet pas la modélisation intentionnelle de domaine. La frontière entre les employés et la liste de tous les employés devient floue. Vous devez toujours vous demander si l'employé est utilisé comme un véritable employé ou comme un mécanisme permettant d'accéder à d'autres employés.
Le mode Entrepôt résout ce problème en appliquant ce partitionnement de base.Son seul objectif est d'identifier une collection d'objets de domaine, et non les objets de domaine eux-mêmes.
🎜Points clés :🎜🎜🎜🎜En dissociant une collection de tous les objets de domaine d'un seul objet de domaine, 🎜Le modèle d'entrepôt incarne le principe de responsabilité unique🎜. 🎜🎜🎜🎜🎜Ne vous répétez pas (DRY)🎜🎜Certains projets dispersent des requêtes de base de données partout dans le projet. Vous trouverez ci-dessous un exemple dans lequel nous obtenons une liste de la base de données et les affichons dans une vue Blade. 🎜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(););🎜Lorsqu'une requête comme celle-ci devient plus complexe et utilisée à plusieurs endroits, envisagez de l'extraire dans une méthode Repository. 🎜🎜Le modèle de référentiel permet de réduire les requêtes en double en les regroupant dans des méthodes d'expression. Si vous devez ajuster la requête, vous ne devez la modifier qu'une seule fois. 🎜
class InMemoryInvoiceRepository implements InvoiceRepositoryInterface { private array $invoices; // implement the methods by accessing $this->invoices... } // --- Test Case: $repo = new InMemoryInvoiceRepository(); $service = new InvoiceService($repo);🎜Désormais, les requêtes ne sont implémentées qu'une seule fois et peuvent être testées de manière isolée et utilisées ailleurs. De plus, le principe de responsabilité unique entre à nouveau en jeu, car le contrôleur n'est pas responsable de l'obtention des données, mais uniquement du traitement de la requête HTTP et du renvoi de la réponse. 🎜🎜🎜À retenir : 🎜🎜🎜🎜Le modèle de référentiel aide à réduire les requêtes en double 🎜🎜🎜🎜🎜Inversion de dépendance🎜🎜Expliquer le principe d'inversion de dépendance mérite son propre article de blog. Je voulais juste illustrer qu'un référentiel peut permettre l'inversion des dépendances. 🎜🎜Lors de la superposition de composants, les composants de niveau supérieur dépendent généralement des composants de niveau inférieur. Par exemple, le contrôleur s'appuiera sur la classe de modèle pour obtenir les données de la base de données : 🎜
$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);🎜La dépendance est descendante et étroitement couplée.
InvoiceController
dépend de la classe Invoice
spécifique. Il est difficile de dissocier ces deux classes, comme par exemple les tester séparément ou remplacer le mécanisme de stockage. En introduisant l'interface Repository, nous pouvons réaliser une inversion de dépendance : 🎜rrreee🎜Controller ne dépend désormais que de l'interface Repository, tout comme l'implémentation Repository 🎜Ces deux classes ne dépendent désormais que d'une seule abstraction🎜, réduisant ainsi le couplage comme je vais l'expliquer. dans la section suivante Cela apporte d'autres avantages, comme expliqué dans 🎜🎜🎜À emporter :🎜🎜存储库 提高了可读性 因为复杂的操作被具有表达性名称的高级方法隐藏了.
访问存储库的代码与底层数据访问技术分离. 如有必要,您可以切换实现,甚至可以省略实现,仅提供 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
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!