ttpn18121996 / simple-repository
Build repository and service patterns quickly and simply.
Requires
- php: ^8.2
- ext-json: *
- illuminate/collections: ^12.0
- illuminate/console: ^12.0
- illuminate/contracts: ^12.0
- illuminate/database: ^12.0
- illuminate/log: ^12.0
- illuminate/support: ^12.0
Requires (Dev)
- friendsofphp/php-cs-fixer: dev-master
- laravel/pint: ^1.13
- dev-master
- v4.x-dev
- v4.4.3
- v4.4.2
- v4.4.1
- v4.3.1
- v4.3.0
- v4.2.3
- v4.2.2
- v4.2.1
- v4.2.0
- v4.1.2
- v4.1.1
- v4.1.0
- v4.0.2
- v4.0.1
- v3.x-dev
- v3.1.3
- v3.1.2
- v3.1.1
- v3.0.1
- v2.x-dev
- v2.5.3
- v2.5.2
- v2.5.1
- v2.4.3
- v2.4.2
- v2.4.1
- v2.3.4
- v2.3.3
- v2.3.2
- v2.3.1
- v2.2.2
- v2.2.1
- v2.1.2
- v2.1.1
- v2.0.1
- v1.x-dev
- v1.1.4
- v1.1.3
- v1.1.2
- v1.1.1
- v1.0.4
This package is auto-updated.
Last update: 2025-06-14 10:23:53 UTC
README
Content
- Installation
- Create repository
- Create service
- Use data filters for your query builder
- Use eloquent helper to customize query builder
- Set an authenticated user for the service
- Implementation and expansion
- ModelFactory attribute
- Use database processing functions safely
- Tips
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 the build by adding the --repo
or -r
option. For example,
if you want to use an external service instead of Eloquent,
now the repository will be created in the path app/Repositories/Api/UserRepository.php
php artisan make:repository UserRepository --repo Api
#OR
php artisan make:repository UserRepository -r Api
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 Api\ProvinceRepository
while still ensuring its accuracy like the old repository.
/app
├---/Providers
| ├---RepositoryServiceProvider.php
├---/Repositories
| ├---/Api
| | ├---ProvinceRepository.php
| ├---/Contracts
| | ├---ProvinceRepository.php
| ├---/Eloquent
| | ├---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 getDataSource() { return new Province(); } public function all() { return $this->getDataSource()->all(); } }
app/Repositories/Api/ProvinceRepository.php
<?php namespace App\Repositories\Api; use App\Repositories\Contracts\ProvinceRepository as ProvinceRepositoryContract; use Illuminate\Support\Facades\Http; class ProvinceRepository implements ProvinceRepositoryContract { public function getDataSource() { return Http::baseUrl('https://api.domain.example'); } public function all() { $response = $this->getDataSource()->get('/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\Api\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
Use data filters for your query builder
use SimpleRepository\Concerns\HasFilter; class UserService extends Service { use HasFilter; }
Override the buildFilter
method in the class to customize the buildFilter
from the request.
use Illuminate\Contracts\Database\Query\Builder; use SimpleRepository\FilterAdapter; use SimpleRepository\FilterDTO; protected function buildFilter(Builder $query, array|FilterDTO $filters = []): Builder { $filters = $filters instanceof FilterDTO ? $filters : FilterAdapter::makeDTO($filters); // ... }
Use eloquent helper to customize query builder
use App\Models\User; use App\Repositories\Eloquent\UserRepository as UserRepositoryContract; use SimpleRepository\Concerns\HasEloquentSupport; class UserRepository implements UserRepositoryContract { use HasEloquentSupport; protected ?string $modelName = User::class; }
Get list with pagination in HasEloquentSupport
$users = app(\App\Repositories\Contracts\UserRepository::class)->getPagination($filters);
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']); }
After overriding the buildRelationships
method,
the getPagination
method will include roles and permissions for each item.
// Get a paginated list of users with roles and permissions for each user. $users = app(\App\Repositories\Contracts\UserRepository::class)->getPagination($filters);
Customize the query builder
If you want to customize the query, you can override the getBuilder
method.
If you use it in combination with HasFilter
, the order will be buildFilter
, buildRelationships
, getBuilder
protected function getBuilder(Builder $query, array $filters = []): Builder { return $query->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 ], // If the value is null, the records that have not been deleted will be queried. // Otherwise, the query will be based on the column name with the value not null. 'deleted' => 'deleted_at', // by default is null ]);
Or you can use DTO as parameter for filter.
use SimpleRepository\FilterDTO; use SimpleRepository\Enums\SortDirection; new FilterDTO( page: 1, perPage: 10, search: [ 'field_1' => 'value1', 'field_2' => 'value2', ], orSearch: [ 'field_1' => 'value1', 'field_2' => 'value2', ], filter: [ 'field_1' => 'value1', 'field_2' => 'value2', ], orFilter: [ 'field_1' => 'value1', 'field_2' => 'value2', ], sortField: 'field_name', sortDirection: SortDirection::tryFrom('asc') ?? SortDirection::ASC, deleted: 'deleted_at', );
To create DTOs dynamically, you can use FilterAdapter
use SimpleRepository\FilterAdapter; public function index(Request $request) { $filterDTO = FilterAdapter::makeDTO($request); }
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\Concerns\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'); } }