PHP Iterator Custom Sort Order (Laravel 4) - php

I have a "Post" object, accessed via the IOC container. Various errors tell me this object's type ends up as a "Collection", which implements several interfaces, including IteratorAggregate and ArrayAccess.
I want to display a user-defined group of posts according to a specific order, e.g.:
$desired=array("69","63","70");//these represent post id's
Just sorting an array in this manner seems complex, but I want to sort my collection. I have been researching various combinations of usort(), uksort(), Eloquent's sortBy(), array_multisort()... but the most obvious solutions result in orders like 3,2,1 or 1,2,3, not 2,1,3.
The closest I have gotten to this goal is to fetch the ones I want,
//BlogController
private function myposts($desired){
$posts=$this->post->whereIn('id',$desired)->get();
...
"convert" the Collection object to an array,
$posts=$posts->toArray();
and treat the array with a custom function: source
function sortArrayByArray($array,$orderArray) {
$ordered = array();
foreach($orderArray as $key) {
if(array_key_exists($key,$array)) {
$ordered[$key] = $array[$key];
unset($array[$key]);
}
}
return $ordered + $array;
}
$sorted=sortArrayByArray($array1,$desired);
I can then vardump the array in the correct order, but since it is now an array, I can't access the $posts object in my view. Can I convert the array back into a post object?
This whole approach feels wasteful anyway, converting to an array and back... is it? Is there a more straightforward way of sorting the contents of a "Collection"?
This is a little better, perhaps (a native php function):
array_multisort($desired,$posts,SORT_STRING);
//only works with string keys
//$desire=(1,2,3) will not work!
Again, this works for arrays, but attempting directly on the "posts" object fails...
Finally, I discovered using a Presenter: https://github.com/robclancy/presenter#array-usage which works in the view, after one of the above is completed in the controller:
#foreach($posts as $post)
<?php $post=new PostPresenter($post) ?>
{{View::make('site.post.article')->with(compact('post'))}}
#endforeach
This finally works, but it still feels like a long way to do it. Is there a better way to accomplish this task? Performance concerns or best practices with one method vs. another? Thanks in advance for anyone able to help.

You can use the Collections own sort method and pass in a callback. The callback compares every two values in your collection tells the sort method which one is "higher". The sort method then sorts them accordingly. If you want a specific value order you just create mapping and sort by that. For more info check uasort
$posts->sort(function($a, $b){
$desired=array("69" => 0,"63" => 1,"70" =>2);
if(!array_key_exists($a,$desired) || !array_key_exists($b,$desired) || $a == $b){
return 0
}
else{
return ($desired[$a] < $desired[$b]) ? -1 : 1;
}
});

Alternatively to #tim's answer, you can re-assign the sorted array to a new Collection object:
$postsArray = $this->posts->toArray();
// Do some sorting/processing, then:
$newCollection = new \Illuminate\Database\Eloquent\Collection( $postsArray );

Related

why foreach print just one value in laravel?

I have a method:
public function winnerDetails()
{
$winners = DB::table('winners')->get();
//dd($winners);
foreach ($winners as $winner) {
$qrMainId = $winner->qrdetails_id;
}
dd($qrMainId);
}
dd($winners); returns to value of array but when i use foreach its returns one value. How can i get who value returns with foreach loop?
dd($winners) output:
and dd($qrMainId); return one value 44. But it should return another value of array 35; Thanks in advance.
To get array of ID's use
foreach ($winners as $winner) {
$qrMainId[]= $winner->qrdetails_id;
For just value use
$qrMainId='';
foreach ($winners as $winner) {
$qrMainId.= $winner->qrdetails_id;
If you want to accomplish this task in a Laravel way use the pluck method to map the array for the key that you want
<?php
public function winnerDetails()
{
$winnersId = DB::table('winners')->get()->pluck('qrdetails_id');
dd($winnersId);
}
I think you are not leveraging the power of Laravel here. Obviously, I don't know what you are trying to do but here are some pointers.
You should probably be making a model for the winners table now models in Laravel return Collections as does the DB facade you are using now. Now the Collection class contains alot of usefull helpers like pluck
At this point it becomes as easy as Winner::all()->pluck('id') and you got yourself an array of id's.
And if you would like to get them comma seperated or anything like that you can use the implode
And you would get Winner::all()->implode('id',',');
$qrMainId is a variable not an array.
It is modified in the foreach during every loop.
So your code has always the last element of the array.
Use an array to collect values like
$qrMainId[] = $winner->qrdetails_id;
or sql select directly the field you want.

PHP iterable to array or Traversable

I'm quite happy that PHP 7.1 introduced the iterable pseudo-type.
Now while this is great when just looping over a parameter of this type, it is unclear to me what to do when you need to pass it to PHP functions that accept just an array or just a Traversable. For instance, if you want to do an array_diff, and your iterable is a Traversable, you will get an array. Conversely, if you call a function that takes an Iterator, you will get an error if the iterable is an array.
Is there something like iterable_to_array (NOT: iterator_to_array) and iterable_to_traversable?
I'm looking for a solution that avoids conditionals in my functions just to take care of this difference, and that does not depend on me defining my own global functions.
Using PHP 7.1
Not sure this is what are you searching for but this is the shortest way to do it.
$array = [];
array_push ($array, ...$iterable);
I'm not very sure why it works. Just I found your question interesting and I start fiddling with PHP
Full example:
<?php
function some_array(): iterable {
return [1, 2, 3];
}
function some_generator(): iterable {
yield 1;
yield 2;
yield 3;
}
function foo(iterable $iterable) {
$array = [];
array_push ($array, ...$iterable);
var_dump($array);
}
foo(some_array());
foo(some_generator());
It would be nice if works with function array(), but because it is a language construct is a bit special. It also doesn't preserve keys in assoc arrays.
For php >= 7.4 this works pretty well out of the box:
$array = [...$iterable];
See https://3v4l.org/L3JNH
Edit: Works only as long the iterable doesn't contain string keys
Can be done like this:
$array = $iterable instanceof \Traversable ? iterator_to_array($iterable) : (array)$iterable;
Is there something like iterable_to_array and iterable_to_traversable
Just add these to your project somewhere, they don't take up a lot of space and give you the exact APIs you asked for.
function iterable_to_array(iterable $it): array {
if (is_array($it)) return $it;
$ret = [];
array_push($ret, ...$it);
return $ret;
}
function iterable_to_traversable(iterable $it): Traversable {
yield from $it;
}
Terms are easy to mix
Traversable
Iterator (I see this as a concrete type, like user-defined class A)
IteratorAggregate
iterable (this is a pseudo-type, array or traversable are accepted)
array (This is a concrete type, and it's not exchangeable with Iterator in context of that a Iterator type is required)
arrayIterator (can be used to convert array to iterator)
So, that's why if function A(iterable $a){}, then it accepts parameter of either array or an instanceof traversable (Iterator, IteratorAggregate are both accepted because it's obvious these two classes implement Traversable. In my test, passing ArrayIterator also works ).
In case Iterator type is specified for parameter, passing in an array will cause TypeError.
You can use iterator_to_array converting your variable to Traversable first:
$array = iterator_to_array((function() use ($iterable) {yield from $iterable;})());
Conversion method is taken from the comment under this question.
Here is working demo.
For the "iterable to array" case it seems there is no single function call you can make and that you'll either need to use a conditional in your code or define your own function like this one:
function iterable_to_array( iterable $iterable ): array {
if ( is_array( $iterable ) ) {
return $iterable;
}
return iterator_to_array( $iterable );
}
For the "iterable to Iterator" case things are much more complicated. Arrays can be easily translated into a Traversable using ArrayIterator. Iterator instances can just be returned as they are. That leaves Traversable instances that are not Iterator. On first glance it looks like you can use IteratorIterator, which takes a Traversable. However that class is bugged and does not work properly when giving it an IteratorAggregate that returns a Generator.
The solution to this problem is too long to post here though I have created a mini-library that contains both conversion functions:
function iterable_to_iterator( iterable $iterable ): Iterator
function iterable_to_array( iterable $iterable ): array
See https://github.com/wmde/iterable-functions
Starting with PHP 8.2 the iterator_to_array() and iterator_count() functions will accept iterable instead of Traversable. Thus they will start to accept arrays and do what you would expect them to do when encountering an array.
Specifically the following equalities hold:
iterator_to_array($array, true) == $array
iterator_to_array($array, false) == array_values($array)
and
iterator_count($array) == count($array)
More details can be found in the corresponding RFC: PHP RFC: Make the iterator_*() family accept all iterables.

What Interface to implement so an object behave like an array [duplicate]

I have a class called Collection which stores objects of same type.
Collection implements array interfaces: Iterator, ArrayAccess, SeekableIterator, and Countable.
I'd like to pass a Collection object as the array argument to the array_map function. But this fails with the error
PHP Warning: array_map(): Argument #2 should be an array
Can I achieve this by implementing other/more interfaces, so that Collection objects are seen as arrays?
The array_map() function doesn't support a Traversable as its array argument, so you would have to perform a conversion step:
array_map($fn, iterator_to_array($myCollection));
Besides iterating over the collection twice, it also yield an array that will not be used afterwards.
Another way is to write your own map function:
function map(callable $fn)
{
$result = array();
foreach ($this as $item) {
$result[] = $fn($item);
}
return $result;
}
Update
Judging by your use-case it seems that you're not even interested in the result of the map operation; therefore it makes more sense to use iterator_apply().
iterator_apply($myCollection, function($obj) {
$obj->method1();
$obj->method2();
return true;
});
array_map wants, as the name suggests, arrays. It's not called iterator_map after all. ;)
Apart from iterator_to_array(), which produces a potentially large temporary array, there's no trick to make iterable objects work with array_map.
The Functional PHP library has a map implementation which works on any iterable collection.
If you're not interested in creating a new array that is a function mapped over the original array, you could just use a foreach loop (because you implement Iterator).
foreach($item in $myCollection) {
$item->method1();
$item->method2();
}
if you actually want to use map, then I think you'll have to implement your own. I would suggest making it a method on Collection, eg:
$mutatedCollection = $myCollection->map(function($item) {
/* do some stuff to $item */
return $item;
});
I would ask yourself if you really want to use map or do you really just mean foreach
I came up with the following solution:
//lets say you have this iterator
$iterator = new ArrayIterator(array(1, 2, 3));
//and want to append the callback output to the following variable
$out = [];
//use iterator to apply the callback to every element of the iterator
iterator_apply(
$iterator,
function($iterator, &$out) {
$current = $iterator->current();
$out[] = $current*2;
return true;
},
array($iterator, &$out) //arguments for the callback
);
print_r($out);
This way, you can generate an array without iterating twice as you would to with the approach like:
$iterator = new ArrayIterator(array(1,2,3));
$array = iterator_to_array($iterator); //first iteration
$output = array_map(function() {}, $array); //second iteration
Good luck!
I just stumbled upon this question and I managed to cast the collection to an array to make it work:
array_map($cb, (array) $collection);
disclaimer For the original question this might not be a suitable option but I found the question while looking to solve a problem which I solved with this solution. I would recommend using a custom iterator map where possible/viable.
another option is to do something like this:
foreach($collection as &$item) {
$item = $cb($item);
}
which will mutate the underlying collection.
EDIT:
It has been pointed out that casting to an array can have unwanted side effects. It would be better to add a method to your collection to return the array from the iterator, and traverse that, or otherwise add a map method which accepts a callback and run a loop on the underlying iterator.

Can I use a function in PHP's array_search or sort function?

I am using Parse.com APIs, though I wouldnt say this is a Parse question.
I have an array of ParseObjects called $groups and an array of ParseObjects called $inputs.
I already have both of these, so I do not want make any new queries.
All of the input objects are children of one of the group objects. I'd like to get an array of all of the $inputs that belong to each $group.
One way I could do this would be:
$groups= // array of ParseObjects
$inputs= // array of ParseObjects
foreach ($groups as $group)
{
$inputsInGroup=array();
foreach ($inputs as $input)
{
if($input->get('parent')==$group)
{
array_push($inputsInGroup,$input);
}
}
//here I can use $inputsInGroup which will contain all elements that have this group as their parent
}
That would work, but seams really inefficient as it has to search the whole list of inputs for each group, including any that it already determined belonged to a previous group.
Is there a way to supply a function to array_search () or sort() to return objects in the array that pass this check $input->get('parent')==$group?
I know you can do this with jQuery's sort() seems like PHP likely has a similar method.
You had an unneeded nested loop there - that's why
$groups; // array of ParseObjects
$inputs; // array of ParseObjects
$ibg; // inputs by group
foreach($inputs as $input) {
var $group = $input->get('parent')->getObjectId();
if(!isset($ibg[$group])){
$ibg[$group] = array();
}
$ibg[$group][] = $input;
}
What you're asking for is to apply a filter, with a custom callback, to an array, which can be done with array_filter. However, the change is only paradigmatic in nature — in that the asymptotic complexity won't change much.
array_filter($groups, function ($group) use ($inputs) {
foreach($inputs as $input) {
if ($input->get('parent') == $group) {
return true;
}
}
});
Both are still O(n * k) in time complexity, but one uses a more functional, as opposed to procedural, approach.

Laravel's array_sort helper DESC e ASC

I want to sort a multidimensionl array by one or more keys using the Laravel helper array_sort.
array(
array('firstname_1','lastname_1'),
array('firstname_2','lastnmae_2')
)
I want to order it first by firstname and then by lastname.
I also want to do this in DESC or ASC order. How can I achieve this?
There are functions aivalable in the internet to do this but I would like to understand how to use the Laravel helper. The doc for array_sort (http://laravel.com/docs/helpers#arrays) I don't find comprehensive.
The array_sort() helper function is a very thin wrapper around the default Illuminate\Support\Collection::sortBy() method. Excluding comments, this is all that it does:
function array_sort($array, Closure $callback)
{
return \Illuminate\Support\Collection::make($array)->sortBy($callback)->all();
}
While handy, it is limiting in its sorting capabilities. Similarly, the Collection class will allow you to change sort direction, but not much else.
In my opinion, you have two options:
Skip a Laravel-only solution and use some normal PHP and array_multisort(). As #Jon commented, there's some great details in this SO question.
Use a combination of grouping and sorting in a Collection object to achieve the results you want.
I'd just stick with #1.
Just as an example how to sort by first name and then by last name with the sort helper of Laravel.
First define some more example data:
$array = array(
array('firstname_1','lastname_1'),
array('firstname_2','lastname_3'),
array('firstname_2','lastname_2'),
array('firstname_3','lastname_3'),
);
In our closure we want to sort by first name first, and then by last name. Laravel will sort by the returned values of the closure. So in your case the trick is to concatenate both strings:
$array = array_sort($array, function($value) {
return sprintf('%s,%s', $value[0], $value[1]);
});
Internally Laravel will now sort the contents of this intermediate array:
$intermediateArray = array(
'firstname_1,lastname_1',
'firstname_2,lastname_3',
'firstname_2,lastname_2',
'firstname_3,lastname_3',
);
This will result in an array which is sorted by first name and than by last name in ascending order.
To use descending order you need first sort the array and than reverse it:
$array = array_reverse(array_sort($array, function($value) {
return sprintf('%s,%s', $value[0], $value[1]);
}));

Categories