Skip to content

[13.x] Cache parent $table override detection in initializeModelAttributes#60009

Closed
olivier-zenchef wants to merge 1 commit intolaravel:13.xfrom
olivier-zenchef:fix/eloquent-reflectionclass-cache
Closed

[13.x] Cache parent $table override detection in initializeModelAttributes#60009
olivier-zenchef wants to merge 1 commit intolaravel:13.xfrom
olivier-zenchef:fix/eloquent-reflectionclass-cache

Conversation

@olivier-zenchef
Copy link
Copy Markdown
Contributor

@olivier-zenchef olivier-zenchef commented May 6, 2026

Problem

PR #59701 (v13.6.0, fixing #59698) added child-class $table override detection so #[Table] attributes on a child override an inherited $table property.
However the implementation creates a fresh ReflectionClass(static::class) on every model construction:

public function initializeModelAttributes()
{
    $table = static::resolveClassAttribute(Table::class);

    $reflection = new ReflectionClass(static::class);

    $declaresTable = $reflection->hasProperty('table')
        && $reflection->getProperty('table')->getDeclaringClass()->getName() === static::class;

    if (! $declaresTable && $reflection->getAttributes(Table::class) !== []) {
        $this->table = $table->name ?? null;
    } else {
        $this->table ??= $table->name ?? null;
    }
    // ...
}

The two questions being asked — "does this class declare $table itself?" and "does this class have a #[Table] attribute?" — are class-level facts that don't vary between instances. The ReflectionClass PHP wrapper is allocated fresh per call.

Reproducible benchmark

laravel-eloquent-bench, N=100,000 constructions, synthetic 120-fillable / 13-cast / 50-relation / 4-trait model on PHP 8.4.14:

Version Wall (ms) µs/construct Δ vs v13.3.0
v13.3.0 515 5.15
v13.6.0 (#59701 lands) 540 5.40 +25 ms (+5%)

On larger fat models with more class metadata (more fillable, relations, traits, larger class file) the delta scales up, because the ReflectionClass payload it instantiates per construct grows with the class.

Fix

Cache the boolean result via the existing static::$classAttributes class-cache pattern (the same cache resolveClassAttribute() already uses). The cached method is invoked from initializeModelAttributes and only does the ReflectionClass work once per class:

protected static function tableAttributeOverridesProperty()
{
    $cacheKey = static::class.'@__tableAttributeOverride';

    if (array_key_exists($cacheKey, static::$classAttributes)) {
        return static::$classAttributes[$cacheKey];
    }

    $reflection = new ReflectionClass(static::class);

    $declaresTable = $reflection->hasProperty('table')
        && $reflection->getProperty('table')->getDeclaringClass()->getName() === static::class;

    return static::$classAttributes[$cacheKey]
        = (! $declaresTable && $reflection->getAttributes(Table::class) !== []);
}

First construction of a given model class still pays the ReflectionClass cost; every subsequent construction is a single array_key_exists hash lookup. The behavior of #59701 (and the bug it fixed in #59698) is preserved exactly. The cache is also automatically flushed by the existing Model::clearBootedModels() (which already clears static::$classAttributes).

If the maintainers prefer a parallel cache property over the sentinel-key approach, I'm happy to switch to:

protected static array $classTableOverridesProperty = [];

— either way the behavior is identical.

Tests

References

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 6, 2026

Thanks for submitting a PR!

Note that draft PRs are not reviewed. If you would like a review, please mark your pull request as ready for review in the GitHub user interface.

Pull requests that are abandoned in draft may be closed due to inactivity.

@olivier-zenchef olivier-zenchef marked this pull request as ready for review May 7, 2026 15:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants