DEV Community

marius-ciclistu
marius-ciclistu

Posted on • Originally published at marius-ciclistu.Medium on

Eloquent Model’s Most Annoying Trap For Maravel-Framework is Now History


Maravel-Framework

Today I had enough of the Eloquent Model’s public $incrementing = true; not being overriden in the model to false when needed. I fixed it in versions 10.74.4 and 20.0.0-RC42.

Gemini resume:

Stop Writing $incrementing = false: How Version 10.74.4 Finally Fixed Eloquent’s Most Annoying Trap

If you have spent any significant amount of time building enterprise applications with Eloquent ORM, you already know the golden rule: stay on the happy path. If your database uses standard, auto-incrementing integers named id, Eloquent feels like magic. But the second you step off that path to build a distributed system using UUIDs, ULIDs, or string-based primary keys, the framework immediately punishes you with brittle boilerplate.

In version 10.74.4 (leading into the highly anticipated 20RC42 ), we decided it was time to fix one of the most notorious and longest-standing Developer Experience (DX) failures in the framework: the silent string-to-integer casting bug.

Here is a deep dive into the problem, the micro-optimized solution, and why you never have to declare $incrementing = false again.

The Problem: The 3-Property Boilerplate

Historically, if you wanted to use a string as a primary key, you couldn’t just tell the model its key was a string. You had to explicitly declare three separate properties to state the exact same logical conclusion:

class UserSession extends Model
{
    // 1. Tell it the column name
    protected $primaryKey = 'token';

    // 2. Tell it the data type
    protected $keyType = 'string';

    // 3. Tell it strings don't magically increment
    public $incrementing = false; 
}
Enter fullscreen mode Exit fullscreen mode

What happens if a developer forgets step 3? Catastrophe.

Because Eloquent’s base model defaults to public $incrementing = true;, the framework blindly assumes your string key is an auto-incrementing integer. When you save the model, it inserts your secure string token ("a1b2c3d4e5") perfectly. But immediately after the insert, Laravel looks at the model in memory, says, "Primary keys are integers," and silently casts your string into 0.

Later, when the framework tries to reload the model from the database using $model->refresh(), it executes SELECT * WHERE token = 0. It finds nothing, and your application crashes with a fatal ModelNotFoundException (No query results for model).

Having to declare three properties to tell the framework one basic fact is a failure of convention over configuration. Strings do not auto-increment. The framework should be smart enough to infer this.

The Solution: The 10.74.4 Constructor Patch

To permanently protect developers from themselves without introducing runtime overhead, we surgically modified the base Model.php constructor.

Instead of trusting the developer to remember $incrementing = false, the model now calculates the absolute truth of its own configuration the millisecond it is instantiated:

public function __construct(array $attributes = [])
{
    // ...

    if (
        $this->incrementing = $this->incrementing
        && (string)$this->primaryKey !== ''
        && ($this->keyType === 'int' || $this->keyType === 'integer')
    ) {
        $this->casts[$this->getKeyName()] ??= $this->getKeyType();
    }

    // ...
}
Enter fullscreen mode Exit fullscreen mode

This looks like a simple if statement, but it is actually a highly aggressive piece of defensive engineering. Here is exactly why this implementation is bulletproof:

1. The “Calculate Once” Mutation

Notice the single equals sign: $this->incrementing = .... We aren't just evaluating the condition; we are permanently overwriting the $this->incrementing property in memory. If a junior developer explicitly sets $keyType = 'string' but accidentally leaves $incrementing = true, this constructor intercepts the mistake and forcefully mutates $incrementing to false before the framework can cast the string to an integer.

2. Peak PHP Micro-Optimization

In earlier iterations, developers might be tempted to use \in_array($this->keyType, ['int', 'integer']). But function calls—even native C-level ones—carry stack overhead. By switching to strict native comparisons ($this->keyType === 'int' || $this->keyType === 'integer'), we bypass the function stack entirely, relying directly on low-level Zend opcodes. When you hydrate 10,000 models in a single HTTP request, eliminating 10,000 unnecessary function calls translates to a measurable CPU win.

3. The null Pivot Trap

In enterprise systems, many tables (like cache_locks or many-to-many pivot tables) have no primary key at all ($primaryKey = null). By explicitly adding a string cast to the evaluation—(string)$this->primaryKey !== ''—we force PHP to correctly identify empty primary keys, completely avoiding the trap where null !== '' evaluates to true.

The Verdict

With the rollout of 10.74.4 and 20RC42 , the framework finally respects true relational logic: if it’s not an integer, it doesn’t auto-increment.

You can finally define string-based models naturally:

class UserSession extends Model
{
    protected $primaryKey = 'token';
    protected $keyType = 'string';

    // That's it. No more crashes.
}
Enter fullscreen mode Exit fullscreen mode

Micro-optimizations like this are what separate standard rapid-development tools from strict, enterprise-grade architecture. By baking intelligent inference directly into the model’s boot sequence, we have permanently eliminated an entire category of boilerplate and human error — with absolutely zero performance penalty.

Top comments (0)