Home >PHP Framework >Laravel >A brief analysis of the advantages of the repository pattern (Repository) in Laravel
Why use the repository pattern (Repository) in Laravel? The following article will introduce to you the advantages of using the repository mode. I hope it will be helpful to you!
In a previous article, I explained what the repository pattern is, how it differs from the Active Record pattern, and how to implement it in Laravel. Now I want to take a closer look at why you should use the repository pattern.
I noticed in the comments of the previous article that the Repository pattern is a controversial topic in the Laravel community. Some people see no reason to use it and stick with the built-in Active Record mode. Others prefer to use other methods to separate data access from the logical domain. Please note that I respect these opinions and will devote an upcoming blog post to this topic.
With this disclaimer, let’s understand the advantages of using the repository pattern.
The Single Responsibility Principle is the main discriminator to differentiate between Active Record mode and Repository mode. Model classes already hold data and provide methods on domain objects. When using the Active Record pattern, data access is an additional responsibility introduced. This is something I want to illustrate in the following example:
/** * @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();
Although the domain model and data access technology have mixed responsibilities, it intuitively makes sense. In our application the employees have to be stored in the database somehow, so why not call save()
on the object. A single object is converted into a single row of data and stored.
But let's go a step further and see what else we can do with employees:
$jack->where('first_name', 'John')->firstOrFail()->delete(); $competition = $jack->where('company_id', $facebookId)->get();
Now, it becomes unintuitive and even violates our domain model. Why would Jack suddenly delete another employee who might even be working at a different company? Or why he was able to bring Facebook employees over?
Of course, this example is contrived, but it still shows how the Active Record pattern does not allow for intentional domain models. The line between employees and the list of all employees becomes blurred. You always have to consider whether the employee is being used as an actual employee or as a mechanism to access other employees.
Warehouse mode solves this problem by enforcing this basic partitioning. Its only purpose is to identify a collection of domain objects, not the domain object itself.
Key points:
class InvoiceController { public function index(): View { return view('invoices.index', [ 'invoices' => Invoice::where('overdue_since', '>=', Carbon::now()) ->orderBy('overdue_since') ->paginate() ]); } }When a query like this becomes more complex and used in multiple places, consider extracting it into a Repository method. The repository pattern helps reduce duplicate queries by packaging them into expression methods. If you have to adjust the query, you only need to change it once.
class InvoiceController { public __construct(private InvoiceRepository $repo) {} public function index(): View { return view('invoices.index', [ 'invoices' => $repo->paginateOverdueInvoices() ]); } }Now the query is implemented only once and can be tested in isolation and used elsewhere. Additionally, the Single Responsibility Principle comes into play again, as the controller is not responsible for getting the data, but only for processing the HTTP request and returning the response.
Takeaway:
Dependency Inversion Principle It deserves its own blog post. I just wanted to illustrate that a repository can enable dependency inversion.
When layering components, typically higher-level components depend on lower-level components. For example, the controller will rely on the model class to get data from the database:class InvoiceController { public function index(int $companyId): View { return view( 'invoices.index', ['invoices' => Invoice::where('company_id', $companyId)->get()] ); } }The dependency is top-down and tightly coupled.
InvoiceController depends on the specific
Invoice class. It is difficult to decouple these two classes, such as testing them separately or replacing the storage mechanism. By introducing the Repository interface, we can achieve dependency inversion:
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 查询构造器实现该方法 } }Controller now only depends on the Repository interface, the same as the Repository implementation.
These two classes now only depend on one abstraction, thus reducing Coupling. As I will explain in the next section, this brings further advantages.
Takeaway:
存储库 提高了可读性 因为复杂的操作被具有表达性名称的高级方法隐藏了.
访问存储库的代码与底层数据访问技术分离. 如有必要,您可以切换实现,甚至可以省略实现,仅提供 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视频教程】
The above is the detailed content of A brief analysis of the advantages of the repository pattern (Repository) in Laravel. For more information, please follow other related articles on the PHP Chinese website!