In most web applications accessing a database makes up a substantial portion of the code base. To avoid sprinkling plain SQL queries all over our application logic, we rely on abstractions, which hide the mechanics of the data access behind PHP methods.
There are several patterns to structure data access, "Active Record" and "Repository" the two most well-known. In this blog post I'll explain them specifically in the context of the Laravel framework. The discussion of the pros and cons of using the Repository pattern will follow in separate blog posts.
Active Record
By default, Laravel uses the Active Record pattern. Every Laravel programmer intuitively uses it, because it's implemented in with the abstract Model
base class and models usually inherit from it. Let's look at an example:
use Illuminate\Database\Eloquent\Model;
/**
* @property int $id
* @property string $first_name
* @property string $last_name
*/
class Person extends Model {
}
// --- Usage:
$person = new Person();
$person->first_name = 'Jack';
$person->last_name = 'Smith';
$person->save();
Of course you can read and write the properties you created on Person
. But to save the model, you can also call methods directly on the model. There is no need for another object -- the model already provides all the methods to access the corresponding database table.
This means, that the domain model combines your custom properties and methods with all the data access methods in the same class. The second part is achieved by inheriting from Model
.
Takeaways:
- π Active Record combines the domain model with data access functionality.
- π Laravel uses the Active Record pattern and implements it with the
Model
class.
Repository
The Repository pattern is an alternative to the Active Record pattern. It also provides an abstraction to handle data access. But more generally, it can be seen as a conceptual repository or collection of domain objects.
In contrast to the Active Record pattern, the Repository pattern separates the data access from the domain model. It provides a high-level interface, where you create, read, update and delete your domain models, without having to think about the actual underlying data store.
The underlying Repository implementation could access a database by building and executing SQL queries, a remote system via a REST API or might just manage an in-memory data structure which contains all the domain models. This can be useful for testing. The key part of the Repository is the high-level interface it provides to the rest of the code.
Takeaways:
- π A Repository represents a conceptual collection of domain objects.
- π It has the single responsibility of encapsulating data access with a high-level interface.
- π Laravel does not provide specific helpers to implement the Repository pattern.
When it comes to implementing the Repository pattern in Laravel, I mainly see two variants.
Variant 1: Specific Methods
In the first variant, the repository methods are focused and specific. The names explain what the caller gets and the options to parameterize the underlying query are limited.
class InvoiceRepository {
public function findAllOverdue(Carbon $since, int $limit = 10): Collection {
return Invoice::where('overdue_since', '>=', $since)
->limit($limit)
->orderBy('overdue_since')
->get();
}
public function findInvoicedToCompany(string $companyId): Collection {
return Invoice::where('company_id', $companyId)
->orderByDesc('created_at')
->get();
}
}
The advantage of this approach lies in the expressiveness of the methods. When the code is read, it is clear what to expect from the methods and how to call them. This leads to fewer mistakes. The Repository methods are easy to test, because the parameters are narrow.
A disadvantage of this approach is, that you might end up with lots of methods in your Repository. Because the methods can't be reused easily, you have to add additional methods for new use cases.
Takeaways:
- π The Repository pattern can be implemented by a class which provides specific methods.
- π Each method wraps one query, exposing only the necessary parameters.
- π Pros: readability and testability
- π Cons: lack of flexibility and lower reusability
Variant 2: General Methods
The approach on the other side of the spectrum is to provide general methods. This leads to less methods. But the methods have a large API surface, because each method can be called with various combinations of arguments.
The key problem which emerges is the parameter representation. The representation should guide the callers to understand the method signature and avoid invalid inputs. For that, you can introduce a special class, for example with the Query Object pattern.
But what I most often see in practice is a mix of scalar parameters and PHP arrays. The caller can pass completely invalid inputs and the type array
alone does not say much about what to pass. But if used carefully you can succeed with this light-weight approach and avoid more cumbersome abstractions.
class InvoiceRepository {
public function find(array $conditions, string $sortBy = 'id', string $sortOrder = 'asc', int $limit = 10): Collection {
return Invoice::where($conditions)
->orderBy($sortBy, $sortOrder)
->limit($limit)
->get();
}
}
// --- Usage:
$repo = new InvoiceRepository();
$repo->find(['overdue_since', '>=', $since], 'overdue_since', 'asc');
$repo->find(['company_id', '=', $companyId], 'created_at', 'asc', 100);
This approach alleviates the problems of the first approach: you get less Repository methods which are more flexible and can be reused more often.
On the negative side, the Repository becomes harder to test because there are more cases to cover. The method signatures are harder to understand and because of that, the caller can make more mistakes. Also, you introduce some kind of query object representation. Whether it is explicit or implicit (like with arrays), your Repository implementation and its callers will get coupled to it.
Takeaways:
- π The Repository pattern can be implemented with a class which provides general methods.
- π The challenge lies in the representation of the method parameters.
- π Pros: greater flexibility and higher reusability
- π Cons: harder to test, less readable, coupling to parameter representation
Of course the two approaches can be combined. Maybe you want some specific methods for complicated queries and a few general methods for simple where
queries.
Implementation
Now, let's talk about how to implement the method bodies.
In the examples above, I used the methods from the Model
class to get access to an Eloquent query builder. So the Repository implementation actually used the Active Record pattern as an implementation.
You don't have to do that. You could use the DB
facade, to get a query builder, while avoiding the Model
class. Or you can write SQL queries directly:
class InvoiceRepository {
public function findAllOverdue(Carbon $since, int $limit = 10): Collection {
return DB::table('invoices')
->where('overdue_since', '>=', $since)
->limit($limit)
->orderBy('overdue_since')
->get();
}
public function findInvoicedToCompany(string $companyId): Collection {
return DB::select('SELECT * FROM invoices
WHERE company_id = ?
ORDER BY created_at
LIMIT 100', [$companyId]);
}
}
The great thing about the Repository pattern is, that the implementation can be anything, as long as it fulfills the interface. You could also manage objects in-memory or wrap (and cache) an API.
But most often, the underlying data store is a SQL database. And for accessing it, you can choose the best implementation on a per-method basis. For performance critical or complex queries, you might want to use SQL statements directly. Simpler queries can use the Eloquent query builder.
When you don't use the Model
class to implement your Repository, you might think about not inheriting from it in your models. But this works against a lot of built-in Laravel magic and is in my opinion not a pragmatic approach.
Takeaways:
- π The Repository pattern is flexible and allows for various implementation techniques.
- π In Laravel, the Eloquent query builder is a pragmatic choice when accessing databases.
Interfaces
Another option you have is, whether to introduce an interface or not. The example above can be separated in an interface and one or more implementations:
// --- Interface:
public interface InvoiceRepositoryInterface {
public function findAllOverdue(Carbon $since, int $limit = 10): Collection;
public function findInvoicedToCompany(string $companyId): Collection;
}
// --- Concrete class, implementing the interface:
class InvoiceRepository implements InvoiceRepositoryInterface {
public function findAllOverdue(Carbon $since, int $limit = 10): Collection {
// implementation
}
public function findInvoicedToCompany(string $companyId): Collection {
// implementation
}
}
Adding an interface is an additional indirection and is not necessarily good. If your application is the only user of the Repository and you don't expect to have more than one implementation of it, I don't see the point of introducing an interface. For testing, the repository can be mocked with PHPUnit, as long as it's not marked as final
.
If you know you are going to have multiple implementation you should definitively use an interface. Different implementations may occur if you are writing a package which is going to be used in multiple projects or if you want a special Repository implementation for testing.
In order to benefit from Laravel's dependency injection, you have to bind the concrete implementation to the interface. This has to be done in the register
method of a Service Provider.
use Illuminate\Support\ServiceProvider;
class RepositoryServiceProvider extends ServiceProvider {
public function register(): void {
$this->app->bind(InvoiceRepositoryInterface::class, InvoiceRepository::class);
}
}
Takeaways:
- π An interface can further decouple the Repository from the rest of the code.
- π Use Repository interfaces, when you expect to have more than one concrete class implementing it.
- π In Laravel, bind the concrete class to the interface in a Service Provider.
π Let me know how you would implement the Repository pattern in Laravel.
π In the next post, I'll explore the advantages of using the Repository pattern.
Top comments (10)
Hello David,
Thank you for this well presented article :)
I'm not a fan of repository pattern, but I recently do a kind of it by splitting Eloquent and Collection from the model.
I don't know if you are aware of it so I'm sharing it to you:
I discovered this possibilty by reading this article
I appreciate your feedback very much!
No, I wasn't aware about this, thanks for sharing the link. It looks neat and I hope to try it out soon.
I certainly see why it might be more popular in the Laravel community. In the end, the design decisions depend a lot on personal taste, the specific project and other factors. But whatever helps you structure your project is great!
Thanks for this article @davidrjenni , very informative and well-written.
One thing I believe is missing is the whole point of having repositories; to abstract entity management from storage layer. One is able to substitute the storage layer at will. This is immensely helpful when you sync data accross several API, here's an example:
Moving data around becomes as easy as:
If the API you're talking to is REST compliant, implementing a repository that talks to it is a breeze (no state to manage.)
There are many other reasons why you would want to use repositories with Laravel. Especially if you're designing with DDD. You can read more about that on my dev.to page π
Thank you, @bdelespierre for your feedback, much appreciated π.
I'll expand on the reasons I think the Repository pattern is helpful in the next blog post. I wanted to keep this post focused on the general concept and how to implement it in Laravel.
I'll definitively check them out π. Thanks for the pointer.
O think repositories are so useless with laravel...
It definitively seems to be a controversial topic in the Laravel community.
In this post, I wanted to be as neutral as possible and just describe what it is and how to use it. I will explore the pros and cons in other posts.
Can be helpful when you use different model entities across several boundaries.
Hey David, Would you consider posting to my Medium publication focused on DevOps and PHP?
Hi Logan, thanks for you feedback.
Sure, why not. Let me know how to move forward.
contact me: logan[at]llbbl.com