Framework: String conversion for collection made from whereIn shows different behaviors depending on the value you specified

Created on 30 Jul 2020  路  6Comments  路  Source: laravel/framework

  • Laravel Version: 7.22.4
  • PHP Version: 7.21.31
  • Database Driver & Version:

Related Issue: https://github.com/laravel/ideas/issues/2299

Description:

I am trying to convert a collection array to string after I created it from whereIn.
As you see below, in the first case I specified the first value [1], and then I get a simple array [{"a":1,"b":1}] .
In the second case I specified the second value [2], and then I get a map of map {"1":{"a":2,"b":2}} .
I have to add flatten to the second case to standardize both cases. Is this behavior expected?

Steps To Reproduce:

$array = collect([['a' => 1, 'b' => 1],['a' => 2, 'b' => 2]]);
print($array->whereIn('a', [1]));
[{"a":1,"b":1}]
print($array->whereIn('a', [2]));
{"1":{"a":2,"b":2}}

This becomes a problem especially when you want to receive the array or any instance as a json response on the client side. The code will be like below.

$array = collect([['a' => 1, 'b' => 1],['a' => 2, 'b' => 2]]);

$case1 = $array->whereIn('a', [1]);
$response1 = response()->json([$case1])
$response1->content()
=> "[[{"a":1,"b":1}]]"

$case2 = $array->whereIn('a', [2]);
$response2 = response()->json([$case2])
$response2->content()
=> "[{"1":{"a":2,"b":2}}]"

Any ideas or advices?
Thanks!

Most helpful comment

When the filtering matches the first element the result is 0-indexed. Sorry if I don't understand clearly your reply.

I added this command to my ./routes/console.php file:

~~~php
Artisan::command('test', function () {
$array = collect([['a' => 1, 'b' => 1],['a' => 2, 'b' => 2]]);
$array->whereIn('a', [1])->dump();

$array->whereIn('a', [2])->dump();
// as index starts at one, serializes to object
$this->info(\json_encode($array->whereIn('a', [2])));

// as index starts at zero serializes to array
$this->info(\json_encode($array->whereIn('a', [2])->values()));

});
~~~

And the output is this:

~bash
$ php artisan test
Illuminate\Support\Collection {#791
#items: array:1 [
0 => array:2 [
"a" => 1
"b" => 1
]
]
}
Illuminate\Support\Collection {#784
#items: array:1 [
1 => array:2 [
"a" => 2
"b" => 2
]
]
}
{"1":{"a":2,"b":2}}
[{"a":2,"b":2}]
~

As you can see the first ->where(), which matches only the first element is keyed with 0. That is why it gets serialized as an array.

Keeping the index after filtering may seem odd when dealing mostly with numeric indexed collections, but it makes sense when a collection's key have meaningful data, for example I added this other command:

~~~php
Artisan::command('other', function () {
$people = collect();

$people->put('John', ['age' => 25, 'preference' => 'gaming']);
$people->put('Mary', ['age' => 22, 'preference' => 'coding']);

$people->dump();

$preferCoding = $people->where('preference', 'coding');

$preferCoding->dump();

$this->info(\json_encode($preferCoding));

// if we reset keys we lose the person's name:
$preferCoding->values()->dump();

$this->info(\json_encode($preferCoding->values()));

});
~~~

Which generates this output:

~bash
$ php artisan other
Illuminate\Support\Collection {#27
#items: array:2 [
"John" => array:2 [
"age" => 25
"preference" => "gaming"
]
"Mary" => array:2 [
"age" => 22
"preference" => "coding"
]
]
}
Illuminate\Support\Collection {#791
#items: array:1 [
"Mary" => array:2 [
"age" => 22
"preference" => "coding"
]
]
}
{"Mary":{"age":22,"preference":"coding"}}
Illuminate\Support\Collection {#784
#items: array:1 [
0 => array:2 [
"age" => 22
"preference" => "coding"
]
]
}
[{"age":22,"preference":"coding"}]
~

In this case we would loose the key when we apply the ->values() after filtering, which for this collection is not desirable.

As the collection cannot know if you want to keep the keys or not applying the ->values automatically is not an options, that is why I add it manually when dealing with collections where its keys doesn't add meaning to the data.

All 6 comments

I have same issue when after use collection filter method.

Hi @KotaroSetoyama

Try doing this:

~php
$case2 = $array->whereIn('a', [2])->values();
~

The ->values() will ignore the filtered keys from the ->where() and create a 0-indexed collection that will be serialized as a json array.

When you create the collection its internal array looks like this:

~php
[
0 => ['a' => 1, 'b' => 1],
1 => ['a' => 2, 'b' => 2]
]
~

After applying the ->where() that matches only the second element, the internal array becomes like this:

~php
[
1 => ['a' => 2, 'b' => 2]
]
~

So when serializing to json it could be either an array or an object with a key equals to "1".

After applying the ->values() method, a new collection is created and its internal array only preserve the old collections values, re-indexing its key from 0:

~php
[
0 => ['a' => 2, 'b' => 2]
]
~

And it gets serializes as you would expect.

As a rule of thumb I always apply the ->values() after filtering a collection (with ->where(), ->filter(), or other methods that create a new collection) when its keys aren't meaningful to the data it holds to avoid ending with an array that is not 0-indexed.

Hi @rodrigopedra
Thanks very much for the quick response.

So then why is the result of the filtering that matches the first element not like this?

[
    0 => ['a' => 1, 'b' => 1]
]

If the array preserves its index whatever you indexed, 0 or 1, the structure will be the same
0 => ['a' => 1, 'b' => 1] or
1 => ['a' => 2, 'b' => 2]
and this is less confusing, isn't it?

When the filtering matches the first element the result is 0-indexed. Sorry if I don't understand clearly your reply.

I added this command to my ./routes/console.php file:

~~~php
Artisan::command('test', function () {
$array = collect([['a' => 1, 'b' => 1],['a' => 2, 'b' => 2]]);
$array->whereIn('a', [1])->dump();

$array->whereIn('a', [2])->dump();
// as index starts at one, serializes to object
$this->info(\json_encode($array->whereIn('a', [2])));

// as index starts at zero serializes to array
$this->info(\json_encode($array->whereIn('a', [2])->values()));

});
~~~

And the output is this:

~bash
$ php artisan test
Illuminate\Support\Collection {#791
#items: array:1 [
0 => array:2 [
"a" => 1
"b" => 1
]
]
}
Illuminate\Support\Collection {#784
#items: array:1 [
1 => array:2 [
"a" => 2
"b" => 2
]
]
}
{"1":{"a":2,"b":2}}
[{"a":2,"b":2}]
~

As you can see the first ->where(), which matches only the first element is keyed with 0. That is why it gets serialized as an array.

Keeping the index after filtering may seem odd when dealing mostly with numeric indexed collections, but it makes sense when a collection's key have meaningful data, for example I added this other command:

~~~php
Artisan::command('other', function () {
$people = collect();

$people->put('John', ['age' => 25, 'preference' => 'gaming']);
$people->put('Mary', ['age' => 22, 'preference' => 'coding']);

$people->dump();

$preferCoding = $people->where('preference', 'coding');

$preferCoding->dump();

$this->info(\json_encode($preferCoding));

// if we reset keys we lose the person's name:
$preferCoding->values()->dump();

$this->info(\json_encode($preferCoding->values()));

});
~~~

Which generates this output:

~bash
$ php artisan other
Illuminate\Support\Collection {#27
#items: array:2 [
"John" => array:2 [
"age" => 25
"preference" => "gaming"
]
"Mary" => array:2 [
"age" => 22
"preference" => "coding"
]
]
}
Illuminate\Support\Collection {#791
#items: array:1 [
"Mary" => array:2 [
"age" => 22
"preference" => "coding"
]
]
}
{"Mary":{"age":22,"preference":"coding"}}
Illuminate\Support\Collection {#784
#items: array:1 [
0 => array:2 [
"age" => 22
"preference" => "coding"
]
]
}
[{"age":22,"preference":"coding"}]
~

In this case we would loose the key when we apply the ->values() after filtering, which for this collection is not desirable.

As the collection cannot know if you want to keep the keys or not applying the ->values automatically is not an options, that is why I add it manually when dealing with collections where its keys doesn't add meaning to the data.

@rodrigopedra
Thanks! I understand how I should use it now. Your example is quite helpful!

@KotaroSetoyama you're welcome :smile:

Was this page helpful?
0 / 5 - 0 ratings