ttpn18121996 / simple-repository
Build repository and service patterns quickly and simply.
Requires
- php: ^8.2
- ext-json: *
- illuminate/collections: ^11.0
- illuminate/console: ^11.0
- illuminate/contracts: ^11.0
- illuminate/database: ^11.0
- illuminate/log: ^11.0
- illuminate/support: ^11.0
Requires (Dev)
- friendsofphp/php-cs-fixer: dev-master
- laravel/pint: dev-main
README
Installation
Install using composer:
composer require ttpn18121996/simple-repository
Next, publish SimpleRepository's resources using the simple-repository:install
command:
php artisan simple-repository:install
Create repository
Default Repository uses Eloquent, Run command make app/Repositories/Eloquent/UserRepository.php
file
and interface app/Repositories/Contracts/UserRepository.php
php artisan make:repository UserRepository
You can specify a model that your repository depends on during creation by adding options --model
or -m
.
php artisan make:repository UserRepository --model=User
#OR
php artisan make:repository UserRepository -m User
Use another repository during build by adding the --repo
or -r
option. For example, if you want to use Redis instead
of Eloquent, now the repository will be created in the path app/Repositories/Redis/UserRepository.php
php artisan make:repository UserRepository -m User -r Redis
After creating the repository remember to declare in
app/Providers/RepositoryServiceProvider.php
where protected $repositories
(By default they will be added automatically)
protected $repositories = [ ... \App\Repositories\Contracts\UserRepository::class => \App\Repositories\Eloquent\UserRepository::class, ]
The example shows the dynamic extension of the repository pattern.
We use province data in the database.
After a while, we realize that using local data is no longer suitable and we want to use a data source from an external web service.
Editing the existing Eloquent\ProvinceRepository
content will result in errors or be difficult to revert to before.
Instead, we will create a new repository called WebService\ProvinceRepository
while still ensuring its accuracy like the old repository.
/app
├---/Providers
| ├---RepositoryServiceProvider.php
├---/Repositories
| ├---/Contracts
| | ├---ProvinceRepository.php
| ├---/Eloquent
| | ├---ProvinceRepository.php
| ├---/WebService
| | ├---ProvinceRepository.php
app/Repositories/Eloquent/ProvinceRepository.php
<?php namespace App\Repositories\Eloquent; use App\Models\Province; use App\Repositories\Contracts\ProvinceRepository as ProvinceRepositoryContract; class ProvinceRepository implements ProvinceRepositoryContract { public function getModelName(): string { return Province::class; } public function all() { return $this->model()->all(); } }
app/Repositories/WebService/ProvinceRepository.php
<?php namespace App\Repositories\WebService; use App\Repositories\Contracts\ProvinceRepository as ProvinceRepositoryContract; use Illuminate\Support\Facades\Http; class ProvinceRepository implements ProvinceRepositoryContract { public function getModelName(): string { return ''; } public function all() { $response = Http::get('https://api.domain.example/provinces'); return $response->success() ? $response->collection() : collect(); } }
Finally, what we need to do is change the binding between the abstract and the concrete in RepositoryServiceProvider
protected $repositories = [ ... // \App\Repositories\Contracts\ProvinceRepository::class => \App\Repositories\Eloquent\ProvinceRepository::class, \App\Repositories\Contracts\ProvinceRepository::class => \App\Repositories\WebService\ProvinceRepository::class, ]
Create service
Run command for make a service. Ex: make app/Services/UserService.php
file.
php artisan make:service UserService
You can specify the models your service depends on during creation by adding the --model or -m option.
php artisan make:service UserService --model=User --model=Role
#OR
php artisan make:service UserService -m User -m Role
You can use repositories instead of models. Specify the repositories your service depends on during creation by adding the --repo or -r option.
php artisan make:service UserService --repo=UserRepository --repo=RoleRepository
#OR
php artisan make:service UserService -r UserRepository -r RoleRepository
Customize the filter builder
Override the buildFilter
method in the repository class to customize the buildFilter
from the request.
protected function buildFilter(Builder $query, array $filters = []): Builder { return $query->orderBy('name') ->when(Arr::get($filters, 'name'), function (Builder $query, $name) { $query->where('name', 'like', "%{$name}%"); }); }
Now you just need to call getAll
or getPagination
,
the query will filter itself according to the filters you pass in.
Customize the relationship builder
Similar to the filter builder, you can override the buildRelationships
method to customize relational query handling.
protected function buildRelationships(): Builder { return $this->model()->with(['roles', 'permissions']); }
Customize the query builder
If you want to customize the query without affecting other methods that are using buildRelationships
and buildFilter
, you can override the getBuilder
method.
protected function getBuilder(array $filters = []): Builder { return $this->model() ->with(['roles', 'permissions']) ->when(Arr::get($filters, 'name'), function (Builder $query, $name) { $query->where('name', 'like', "%{$name}%"); }) ->orderBy('name'); }
Set an authenticated user for the service
Use authenticated users to use permission checks in the service.
class UserController { public function index(Request $request) { $users = $this->userService ->useAuthUser($request->user()) ->getList($request->query()); ... } }
class UserService extends Service { public function getList(array $filters = []) { if ($this->authUser()->can('view_user')) { // Do something } ... } }
Implementation and expansion
The simple repository provides two trait classes "HasFilter" and "Safetyable" that serve to build queries that handle sorting and filtering of data (HasFilter) and use transactions for data interaction (Safetyable). By default, the base service and base repository classes extend these two trait classes.
HasFilter provides a buildFilter
method. To use this feature, we need to pass the filters
parameter in the format:
$this->buildFilter(query: $query, filters: [ 'search' => [ // Relative search (operator "like") 'field_1' => 'value1', 'field_2' => 'value2', ], 'or_search' => [ // Relative search (operator "like"). Use the "orWhere" method 'field_1' => 'value1', 'field_2' => 'value2', ], 'filter' => [ // Absolute search (operator "=") 'field_1' => 'value1', 'field_2' => 'value2', ], 'or_filter' => [ // Absolute search (operator "="). Use the "orWhere" method 'field_1' => 'value1', 'field_2' => 'value2', ], 'sort' => [ // Sort data 'field' => 'field_name', 'direction' => 'asc' // asc | desc ], ]);
To fix the problem of 2 tables having the same field name or you want to change the field on the url to avoid revealing the table and field names in the database, the solution is to create a "transferredFields" property in your service class.
For example: users and roles tables both have a field of "name".
namespace App\Services; class UserService extends Service { protected array $transferredFields = [ 'name' => 'users.name', 'role_name' => 'roles.name', ]; }
Or you can also directly override the "getTransferredField" method to transfer the field name.
namespace App\Services; class UserService extends Service { protected function getTransferredField(string $field): string { return [ 'name' => 'users.name', 'role_name' => 'roles.name', ][$field] ?? $field; } }
ModelFactory attribute
When using model in service, it will look like this:
<?php namespace App\Services; use App\Models\User; class UserService extends Service { public function __construct( public User $user, ) { } public function getById($id) { return $this->user->find($id); } }
Instead of when using a service, dependent models will be initialized and injected automatically through the container service. Now, only when you call them are they initialized and stored. You can declare and use the model in the service through the ModelFactory attribute like this:
<?php namespace App\Services; use App\Models\User; use SimpleRepository\Attributes\ModelFactory; class UserService extends Service { #[ModelFactory(User::class)] public ?User $user = null; public function getById($id) { return $this->getModel('user')->find($id); } }
Use database processing functions safely
Instead of like this:
DB::beginTransaction(); try { // Do something DB::commit(); return $data; } catch (\Throwable $e) { DB::rollback(); logger()->error($e->getMessage()); return null; }
You can use the handleSafely() method instead. The first parameter is a callback for processing logic, the second parameter is the title of the logging. Let's say you get an exception, it will be of the form "Title: {message content}"
use SimpleRepository\Traits\Safetyable; class MyClass { use Safetyable; public function doSomething($params) { return $this->handleSafely(function () { // Do something return $data; }, 'Do something'); } }
Services and repositories by default use the Safetyable trait. You can directly invoke the handleSafely() method within services/repositories.
class UserService extends Service { #[ModelFactory(User::class)] protected ?User $user = null; public function create(array $data) { return $this->handleSafely(function () use ($data) { $user = $this->getModel('user', $data); $user->save(); return $user; }, 'Create user'); } }
Set up a log channel for the handleSafely() method to log when something goes wrong in the config/simple-repository.php
file
<?php return [ ... 'log_channel' => 'stack', ];
Tips
Inside the service class, you can call other services with the same namespace without importing them and instantiating
them. You can call them via the getService
method with the service name as the parameter value. For example,
App\Services\UserService
wants to use App\Services\RoleService
.
namespace App\Services\UserService; public function sampleMethod() { /** * @var \App\Services\RoleService */ $roleService = $this->getService('RoleService'); }
namespace App\Services\UserService; use App\Services\RoleService; use SimpleRepository\Attributes\ServiceFactory; class UserService extends Service { #[ServiceFactory(RoleService::class)] public ?RoleService $role = null; public function sampleMethod() { /** * @var \App\Services\RoleService */ $roleService = $this->getService('role'); } }