orchestra/model
Orchestra Model provides a reusable base Eloquent model for Laravel and Orchestra Platform applications, offering common model conventions and helpers that simplify building consistent, maintainable database-backed features.
Install against the release line that matches your Laravel version, and pin it — this package tracks Laravel majors on separate branches, so a floating constraint is how you get an upgrade you didn't plan for.
composer require "orchestra/model:^6.0" # v6 line targets Laravel 8
composer why-not orchestra/model ^6.0 # run this first; treat blockages as stop signals
Before writing any code, read these in order:
src/Model.php — the base class. Note what it changes versus Illuminate\Database\Eloquent\Model: extra events, contracts, and which traits it already pulls in. Do not extend it blind.Validable, Searchable, Metable, Presenter) — this is the entire value proposition. Four files, readable in twenty minutes. Read them rather than trusting the description; the behaviour you actually get is whatever is in there, not what the README implies.src/User.php and src/Role.php — the shipped base models. Treat these as reference implementations, not as classes to adopt wholesale.Do not extend Orchestra\Model\Model in domain models. Create exactly one bridge:
namespace App\Models;
abstract class BaseModel extends \Orchestra\Model\Model
{
// deliberately empty — this file exists so the dependency has a single exit point
}
Every domain model extends App\Models\BaseModel. That one indirection is the difference between a weekend migration and a quarter-long one later.
First use case: pick the model you understand best, port it to BaseModel, run the existing test suite, and diff behaviour against vanilla Eloquent. If your suite is thin, write throwaway tests for mass assignment, casting, and save events before you trust the swap. If the diff is empty and you're not actually using any of the four traits, revert and go back to Illuminate\Database\Eloquent\Model.
If you need Orchestra's testing helpers, orchestra/testbench comes along with its own constraints. Resolve that in the same pass as the install, on a throwaway branch, before you've ported anything.
Containment first, features second. Land App\Models\BaseModel, a pinned constraint, a committed lockfile, and a written note of which traits you actually rely on before migrating more than a handful of models. Model count is your exit cost; keep it visible.
Validation on save (Validable). The trait binds validation to the saving event, so rules live on the model and throwing happens at save time rather than at the controller boundary.
class Post extends BaseModel
{
use \Orchestra\Model\Validable; // confirm exact namespace in src/
protected $rules = [
'title' => ['required', 'max:200'],
'body' => ['required'],
];
}
Use it for invariants that must hold no matter who writes the row — imports, console commands, queued jobs. Keep FormRequest validation for HTTP-facing UX (field-level messages, old() repopulation). The two are not redundant: the request gives good feedback, the model guarantees the invariant.
Request-driven scoping (Searchable). Where this trait pays off is admin index pages — pass the request in, get a filtered query back, paginate.
$posts = Post::query()->search($request->only(['q', 'status']))->paginate(25);
Always whitelist the keys you hand it. Never pass $request->all() into a query builder trait; you're one careless key away from a filter your users can invert to enumerate data.
Model metadata (Metable). Good for sparse, schema-hostile attributes — arbitrary settings, per-record flags, content authored by non-developers. Bad for anything you'd ever want to WHERE on at scale or index. If you find yourself filtering or sorting on meta values, promote the column.
$post->meta()->create(['key' => 'seo_title', 'value' => $title]);
// eager load when rendering lists — otherwise N+1
Post::with('meta')->paginate(25);
Don't modify the vendored base models. Orchestra\Model\User and Role are starting points. Subclass them in App\Models, override there, and keep the vendor tree pristine so a fork or a removal stays mechanical.
Testing. Because these traits hook saving, your factories and seeders exercise them too. Add a factory state for "minimally valid" records so tests that aren't about validation don't trip over a rule you added six months ago.
Exit rehearsal. Once a quarter, swap BaseModel to extend Illuminate\Database\Eloquent\Model on a branch and run the suite. Two things to learn: how many tests fail, and whether the failures are in your models or in traits you'd need to reimplement. That's your real migration estimate, refreshed for free.
Trait boot order and collisions. Traits that hook boot()/booted() are order-sensitive when combined with your own model events, observers, and $dispatchesEvents. If a save behaves unexpectedly, dd(static::getEventDispatcher()->getListeners('eloquent.saving: '.static::class)) and see what's actually registered.
saving doesn't fire on bulk writes. Model::insert(), upsert(), query-builder updates, and many DB::table() calls bypass model events entirely — so Validable silently doesn't run. Anything that must be enforced regardless of path needs a database constraint, not just a model rule.
Searchable + pagination + sort is an injection surface. Sorting parameters, in particular, get passed to orderBy more often than they get validated. Whitelist column names explicitly; an arbitrary ?sort= reaching orderBy is a classic.
Validation errors at the wrong layer. A ValidationException thrown from a queued job or an artisan command is a stack trace, not a form error. If a model save can fail validation from a non-HTTP context, catch and handle it there deliberately.
Silent semantic drift across versions. A base class that adds casting, mutators, or save-time events changes behaviour subtly between releases with little changelog discipline. Pin exact versions, commit the lockfile, and re-run the behavioural diff after every bump. Keep $casts declared explicitly on your models rather than relying on inherited defaults — it makes drift visible.
The Laravel ceiling is real. The v6 line targets Laravel 8. Before adopting, run composer why-not orchestra/model <version> against your target Laravel and PHP matrix, and test on the oldest and newest PHP you support. If no release line exists for your target Laravel major, the package is not an option regardless of how convenient the traits look — a base class is the one thing you cannot quietly patch around mid-upgrade.
orchestra/testbench is a second dependency. It brings its own constraints and they typically collide late, after models are already ported, breaking CI rather than production. Resolve it on day one.
Undocumented magic is the onboarding cost. The API is familiar Eloquent, so engineers assume they know it — and then hit trait-specific behaviour at save or query time. Write a short internal note listing which traits you use, what each is expected to do, and where to look when it misbehaves. When someone debugs something surprising, append it.
Reproduce before you inherit. Validable and Searchable are tens to low hundreds of lines. If a spike shows those are the only two you need, put them in App\Models\Concerns, extend Illuminate\Database\Eloquent\Model, and delete the dependency. Owning two small traits is cheaper than owning a base class, a test harness constraint, and an upgrade ceiling.
Fork-or-remove trigger, decided in advance. Agree now that a missed Laravel major triggers either an internal fork (vendor it under your namespace, documented patch policy, license permitting) or removal. Deciding this during a panic upgrade is how a contained dependency becomes load-bearing.
How can I help you explore Laravel packages today?