Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

jjudge's avatar

Using a string primary key

I am selecting model instances from a table that uses UUIDs as primary keys. That works fine when I pass a string into find() to fetch the model instance.

MyModel::find('18af8e5b-97ce-b26d-9037-4e6e0fb82f20');
// Returns my instance.

MyModel::find('18');
// Rightly returns null.

MyModel::find(18);
// Returns my model instance again. Yikes!

It seems that eloquent is passing the data type provided into the query, and MySQL is doing some casting (a-la-PHP!) to return a row that it should not return.

How can I set up the model to make sure find() will always do a string search on the primary key, regardless of what has been passed in?

As a bonus, this returns the first row it finds:

MyModel::find(0);

No, eloquent, naughty! How do I whip it into shape? None of the following on the model work:

public $incrementing = false;
protected $casts = [
    'id' => 'string',
];
protected $keyType = 'string';

So is my only option to override static find()?

0 likes
9 replies
lostdreamer_nl's avatar

In your model you'll have to tell eloquent that it is not using an icrementing ID:

public $incrementing = false;

But the find(18) is something with PHP:


dd(
'1jufgidufg' == 1,
'2ufisufs' == 2,
'18oidfjgodijg' == 18
'sdwerdsf' == 0
);

// all true

click's avatar

It is is mysql thing. If you run a custom query and search for an integer it also gives you a record back:

\DB::select('SELECT * FROM table WHERE uuid = ?', [18])

or

SELECT * FROM table WHERE uuid = 18

With plain PDO statements you have the option to set a bind parameter type to PDO::PARAM_STR which solves the issue. But... it ain't a solution for your situation.

$value = 18; // no results
$value = '18'; // no results
$value = '18af8e5b-97ce-b26d-9037-4e6e0fb82f20'; // result
$pdo = \DB::getPdo();
$stmt = $pdo->prepare('SELECT * FROM table WHERE uuid = :uuid');
$stmt->bindParam('uuid', $value, \PDO::PARAM_STR);
$res = $stmt->execute();
dd($stmt->fetchAll());
1 like
jjudge's avatar

Firstly, I have tried the $incrementing thing, and that only tells Laravel not to try to add an incrementing value when creating an record, I'm not creating anything here - these are read-only (a great package michaelachrisco/readonly makes that dead easy).

Secondly, this cannot be a PHP thing. This is an eloquent model. What gets selected from the database is what the query in the database returns. It is not as though ALL records are selected then sifted out by PHP.

If I run this query through MySQL Workbench:

select id, first_name from users where id = 18;
// Brings back: 18af8e5b-97ce-b26d-9037-xxxxxxxxxxxx    Albe

then it will return a row with the id `'18123'. MySQL is (incorrectly IMO) casting the IDs it is querying over to a number before doing the comparison, and getting the same result as PHP. Maybe there is a database option I can set to turn it off.

Other people have found this too: https://stackoverflow.com/questions/33543671/mysql-automatic-string-to-integer-casting-in-where-clause and it looks like it has been around for a long time. If I cannot turn off this feature in MySQL, then eloquent needs to protect the database by always passing in a number to the id field, if only I could somehow tell it to.

All my years using MySQL, and I never knew this auto-casting happened in this direction in expressions.

click's avatar

So yeah... I don't think using the PDO objects is any helpful here. Overwriting the find method would be more helpful but won't solve all of your cases.

$model->where('name, 'John')->where('uuid', 18);

will still fail if you only overwrite the find method.

Not fool proof solution but... Can't you just cast the value to a string before you add it to the query? Or you create you own scope for search on Uuid and cast it as a string like:

public function scopeWhereUuid($builder, $value)
{
    return $builder->where('uuid', (string) $value);
}

// and use like
$model->whereUuid(18)->first(); 
jjudge's avatar

@mushood Luckily I'm not having to generate them, as I'm just reading from an external system. I'm using the eloquent models with global scope closures ("global", but locally defined) to pull in data from relationships and related tables, which abstracts the external database structure out of sight very nicely. So the convenience of using eloquent for the models here is a big benefit.

It's a good article explaining how it would work, and it's handy having it all put into a trait.

mushood's avatar

@consil Yh but its a really nice article. Thank you for sharing the issue. Got to learn another to doing things. I never thought about UUID before -.-'''

jjudge's avatar

@click Yes, I can cast the values before using them to find a record. I was just hoping this was a well-known problem with a solution already in place and just hiding from me. I guess not, and I just need to be careful instead.

My first thought was about how many systems are out there where I can just put a zero into URLs with a UUID just to see what happens. It just kind of adds an unnecessary risk.

click's avatar

@consil I understand.

Btw, I wouldn't worry about the 'entering a zero in URL'. The first thing is that most of the variables are already strings (form values, route parameters, etc) so that already prevents it to retrieve the first table record that starts with a zero.

And besides that, there should always be some authorization to prevent users seeing models they are not allowed to see.

Please or to participate in this conversation.