expertapps / laravel-repository-specs
Composable DDD Specifications and Query-Based Row-Level Security (RLS) Repositories for Laravel 13
Package info
github.com/ab0Yazan/laravel-repository-specs
pkg:composer/expertapps/laravel-repository-specs
Requires
- php: ^8.3
- illuminate/console: ^13.0
- illuminate/database: ^13.0
- illuminate/support: ^13.0
Requires (Dev)
- laravel/pint: ^1.18
- orchestra/testbench: ^11.0
- pestphp/pest: ^3.0
- phpstan/phpstan: ^2.0
README
Composable Specification Pattern (DDD) and Query-Based Row-Level Security (RLS) for Laravel 13 & PHP 8.3+.
Key Features
- 🎯 Dual-Use Specifications: Write business rules once and evaluate them in both SQL database queries and in-memory PHP objects.
- 🔐 Query-Based Row-Level Security (RLS): Automatically inject security constraints (tenant boundaries, clearance, ACLs) directly into the SQL
WHEREclause prior to execution. - 🧱 Fluent Logical Operators: Compose specifications effortlessly using
and(),or(), andnot(). - 🛡️ Secured Repository Decorator: Enforce security policies across all repository lookups without manually checking permissions in controllers or services.
- ⚡ Laravel 13 & PHP 8.3 Ready: Built with strict types,
readonlyclasses, asymmetric visibility, and constructor promotion.
Installation
Install the package via Composer:
composer require expertapps/laravel-repository-specs
Optionally publish the package configuration file:
php artisan vendor:publish --tag="repository-specs-config"
1. Specifications Core (Domain-Driven Design)
Specifications encapsulate business rules into reusable domain objects.
Creating a Specification
Extend AbstractSpecification to create a dual-use specification:
namespace App\Domain\Specifications; use ExpertApps\RepositorySpecs\Domain\Specifications\AbstractSpecification; use Illuminate\Contracts\Database\Eloquent\Builder as EloquentBuilder; use Illuminate\Contracts\Database\Query\Builder as QueryBuilder; final readonly class ActiveUsersSpecification extends AbstractSpecification { public function apply(EloquentBuilder|QueryBuilder $query): EloquentBuilder|QueryBuilder { return $query->where('status', '=', 'active'); } public function isSatisfiedBy(object $entity): bool { return ($entity->status ?? null) === 'active'; } }
Or generate one using the Artisan CLI command:
php artisan make:spec ActiveUsersSpecification
Logical Composition (AND, OR, NOT)
Chain specifications fluently:
use App\Domain\Specifications\ActiveUsersSpecification; use App\Domain\Specifications\VerifiedEmailSpecification; use ExpertApps\RepositorySpecs\Domain\Specifications\SearchKeywordSpecification; $activeUsersSpec = new ActiveUsersSpecification(); $verifiedSpec = new VerifiedEmailSpecification(); $keywordSpec = new SearchKeywordSpecification('John', ['name', 'email']); // Compose: (Active AND Verified) OR Keyword Search $eligibleUsersSpec = $activeUsersSpec ->and($verifiedSpec) ->or($keywordSpec);
Dual-Use Evaluation
A. Database Context (SQL Query Builder)
Use specifications with the repository layer or via the fluent whereSpec Eloquent macro:
use App\Models\User; $users = User::query()->whereSpec($eligibleUsersSpec)->get();
B. In-Memory Context (PHP Object Validation)
Evaluate domain entities or loaded models directly in memory without hitting the database:
if ($eligibleUsersSpec->isSatisfiedBy($userEntity)) { // Grant promo or execute business action }
2. Query-Based Row-Level Security (RLS)
Traditional authorization checks happen after fetching records from the database, causing memory bloat and pagination bugs. Query-Based RLS injects permission constraints into the SQL query before execution.
Implementing a Security Scope Policy
Implement QueryScopePolicyInterface to define authorization boundaries:
namespace App\Domain\Security; use ExpertApps\RepositorySpecs\Domain\Security\Contracts\QueryScopePolicyInterface; use Illuminate\Contracts\Database\Eloquent\Builder as EloquentBuilder; use Illuminate\Contracts\Database\Query\Builder as QueryBuilder; final readonly class DocumentRowLevelSecurityPolicy implements QueryScopePolicyInterface { public function applyScope(EloquentBuilder|QueryBuilder $query, object $user): EloquentBuilder|QueryBuilder { return $query->where('organization_id', '=', $user->organization_id) ->where(function (EloquentBuilder|QueryBuilder $sub) use ($user): void { $sub->where('owner_id', '=', $user->id) ->orWhere('department_id', '=', $user->department_id); }); } public function isSatisfiedForUser(object $entity, object $user): bool { if (($entity->organization_id ?? null) !== ($user->organization_id ?? null)) { return false; } return ($entity->owner_id ?? null) === ($user->id ?? null) || ($entity->department_id ?? null) === ($user->department_id ?? null); } }
3. Repositories & SecuredRepositoryDecorator
Creating a Repository
Generate a repository using Artisan:
php artisan make:repository DocumentRepository
Implementation extending EloquentRepository:
namespace App\Infrastructure\Repositories; use App\Models\Document; use ExpertApps\RepositorySpecs\Infrastructure\Repositories\EloquentRepository; final class DocumentRepository extends EloquentRepository { public function __construct() { parent::__construct(Document::class); } }
Enforcing Security via Decorator
Wrap any repository with SecuredRepositoryDecorator to automatically force RLS policies into all queries:
use App\Domain\Security\DocumentRowLevelSecurityPolicy; use App\Infrastructure\Repositories\DocumentRepository; use ExpertApps\RepositorySpecs\Domain\Specifications\SearchKeywordSpecification; use ExpertApps\RepositorySpecs\Infrastructure\Repositories\SecuredRepositoryDecorator; // 1. Authenticated User Context $currentUser = auth()->user(); // 2. Wrap Base Repository with Security Decorator $securedRepo = new SecuredRepositoryDecorator( repository: new DocumentRepository(), policy: new DocumentRowLevelSecurityPolicy(), user: $currentUser ); // 3. Perform queries normally. RLS constraints are automatically injected into SQL! $keywordSpec = new SearchKeywordSpecification('Q4 Financials'); $documents = $securedRepo->match($keywordSpec);
Generated SQL:
SELECT * FROM "documents" WHERE ( "organization_id" = 10 AND ("owner_id" = 42 OR "department_id" = 5) ) AND ( "title" LIKE '%Q4 Financials%' OR "description" LIKE '%Q4 Financials%' )
4. Console Commands
Scaffold new package classes using standard Artisan CLI generators:
# Create a new Domain Specification php artisan make:spec FilterByStatusSpecification # Create a new Eloquent Repository php artisan make:repository UserDirectoryRepository
5. Testing
The package includes a comprehensive Pest PHP test suite covering unit SQL inspection and SQLite in-memory integration testing.
Run the test suite:
composer test
Run static analysis:
composer analyse
License
The MIT License (MIT). Please see the License File for more information.