130

In Laravel, if I perform a query:

$foods = Food::where(...)->get();

...then $foods is an Illuminate Collection of Food model objects. (Essentially an array of models.)

However, the keys of this array are simply:

[0, 1, 2, 3, ...]

...so if I want to alter, say, the Food object with an id of 24, I can't do this:

$desired_object = $foods->get(24);
$desired_object->color = 'Green';
$desired_object->save();

...because this will merely alter the 25th element in the array, not the element with an id of 24.

How do I get a single (or multiple) element(s) from a collection by ANY attribute/column (such as, but not limited to, id / color / age / etc.)?

Of course, I can do this:

foreach ($foods as $food) {
    if ($food->id == 24) {
        $desired_object = $food;
        break;
    }
}
$desired_object->color = 'Green';
$desired_object->save();

...but, that's just gross.

And, of course, I can do this:

$desired_object = Food::find(24);
$desired_object->color = 'Green';
$desired_object->save();

...but that's even more gross, because it performs an additional unnecessary query when I already have the desired object in the $foods collection.

EDIT:

To be clear, you can call ->find() on an Illuminate Collection without spawning another query, but it only accepts a primary ID. For instance:

$foods = Food::all();
$desired_food = $foods->find(21);  // Grab the food with an ID of 21

However, there is still no clean (non-looping, non-querying) way to grab an element(s) by an attribute from a Collection, like this:

$foods = Food::all();
$green_foods = $foods->where('color', 'green'); // This won't work.  :(
starball
  • 20,030
  • 7
  • 43
  • 238
Leng
  • 2,948
  • 2
  • 21
  • 30

9 Answers9

179

You can use filter, like so:

$desired_object = $food->filter(function($item) {
    return $item->id == 24;
})->first();

filter will also return a Collection, but since you know there will be only one, you can call first on that Collection.

You don't need the filter anymore (or maybe ever, I don't know this is almost 4 years old). You can just use first:

$desired_object = $food->first(function($item) {
    return $item->id == 24;
});
kalley
  • 18,072
  • 2
  • 39
  • 36
  • 7
    Hey, thanks! I think I can live with that. Still unusually verbose in my opinion for what is usually such an 'Eloquent' framework haha. But it's still much cleaner than the alternatives so far, so I'll take it. – Leng Jan 05 '14 at 07:09
  • As @squaretastic is pointing out in the other answer, inside your closure you're making an assignement and not a comparison (i.e. you should == and not = ) – ElementalStorm Jun 09 '14 at 12:57
  • 28
    Actually it's not even necessary to call `filter()->first()` you can just call `first(function(...))` – lukasgeiter Feb 04 '15 at 20:10
  • from Laravel Collection documentation. https://laravel.com/docs/5.5/collections#method-first `collect([1, 2, 3, 4])->first(function ($value, $key) { return $value == 2; });` – Shiro Dec 12 '17 at 15:25
  • 3
    You can do same thing with where function. `$desired_object = $food->where('id', 24)->first();` – Bhavin Thummar Aug 28 '18 at 11:02
  • When the compared value is "dynamic" (means a `$variable`), do the usual way `filter(function MyModel $model) use ($someVar) {})`. `MyModel` is a type-hint for PHP, `use ($somVar)` is here the point, to handle over variables to the closure. – Roland Oct 01 '18 at 09:41
  • And if you need to pass a variable into this anonymous function, you can do `->first(function($item) use ($passthru) {`. More info in [this SO answer](https://stackoverflow.com/a/29548311/3911632) – Chad Feb 05 '19 at 16:43
133

Laravel provides a method called keyBy which allows to set keys by given key in model.

$collection = $collection->keyBy('id');

will return the collection but with keys being the values of id attribute from any model.

Then you can say:

$desired_food = $foods->get(21); // Grab the food with an ID of 21

and it will grab the correct item without the mess of using a filter function.

Maksym
  • 3,276
  • 3
  • 22
  • 27
  • 2
    Really useful, especially for performance, ->first() can be slow when called multiple times (foreach in foreach...) so you can "index" your collection like : `$exceptions->keyBy(function ($exception) { return $exception->category_id . ' ' . $exception->manufacturer_id;` and use `->get($category->id . ' ' . $manufacturer->id)` after ! – François Breton Apr 25 '16 at 16:32
  • Does this key continue to be used when new items are added to the collection? Or do I need to use keyBy() every time a new object or array is pushed onto the collection? – Jason Sep 22 '16 at 14:22
  • Most likely you have to call it again since `keyBy` returns new collection from what I remember, not sure though, you can check `Illuminate/Support/Collection` to find it out. (Not working in Laravel for quite some time so someone can correct me). – Maksym Dec 04 '16 at 20:44
  • This didn't worked for me, it returned another item, the next item, if I type get(1) it will return the item which has number 2 as id. – Jaqueline Passos Jan 21 '17 at 16:42
  • Batch loading a table and it took a day. Used this solution and it took minutes. – Jed Lynch Apr 27 '19 at 04:29
  • I love Laravel sooo much! – waterloomatt Jul 18 '19 at 23:51
49

As from Laravel 5.5 you can use firstWhere()

In you case:

$green_foods = $foods->firstWhere('color', 'green');
Victor Timoftii
  • 2,717
  • 1
  • 16
  • 15
9

Use the built in collection methods contain and find, which will search by primary ids (instead of array keys). Example:

if ($model->collection->contains($primaryId)) {
    var_dump($model->collection->find($primaryId);
}

contains() actually just calls find() and checks for null, so you could shorten it down to:

if ($myModel = $model->collection->find($primaryId)) {
    var_dump($myModel);
}
Ziad Hilal
  • 316
  • 3
  • 5
  • We understand that find() accepts a primary ID. What we want is a method that accepts *any* attribute, such as "color" or "age". So far, kalley's method is the only one that works for any attribute. – Leng Jan 13 '15 at 01:08
7

Since I don't need to loop entire collection, I think it is better to have helper function like this

/**
 * Check if there is a item in a collection by given key and value
 * @param Illuminate\Support\Collection $collection collection in which search is to be made
 * @param string $key name of key to be checked
 * @param string $value value of key to be checkied
 * @return boolean|object false if not found, object if it is found
 */
function findInCollection(Illuminate\Support\Collection $collection, $key, $value) {
    foreach ($collection as $item) {
        if (isset($item->$key) && $item->$key == $value) {
            return $item;
        }
    }
    return FALSE;
}
Rohith Raveendran
  • 410
  • 1
  • 6
  • 14
7

I know this question was originally asked before Laravel 5.0 was released, but as of Laravel 5.0, Collections support the where() method for this purpose.

For Laravel 5.0, 5.1, and 5.2, the where() method on the Collection will only do an equals comparison. Also, it does a strict equals comparison (===) by default. To do a loose comparison (==), you can either pass false as the third parameter or use the whereLoose() method.

As of Laravel 5.3, the where() method was expanded to work more like the where() method for the query builder, which accepts an operator as the second parameter. Also like the query builder, the operator will default to an equals comparison if none is supplied. The default comparison was also switched from strict by default to loose by default. So, if you'd like a strict comparison, you can use whereStrict(), or just use === as the operator for where().

Therefore, as of Laravel 5.0, the last code example in the question will work exactly as intended:

$foods = Food::all();
$green_foods = $foods->where('color', 'green'); // This will work.  :)

// This will only work in Laravel 5.3+
$cheap_foods = $foods->where('price', '<', 5);

// Assuming "quantity" is an integer...
// This will not match any records in 5.0, 5.1, 5.2 due to the default strict comparison.
// This will match records just fine in 5.3+ due to the default loose comparison.
$dozen_foods = $foods->where('quantity', '12');
patricus
  • 59,488
  • 15
  • 143
  • 145
1

Elegant solution for finding a value (http://betamode.de/2013/10/17/laravel-4-eloquent-check-if-there-is-a-model-with-certain-key-value-pair-in-a-collection/) can be adapted:

$desired_object_key = $food->array_search(24, $food->lists('id'));
if ($desired_object_key !== false) {
   $desired_object = $food[$desired_object_key];
}
softfrog
  • 60
  • 1
  • 8
0

As the question above when you are using the where clause you also need to use the get Or first method to get the result.

/**
*Get all food
*
*/

$foods = Food::all();

/**
*Get green food 
*
*/

$green_foods = Food::where('color', 'green')->get();
Marco
  • 624
  • 1
  • 6
  • 21
0

If you have one-to-many relationship in Laravel, you can write simply the following.
(for example you have a car manufacturer and car models)

            /** Initialize array */
            $data = [];

            /** Extract collection*/
            foreach (Model::all() as $model) {
                /** Initialize relation model array */
                $relationObjects = [];

                /** Iterate and make associative array */
                foreach ($model->relationObjects as $relObject) {
                    $relationObjects[] = $relObject->name; // name or whatever property you want
                }

                /** Push 'relationObjects' to coresponding 'modelName' key */
                $data[$model->name][] = $relationObjects;
            } 

The $data will be in a form as:

 [   
    "Porsche": [
        [
            "Cayenne",
            "911 GT3"
        ]
    ],
    "Ford": [
        [
            "Mustang"
        ]
    ],
]
LucianDex
  • 469
  • 2
  • 12