At thinkcru.com the goal is to implement quality software, that is reliable and well tested. There are many software design patterns available to our developer/engineers, but one that is used quite often within our team is the infamous Repository Pattern.
At thinkcru.com the goal is to implement quality software, that is reliable and well tested. This directly impacts how features are created by increasing the efficiency in the development process, which leads to many of our happy clients. There are many software design patterns available to our engineers, but one that is used quite often within our team is the infamous Repository Pattern. Internally, we also refer this to the Data Access Layer or DAL.
As mentioned in the book Patterns of Enterprise Application Architecture, Martin Fowler explains a repository as follows:
A repository performs the tasks of an intermediary between the domain model layers and data mapping, acting in a similar way to a set of domain objects in memory. Client objects declaratively build queries and send them to the repositories for answers. Conceptually, a repository encapsulates a set of objects stored in the database and operations that can be performed on them, providing a way that is closer to the persistence layer. Repositories, also, support the purpose of separating, clearly and in one direction, the dependency between the work domain and the data allocation or mapping.
The Repository pattern helps separate the code base in more manageable chunks of code by not polluting the base model classes. For example, in Laravel, the model classes extend the Illuminate\Database\Eloquent\Model
class. These models are another layer built into the Laravel framework that manages the actual Unit Of Work for whatever record is being stored in the transaction.
A Unit Of Work is the actual mechanism that does the insert, update, delete, etc. transactions in memory. This can be done with any type of in memory database: MongoDB, MySQL, CSV file, Redis Cache, an API endpoint-- just about any data source you can imagine. The point is that the domain layer where the business logic resides should not care about how the data is stored or retrieved. It should only focus on what is relevant to what the logic in the class is responsible for, obeying the SOLID responsibility principles.
The repository pattern makes unit testing the logic in isolation much easier as well. Our team at thinkcru.com repeatedly reaches for the the Dependency Injection mechanism to implant a specific class into the logic. This is also commonly referred to as Inversion of Control (IoC). IoC is just a fancy terminology of removing a hard-coded class dependency and allowing the injection of any concrete class at run-time.
In summary, the benefits of the Repository pattern makes the code more:
In the following example code, there are two classes associated to every model we work with in Laravel. When building the architecture for the Repository pattern we organize the code base in a hierarchy of class inheritances:
BaseRepository
- a common base class that is reused for every repository to stay on the D.R.Y. pathRepositoryInterface
- We need an interface to enable the Dependency Injection in Laravel's IoC containerDalServiceProvider
- The actual Service Provider that tells our logic what specific classes can be swapped in and out. Notice we prefix this with Dal
to document this is our Data Access Layer, you can call this whatever you want.FooRepository
- A Foo
Model has an associated repository with specific logic that is unique this model itself. The example code below, for UserRepository
has unique code only needed for the User
model. In the code example we create a common base layer that is used for all the repositories called the BaseRepository
. This illustrates good house keeping practices and keeping with the D.R.Y. mentality.
Notice how we pass in a Model
class, think of this as an easily swappable module for any type of model source you can think of. You could even have a Model from a API endpoint, it does not matter, that unit of work transaction is outside your "concerns". In this example, we've used the Laravel Eloquent\Model
class, but this can be replaced with anything that makes sense for your needs.
The next step is to make sure we have associated a contract or Interface for our RepositoryInterface
.
Next, the Laravel framework makes dependency injection super easy so that we an bind to the container. This is achieved using the $this-app->bind()
method. The key ingredient to making this work is the Interface should always be injected, and then a concrete implementation should be bound to the interface. To reason about this process we like to keep our DalServiceProvider
clean and separate.
DalServiceProvider
- use the register()
method from within the ServiceProvider
class (which we extend) to loop around and bind all the concrete implementations of the interfaces.AppServiceProvider
- the main application service provider in Laravel's bootstrap must register()
the DalServiceProvider
The beauty of using the for loop enables you to list all the different repositories to the $toBind
array. If there are 20 model types, then their may be 20 repositories, one for each model.
Next, it is important to register the repositories service provider (DalServiceProvider
) within the bootstrap of the the Laravel application.
As mentioned earlier, we typically create a repository for every model (this is not a requirement, it is just how we do it). In this example, the UserRepository
has a unique set of business logic that needs to retrieve the user record using he findByLoginIdentifier()
. Here we establish the Interface
so that this can be swapped out for any logic needs we need.
The PostgresUser
is the concrete implementation, but the UserRepository
itself gets injected and Laravel's Service Provider provides the link. You set-and-forget the UserRepository
and then you just touch the DalServiceProvider
to adjust your code if you have a swap in a new implementation.
The best home for the raw Eloquent\Builder
queries should be isolated in the PostgresUser
. The models are only used for data transfer objects between the business logic layer and the data access layer. In other words, we can easily swap out some unique query logic for MongoDB here, or even a RESTful API endpoint source.
Pro Tip: Some junior developers get caught up with inserting logic like the query you see into an Eloquent\Model
class. This is fine if you have a small project, but when features and code complexity scales then the Model
class should not get polluted with raw queries.
See here the agility of the example, after creating our "contract" interface
for the generic User
model, we can now put in unique logic that is related to query a PostgreSQL DB source using the PostgresUser
. Now, when we inject the UserRepository
Interface in any Laravel controller, job, artisan console, etc component we can reach for this postgres user.
The previous code snippets show how to get the foundation ready for the repository. Now that we have decoupled our eloquent models and DB queries from our domain layer, we are ready to use use the repository in our Business Logic Layer.
The class LoginUsingCredentialsAction
class is what we are targeting in unit testing in isolation. This class responsibility is managing the login of a user, it does not care how the user data is actually being accessed. It is not the "concern" of this class, its only responsibility is to pass the credentials in and handle whether the user is allowed access based on the whether the login credentials are correct. The UserRepository
class and all the dependencies that come along with it is handled by the Laravel IoC container.
This makes unit testing the LoginUsingCredentialsAction
class a breeze and we can focus on testing the execute()
method in isolation.
We can now use Mockery
too emulate the UserRepository::class
and the actual LoginCredentialsDto::class
to check that our class:
InvalidLoginCredentialsException
if the credentials are invalid.The Repository Pattern is a useful tool in the developer's tool belt. Whether working in a framework such as: Laravel, Symfony, or Flask; our team at Thincru uses these methods of the trade to build great software. Our goal is to always shoot to deliver quality code that has been well tested and implemented according to the client's requirements. Decoupling the business logic layer and the data access layer makes developing new features an efficient process as the application scales in scope and complexity.