Laravel: Obtener objeto de la colección por atributo

91

En Laravel, si realizo una consulta:

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

... entonces $foodses una colección Illuminate de Foodobjetos modelo. (Esencialmente una variedad de modelos).

Sin embargo, las claves de esta matriz son simplemente:

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

... así que si quiero alterar, digamos, el Foodobjeto con un id24, no puedo hacer esto:

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

... porque esto simplemente alterará el elemento 25 de la matriz, no el elemento con un valor idde 24.

¿Cómo obtengo un elemento (s) único (o múltiple) de una colección por CUALQUIER atributo / columna (como, entre otros, id / color / age / etc.)?

Por supuesto, puedo hacer esto:

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

... pero eso es asqueroso.

Y, por supuesto, puedo hacer esto:

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

... pero eso es aún más asqueroso , porque realiza una consulta adicional innecesaria cuando ya tengo el objeto deseado en la $foodscolección.

Gracias de antemano por cualquier orientación.

EDITAR:

Para ser claros, puede llamar ->find()a una Colección Illuminate sin generar otra consulta, pero solo acepta una ID principal. Por ejemplo:

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

Sin embargo, todavía no hay una forma limpia (sin bucles, sin consultas) de tomar un elemento (s) por un atributo de una colección, como este:

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

Respuestas:

126

Puedes usar filter, así:

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

filterTambién devolverá una Collection, pero ya que usted sabe que habrá una sola, puede llamar firsten ese Collection.

Ya no necesita el filtro (o tal vez nunca, no sé que tiene casi 4 años). Puedes usar first:

$desired_object = $food->first(function($item) {
    return $item->id == 24;
});
kalley
fuente
7
¡Hey gracias! Creo que puedo vivir con eso. Todavía inusualmente detallado en mi opinión para lo que suele ser un marco tan 'elocuente' jaja. Pero todavía es mucho más limpio que las alternativas hasta ahora, así que lo tomaré.
Leng
Como @squaretastic está señalando en la otra respuesta, dentro de su cierre está haciendo una asignación y no una comparación (es decir, debería == y no =)
ElementalStorm
24
En realidad, ni siquiera es necesario llamar filter()->first(), solo puede llamarfirst(function(...))
lukasgeiter
de la documentación de Laravel Collection. laravel.com/docs/5.5/collections#method-first collect([1, 2, 3, 4])->first(function ($value, $key) { return $value == 2; });
Shiro
2
Puedes hacer lo mismo con la función where. $desired_object = $food->where('id', 24)->first();
Bhavin Thummar
111

Laravel proporciona un método llamado keyByque permite establecer claves por clave dada en el modelo.

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

devolverá la colección pero con claves como valores de idatributo de cualquier modelo.

Entonces puedes decir:

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

y tomará el elemento correcto sin el lío de usar una función de filtro.

Maksym Cierzniak
fuente
2
Realmente útil, especialmente para el rendimiento, -> first () puede ser lento cuando se llama varias veces (foreach en foreach ...) para que pueda "indexar" su colección como: ¡ $exceptions->keyBy(function ($exception) { return $exception->category_id . ' ' . $exception->manufacturer_id;y usar ->get($category->id . ' ' . $manufacturer->id)después!
François Breton
¿Se sigue utilizando esta clave cuando se agregan nuevos elementos a la colección? ¿O necesito usar keyBy () cada vez que se inserta un nuevo objeto o matriz en la colección?
Jason
Lo más probable es que tenga que volver a llamarlo ya que keyBydevuelve una nueva colección de lo que recuerdo, aunque no estoy seguro, puede verificarlo Illuminate/Support/Collectionpara averiguarlo. (No trabajo en Laravel durante bastante tiempo para que alguien pueda corregirme).
Maksym Cierzniak
Esto no funcionó para mí, devolvió otro elemento, el siguiente elemento, si escribo get (1) devolverá el elemento que tiene el número 2 como id.
Jaqueline Passos
Lote de carga de una mesa y tomó un día. Usé esta solución y me llevó unos minutos.
Jed Lynch
23

A partir de Laravel 5.5 puedes usar firstWhere ()

En tu caso:

$green_foods = $foods->firstWhere('color', 'green');
Victor Timoftii
fuente
3
Esta debería ser la respuesta aceptada después de Laravel 5.5
beerwin
7

Como no necesito repetir la colección completa, creo que es mejor tener una función de ayuda como esta

/**
 * 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
fuente
7

Utilice los métodos de colección incorporados contener y buscar , que buscarán por ID primarios (en lugar de claves de matriz). Ejemplo:

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

contiene () en realidad solo llama a find () y comprueba si hay nulo, por lo que puede acortarlo a:

if ($myModel = $model->collection->find($primaryId)) {
    var_dump($myModel);
}
Ziad Hilal
fuente
Entendemos que find () acepta un ID principal. Lo que queremos es un método que acepte cualquier atributo, como "color" o "edad". Hasta ahora, el método de kalley es el único que funciona para cualquier atributo.
Leng
5

Sé que esta pregunta se hizo originalmente antes de que se lanzara Laravel 5.0, pero a partir de Laravel 5.0, las colecciones admiten el where()método para este propósito.

Para Laravel 5.0, 5.1 y 5.2, el where()método en el Collectionsolo hará una comparación igual. Además, hace una comparación estricta de iguales ( ===) de forma predeterminada. Para hacer una comparación suelta ( ==), puede pasar falsecomo tercer parámetro o usar el whereLoose()método.

A partir de Laravel 5.3, el where()método se amplió para que funcione más como el where()método del generador de consultas, que acepta un operador como segundo parámetro. También al igual que el generador de consultas, el operador utilizará por defecto una comparación igual si no se proporciona ninguna. La comparación predeterminada también se cambió de estricta por defecto a suelta por defecto. Por lo tanto, si desea una comparación estricta, puede usar whereStrict()o simplemente usar ===como operador para where().

Por lo tanto, a partir de Laravel 5.0, el último ejemplo de código en la pregunta funcionará exactamente como se esperaba:

$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
fuente
3

Debo señalar que hay un error pequeño pero absolutamente CRÍTICO en la respuesta de kalley. Luché con esto durante varias horas antes de darme cuenta:

Dentro de la función, lo que está devolviendo es una comparación y, por lo tanto, algo como esto sería más correcto:

$desired_object = $food->filter(function($item) {
    return ($item->id **==** 24);
})->first();
cuadradotastic
fuente
1
Sí, gracias por señalar esto. También es importante tener en cuenta que la función de filtro no es diferente de mi foreach()ejemplo en cuanto al rendimiento, porque simplemente hace el mismo tipo de bucle ... de hecho, mi foreach()ejemplo tiene un mejor rendimiento porque se rompe al encontrar el modelo correcto. Además ... {Collection}->find(24)tomará por clave principal, lo que la convierte en la mejor opción aquí. El filtro propuesto por Kalley es en realidad idéntico $desired_object = $foods->find(24);.
Leng
1
Nunca he visto al **==**operador, ¿qué hace?
kiradotee
@kiradotee Creo que el OP solo intentaba enfatizar el operador de comparación doble igual ( == ). La respuesta original solo usaba un signo igual, por lo que estaba haciendo una asignación en lugar de una comparación. OP estaba tratando de enfatizar que debería haber dos signos iguales.
patricus
0

Como en la pregunta anterior, cuando utiliza la cláusula where, también debe utilizar el método get Or first para obtener el resultado.

/**
*Get all food
*
*/

$foods = Food::all();

/**
*Get green food 
*
*/

$green_foods = Food::where('color', 'green')->get();
Marco
fuente