bytetcore/serpo

Laravel Repository Pattern - Elegant data layer abstraction with criteria-based filtering for Eloquent models.

Maintainers

Package info

github.com/ByteTCore/serpo

Documentation

pkg:composer/bytetcore/serpo

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 0

1.1.0 2026-05-29 09:54 UTC

This package is auto-updated.

Last update: 2026-05-29 14:23:45 UTC


README

Latest Version on Packagist License PHP Version Latest Stable Version Total Downloads Latest Unstable Version

Elegant data layer abstraction for Laravel using the Repository Pattern with powerful criteria-based filtering. Keep your controllers clean and your data logic reusable.

Features

  • 🏗️ Repository Pattern — Abstract Eloquent models behind clean interfaces
  • 🔍 Criteria System — Composable, reusable query filters (where, like, date, JSON, null, in)
  • Zero Boilerplate — Artisan generators for repositories, services, and criteria
  • 🔗 Fluent Chaining — Chain any Eloquent Builder method directly on repositories
  • 🔄 Auto Query Reset — Query state resets after execution, preventing stale queries
  • 📦 Laravel Auto-Discovery — Install and go, no manual provider registration

Requirements

  • PHP 8.1+
  • Laravel 9.0+

Installation

composer require bytetcore/serpo

Publish the configuration:

php artisan vendor:publish --tag=serpo-config

Quick Start

1. Generate a Repository

# Basic repository
php artisan make:repository UserRepository

# With a specific model
php artisan make:repository UserRepository --model=User

# With a corresponding service class
php artisan make:repository UserRepository --model=User --service

2. Generate a Service

php artisan make:service UserService

3. Generate a Custom Criteria

php artisan make:criteria ActiveUserCriteria

Usage

Basic Repository

namespace App\Repositories;

use App\Models\User;
use ByteTCore\Serpo\Repositories\BaseRepository;

class UserRepository extends BaseRepository
{
    public function __construct(User $model)
    {
        parent::__construct($model);
    }
}

Query Methods

$repo = app(UserRepository::class);

// Get all records
$users = $repo->all();

// Get with specific columns
$users = $repo->get(['id', 'name', 'email']);

// Get first record
$user = $repo->first();

// Get first or throw exception
$user = $repo->firstOrFail();

// Get latest record
$user = $repo->last();
$user = $repo->last('updated_at');

Eloquent Builder Chaining

All Eloquent Builder methods are available directly on the repository:

$users = $repo->where('active', true)
    ->orderBy('name')
    ->limit(10)
    ->get();

$count = $repo->where('role', 'admin')->count();

$users = $repo->with('posts')->whereHas('posts')->get();

Criteria-Based Filtering

Define reusable filters in your repository using the fluent Condition API:

use ByteTCore\Serpo\Criteria\Condition;
use App\Criteria\ActiveUserCriteria;

class UserRepository extends BaseRepository
{
    protected function conditions(): array
    {
        return [
            'status'   => Condition::where(),
            'keyword'  => Condition::like('name', 'email')->or(),
            'min_age'  => Condition::where('age')->gte(),
            'created_from' => Condition::date('created_at')->gte(),
            'price_between' => Condition::between('price'),
            'tags'     => Condition::whereIn('tags'),
            'deleted'  => Condition::whereNull('deleted_at'),
            'birth_year' => Condition::year('birth_date'),
            'active'   => Condition::custom(ActiveUserCriteria::class),
            'search'   => Condition::orGroup(
                Condition::like('name'),
                Condition::where('email'),
            ),
        ];
    }

    public function __construct(User $model)
    {
        parent::__construct($model);
    }
}

Apply filters from request data:

// In your controller
$users = $repo->filters($request->only(['status', 'keyword', 'min_age']))->get();

Controlling Filter Application Order

By default, filters are applied in the order declared in conditions(). Override filterOrder() to change the order without reordering the conditions() map:

protected function filterOrder(): array
{
    return [
        'keyword',  // applied first
        'status',   // applied second
    ];
    // 'min_age', 'created_from', ... — any keys not listed are appended in original order
}

Available Criteria

Factory Criteria SQL
Condition::where() WhereCriteria WHERE col op val
Condition::like() LikeCriteria WHERE col LIKE pattern
Condition::date() DateCriteria WHERE DATE(col) op val
Condition::between() BetweenCriteria WHERE col BETWEEN ? AND ?
Condition::notBetween() NotBetweenCriteria WHERE col NOT BETWEEN ? AND ?
Condition::year() YearCriteria WHERE YEAR(col) op val
Condition::month() MonthCriteria WHERE MONTH(col) op val
Condition::whereIn() InCriteria WHERE col IN (...)
Condition::whereNotIn() NotInCriteria WHERE col NOT IN (...)
Condition::whereNull() NullCriteria WHERE col IS NULL
Condition::whereNotNull() NotNullCriteria WHERE col IS NOT NULL
Condition::jsonContains() JsonContainsCriteria whereJsonContains
Condition::jsonNotContains() JsonNotContainsCriteria whereJsonDoesntContain
Condition::orderBy() OrderByCriteria ORDER BY col asc/desc
Condition::andGroup(...) NestedCriteria Nested AND group
Condition::orGroup(...) NestedCriteria Nested OR group
Condition::custom() Any custom class

Fluent modifiers

Modifier Applies to Example
.columns(...) All Condition::where('name', 'email')
.and() / .or() All Condition::like('name', 'email')->or()
.gt() / .gte() / .lt() / .lte() where, date, year, month Condition::where('age')->gte()
.not() where Condition::where('status')->not()
.contains() / .startsWith() / .endsWith() like Condition::like('name')->startsWith()
.asc() / .desc() orderBy Condition::orderBy('created_at')->desc()

Detailed Criteria Walkthrough

Database Comparisons

'status'     => Condition::where(),              // WHERE status = input
'price_over' => Condition::where('price')->gte(), // WHERE price >= input
'not_admin'  => Condition::where('role')->not(), // WHERE role != input

Search & Text (LikeCriteria)

'keyword' => Condition::like('title', 'content', 'author')->or(),
// WHERE (title LIKE '%val%' OR content LIKE '%val%' OR author LIKE '%val%')

'prefix' => Condition::like('name')->startsWith(),
// WHERE name LIKE 'val%'

'suffix' => Condition::like('name')->endsWith(),
// WHERE name LIKE '%val'

Ranges (BetweenCriteria)

'price_range' => Condition::between('price'),
// expects $request->price_range = [100, 500]

Working with Dates

'created_at'  => Condition::date('created_at'),
'birth_year'  => Condition::year('birth_date'),
'birth_month' => Condition::month('birth_date'),

Sets & Arrays

'tags'   => Condition::whereIn('tags'),
'ignore' => Condition::whereNotIn('id'),

Nullity Checks

'unverified' => Condition::whereNull('email_verified_at'),
'active'     => Condition::whereNotNull('email_verified_at'),

JSON Columns

'has_tag' => Condition::jsonContains('tags'),

Dynamic Sorting

'sort_by' => Condition::orderBy('created_at')->desc(),

Nested Groups

Combine different criteria types in a single parenthesized group:

'search' => Condition::orGroup(
    Condition::like('name'),
    Condition::where('email'),
),
// WHERE ((name LIKE '%val%') OR (email = 'val'))

'date_range' => Condition::andGroup(
    Condition::date('created_at')->gte(),
    Condition::date('created_at')->lte(),
),
// WHERE ((DATE(created_at) >= 'val') AND (DATE(created_at) <= 'val'))

Custom Criteria

Create your own criteria by extending BaseCriteria, then reference it with Condition::custom():

namespace App\Criteria;

use ByteTCore\Serpo\Criteria\BaseCriteria;
use Illuminate\Database\Eloquent\Builder;

class ActiveWithRecentPostsCriteria extends BaseCriteria
{
    public function apply(Builder $query): void
    {
        $query->where('active', true)
            ->whereHas('posts', fn (Builder $q) => $q->where('created_at', '>=', now()->subDays(30)));
    }
}
// In your repository:
use App\Criteria\ActiveWithRecentPostsCriteria;

protected function conditions(): array
{
    return [
        'recent_active' => Condition::custom(ActiveWithRecentPostsCriteria::class),
    ];
}

Auto-Reset Behavior

By default, the query builder resets after each execution to prevent stale state:

$active = $repo->where('active', true)->get();   // query resets after get()
$all = $repo->all();                              // fresh query — returns all records

Disable auto-reset when you need to reuse the query:

$repo->withoutAutoReset()
    ->where('active', true);

$count = $repo->count();       // same filtered query
$users = $repo->get();         // same filtered query

Configuration

// config/serpo.php
return [
    'repository' => [
        'namespace' => env('SERPO_REPOSITORY_NAMESPACE', 'Repositories'),
    ],
    'service' => [
        'namespace' => env('SERPO_SERVICE_NAMESPACE', 'Services'),
    ],
    'criteria' => [
        'namespace' => env('SERPO_CRITERIA_NAMESPACE', 'Criteria'),
    ],
];

Testing

composer test

Changelog

See CHANGELOG.md for release history.

Contributing

See CONTRIBUTING.md for guidelines.

License

Licensed under the Apache License 2.0.