왜 Laravel에서 저장소 패턴(Repository)을 사용하나요? 다음 글에서는 저장소 모드 사용의 장점을 소개하겠습니다. 도움이 되길 바랍니다.
이전 글에서는 저장소 패턴이 무엇인지, Active Record 패턴과 어떻게 다른지, Laravel에서 구현하는 방법에 대해 설명했습니다. 이제 저장소 패턴을 사용해야 하는 이유를 자세히 살펴보겠습니다.
이전 게시물의 댓글에서 Repository 패턴이 Laravel 커뮤니티에서 논란이 되는 주제라는 것을 알아냈습니다. 어떤 사람들은 사용할 이유가 없다고 생각하고 내장된 Active Record 모드를 고수합니다. 다른 사람들은 논리적 도메인에서 데이터 액세스를 분리하기 위해 다른 방법을 사용하는 것을 선호합니다. 나는 이러한 의견을 존중하며 이 주제에 대해 향후 블로그 게시물을 작성할 예정입니다.
이 고지 사항을 통해 저장소 패턴 사용의 이점을 이해해 보겠습니다.
단일 책임 원칙은 Active Record 패턴과 Repository 패턴을 구별하는 주요 판별자입니다. 모델 클래스는 이미 데이터를 보유하고 도메인 개체에 대한 메서드를 제공합니다. Active Record 패턴을 사용하면 데이터 액세스에 대한 추가 책임이 발생합니다. 이는 다음 예에서 설명하고 싶은 내용입니다.
/** * @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();
도메인 모델과 데이터 액세스 기술의 책임이 혼합되어 있지만 직관적으로 이해됩니다. 우리 애플리케이션에서는 직원이 어떻게든 데이터베이스에 저장되어야 하므로 객체에 대해 save()
를 호출하는 것은 어떨까요? 단일 객체는 단일 데이터 행으로 변환되어 저장됩니다. 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 查询构造器实现该方法 } }
Warehouse 모드는 기본 파티셔닝을 적용하여 이 문제를 해결합니다. 유일한 목적은 도메인 개체 자체가 아닌 도메인 개체 모음을 식별하는 것입니다.
🎜🎜🎜핵심 사항:🎜🎜🎜🎜모든 도메인 개체 컬렉션을 단일 도메인 개체에서 분리함으로써 🎜웨어하우스 패턴은 단일 책임 원칙🎜을 구현합니다. 🎜🎜🎜🎜반복하지 마세요(DRY)🎜🎜🎜일부 프로젝트에서는 프로젝트 전체에 데이터베이스 쿼리를 뿌립니다. 다음은 데이터베이스에서 목록을 가져와 블레이드 보기에 표시하는 예입니다. 🎜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(); );
class InMemoryInvoiceRepository implements InvoiceRepositoryInterface { private array $invoices; // implement the methods by accessing $this->invoices... } // --- Test Case: $repo = new InMemoryInvoiceRepository(); $service = new InvoiceService($repo);
$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);
InvoiceController
는 특정 Invoice
클래스에 따라 다릅니다. 별도로 테스트하거나 저장 메커니즘을 교체하는 등 이 두 클래스를 분리하는 것은 어렵습니다. Repository 인터페이스를 도입함으로써 우리는 종속성 반전을 달성할 수 있습니다. 🎜rrreee🎜Controller는 이제 Repository 구현과 마찬가지로 Repository 인터페이스에만 의존합니다. 🎜이 두 클래스는 이제 하나의 추상화에만 의존하므로 설명하겠습니다. 다음 섹션에서 🎜🎜🎜테이크아웃:🎜🎜에 설명된 대로 추가 이점을 제공합니다.存储库 提高了可读性 因为复杂的操作被具有表达性名称的高级方法隐藏了.
访问存储库的代码与底层数据访问技术分离. 如有必要,您可以切换实现,甚至可以省略实现,仅提供 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 实现,这 ** 本身可能需要大量工作**。 在我看来,这在两种情况下是合理的:
您正在开发一个(与框架无关的)库,它本身不提供存储库实现。
测试用例复杂,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
【相关推荐:laravel视频教程】
위 내용은 Laravel의 리포지토리 패턴(Repository)의 장점에 대한 간략한 분석의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!