2

I have a table of players with a field called name (which is a string, of course). When adding a player to the database, I check for uniqueness using a custom rule:

public function passes($attribute, $value)
{
    return !($this->players->where('name', $value)->count());
}

This works most of the time. I noticed, however, that strings are cast to numbers whenever possible. For example, if there is a player with the name "123", then I cannot add a new player whose name is "0000123" or "123." or other such variations with leading or trailing zeroes. Or, for example, if the DB has a player called "9.87e2", then I can't add "987".

As a sanity check, I logged gettype($value) inside the function and confirmed that it is always a string. I then tried replicating the issue in a SQL sandbox, but everything worked as expected. Does that mean this is a bug in Laravel?

5
  • This doesn't seem right. What is the datatype of name in your database?(DESCRIBE players;, or post your migration). If the database can handle 00123 and 123 as separate entities, and you don't have protected $casts = ['name' => 'integer']; on your Player model, then all should be well. Sidenote, $this->players->where() doesn't directly query the database, but rather the resulting Collection of $this->players()->get(). This might be better as $this->players()->where('name', $value)->count() (->players() vs ->players, without the ()). Commented Jun 22, 2021 at 19:03
  • @TimLewis Aha! Your last suggestion (i.e., adding the parentheses) fixed it. Thanks for your help. I guess I don't understand the difference between ->players and ->players(). Commented Jun 22, 2021 at 19:33
  • 1
    No worries! Quick explanation,->players (without the ()) will execute $this->players()->get() behind the scenes, which results in a Collection, which has a ->where() method, which searches against the records in that Collection and not the Database. ->players(), with the (), is a Query, which searches directly against the DB. You'd then execute ->get(), ->count(), etc, like $this->players()->where(...)->get() Commented Jun 22, 2021 at 19:36
  • @TimLewis Okay, that makes more sense now, and also explains why I wasn't able to use players->get() when debugging, because the players were already implicitly got. Thanks again. (And if you copy/paste your comment to an answer, I'll accept it.) Commented Jun 22, 2021 at 19:43
  • 1
    Sure thing! When I get a sec, I'll translate that to an answer. Cheers! And you're correct; ->players->get() can work, but you need to pass something to ->get(), like ->players->get('name'). See laravel.com/docs/8.x/collections#method-get Commented Jun 22, 2021 at 19:55

1 Answer 1

2

The issue doesn't seem to be casting, but rather loose comparison on name when using ->where() on the resulting Collection vs ->where() on the Database layer. Compare:

$this->players->where('name', $value)->count();

In the above, $this->players will be a Collection of all Player records from the Database associated with $this (another Model I assume). This is the equivalent of something like:

$players = collect([
  ['id' => 1, 'name' => '123'],
  ['id' => 2, 'name' => '234'],
  // ...
]);

$players->where('name', '123')->count();    // 1
$players->where('name', '000123')->count(); // 1 (That's odd...)
$players->where('name', '234')->count();    // 1
$players->where('name', '345')->count();    // 0
// ... Ect etc.

In the above examples, you can see that the "loose" comparison on name, using '00123' and '123' both returns 1. This is problematic, as the Database can handle '000123' and '123' as distinct entities, but querying against a Collection cannot.

To get around this, we should query directly against the Database, using the players method instead of property (() denotes a method, lack of () denotes a property):

$query = $this->players()->where('name', $value); // Omitted the `count()` for now

At this point in time, $query is a Database Query (Builder instance). We can see the SQL that would be executed by calling ->toSql():

dd($query->toSql());
// SELECT * FROM players WHERE related_id = ? AND name = ?

(Replace related_id with whatever $this represents in the original question).

Chaining the method ->count() will execute this as a SELECT count(...) ... query, instead of counting the length of the Collection.

So, to put it all together; Query directly against the Database using ->players(), execute a ->count() or ->exists() query:

return !$this->players()->where('name', $value)->count();  // 0 or # > 1
// OR
return !$this->players()->where('name', $value)->exists(); // true or false
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.