andydefer/laravel-repository
最新稳定版本:v2.7.0
Composer 安装命令:
composer require andydefer/laravel-repository
包简介
A lightweight, type-safe repository pattern implementation for Laravel
README 文档
README
# Laravel Repository **Une implémentation légère et typée du pattern Repository pour Laravel avec intégration Records et Eloquent.** [](https://php.net) [](https://laravel.com) [](LICENSE) --- ## Table des matières 1. [Installation](#installation) 2. [Concepts fondamentaux](#concepts-fondamentaux) 3. [Créer votre premier Repository](#créer-votre-premier-repository) 4. [Référence de l'API](#référence-de-lapi) 5. [Méthodes à surcharger](#méthodes-à-surcharger) 6. [Bonnes pratiques](#bonnes-pratiques) 7. [Exemple complet avec filtres complexes](#exemple-complet-avec-filtres-complexes) 8. [Tests](#tests) 9. [Génération de code avec Directive Forge](#génération-de-code-avec-directive-forge) 10. [Licence](#licence) --- ## Installation ```bash composer require andydefer/laravel-repository
Prérequis
- PHP 8.1 ou supérieur
- Laravel 12.x, 13.x, 14.x ou 15.x
- Dépendances automatiques :
andydefer/php-records(structures typées)laravel/framework
Publier la configuration (Optionnel)
php artisan vendor:publish --tag=repository-config
Concepts fondamentaux
Le Record
Un Record est un DTO typé qui sert d'interface entre votre code et le Repository.
use AndyDefer\DomainStructures\Abstracts\AbstractRecord; final class UserRecord extends AbstractRecord { public function __construct( public readonly ?string $name = null, public readonly ?string $email = null, public readonly ?UserStatus $status = null, ) {} }
Règles pour les Records :
- ✅ Étendre
AbstractRecord - ✅ Propriétés
public readonly - ✅ Les champs optionnels =
nullpar défaut - ❌ Pas de logique métier
- ❌ Pas de tableaux bruts (utiliser
TypedCollection)
Records de configuration
Le package fournit des Records standardisés pour les opérations :
FindByRecord
use AndyDefer\Repository\Records\FindByRecord; $findBy = new FindByRecord( filters: new UserFiltersRecord(status: UserStatus::ACTIVE), limit: 10, sortBy: 'name', sortDir: SortDirection::ASC, columns: new SelectColumns(['id', 'name', 'email']), );
| Propriété | Type | Défaut | Description |
|---|---|---|---|
filters |
AbstractRecord |
EmptyRecord |
Filtres de recherche |
limit |
?int |
null |
Limite de résultats |
sortBy |
?string |
null |
Champ de tri |
sortDir |
SortDirection |
SortDirection::ASC |
Direction du tri |
columns |
SelectColumns |
SelectColumns::all() |
Colonnes à sélectionner |
PaginateRecord
use AndyDefer\Repository\Records\PaginateRecord; $paginate = new PaginateRecord( perPage: 15, page: 1, sortBy: 'name', sortDir: SortDirection::ASC, filters: new UserFiltersRecord(status: UserStatus::ACTIVE), columns: new SelectColumns(['id', 'name', 'email']), );
| Propriété | Type | Défaut | Description |
|---|---|---|---|
perPage |
int |
15 |
Éléments par page |
page |
int |
1 |
Numéro de page |
sortBy |
?string |
null |
Champ de tri |
sortDir |
SortDirection |
SortDirection::ASC |
Direction du tri |
filters |
AbstractRecord |
EmptyRecord |
Filtres de recherche |
columns |
SelectColumns |
SelectColumns::all() |
Colonnes à sélectionner |
RepositoryInfoRecord
use AndyDefer\Repository\Records\RepositoryInfoRecord; $info = $repository->info(); // RepositoryInfoRecord { // modelClass: 'App\Models\User', // recordClass: 'App\Records\UserRecord', // }
Énumération SortDirection
use AndyDefer\Repository\Enums\SortDirection; $direction = SortDirection::ASC; $direction->isAsc(); // true $direction->isDesc(); // false $direction->opposite(); // SortDirection::DESC $direction->toSql(); // 'asc'
Objet Valeur SelectColumns
use AndyDefer\Repository\ValueObjects\SelectColumns; // Créer avec des colonnes spécifiques $columns = new SelectColumns(['id', 'name', 'email']); // Sélectionner toutes les colonnes $allColumns = SelectColumns::all(); // Ajouter des colonnes (retourne une nouvelle instance) $extended = $columns->add('created_at', 'updated_at'); // Vérifier si une colonne existe if ($columns->has('email')) { // ... } // Obtenir le nombre $count = $columns->count(); // 3 // Convertir en tableau $array = $columns->toArray(); // ['id', 'name', 'email']
Créer votre premier Repository
1. Créer le Modèle
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; final class User extends Model { protected $fillable = ['name', 'email', 'status']; }
2. Créer le Record
<?php namespace App\Records; use AndyDefer\DomainStructures\Abstracts\AbstractRecord; final class UserRecord extends AbstractRecord { public function __construct( public readonly ?string $name = null, public readonly ?string $email = null, public readonly ?UserStatus $status = null, ) {} }
3. Créer le Record de filtres (Optionnel)
<?php namespace App\Records; use AndyDefer\DomainStructures\Abstracts\AbstractRecord; final class UserFiltersRecord extends AbstractRecord { public function __construct( public readonly ?string $name = null, public readonly ?string $email = null, public readonly ?UserStatus $status = null, ) {} }
4. Créer le Repository
<?php namespace App\Repositories; use AndyDefer\Repository\AbstractRepository; use AndyDefer\DomainStructures\Abstracts\AbstractRecord; use App\Models\User; use App\Records\UserRecord; use App\Records\UserFiltersRecord; use Illuminate\Database\Eloquent\Builder; final class UserRepository extends AbstractRepository { public function __construct() { parent::__construct(User::class, UserRecord::class); } protected function applyFilters(Builder $query, AbstractRecord $filters): void { if (!$filters instanceof UserFiltersRecord) { return; } if ($filters->name !== null) { $query->where('name', 'like', '%' . $filters->name . '%'); } if ($filters->email !== null) { $query->where('email', 'like', '%' . $filters->email . '%'); } if ($filters->status !== null) { $query->where('status', $filters->status); } } }
5. Utiliser le Repository
use App\Repositories\UserRepository; use App\Records\UserRecord; use App\Records\UserFiltersRecord; use AndyDefer\Repository\Records\FindByRecord; use AndyDefer\Repository\Records\PaginateRecord; use AndyDefer\Repository\Enums\SortDirection; class UserService { public function __construct( private readonly UserRepository $repository, ) {} // Créer un utilisateur public function createUser(string $name, string $email): User { return $this->repository->create(new UserRecord( name: $name, email: $email, status: UserStatus::ACTIVE, )); } // Trouver un utilisateur public function findUser(int $id): ?User { return $this->repository->find($id); } // Mettre à jour un utilisateur (uniquement les champs non-nuls) public function updateUser(int $id, string $name): User { return $this->repository->update($id, new UserRecord(name: $name)); } // Supprimer un utilisateur public function deleteUser(int $id): bool { return $this->repository->delete($id); } // Lister avec filtres public function listActiveUsers(): array { $filters = new UserFiltersRecord(status: UserStatus::ACTIVE); $findBy = new FindByRecord( filters: $filters, limit: 50, sortBy: 'name', sortDir: SortDirection::ASC, ); return $this->repository->findBy($findBy)->all(); } // Paginer les résultats public function getPaginatedUsers(int $page = 1): LengthAwarePaginator { $paginate = new PaginateRecord( perPage: 15, page: $page, sortBy: 'created_at', sortDir: SortDirection::DESC, ); return $this->repository->paginate($paginate); } // Compter les enregistrements public function countActiveUsers(): int { $filters = new UserFiltersRecord(status: UserStatus::ACTIVE); return $this->repository->count($filters); } // Vérifier l'existence public function userExists(string $email): bool { $filters = new UserFiltersRecord(email: $email); return $this->repository->exists($filters); } // Suppression groupée public function deleteInactiveUsers(): int { $filters = new UserFiltersRecord(status: UserStatus::INACTIVE); return $this->repository->deleteBulk($filters); } }
Référence de l'API
AbstractRepository
| Méthode | Paramètres | Retour | Description |
|---|---|---|---|
info() |
- | RepositoryInfoRecord |
Informations du repository |
create(AbstractRecord $record) |
$record |
Model |
Créer un nouvel enregistrement |
createRaw(array $data) |
$data |
Model |
Créer un enregistrement à partir de données brutes |
find(int $id) |
$id |
`Model | null` |
findWithTrashed(int $id) |
$id |
`Model | null` |
findBy(FindByRecord $record) |
$record |
Collection<Model> |
Rechercher avec critères |
update(int $id, AbstractRecord $record) |
$id, $record |
Model |
Mettre à jour (champs non-nuls uniquement) |
updateRaw(int $id, array $data) |
$id, $data |
Model |
Mettre à jour avec données brutes |
delete(int $id) |
$id |
bool |
Supprimer par ID (soft delete si disponible) |
restore(int $id) |
$id |
bool |
Restaurer un soft deleted |
forceDelete(int $id) |
$id |
bool |
Supprimer définitivement |
count(?AbstractRecord $criteria) |
$criteria |
int |
Compter les enregistrements |
exists(AbstractRecord $criteria) |
$criteria |
bool |
Vérifier l'existence |
paginate(PaginateRecord $record) |
$record |
LengthAwarePaginator |
Résultats paginés |
deleteBulk(AbstractRecord $criteria) |
$criteria |
int |
Suppression groupée (soft delete si disponible) |
forceDeleteBulk(AbstractRecord $criteria) |
$criteria |
int |
Suppression définitive groupée |
Méthodes à surcharger
| Méthode | Description |
|---|---|
applyFilters(Builder $query, AbstractRecord $filters) |
Appliquer les filtres de recherche (doit être surchargée) |
Exceptions
| Exception | Quand |
|---|---|
ModelNotFoundException |
update() ou updateRaw() sur un ID inexistant |
InvalidArgumentException |
Nom de colonne invalide dans SelectColumns |
Bonnes pratiques
1. Un Record par Entité
// ✅ BON final class UserRecord extends AbstractRecord { ... } final class PostRecord extends AbstractRecord { ... } // ❌ MAUVAIS final class UserPostRecord extends AbstractRecord { ... }
2. Record de filtres séparé pour les cas complexes
// ✅ BON - Pour les filtres complexes final class UserFiltersRecord extends AbstractRecord { ... } // ✅ BON - Pour les cas simples, réutiliser le Record principal $filters = new UserRecord(status: UserStatus::ACTIVE);
3. Utiliser des valeurs par défaut pour les champs optionnels
// ✅ BON public function __construct( public readonly ?string $name = null, // Optionnel public readonly string $email, // Requis ) {} // ❌ MAUVAIS public function __construct( public readonly ?string $name, public readonly string $email, ) {}
4. Implémenter applyFilters() proprement
protected function applyFilters(Builder $query, AbstractRecord $filters): void { // Vérification du type si utilisation d'un Record de filtres dédié if (!$filters instanceof UserFiltersRecord) { return; } // Utiliser when() pour les conditions complexes $query->when($filters->name ?? null, fn($q, $name) => $q->where('name', 'like', '%' . $name . '%') ); $query->when($filters->status ?? null, fn($q, $status) => $q->where('status', $status) ); }
5. Utiliser createRaw pour des données brutes
// ✅ BON - Quand vous avez déjà des données brutes $data = [ 'name' => 'John Doe', 'email' => 'john@example.com', 'status' => 'active', ]; $user = $repository->createRaw($data); // ✅ BON - Pour créer avec des valeurs null explicites $data = [ 'name' => 'User Without Email', 'email' => null, 'status' => 'active', ]; $user = $repository->createRaw($data);
6. Tester vos Repositories
final class UserRepositoryTest extends IntegrationTestCase { private UserRepository $repository; protected function setUp(): void { parent::setUp(); $this->repository = new UserRepository(); } public function test_create_persiste_utilisateur(): void { $record = new UserRecord(name: 'John', email: 'john@example.com'); $user = $this->repository->create($record); $this->assertDatabaseHas('users', [ 'id' => $user->id, 'name' => 'John', 'email' => 'john@example.com', ]); } public function test_create_raw_accepte_null(): void { $data = [ 'name' => 'User With Null Email', 'email' => null, 'status' => 'active', ]; $user = $this->repository->createRaw($data); $this->assertDatabaseHas('users', [ 'id' => $user->id, 'name' => 'User With Null Email', 'email' => null, ]); } }
Exemple complet avec filtres complexes
final class OrderRepository extends AbstractRepository { public function __construct() { parent::__construct(Order::class, OrderRecord::class); } protected function applyFilters(Builder $query, AbstractRecord $filters): void { if (!$filters instanceof OrderFiltersRecord) { return; } // Filtre de plage de dates if ($filters->fromDate !== null) { $query->whereDate('created_at', '>=', $filters->fromDate); } if ($filters->toDate !== null) { $query->whereDate('created_at', '<=', $filters->toDate); } // Filtre de plage de montants if ($filters->minAmount !== null) { $query->where('total', '>=', $filters->minAmount); } if ($filters->maxAmount !== null) { $query->where('total', '<=', $filters->maxAmount); } // Filtre de statut if ($filters->status !== null) { $query->where('status', $filters->status); } // Filtre de recherche textuelle if ($filters->search !== null) { $query->where(function ($q) use ($filters) { $q->where('order_number', 'like', '%' . $filters->search . '%') ->orWhere('customer_name', 'like', '%' . $filters->search . '%'); }); } } } // Utilisation $filters = new OrderFiltersRecord( fromDate: '2024-01-01', toDate: '2024-12-31', minAmount: 100, status: OrderStatus::PAID, search: 'ACME', ); $paginate = new PaginateRecord( perPage: 20, page: 1, sortBy: 'created_at', sortDir: SortDirection::DESC, filters: $filters, columns: new SelectColumns(['id', 'order_number', 'total', 'status']), ); $orders = $repository->paginate($paginate);
Support Soft Delete
Le repository détecte automatiquement si votre modèle utilise le trait SoftDeletes et adapte son comportement :
| Méthode | Comportement standard | Avec SoftDeletes |
|---|---|---|
delete() |
Suppression définitive | Soft delete (deleted_at rempli) |
find() |
Retourne tous les modèles | Exclut les soft deleted |
findWithTrashed() |
Comportement standard | Inclut les soft deleted |
restore() |
Non disponible | Restaure un soft deleted |
forceDelete() |
Non disponible | Suppression définitive |
deleteBulk() |
Suppression définitive groupée | Soft delete groupé |
forceDeleteBulk() |
Non disponible | Suppression définitive groupée |
// Modèle avec SoftDelete final class Product extends Model { use SoftDeletes; protected $fillable = ['name', 'price', 'quantity']; } // Utilisation $product = $repository->create(new ProductRecord(name: 'Laptop', price: 999.99)); // Soft delete $repository->delete($product->id); // Le find normal ne le trouve pas $found = $repository->find($product->id); // null // findWithTrashed le trouve $deleted = $repository->findWithTrashed($product->id); // Product instance // Restauration $repository->restore($product->id); // Suppression définitive (hard delete) $repository->forceDelete($product->id); // Nettoyage de masse : supprimer définitivement tous les soft deleted $filters = new ProductFiltersRecord(includeDeleted: true); $count = $repository->forceDeleteBulk($filters);
Tests
Configuration des tests
Le package utilise SQLite en mémoire pour les tests d'intégration :
// tests/IntegrationTestCase.php protected function defineEnvironment($app): void { $app['config']->set('database.default', 'testbench'); $app['config']->set('database.connections.testbench', [ 'driver' => 'sqlite', 'database' => ':memory:', 'prefix' => '', ]); }
Exécuter les tests
composer test
Génération de code avec Directive Forge
Ce package s'intègre à directive-forge pour générer automatiquement les repositories, records et filtres.
Installer Directive Forge
composer require andydefer/directive-forge --dev
Commandes disponibles
# Générer un repository ./vendor/bin/directive make-repository user # Générer un record ./vendor/bin/directive make-record user-record
Exemple de génération
# Créer un repository User ./vendor/bin/directive make-repository user # Génère : # - app/Repositories/UserRepository.php # - app/Records/UserRecord.php (optionnel) # - app/Records/UserFiltersRecord.php (optionnel)
Licence
MIT © Andy Defer
统计信息
- 总下载量: 21
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 2
- 依赖项目数: 1
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2026-05-25