Laravel: Obter Objeto da Coleção por Atributo

92

No Laravel, se eu fizer uma consulta:

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

... então $foodsé uma coleção Illuminate de Foodobjetos modelo. (Essencialmente, uma variedade de modelos.)

No entanto, as chaves desta matriz são simplesmente:

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

... então, se eu quiser alterar, digamos, o Foodobjeto com um idde 24, não posso fazer isso:

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

... porque isso apenas alterará o 25º elemento na matriz, não o elemento com um idde 24.

Como obtenho um único (ou vários) elemento (s) de uma coleção por QUALQUER atributo / coluna (como, mas não se limitando a id / cor / idade / etc.)?

Claro, eu posso fazer isso:

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

... mas, isso é simplesmente nojento.

E, claro, posso fazer isso:

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

... mas isso é ainda mais grosseiro , porque realiza uma consulta adicional desnecessária quando já tenho o objeto desejado na $foodscoleção.

Desde já, agradeço qualquer indicação.

EDITAR:

Para ser claro, você pode chamar ->find()uma coleção Illuminate sem gerar outra consulta, mas ela aceita uma ID primária. Por exemplo:

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

No entanto, ainda não há uma maneira limpa (sem loop, sem consulta) de pegar um (s) elemento (s) por um atributo de uma coleção, como este:

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

Respostas:

128

Você pode usar filter, assim:

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

filtertambém retornará um Collection, mas como você sabe que haverá apenas um, você pode chamá first-lo Collection.

Você não precisa mais do filtro (ou talvez nunca, não sei, isso tem quase 4 anos). Você pode apenas usar first:

$desired_object = $food->first(function($item) {
    return $item->id == 24;
});
Kalley
fonte
7
Ei, obrigado! Acho que posso viver com isso. Ainda incomumente prolixo na minha opinião para o que normalmente é uma estrutura 'Eloquent' haha. Mas ainda é muito mais limpo do que as alternativas até agora, então vou aceitá-lo.
Duração
Como @squaretastic está apontando na outra resposta, dentro do seu encerramento você está fazendo uma atribuição e não uma comparação (ou seja, você deve == e não =)
ElementalStorm
24
Na verdade, nem é necessário ligar, filter()->first()você pode apenas ligarfirst(function(...))
lukasgeiter
da documentação da coleção Laravel. laravel.com/docs/5.5/collections#method-first collect([1, 2, 3, 4])->first(function ($value, $key) { return $value == 2; });
Shiro
2
Você pode fazer a mesma coisa com a função where. $desired_object = $food->where('id', 24)->first();
Bhavin Thummar
111

O Laravel fornece um método chamado keyByque permite definir as chaves por uma determinada chave no modelo.

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

retornará a coleção, mas com as chaves sendo os valores dos idatributos de qualquer modelo.

Então você pode dizer:

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

e pegará o item correto sem a confusão de usar uma função de filtro.

Maksym Cierzniak
fonte
2
Realmente útil, especialmente para desempenho, -> first () pode ser lento quando chamado várias vezes (foreach in foreach ...) para que você possa "indexar" sua coleção como: $exceptions->keyBy(function ($exception) { return $exception->category_id . ' ' . $exception->manufacturer_id;e usar ->get($category->id . ' ' . $manufacturer->id)depois!
François Breton
Esta chave continua a ser usada quando novos itens são adicionados à coleção? Ou preciso usar keyBy () toda vez que um novo objeto ou array é colocado na coleção?
Jason,
Provavelmente você terá que chamá-lo novamente, pois keyByretorna uma nova coleção do que eu me lembro, mas não tenho certeza, você pode verificar Illuminate/Support/Collectionpara descobrir. (Não estou trabalhando no Laravel há algum tempo para que alguém possa me corrigir).
Maksym Cierzniak
Isso não funcionou para mim, ele retornou outro item, o próximo item, se eu digitar get (1) ele retornará o item que tem o número 2 como id.
Jaqueline Passos
O carregamento em lote de uma mesa demorou um dia. Usei essa solução e demorou alguns minutos.
Jed Lynch
24

A partir do Laravel 5.5 você pode usar firstWhere ()

No seu caso:

$green_foods = $foods->firstWhere('color', 'green');
Victor Timoftii
fonte
3
Esta deve ser a resposta aceita após Laravel 5.5
beerwin
7

Como não preciso fazer um loop de coleção inteira, acho melhor ter uma função auxiliar 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
fonte
7

Use os métodos de coleção internos contêm e localizam , que pesquisam por ids primários (em vez de chaves de array). Exemplo:

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

contains () na verdade apenas chama find () e verifica se há nulo, então você pode encurtá-lo para:

if ($myModel = $model->collection->find($primaryId)) {
    var_dump($myModel);
}
Ziad Hilal
fonte
Entendemos que find () aceita um ID primário. O que queremos é um método que aceite qualquer atributo, como "cor" ou "idade". Até agora, o método de kalley é o único que funciona para qualquer atributo.
Duração
5

Eu sei que esta pergunta foi feita originalmente antes do lançamento do Laravel 5.0, mas a partir do Laravel 5.0, as Coleções suportam o where()método para este propósito.

Para o Laravel 5.0, 5.1 e 5.2, o where()método no Collectionfará apenas uma comparação de igual. Além disso, ele faz uma comparação de igual estrita ( ===) por padrão. Para fazer uma comparação flexível ( ==), você pode passar falsecomo o terceiro parâmetro ou usar o whereLoose()método.

A partir do Laravel 5.3, o where()método foi expandido para funcionar mais como o where()método do construtor de consultas, que aceita um operador como o segundo parâmetro. Também como o construtor de consultas, o operador assumirá como padrão uma comparação igual se nenhuma for fornecida. A comparação padrão também foi alterada de estrita por padrão para flexível por padrão. Portanto, se desejar uma comparação estrita, você pode usar whereStrict(), ou apenas usar ===como o operador para where().

Portanto, a partir do Laravel 5.0, o último exemplo de código na questão funcionará exatamente como pretendido:

$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');
patrício
fonte
3

Devo salientar que há um pequeno erro, mas absolutamente CRÍTICO na resposta de Kalley. Lutei com isso por várias horas antes de perceber:

Dentro da função, o que você está retornando é uma comparação e, portanto, algo assim seria mais correto:

$desired_object = $food->filter(function($item) {
    return ($item->id **==** 24);
})->first();
esquaretastic
fonte
1
Sim, obrigado por apontar isso. Também é importante observar que a função de filtro não é diferente do meu foreach()exemplo em termos de desempenho, porque ele apenas faz o mesmo tipo de loop ... na verdade, meu foreach()exemplo tem melhor desempenho porque quebra ao encontrar o modelo correto. Além disso ... {Collection}->find(24)será capturado pela chave primária, o que o torna a melhor opção aqui. O filtro proposto por Kalley é realmente idêntico $desired_object = $foods->find(24);.
Leng
1
Nunca vi a **==**operadora, o que ela faz?
Kiradotee 01 de
@kiradotee Acho que o OP estava apenas tentando enfatizar o operador de comparação duplo igual ( == ). A resposta original usava apenas um sinal de igual, portanto, estava fazendo uma tarefa em vez de uma comparação. OP estava tentando enfatizar que deveria haver dois sinais de igualdade.
patricus
0

Como a pergunta acima, quando você está usando a cláusula where, você também precisa usar o método get Or first para obter o resultado.

/**
*Get all food
*
*/

$foods = Food::all();

/**
*Get green food 
*
*/

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