Looking to hire Laravel developers? Try LaraJobs

laravel-repository-specs maintained by expertapps

Description
Composable DDD Specifications and Query-Based Row-Level Security (RLS) Repositories for Laravel 13
Last update
2026/08/04 14:40 (dev-main)
License
Links
Downloads
0

Comments
comments powered by Disqus

Laravel Repository Specs

Latest Version on Packagist Software License Build Status

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 WHERE clause prior to execution.
  • 🧱 Fluent Logical Operators: Compose specifications effortlessly using and(), or(), and not().
  • 🛡️ 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, readonly classes, 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.