This package builds a structure to domain-oriented APIs (not DDD, they are different things). With search filters, validations and clean code.
- PHP 7.2+, 8.0 (new version)
- Laravel 7.x, 8 (prefer-stable)
My need was simple: build structures in an organized and productive way. A structure that supports filters, validations and data caching (CQRS).
Before proceeding, take a look at the final structure:
app
├── ...
├── Domain
│ └── Dummy
│ ├── DummyFilterService.php
│ ├── DummyPersistenceModel.php
│ ├── DummyPersistenceService.php
│ ├── DummyPolicy.php
│ ├── DummyResource.php
│ ├── DummySearchModel.php
│ ├── DummySearchService.php
│ └── DummyValidateService.php
├── Http
│ ├── Controllers
│ │ ├── ...
│ │ └── DummyController.php
├── ...
database
├── factories
│ └── ...
│ └── DummyFactory.php
├── migrations
│ ├── ...
│ └── 2021_01_06_193044_create_dummies_table.php
└── seeders
├── DatabaseSeeder.php
└── DummySeeder.php
You must be asking yourself:
- Why not use Repository Pattern?
A. It is not possible to obtain a database abstraction more than what Eloquent offers. 1 - What is the idea of PersistenceModel, and SearchModel?
A. In fact, I go further. Model instances of Eloquent should not be returned. So we guarantee a "read-only" instance (which is not used for persistence in the database) 2 3 - There are a lot of files, how do I build it all?
A. It's simple, get a coffee and let's do it...
- Run this Composer command to install the latest version
$ composer require adhenrique/laravel-domain-oriented
- If you prefer, you can export the location files:
php artisan vendor:publish --provider="LaravelDomainOriented\ServiceProvider" --tag="lang"
- Run this command to build the domain structure:
$ php artisan domain:create Dummy
- Stay calm. If the structure already exists, the console asks you if you want to rewrite it, unless you pass the
--force
flag:
$ php artisan domain:create Dummy --force
- And of course, if you want to remove the structure, just run this command:
$ php artisan domain:remove Dummy
That's it enjoy!
Our Model's follow the Eloquent Model Conventions
- PersistenceModel: used only for persistence in the database. Define your fields, casts, etc...
- SearchModel: used for searches. It is very likely that your relationship will be here.
Our Migrations follow the Laravel Migration Structure
Here, too, we follow the Laravel way of doing things:
Again, Policies follow the Laravel Policy Authorization
Note: You don't have to worry about registering your policies, as we do it behind the scenes. However, here we follow a class name convention. When creating a domain, your class must be named SomethingPolicy and belong to the App\Domain\Something namespace.
ValidateService is located at app/Domain/YourDomainName/*
:
use LaravelDomainOriented\Services\ValidateService;
class DummyValidateService extends ValidateService
{
protected array $rules = [
// You can define general validation rules, which will be inherited
// for all actions, or you can define validation rules for each action:
// SHOW, STORE, UPDATE, DESTROY
// General rules validation.
// If any action validation rule is not defined, it will inherit from here.
'name' => 'required|string',
// Specific action rules validation. If set, ignores general validations.
self::SHOW => [
'id' => 'required|integer',
],
self::UPDATE => [
'id' => 'required|integer',
'name' => 'required|string',
],
self::DESTROY => [
'id' => 'required|integer',
],
];
}
We follow Laravel routes pattern. But as we are dealing with API, modify the file routes/api.php
, adding the following routes:
Route::get('dummies', 'App\Http\Controllers\DummyController@index');
Route::get('dummies/{id}', 'App\Http\Controllers\DummyController@show');
Route::post('dummies', 'App\Http\Controllers\DummyController@store');
Route::put('dummies/{id}', 'App\Http\Controllers\DummyController@update');
Route::delete('dummies/{id}', 'App\Http\Controllers\DummyController@destroy');
In the SearchService class you have two methods that help you to pre-start queries according to your needs: beforeAll
and beforeFindById
.
Each method receives 2 parameters: builder
with the Eloquent instance started and auth
, with the user session - if are logged in.
You just need to override the methods, but ensure that the return is eloquent's Builder
. Look:
class DummySearchService extends SearchService
{
protected SearchModel $model;
protected FilterService $filterService;
public function __construct(DummySearchModel $model, DummyFilterService $filterService)
{
$this->model = $model;
$this->filterService = $filterService;
}
public function beforeAll(Builder $builder, Guard $auth): Builder
{
return $builder;
}
public function beforeFindById(Builder $builder, Guard $auth): Builder
{
return $builder;
}
}
In my use case, logged in as admin, I usually filter from the list of users my own user. Look:
// ...
public function beforeAll(Builder $builder, Guard $auth): Builder
{
return $this->removeLoggedFromSearches($builder, $auth);
}
private function removeLoggedFromSearches($builder, $auth)
{
$id = $auth->id();
return $builder->where('id', '<>', $id);
}
You can filter and paginate the data on the listing routes. To do this, send a payload on the request, using your favorite client:
Simple Where:
{
"name": "adhenrique",
"email": "eu@adhenrique.com.br"
}
Where in:
{
"id": [1,2,3]
}
Where by operator (like, >, =>, <, <=, <>):
{
"name": {
"operator": "like",
"value": "%adhenrique%"
}
}
Where between:
{
"birthdate": {
"start": "1988-13-12",
"end": "2021-01-01"
}
}
Paginate results
{
"paginate": {
"per_page": 1,
"page": 1
}
}
Note: You can use the filters and pagination together.
- CQRS
- Support for old Laravel versions
- Or Where filter
- OOP improvements
- Add beforeAll and beforeFindById tests
- Ask to confirm name
- Add way to test Policies
$ composer test
Please see CHANGELOG for more information what has changed recently.
Please see CONTRIBUTING for details.
If you discover any security related issues, please email eu@adhenrique.com.br instead of using the issue tracker.
The MIT License (MIT). Please see License File for more information.
[1] Please, stop talking about Repository pattern with Eloquent
[2] Useful Eloquent Repositories?
[3] Você entende Repository Pattern? Você está certo disso?
Laravel — Why you’ve been using the Repository Pattern the wrong way