onelearningcommunity/grease
Grease is a menu of independent, byte-identical performance tiers for Laravel's hot paths—Eloquent hydration, casting and serialization, events, Blade, config, validation, the container and router. Opt in per tier; the model trait is the zero-config starting point. Zero framework changes, and anythi...
Check your framework floor first. v0.9.1 requires laravel/framework ^12.69.2 || ^13.33 (PHP 8.2–8.5). Composer will not offer the package below those floors, so bump within your current major before installing:
composer update laravel/framework --with-dependencies
composer require onelearningcommunity/grease
If you are stuck on an older 12.x/13.x patch, stay on 0.9.0 — do not force the constraint.
Publish config (if needed)
php artisan vendor:publish --provider="OneLearningCommunity\Grease\GreaseServiceProvider"
'enabled' => env('GREASE_ENABLED', true),
First use case: prove parity, not speed. Grease overrides framework internals, so the first thing to run on your codebase is the shipped parity trait against your hairiest model — one with casts, a custom getTable(), or a runtime setTable():
use OneLearningCommunity\Grease\Testing\HasGreaseWholeStateParityTest;
class InvoiceParityTest extends TestCase
{
use HasGreaseWholeStateParityTest;
protected function modelClass(): string
{
return Invoice::class;
}
}
It compares the entire instance state (cold / warm / hydrated) against vanilla. Green there means you can trust the savings; measure with Debugbar/XHProf afterwards.
Upgrade 0.9.0 → 0.9.1 in the right order. Framework patch first, then composer update onelearningcommunity/grease, then the full suite. 0.9.1 mirrors upstream drift since 0.9.0, so a mixed state (new Grease, stale Laravel) is not a supported combination.
Query-time casting now round-trips. Model::query()->withCasts([...]) is applied to greased rows exactly as vanilla. If you had a workaround applying casts manually after hydration, delete it.
Partitioned / archive tables are now safe. Runtime setTable() propagates to hydrated rows, so save()/delete() hit the query's table rather than the class table. The old caveat is gone — this is the pattern to reach for:
$rows = Order::query()->setTable('orders_2025_09')->where('status', 'open')->get();
$rows->first()->save(); // writes to orders_2025_09
Convention-named models stay cheap; overrides don't. Models without $table get the derived name memoized per class. Any getTable() override (sharded, AsPivot, your own) is called per row, as vanilla does — budget for that in hot loops.
Routing: upgrade if you use GreaseRoutingServiceProvider. route() parameters containing %, ? or # were emitted raw by the URL fast path, truncating the path. Fixed on 12.69.2 / 13.31. On 12.69.2+, 0.9.1 is required, not optional.
Blade/Livewire surfaces. ComponentAttributeBag::merge() now terminates a style default with ; (12.63 / 13.19 parity), and the Livewire compatibility proof now covers Livewire 4. Re-run component snapshot tests after upgrading.
#[Refreshes]. It is the cheapest regression net you will get for a package that patches internals.laravel/framework to a known-good patch and let your scheduled build (not your deploy) discover drift — Grease's own CI runs weekly against latest releases precisely because upstream moves.GREASE_ENABLED=false wired to an env flag so you can bisect a behaviour difference in one deploy.composer update silently does nothing. If you are below the floors, Composer just won't offer 0.9.1 and you'll assume you're current. Verify with composer show onelearningcommunity/grease after updating.
Request::all() key order differs by major. On 13.26+ input keys keep position and win a collision with an uploaded file; 12.x keeps the old order (probed once per process). Tests asserting array order across majors will lie to you.
#[Refreshes] was being dropped after the first model per class before 0.9.1 — if you saw stale attributes on second-and-later models, that was the bug, not your code.
resolveClassAttribute() now reads attributes on traits (13.21/13.22) and uses a property-keyed cache. Trait-declared attributes that appeared ignored are now honoured — re-check any test that asserted the old behaviour.
The parity tripwire is a feature. HasGreaseWholeStateParityTest fails deliberately when Laravel ships a new Eloquent attribute class. Don't mute it; that failure is your early warning that Grease hasn't mirrored the new state yet.
Caching conflicts still apply. Avoid remember()-style caching around rows mutated via touch()/updated_at, and clear cache when stale data appears:
php artisan cache:clear
$vanilla = Model::withoutGrease()->find(1); // or run with GREASE_ENABLED=false
$greased = Model::find(1);
dd($vanilla->getAttributes(), $greased->getAttributes());
\DB::enableQueryLog();
$results = User::where(...)->get();
dd(\DB::getQueryLog());
\OneLearningCommunity\Grease\Facades\Grease::disable();
'log_optimizations' => true in config/grease.php when you need to see which paths were taken.Custom optimizers via the service provider — but treat every override as a parity liability and cover it with the whole-state test:
Grease::extend(function ($query, $model) {
if ($model instanceof Post) {
$query->select(['id', 'title', 'created_at']);
}
});
Query builder overrides remain available for app-specific builders:
use OneLearningCommunity\Grease\Concerns\OptimizedQuery;
class CustomQueryBuilder extends Builder
{
use OptimizedQuery;
}
Version-gated behaviour. With two supported majors in flight (12 and 13), branch any extension that depends on ordering or attribute resolution rather than assuming one shape:
if (app()->runningInConsole()) {
\OneLearningCommunity\Grease\Facades\Grease::disable();
}
How can I help you explore Laravel packages today?