How to access the nth item in a Laravel collection? - laravel

I guess I am breaking all the rules by deliberately making a duplicate question...
The other question has an accepted answer. It obviously solved the askers problem, but it did not answer the title question.
Let's start from the beginning - the first() method is implemented approximately like this:
foreach ($collection as $item)
return $item;
It is obviously more robust than taking $collection[0] or using other suggested methods.
There might be no item with index 0 or index 15 even if there are 20 items in the collection. To illustrate the problem, let's take this collection out of the docs:
$collection = collect([
['product_id' => 'prod-100', 'name' => 'desk'],
['product_id' => 'prod-200', 'name' => 'chair'],
]);
$keyed = $collection->keyBy('product_id');
Now, do we have any reliable (and preferably concise) way to access nth item of $keyed?
My own suggestion would be to do:
$nth = $keyed->take($n)->last();
But this will give the wrong item ($keyed->last()) whenever $n > $keyed->count(). How can we get the nth item if it exists and null if it doesn't just like first() behaves?
Edit
To clarify, let's consider this collection:
$col = collect([
2 => 'a',
5 => 'b',
6 => 'c',
7 => 'd']);
First item is $col->first(). How to get the second?
$col->nth(3) should return 'c' (or 'c' if 0-based, but that would be inconsistent with first()). $col[3] wouldn't work, it would just return an error.
$col->nth(7) should return null because there is no seventh item, there are only four of them. $col[7] wouldn't work, it would just return 'd'.
You could rephrase the question as "How to get nth item in the foreach order?" if it's more clear for some.

I guess faster and more memory-efficient way is to use slice() method:
$collection->slice($n, 1);

You can try it using values() function as:
$collection->values()->get($n);

Based on Alexey's answer, you can create a macro in AppServiceProvider (add it inside register method):
use Illuminate\Support\Collection;
Collection::macro('getNth', function ($n) {
return $this->slice($n, 1)->first();
});
and then, you can use this throughout your application:
$collection = ['apple', 'orange'];
$collection->getNth(0) // returns 'apple'
$collection->getNth(1) // returns 'orange'
$collection->getNth(2) // returns null
$collection->getNth(3) // returns null

you may use offsetGet since Collection class implements ArrayAccess
$lines->offsetGet($nth);

Maybe not the best option, but, you can get item from array inside collection
$collection->all()[0]

Related

Put specific item on top without sorting others in laravel collection

I have an ordered laravel collection and i need too put element with id = 20 on top, without sorting other elements. Is it possible to do with sortBy?
You can try to use filter method
// Say $originalCollection is the response from the large request, with data from the database
$modifiedCollection = $originalCollection->filter(fn($item) => $item->id === 20)
->concat($originalCollection->filter(fn($item) => $item->id !== 20));
Or to be more intuitive you can use filter and reject methods
$modifiedCollection = $originalCollection->filter(fn($item) => $item->id === 20)
->concat($originalCollection->reject(fn($item) => $item->id === 20));
The $modifiedCollection will have record with id = 20 at the top and rest of the records will remain in the same order as in $originalCollection
if you want to put a specific item at the top of the array, simply add it separately.
$type = ['20' => 'Select Type'] + $your_sorted_array ;
Example:
$country = ['1' => 'Andorra'] + Countries::orderby('nicename')->pluck('name', 'id')->toArray();
Edit 1:Given new information, the on way you could "manually" do this is by using a combination of unset and unshift AFTER the array is built from the collection.
$key_value = $country[20];
unset($country[20]);
array_unshift($country, $key_value );
if your collection is not very large you can use combination of keyBy, pull and prepend methods
$originalCollection = Model::hereYourBigQuery()->get()->keyBy('id');
/*
now collection will look like this
{
'id1' => objectWithId1,
'id2' => objectWithId2,
...
20 => objectWithId20,
...
}
*/
// pull takes off element by its key
$toMakeFirst = $originalCollection->pull(20);
// prepend adding item into begining of the collection
// note that prepend will reindex collection so its keys will be set by default
$originalCollection->prepend($toMakeFirst);
upd:
if you want to stick with sort there is a way
$collection = Model::yourBigQuery()->get();
$sorted = $collection->sort(function($a, $b){return $a->id == 20 ? -1 : 1;})->values();
as said in docs method sort can take closure as argument and utilizes php uasort under the hood

Eloquent delete all rows with where statement

I want to delete all the votes where the flag "isOnly" is true which means the article is voted before the plenary session.
I have this code written, which deletes ALL the votes.
foreach($commision->articles as $article) {
$article->votes()->delete();
$article->update([
'isVoted' => false
]);
}
What is the right way to delete all the votes with the flag 'isOnly' == true
You can stack where methods with delete call
$article->votes()->where('isOnly', true)->delete();
One better solution would be to avoid the foreach all together so you run only one query
$articleIds = $commision->pluck('articles.id'); //if the articles are already loaded calling a collection method pluck()
$articleIds = $commision->articles()->pluck('id'); // if articles are not loaded calling a query builder method pluck()
Votes::whereHas('article', function($articleQueryBuilder) use($articleIds) {
$articleQueryBuilder->whereIn('id', $articleIds);
})->where('isOnly', true)->delete();
Article::whereIn('id', $articleIds)->update([
'isVoted' => false
]);
This will result in a faster processing of your delete() & update().
Here is one way to delete by condition
$article->votes()->get()->filter(function($item){
return $item->isOnly == true;
})->each(function($vote){$vote->delete();});
This statement will get all votes and apply filter funtion on votes which will give us votes which has isOnly == true rows. Then each function will delete returned votes.
This will help. :)

Laravel: Returning 2 collections in single view

This is not really a problem. but i feel like this can be done in much cleaner way.
I'm returning 2 collections in one view and this is how i do it.
I'm wondering if this is "the right" way of doing it?
Thanks in advance you intelligent humans.
This is within my controller.
$projects = Project::with('client')->where('project_id','=',$id)->get();
foreach($projects as $project){
$project;
}
$clients = DB::table('clients')->select('client_name')->get();
return view('admin.projects.update')->with('project',$project)->with('clients', $clients);
You can try compact method. In the last line write,
return view('admin.projects.update',compact('project','clients'));
(P.S. remove "return $clients;". Otherwise it won't return anything.)
You do like this also
return view('admin.projects.update')->with([project' => $project,'clients' => $clients]);
And like this too
return view('admin.projects.update',[project' => $project,'clients' => $clients]);

Merging multiple objects which uses same id

I'm trying to merge multiple objects (like Receipts, Reports, etc) with Collection->merge().
This is the code I used:
$receipts = Receipt::all();
$reports = Report::all();
$collection = $receipts->merge($reports);
This is the result:
The above screenshot shows two elements, but the third element is missing because it has the same id (id: "1") as the first one. What I'm trying to achieve is to display all three of them as a collection.
EDIT:
I need the result to be objects (collection) because I also use the code on my view, where I check the class to determine what to display. Also, I use this function to sort the objects in the collection.
$collection->sort(function($a, $b)
{
$a = $a->created_at;
$b = $b->created_at;
if ($a === $b) {
return 0;
}
return ($a > $b) ? 1 : -1;
});
I know that this is an old question, but I will still provide the answer just in case someone comes here from the search like I did.
If you try to merge two different eloquent collections into one and some objects happen to have the same id, one will overwrite the other. I dunno why it does that and if that's a bug or a feature - more research needed. To fix this just use push() method instead or rethink your approach to the problem to avoid that.
Example of a problem:
$cars = Car::all();
$bikes = Bike::all();
$vehicles = $cars->merge($bikes);
// if there is a car and a bike with the same id, one will overwrite the other
A possible solution:
$collection = collect();
$cars = Car::all();
$bikes = Bike::all();
foreach ($cars as $car)
$collection->push($car);
foreach ($bikes as $bike)
$collection->push($bike);
Source: https://medium.com/#tadaspaplauskas/quick-tip-laravel-eloquent-collections-merge-gotcha-moment-e2a56fc95889
I know i'm bumping a 4 years old thread but i came across this and none of the answers were what i was looking for; so, like #Tadas, i'll leave my answer for people who will come across this. After Looking at the laravel 5.5 documentation thoroughly i found that concat was the go-to method.
So, in the OP's case the correct solution would be:
$receipts = Receipt::all();
$reports = Report::all();
$collection = $receipts->concat($reports);
This way every element in the Report collection will be appended to every element in the Receipts collection, event if some fields are identical.
Eventually you could shuffle it to get a more visual appealing result for e.g. a view:
$collection->shuffle();
Another way to go about it is to convert one of your collections to a base collection with toBase() method. You can find it in Illuminate\Support\Collection
Method definition:
/**
* Get a base Support collection instance from this collection.
*
* #return \Illuminate\Support\Collection
*/
public function toBase()
{
return new self($this);
}
Usage:
$receipts = Receipt::all();
$reports = Report::all();
$collection = $receipts->toBase()->merge($reports);
You could put all collections in an array and use this. Depends on what you want to do with the collection.
$list = array();
$list = array_merge($list, Receipt::all()->toArray());
$list = array_merge($list, Report::all()->toArray());

Where statements are not being appended

I am trying to build a query dynamically. It's initial state is fine. This is the initial where clause that should be present on every query
$qb->add('where', $qb->expr()->andx(
$qb->expr()->eq('s.competitor', $competitor),
$qb->expr()->eq('s.ignored', $ignored),
$qb->expr()->eq('s.id', $params['s_id']),
$qb->expr()->eq('s.id', 'k.targetSite')
), true);
But the app I am building allows users to filter. When that happens, I want to add additional where clauses into my query builder. When this line is executed later in the code, it overwrites the above where statement.
$qb->add('where', $qb->expr()->like($col, $val), true );
From what I have read, the 3rd parameter $append should keep the previous statements, but that's not happening. In Doctrine 1.2, I could just do something like this:
foreach($filter as $col => $val) {
$dql->addWhere($col = ?, array($val));
}
How do I dynamically add where clauses to my QueryBuilder?
Update
Here is a full statement
$where = array('col' => 'k.text', 'val' => 'some word%');
$qb = $this->entityManager->createQueryBuilder()
->select('s, sc')
->from('Dashboard\Entity\Section', 'sc')
->innerJoin('sc.keyword', 'k')
->innerJoin('sc.site', 's')
->leftJoin('k.keywordCategory', 'kc')
->leftJoin('k.keywordSubCategory', 'ksc');
$qb->add('where', $qb->expr()->andx(
$qb->expr()->eq('s.competitor', $competitor),
$qb->expr()->eq('s.ignored', $ignored),
$qb->expr()->eq('s.id', $params['s_id']),
$qb->expr()->eq('s.id', 'k.targetSite')
), true);
if ($where) {
$qb->add('where', $qb->expr()->andx(
$qb->expr()->like($where['col'], $where['val'])
), true);
}
$qb->addGroupBy('k.id');
$qb->addGroupBy('s.id');
$qb->setFirstResult( $params['start'] )
->setMaxResults( $params['limit'] );
$q = $qb->getQuery();
echo $q->getSql();
And the output is
SELECT s0_.id AS id0, k1_.id AS id1, k1_.name AS name2, k2_.id AS id3, k2_.name AS name4, k3_.id AS id5, k3_.text AS text6, k3_.search_vol AS search_vol7, s4_.id AS id8, s4_.sub_domain AS sub_domain9, MIN(s0_.rank) AS sclr10, MAX(s0_.created) AS sclr11
FROM section s0_
INNER JOIN keyword k3_ ON s0_.k_id = k3_.id
INNER JOIN site s4_ ON s0_.s_id = s4_.id
LEFT JOIN keyword_category k1_ ON k3_.k_cat_id = k1_.id
LEFT JOIN keyword_sub_category k2_ ON k3_.k_subcat_id = k2_.id
WHERE k3_.text LIKE 'some word%'
GROUP BY k3_.id, s4_.id LIMIT 25 OFFSET 0
If I don't add in that if ($where) clause, then the first andx where statements are still in place. But when I try to dynamically add them, only the final WHERE statement is added, all others are cleared. I should also add, that I tried it like this as well.
if ($where) {
$qb->add('where', $qb->expr()->like($where['col'], $where['val']), true);
}
I can successfully use
$qb->andWhere( $qb->expr()->like($where['col'], $where['val']) );
But the API docs for Query Builder state the way I am trying to use it should be valid too. Wanted to make sure I was doing it right, or if it was a bug.
You're able to use the ->andWhere() normally (and it will also fixes your issue).
Doctrine 2 QueryBuilder is a rather innovative concept (because it mixes both programmatic and fluent styles), and there are likely bugs associated to it.
One point that you should notice in your code: Instead of play with ->add('where', ...), you should think programmatic. Add more items to andX() object and at the end associate to the ->add('where', ...) (or even better: ->where(...))
Looking at the code, it seems like what you're trying to do won't work, though whether that's documented anywhere or is just a bug is open to question, I think.
In add(), the DQL part you pass in seems only ever to be appended if the part is already stored as an array in the query builder:
$isMultiple = is_array($this->_dqlParts[$dqlPartName]);
...
if ($append && $isMultiple) {
if (is_array($dqlPart)) {
$key = key($dqlPart);
$this->_dqlParts[$dqlPartName][$key][] = $dqlPart[$key];
} else {
$this->_dqlParts[$dqlPartName][] = $dqlPart;
}
} else {
$this->_dqlParts[$dqlPartName] = ($isMultiple) ? array($dqlPart) : $dqlPart;
}
And the WHERE clause, unlike most of the other DQL parts, is not initialised to an array:
private $_dqlParts = array(
'select' => array(),
'from' => array(),
'join' => array(),
'set' => array(),
'where' => null,
'groupBy' => array(),
'having' => null,
'orderBy' => array()
);
Now, this looks a bit like it's by design, but Doctrine 2 is fairly new and this bit seems to have been evolving recently. Personally, I'd raise a bug against this behaviour and see what the project people say. If nothing else, you might end up with improved documentation...

Resources