Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Grease Laravel Package

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...

View on GitHub
Deep Wiki
Context7
v0.9.1

A correctness release — the last stop before 1.0. Laravel patch releases since 0.9.0 changed several methods Grease overrides, and a deep audit found two hydration bugs. All fixed, each with a parity test that fails without the fix.

CI: full parity matrix (PHP 8.2–8.5 × Laravel 12/13 + lowest-deps on both floors) green on the tagged commit.

⚠️ Requires Laravel ^12.69.2 || ^13.33

Grease overrides framework internals, so it now tracks each supported major's current patch line and mirrors its latest vanilla behaviour. Composer won't offer 0.9.1 below the floors — composer update within your major first.

Hydration correctness

  • Query-time casting (Model::query()->withCasts([...])) was ignored on greased rows — now applied, as vanilla.
  • Runtime setTable() on the model a query is built from (partitioned / archive tables) produced rows whose save()/delete() hit the class table — rows now carry the query model's table. The old caveat is gone.
  • Convention-named models (no $table) get their derived name written onto hydrated rows like vanilla, memoized per class so it stays cheap; any getTable() override (sharded, AsPivot, yours) is called per row, as vanilla.

Upstream drift mirrored

  • #[Refreshes] (13.33) — was dropped after the first model per class.
  • resolveClassAttribute() (13.21/13.22) — property-keyed cache; attributes on traits are read.
  • Request::all() (13.26) — input keys keep position and win a collision with an upload (12.x keeps the old order; probed once per process).
  • route() parameters (12.69.2 / 13.31) — % ? # are escaped; the URL fast path emitted them raw, truncating the path. If you use GreaseRoutingServiceProvider on 12.69.2+, upgrade.
  • ComponentAttributeBag::merge() (12.63 / 13.19) — a style default is terminated with ;.

Guardrails

  • HasGreaseWholeStateParityTest compares a greased model's entire instance state to vanilla (cold / warm / hydrated), with a tripwire that fails when Laravel adds an Eloquent attribute class.
  • CI now runs weekly against the latest releases, so upstream drift turns the build red within days.
  • Livewire compatibility proof now also covers Livewire 4.

See the CHANGELOG.

v0.9.0

Three byte-identical drop-ins for the HTTP request path — kept off the main tier menu as Conditional Helpers because the "condition" is whether the speedup materialises for your traffic, never about correctness.

CI: full parity matrix (PHP 8.2–8.5 × Laravel 12/13 + lowest-deps) green on the tagged commit.

CleanRequestInput — one input walk instead of two

The default stack runs TrimStrings and ConvertEmptyStringsToNull, each recursively walking and rebuilding the whole input tree (and TrimStrings runs Str::is() on every string leaf). The fused middleware does both transforms in one walk, hoists the except-merge out of the leaf loop, and matches excepts through a compiled CompiledPatternSet.

  • −59% (small body) to −68% (large nested body) of the input-scrub cost on Linux, byte-identical (CleanRequestInputParityTest).
  • Opt in by swapping the two middleware in bootstrap/app.php.
  • One caveat: per-middleware skipWhen isn't reproduced — keep the stock pair if you rely on it.

CompiledPatternSet — amortised Str::is() for many values

An array-aware matcher compiled once into the fastest faithful Str::is($patterns, $value) equivalent: literals to an isset() hash, bare * to a short circuit, wildcards to one merged alternation regex.

  • Wins when the build is amortised — one set matched against many values (~−96% in the middleware shape), or a long wildcard list (~−96% vs a per-pattern loop at ~50 patterns, roughly flat as the list grows).
  • A single pattern matched once should still use Str::is — the compile only pays off amortised.

A greased Request::is() (ships with the Request tier)

Vanilla recompiles Str::is and recomputes decodedPath() per pattern, per call; the override memoises the decoded path and matches through a per-worker-cached CompiledPatternSet.

  • ~−86% to −87% warm on Linux (single check / a 12-link nav partial).
  • Tuned for the persistent-worker (Octane) model; past a 1,000-pattern cache cap it defers to vanilla, so it is never unbounded-slower.

See the CHANGELOG and the Conditional Helpers guide for details.

v0.8.0

Two new standalone, byte-identical model opt-ins — both deliberately kept out of HasGrease because each is either narrow or carries a promise only you can make.

CI: full parity matrix (PHP 8.2–8.5 × Laravel 12/13 + lowest-deps) green on the tagged commit.

HasGreasedAcyclicSerialization — drop the recursion guard

Eloquent runs a debug_backtrace circular-reference guard on every toArray, getQueueableRelations, touchOwners, and push — a 100% tax for a <1% (self-referential-tree) problem ordinary eager loading can't even produce. One withoutRecursion() override removes it for a model that promises an acyclic graph.

  • Byte-identical for acyclic data (the guard only changes output on a real cycle).
  • ~1,230 ns saved per relation-bearing model → −36% on a 100-row with() list response (Linux, JIT) — the most common API shape.
  • Your one responsibility: leave it off self-referential tree models, or it recurses forever.

HasGreasedDecimalCasts — skip Brick\Math for canonical decimals

decimal:N reads round-trip through BigDecimal::of()->toScale() per row. When the stored value is already a canonical scaled string, that's the identity — so return it verbatim and skip Brick.

  • Never rounds: anything off-scale/garbage defers to Brick untouched. Worst case is "no speedup", never a wrong number (proven over 1.1M fuzzed cases vs the Brick oracle).
  • Driver-gated win: MySQL/PostgreSQL return canonical strings (fires); SQLite returns floats (defers). −3.7% on a greased transaction model.

Also

  • benchmarks/stack_excimer.php — an Excimer profiler over the fully-greased cumulative stack (how both of these were found).
  • A Tiers at a Glance docs menu ranking every tier by effort / risk / expected gain.

See the CHANGELOG for details.

v0.7.0

A sixth opt-in axis lands on the routing tier: Grease\Routing\UrlGenerator, an eager-index fast path for route().

Vanilla re-runs the full RouteUrlGenerator::to() assembly on every call — formatParameters / replaceRouteParameters / replaceNamedParameters / addQueryString / the optional-param walk, over a thick Arr/Collection layer. None of that is the Symfony route compile (that's cached on Route::$compiled and persisted through route:cache) — it's pure per-call string assembly, paid thousands of times in a link-heavy API-Resource payload for a URI shape that never changes.

The tier caches each route's class-pure [segments, params] shape (lazily, or pre-seeded by grease:route-cache) and replays it as a concat.

What you get (Linux, opcache + JIT)

  • −93% per route() call
  • ~−26% of an already-model-greased API-Resource response — the assembly cost is fixed while the model tiers shrink everything else, so it compounds (−84.7% full-stack on a 500-row × ~5-link payload)
  • Inert where there's no route(): in the cumulative stack, api_resource.json goes −74% → −82% and api_resource.blade −28% → −54%, while non-route() routes stay within noise

Byte-identical, or it defers

Absolute (the route() default) and relative are both accelerated, reusing vanilla's memoized formatRoot()/formatScheme(). Anything the fast path can't prove identical falls through to parent:: — domain / optional {param?} / scoped {param:field} / route-default / duplicate-name routes, extra params (query string), arity mismatches, empty-string / non-scalar values, signed URLs, and any URL::defaults()/formatHostUsing()/formatPathUsing() customization. The relative path derives from the full absolute URI and strips exactly what vanilla strips, so forceRootUrl()/useOrigin() root paths and subdirectory base paths are preserved.

Opt in

Register Grease\Routing\GreaseRoutingServiceProvider — it swaps the lazily-resolved url singleton (no bootstrap/app.php edit; signed URLs and route-cache rebinding keep working). grease:route-cache now writes the URL shape index alongside the middleware index.

Verified by UrlGeneratorParityTest (oracle = vanilla across absolute/relative, every defer case, encoding, exceptions, secure scheme, subdirectory + forced-root apps, prewarm-seed parity). Full matrix green on PHP 8.2–8.5 × Laravel 12/13.

Full changelog: https://github.com/One-Learning-Community/grease/blob/main/CHANGELOG.md

v0.6.0

The greased-pivot feature release. Byte-/behaviour-identical to vanilla, Linux-confirmed, and survived a three-agent adversarial parity audit.

Added

Greased many-to-many pivot (Grease\Eloquent\Pivot + HasGreasedPivots, folded into HasGrease). The pivot a belongsToMany hydrates per related row is a "dynamic model" the framework builds internally — and it never carried Grease's tiers, so a pivot-heavy belongsToMany()->get() paid the full per-row cost (the initialize* booters, resolveClassAttribute, and the timestamp Carbon round-trip on withTimestamps() pivots) the model tiers exist to remove. It was the one model class Grease structurally couldn't reach.

  • −75% on a 1,000-pivot-row get() (Linux/NAS).
  • Automatic for any model already using HasGrease — no API change.
  • A using(CustomPivot::class) relation keeps that class, and morphToMany pivots stay vanilla MorphPivot — both unaccelerated but byte-identical.

Performance

  • Eloquent builder __call dispatch verdict memoized — a standalone per-model opt-in (use HasGreasedQueries;), deliberately not bundled into HasGrease: it's byte-identical but swaps a custom builder in app-wide for a sub-0.1%-of-a-request gain, so it's reserved for query-construction-heavy paths.
  • wrapTable() identifier quoting memoized alongside the shipped wrap() memo — a sub-µs/query rider that compounds under Octane.

Changed

  • The published package is trimmed to LICENSE, composer.json, and src/ — a smaller install.

Parity

589 tests / 1648 assertions green (PHP 8.2–8.5 × Laravel 12/13), confirmed on Linux. The audited edge cases — pivot write-path, using/morph defers, __call precedence/STI/macros — are locked as regression guards.

Full notes in the CHANGELOG.

v0.5.1

A byte-identical hardening release. A full parity audit of the whole package surfaced four silent output divergences from vanilla, plus a few normal-usage cache-staleness edges. No API changes; every fix ships with a regression test that fails on the old behaviour, and the dispatch path actually came out faster.

Fixed

  • Event dispatcher: interface listeners are no longer dropped on a string-dispatched class event. The no-listener fast path gated on hasListeners(), which doesn't see interface listeners — so dispatch(SomeEvent::class) whose only listener was bound to an interface it implements silently fired nothing (vanilla fires it via addInterfaceListeners()). The fast path now also rules out interface listeners, gated on a boot-time flag set only when an interface-named listener is registered — so apps without them pay nothing, and no-listener dispatch is faster than before (the flag replaces a per-dispatch reachability check).

  • Casts: an enum that also implements Castable now routes to its castUsing() cast. The enum fast path gated on enum_exists() alone; vanilla's isEnumCastable() excludes Castable subclasses, so such an enum returned its raw case instead of the cast's output. Now mirrored — the extra check is memoized per cast type, so zero hot-path cost.

  • Serialization: out-of-range / zero dates defer to vanilla instead of being rewritten verbatim. The date fast path's shape check was purely syntactic, so a legacy MySQL 0000-00-00 00:00:00 (or 2026-02-30) was emitted as-is while vanilla overflow-normalizes it through Carbon (-0001-11-30). The guard now also confirms the date is real (checkdate + time ranges) before fast-pathing; valid dates still skip the Carbon round-trip. Same fix on the fromDateTime() write path, which feeds the save() dirty check.

  • Serialization: toArray() no longer drops a relation an appended accessor lazily loads. The relation-less short-circuit checked relations === [] before running accessors — but an $appends accessor reading $this->someRelation loads it as a side effect, which vanilla emits (relations serialize after attributes). The short-circuit now applies only if relations are still empty after the attributes are built.

  • Attributes: getDates() is no longer poisoned by a timestamps-disabled instance. The per-class cache ignored $model->timestamps = false / withoutTimestamps(), so a timestamps-off instance could blank the cache for timestamps-on instances of the same class (dropping created_at/updated_at from their output). Now keyed by usesTimestamps().

  • Attributes: runtime setKeyName() / setKeyType() / setIncrementing() invalidate the cast cache. These feed getCasts()'s key entry; the per-class cache now steps aside for an instance that mutates them at runtime, exactly like mergeCasts() / withCasts().

  • Request: isJson() is read live, not memoized. A content-type header changed after construction (content-negotiation middleware) left the memo stale; it now delegates to vanilla every call — the input-base memo already keeps it off the hot path.

Changed

  • Documented two runtime-surgery narrowings — reassigning a model's table (setTable() on a query prototype) or date format (setDateFormat() per instance) after it's in use — on the Caveats page, alongside a new guiding principle: Grease optimizes the 95–99% case; if you do something unusual with your models or the request, run your own tests with Grease enabled.
  • Added a compiler drift guard pinning the class-component opening emit byte-for-byte to the framework's static plus Grease's one seed line, so a future Laravel change to that emit (including a 12-vs-13 difference) fails CI instead of shipping a divergence.
v0.3.0

The performance surface roughly doubles. The Eloquent model trait grows from four tiers to seven and gains three byte-identical short-circuits on the hot read/write paths; a full drop-in Blade view Factory joins the compiler as a second render-path axis; the event dispatcher's view-event guard is memoized; and every published benchmark number is now generated live from the parity-gated harness. Output stays byte-identical to vanilla throughout — the parity suite grows to 317 tests / 867 assertions.

Added

  • Three new Eloquent model tiers — HasGrease now composes seven, each a method override that reads the per-class blueprint and falls back to parent:::

    • HasGreasedClassAttributes — caches Model::resolveClassAttribute() (the #[Table]/#[Fillable]/#[Hidden]/#[Appends]/… lookups the per-instance initialize* booters run ~13× per new model) behind a concat-free [class][attribute] carve-out, replacing vanilla's freshly-built "$class@$attr" cache key. Byte-identical bug-for-bug, including vanilla's property-less cache-key collision quirk. −13% on a 2,100-row eager get().
    • HasGreasedInitializers — freezes the four surviving initialize* trait booters (guards / hides / timestamps / touches) per class: the cold path runs parent:: once and snapshots the resulting properties; warm instances apply the snapshot by copy, eliminating the resolveClassAttribute() calls entirely. −8.4% on the eager get(), on top of the cache above.
    • HasGreasedCastProbes — memoizes the per-key cast-classification probes (isEnumCastable / isClassCastable / isClassSerializable) that addCastAttributesToArray() reruns on every row during toArray(). The verdict is a pure function of the cast type; memoized per [class][probe][key] (with array_key_exists, since the common verdict is false), reusing the casts-divergence guard. −10% on a rich-cast get()->toArray().
  • A greased Blade view Factory (Grease\View\Factory) — a second Blade render-path axis, bound as the view singleton by GreaseViewServiceProvider alongside the compiler. It overrides the hottest, allocation-shaped frames on the render path, each byte-identical and gated by a parity-asserted macro:

    • [@foreach](https://github.com/foreach) $loop bookkeeping — an in-place by-ref index update instead of an array_merge of the 10-key state every iteration (−27% on a $loop-heavy table);
    • [@yield](https://github.com/yield) — one preg_replace_callback over three non-overlapping markers in place of vanilla's three str_replace scans of the whole section (−19% on a layout);
    • [@push](https://github.com/push)/[@prepend](https://github.com/prepend) stack assembly — drops the per-pop tap() closure (−18% on asset stacks);
    • a one-line emit seed so class/no-[@props](https://github.com/props) components build the greased ComponentAttributeBag from the start, and getCompiledPath() memoization.
  • Grease\View\ComponentAttributeBag — a ComponentAttributeBag subclass whose merge() is two plain foreach loops instead of vanilla's ~5-allocation Collection pipeline (partition/mapWithKeys/->merge/->all). The compiler hands components this subclass, so the $attributes->merge([...]) nearly every component runs takes the fast path; merge() returns new static to stay greased down a chain. Together with the compiler and Factory: −38.7% simple / −30.6% rich on a 1,000-component render.

  • Live benchmark pipeline — bash benchmarks/export-metrics.sh runs every family on the canonical Linux Docker image and writes a single parity-gated docs/.vitepress/data/benchmarks.json that the docs (and the README's one-line summary) render from. The published numbers are now exactly what the harness measured, and a PARITY FAIL in any family aborts before its JSON is written.

  • A greased Blade compiler (Grease\View\Compiler) — a new opt-in tier that rewrites one emit, [@props](https://github.com/props) resolution, the per-render hot path every component pays. Vanilla compiles [@props](https://github.com/props) to a 20-line block that, on each render, rebuilds a flat name list (ComponentAttributeBag::extractPropNames), partitions incoming attributes with in_array (a linear scan per attribute), evaluates the [@props](https://github.com/props) array literal twice (once for the names, once for the array_filter that finds the defaults), allocates a second attribute bag, and snapshots the whole scope with get_defined_vars() to unset attribute-named locals. The greased emit collapses all of that into one call — Grease\View\Props::mergeAttributes($site, $decl, $attributes) — that partitions, applies defaults, and returns a single map (the resolved prop locals plus attributes, the surviving bag) which a tight $$key = $value loop binds into scope. The name set and which keys carry defaults are memoized per [@props](https://github.com/props) site (compile-time constants), so only fresh default values are read each render; the declaration is evaluated once; and the loop (not extract, which is slower and skips non-identifier keys) reproduces vanilla's locals exactly — including the inaccessible ${'icon-name'} kebab-alias local. Bind it by registering the (non-auto-discovered) Grease\View\GreaseViewServiceProvider, which extends blade.compiler via Compiler::fromBase() (registered directives/components carry over). Behaviour- identical, asserted A/B against the stock compiler across declaration/attribute scenarios (execution parity, incl. get_defined_vars). **−14%** on a full 1,000-anonymous-component render (benchmarks/blade.php, Taylor's 2024 challenge), holding across simple and prop-heavy components — a real, free, parity-safe slice of every component render that compounds with the other tiers.

    Narrowing: the declaration is evaluated once, not twice — so a non-deterministic default (e.g. [@props](https://github.com/props)(['id' => uniqid()])) yields one value rather than vanilla's second-evaluation value. Byte-identical for deterministic defaults (the norm).

Changed

  • Three byte-identical short-circuits on the model hot paths, added to the existing hydration and serialization tiers:
    • Empty fill([]) — the no-op that __construct runs on every hydrated row (via newFromBuilder's new static) still computed totallyGuarded() and fillableFromArray([]) up front. HasGreasedHydration now returns $this early on []. −5% on an eager get().
    • Relation-less toArray() — vanilla wraps every toArray() in withoutRecursion() (a debug_backtrace + Onceable trace-hash + WeakMap, to guard circular relations). With no relations loaded there is nothing to recurse into, so HasGreasedSerialization returns attributesToArray() directly; any loaded relation defers to parent::, guard intact. Byte-identical, including the circular-relation case. −27% on a relation-less get()->toArray() (index endpoint −81% → −86%).
    • fromDateTime() on dirty-checks — originalIsEquivalent() on save() compares a date column as fromDateTime($attr) === fromDateTime($original), where the operands are storage strings — so vanilla parses each into Carbon and formats it straight back to the identical string. A per-class probe certifies the round-trip and the fast path returns the string as-is. −38% on a save()-heavy dirty-check.
  • Grease\Events\Dispatcher::hasListeners() now memoizes its presence check per event name, against the same cache the dispatch() fast path already used (reset on listen()/forget(), so behaviour is identical). Previously only dispatch() consumed the cache; the public hasListeners() re-scanned every wildcard on each call. This matters because the framework fires view events through a hasListeners() guard (ManagesEvents::callCreator/callComposer), not a bare dispatch() — so the Blade/Livewire render path never reached the cache before. Re-rendering the same components now costs one wildcard scan per distinct view name instead of one per render: −92% on a Livewire-shaped render with realistic wildcards registered (557 μs → 47 μs, new ViewEventBench).
v0.2.0

Two opt-in serialization helpers for code that builds its output array by hand (Scout toSearchableArray, a JsonResource, an export) and so bypasses the attributesToArray() date tier. Both are byte-identical to a named vanilla expression; the parity suite grows to 225 tests.

Added

  • greaseSerializeDate(string $key): ?string on HasGreasedSerialization — the date-serialization tier's fast path exposed as a standalone primitive, so code that hand-picks attributes (Scout toSearchableArray, a JsonResource, an export) captures the date win without routing the whole model through attributesToArray(). Returns byte-identical output to serializeDate(asDateTime($this->attributes[$key])) (the toJSON form), reusing the existing per-class probe — certified classes skip the Carbon round-trip, everything else defers to the exact vanilla composition. Eligible for timestamps and plain datetime / immutable_datetime casts; −80% on the hand-pick date-serialization path (62.8μs → 12.3μs, fresh-hydrated).
  • greaseSerializeOnly(array $keys): array on HasGreasedSerialization — serialize a curated subset of a model to its array form, byte-identical to Arr::only($model->attributesToArray(), $keys) but without serializing the columns the filter would discard. The greased array path (date tier included) runs over the narrowed set; the model's own visible/hidden config is honored and its visible list is restored before returning (non-mutating, no clone). Picking 3 of 23 cast- heavy columns: −87% vs the naive serialize-all-then-filter (175.8μs → 22.1μs), and a statistical tie with mutating setVisible(...)->attributesToArray() — the win is the skipped work plus non-mutation, so no per-key-set precompute was added (it had no measurable headroom to recover).
v0.1.0

First release. Opt-in performance for Laravel's hot paths, with output asserted byte-identical to vanilla Eloquent across every cast type, edge value, and dirty-check.

Install

composer require onelearningcommunity/grease
use Grease\Concerns\HasGrease;

class User extends Model
{
    use HasGrease;
}

What's in it

Four composable per-model tiers over a single lazily-built, per-class blueprint (HasGrease, or the GreasedModel base class):

  • HasGreasedHydration — removes the per-row ReflectionClass, the per-instance casts rebuild, and newInstance's redundant work during hydration.
  • HasGreasedAttributes — memoizes getCasts / getCastType / getDates / mutator probes / getDateFormat; a divergence guard keeps runtime mergeCasts() / withCasts() correct.
  • HasGreasedCasts — flyweight cast dispatch replacing the per-access switch, an enum fast path that delegates conversion to the framework, and synonym-cast flyweight dedup.
  • HasGreasedSerialization — eliminates the Carbon parse-and-reformat round-trip for date serialization when a per-class probe certifies a byte-identical rewrite; defers to vanilla otherwise.

Grease\Events\Dispatcher — a drop-in, behaviour-identical faster event dispatcher (no-listener fast path, cached listener resolution, pre-compiled wildcard patterns), opt-in via GreaseEventServiceProvider. Speeds up every dispatch app-wide, not just model events.

Requirements

PHP 8.2+ · Laravel 12 / 13

Links

📖 Documentation · 📋 Changelog

Built proudly with Claude.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
ippanelcom/laravel-sdk
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus