Model#
Shamaseen\Repository\Utility\Model is the base Eloquent model every generated model extends. It composes two traits that add per-request caching and criteria-driven query scopes to every model in your application.
use Shamaseen\Repository\Utility\Model;
class Post extends Model
{
protected $fillable = ['title', 'body', 'status'];
}
CachePerRequest#
The CachePerRequest trait caches SELECT query results for the lifetime of the current HTTP request using Laravel's built-in array cache driver. Identical queries (same SQL + bindings) executed more than once within the same request are served from memory instead of hitting the database again.
How it works#
The trait replaces the model's database connection with a thin ConnectionProxy. Every select / selectOne call builds a cache key from the fully-resolved SQL string. On a cache hit the result is returned immediately; on a miss the real query runs and the result is stored before being returned.
Disabling cache globally#
Set disable_cache to true in config/repository.php to turn off caching for every model. This flag cannot be overridden at runtime and is useful for testing environments.
// config/repository.php
'disable_cache' => true,
Disabling cache per model#
Set the $requestCacheEnabled property to false on a specific model to opt it out of caching entirely.
class AuditLog extends Model
{
public bool $requestCacheEnabled = false;
}
Runtime cache manipulation#
Three chainable query scopes let you control caching on a per-query basis:
| Scope | Effect |
|---|---|
disableCache() |
Turns off caching for subsequent queries on this builder |
enableCache() |
Turns caching back on |
clearCache() |
Wipes the in-memory cache store for this model |
// Run a query without using or storing the cache
$posts = Post::disableCache()->where('status', 'draft')->get();
// Clear stale cache entries then re-query
$posts = Post::clearCache()->where('status', 'published')->get();
// Chain with any other builder methods
Post::disableCache()->where('user_id', $userId)->orderBy('created_at')->get();
Note:
refresh()on a model instance automatically bypasses the cache so the reloaded attributes always reflect the current database state.
Criteriable#
The Criteriable trait provides three Eloquent query scopes that translate an associative $criteria array — typically built from HTTP request parameters — into WHERE, LIKE, and ORDER BY clauses.
$criteria = $request->all(); // or $request->validated()
Post::filterByCriteria($criteria)
->searchByCriteria($criteria)
->orderByCriteria($criteria)
->paginate();
Filterable, Searchable, and Sortable columns#
By default all $fillable columns that are not in $hidden are filterable, searchable, and sortable. Override this for any or all three by declaring the corresponding property on your model:
class Post extends Model
{
protected $fillable = ['title', 'body', 'status', 'published_at'];
protected $hidden = ['body'];
// Only these columns can be filtered
protected ?array $filterables = ['status', 'published_at'];
// Only these columns are searched with LIKE
protected ?array $searchables = ['title'];
// Only these columns are accepted as sort keys
protected ?array $sortables = ['title', 'published_at'];
}
scopeSearchByCriteria — LIKE search#
Looks for a search key in $criteria and applies WHERE column LIKE '%value%' across all searchable columns.
Criteria key: search
// GET /posts?search=laravel
$criteria = ['search' => 'laravel'];
Post::searchByCriteria($criteria)->get();
// → WHERE (title LIKE '%laravel%')
Searching in a relationship#
Add the relation name as the key and its columns as the value in $searchables:
protected ?array $searchables = [
'title',
'author' => ['name', 'bio'], // searches inside the "author" relation
];
$criteria = ['search' => 'john'];
Post::searchByCriteria($criteria)->get();
// → WHERE (title LIKE '%john%'
// OR EXISTS (SELECT * FROM authors WHERE posts.author_id = authors.id
// AND (name LIKE '%john%' OR bio LIKE '%john%')))
Full-text search#
For MySQL/MariaDB full-text indexes declare $fulltextSearch on your model. Each entry in the array becomes a separate MATCH … AGAINST clause.
// Local full-text index on (firstname, lastname)
protected ?array $fulltextSearch = [
['firstname', 'lastname'],
];
$criteria = ['search' => 'Jane Doe'];
User::searchByCriteria($criteria)->get();
// → WHERE (MATCH(firstname, lastname) AGAINST('Jane Doe'))
Full-text search in a relationship:
protected ?array $fulltextSearch = [
'posts' => ['title', 'body'],
];
User::searchByCriteria(['search' => 'eloquent'])->get();
// → WHERE EXISTS (SELECT * FROM posts WHERE …
// AND MATCH(title, body) AGAINST('eloquent'))
Both syntaxes can be combined:
protected ?array $fulltextSearch = [
['firstname', 'lastname'], // local index
'posts' => ['title', 'body'], // relation index
];
Limitation: A relation may only have one full-text index entry (a single flat array of columns). Multiple index arrays for the same relation are not supported.
scopeFilterByCriteria — exact / operator filters#
Applies WHERE column = value for each filterable column found in $criteria.
Criteria keys: the column names themselves (or nested under a filter_key — see Filter Key).
Simple (equality) filter#
// GET /posts?status=published
$criteria = ['status' => 'published'];
Post::filterByCriteria($criteria)->get();
// → WHERE status = 'published'
Multiple filters are combined with AND:
$criteria = ['status' => 'published', 'published_at' => '2024-01-01'];
Post::filterByCriteria($criteria)->get();
// → WHERE status = 'published' AND published_at = '2024-01-01'
Operator filters#
Pass an associative array as the column's value to use a comparison operator. Supported operators:
| Key | SQL operator |
|---|---|
eq |
= |
lt |
< |
gt |
> |
lte |
<= |
gte |
>= |
ne |
!= |
// GET /posts?published_at[gte]=2024-01-01&published_at[lt]=2025-01-01
$criteria = [
'published_at' => [
'gte' => '2024-01-01',
'lt' => '2025-01-01',
],
];
Post::filterByCriteria($criteria)->get();
// → WHERE published_at >= '2024-01-01' AND published_at < '2025-01-01'
Multiple operators on the same column are all applied:
$criteria = [
'views' => ['gte' => 100, 'lte' => 500],
];
Post::filterByCriteria($criteria)->get();
// → WHERE views >= 100 AND views <= 500
Relation filter#
Add the relation name as the key and its filterable columns as the value in $filterables. The criteria array must then nest the filter values under the relation name:
protected ?array $filterables = [
'tags' => ['name', 'slug'],
];
// GET /posts?tags[name]=php
$criteria = [
'tags' => ['name' => 'php'],
];
Post::filterByCriteria($criteria)->get();
// → WHERE EXISTS (SELECT * FROM tags WHERE … AND name = 'php')
Filter Key#
By default, every key in $criteria is treated as a potential filter. If you want filters to live under a dedicated namespace (e.g. to keep them separate from pagination and search parameters), set filter_key in config/repository.php:
// config/repository.php
'filter_key' => 'filters',
// GET /posts?filters[status]=published&page=2
$criteria = [
'filters' => ['status' => 'published'],
'page' => 2, // ignored by filterByCriteria
'search' => 'laravel', // used by searchByCriteria
];
Post::filterByCriteria($criteria)->get();
// → WHERE status = 'published'
scopeOrderByCriteria — sorting#
Looks for order (column) and direction (asc / desc, defaults to desc) keys in $criteria. The column must be in the model's sortable list or it is silently ignored.
Criteria keys: order, direction
// GET /posts?order=published_at&direction=asc
$criteria = ['order' => 'published_at', 'direction' => 'asc'];
Post::orderByCriteria($criteria)->get();
// → ORDER BY published_at ASC
// Direction defaults to desc when omitted
Post::orderByCriteria(['order' => 'title'])->get();
// → ORDER BY title DESC
Combining all three scopes#
$criteria = $request->all();
// e.g. ?search=laravel&status=published&views[gte]=100&order=published_at&direction=asc
Post::searchByCriteria($criteria)
->filterByCriteria($criteria)
->orderByCriteria($criteria)
->paginate(15);
Runtime manipulation#
Override or extend filterable / searchable / sortable columns at runtime without changing the model definition:
// Replace the list entirely
Post::setSearchables(['title', 'excerpt'])->searchByCriteria($criteria)->get();
Post::setFilterables(['status'])->filterByCriteria($criteria)->get();
Post::setSortables(['title', 'created_at'])->orderByCriteria($criteria)->get();
// Append to the existing list
Post::appendSearchables(['summary'])->searchByCriteria($criteria)->get();
Post::appendFilterables(['category_id'])->filterByCriteria($criteria)->get();
Post::appendSortables(['views'])->orderByCriteria($criteria)->get();
These scopes are chainable with any other builder call:
Post::setFilterables(['status', 'category_id'])
->filterByCriteria($criteria)
->where('user_id', auth()->id())
->latest()
->get();