마지막 글제
- Laravel의 리포지토리 패턴을 어디에 사용하나요?
에서 Repository 패턴이 무엇인지, Active Record 패턴과 어떻게 다른지, Laravel에서 구현하는 방법에 대해 설명했습니다. 이제 저장소 패턴을 사용해야 하는 이유를 자세히 살펴보겠습니다. 이전 게시물의 댓글에서
Repository 패턴이 Laravel 커뮤니티에서 논란이 되는 주제라는 것을 알아냈습니다. 어떤 사람들은 사용할 이유가 없다고 생각하고 내장된 Active Record 모드를 고수합니다. 다른 사람들은 논리적 도메인에서 데이터 액세스를 분리하기 위해 다른 방법을 사용하는 것을 선호합니다. 저는 이러한 의견을 존중하며 이 주제에 대해 향후 블로그 게시물을 작성할 예정입니다. 이 고지 사항을 통해 저장소 패턴 사용의 이점을 이해해 보겠습니다. [관련 튜토리얼 권장 사항:
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();
도메인 모델과 데이터 액세스 기술의 책임이 혼합되어 있지만 직관적으로 이해됩니다. 우리 애플리케이션에서는 직원이 어떻게든 데이터베이스에 저장되어야 하므로 객체에 대해 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 查询构造器实现该方法 }}이제 이는 직관적이지 않고 도메인 모델에도 어긋납니다. Jack이 다른 회사에서 일하고 있을 수도 있는 다른 직원을 갑자기 삭제하는 이유는 무엇입니까? 아니면 왜 페이스북 직원들을 데려올 수 있었을까요?
물론 이 예는 인위적인 것이지만 Active Record 패턴이 어떻게 의도적인 도메인 모델링을 허용하지 않는지 보여줍니다. 직원과 전체 직원 목록 사이의 경계가 흐려집니다. 직원이 실제 직원으로 사용되고 있는지, 아니면 다른 직원에 접근하기 위한 메커니즘으로 사용되고 있는지 항상 고려해야 합니다.
Warehouse 모드는 기본 파티셔닝을 적용하여 이 문제를 해결합니다.유일한 목적은 도메인 개체 자체가 아닌 도메인 개체 모음을 식별하는 것입니다.
🎜핵심 사항:🎜🎜🎜🎜모든 도메인 개체 컬렉션을 단일 도메인 개체에서 분리함으로써 🎜웨어하우스 패턴은 단일 책임 원칙🎜을 구현합니다. 🎜🎜🎜🎜🎜Don't Repeat Yourself (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(););🎜 이와 같은 쿼리가 더 복잡해지고 여러 곳에서 사용된다면 Repository 방식으로 추출하는 것을 고려해 보세요. 🎜🎜리포지토리 패턴은 쿼리를 표현 방법으로 패키징하여 중복 쿼리를 줄이는 데 도움이 됩니다. 쿼리를 조정해야 하는 경우 한 번만 변경하면 됩니다. 🎜
class InMemoryInvoiceRepository implements InvoiceRepositoryInterface { private array $invoices; // implement the methods by accessing $this->invoices... } // --- Test Case: $repo = new InMemoryInvoiceRepository(); $service = new InvoiceService($repo);🎜 이제 쿼리는 한 번만 구현되며 별도로 테스트하고 다른 곳에서 사용할 수 있습니다. 또한 컨트롤러는 데이터 가져오기는 담당하지 않고 HTTP 요청 처리 및 응답 반환만 담당하므로 단일 책임 원칙이 다시 적용됩니다. 🎜🎜🎜요점: 🎜🎜🎜🎜저장소 패턴은 중복 쿼리를 줄이는 데 도움이 됩니다. 🎜🎜🎜🎜🎜종속성 역전🎜🎜종속성 역전 원칙을 설명하는 것은 자체 블로그 게시물이 필요합니다. 저는 저장소가 종속성 반전을 활성화할 수 있다는 것을 보여주고 싶었습니다. 🎜🎜구성 요소를 계층화할 때 일반적으로 상위 수준 구성 요소는 하위 수준 구성 요소에 종속됩니다. 예를 들어, 컨트롤러는 모델 클래스를 사용하여 데이터베이스에서 데이터를 가져옵니다. 🎜
$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 实现,这 ** 本身可能需要大量工作**。 在我看来,这在两种情况下是合理的:
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
위 내용은 Laravel에서 저장소 패턴(Repository)의 용도가 무엇인지 자세히 설명해주세요.의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!