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.
Related
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.
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 );
I want to use a foreach loop with a variable, but this variable can be many different types, NULL for example.
So before foreach I test it:
if(is_array($var)){
foreach($var as ...
But I realized that it can also be a class that implements Iterator interface. Maybe I am blind but how to check whether the class implements interface? Is there something like is_a function or inherits operator? I found class_implements, I can use it, but maybe there is something simpler?
And second, more important, I suppose this function exist, would be enough to check if the variable is_array or "implements Iterator interface" or should I test for something more?
If you are using foreach inside a function and you are expecting an array or a Traversable object you can type hint that function with:
function myFunction(array $a)
function myFunction(Traversable)
If you are not using foreach inside a function or you are expecting both you can simply use this construct to check if you can iterate over the variable:
if (is_array($a) or ($a instanceof Traversable))
foreach can handle arrays and objects. You can check this with:
$can_foreach = is_array($var) || is_object($var);
if ($can_foreach) {
foreach ($var as ...
}
You don't need to specifically check for Traversable as others have hinted it in their answers, because all objects - like all arrays - are traversable in PHP.
More technically:
foreach works with all kinds of traversables, i.e. with arrays, with plain objects (where the accessible properties are traversed) and Traversable objects (or rather objects that define the internal get_iterator handler).
(source)
Simply said in common PHP programming, whenever a variable is
an array
an object
and is not
NULL
a resource
a scalar
you can use foreach on it.
You can check instance of Traversable with a simple function. This would work for all this of Iterator because Iterator extends Traversable
function canLoop($mixed) {
return is_array($mixed) || $mixed instanceof Traversable ? true : false;
}
<?php
$var = new ArrayIterator();
var_dump(is_array($var), ($var instanceof ArrayIterator));
returns bool(false) or bool(true)
PHP 7.1.0 has introduced the iterable pseudo-type and the is_iterable() function, which is specially designed for such a purpose:
This […] proposes a new iterable pseudo-type. This type is analogous to callable, accepting multiple types instead of one single type.
iterable accepts any array or object implementing Traversable. Both of these types are iterable using foreach and can be used with yield from within a generator.
function foo(iterable $iterable) {
foreach ($iterable as $value) {
// ...
}
}
This […] also adds a function is_iterable() that returns a boolean: true if a value is iterable and will be accepted by the iterable pseudo-type, false for other values.
var_dump(is_iterable([1, 2, 3])); // bool(true)
var_dump(is_iterable(new ArrayIterator([1, 2, 3]))); // bool(true)
var_dump(is_iterable((function () { yield 1; })())); // bool(true)
var_dump(is_iterable(1)); // bool(false)
var_dump(is_iterable(new stdClass())); // bool(false)
You can also use the function is_array($var) to check if the passed variable is an array:
<?php
var_dump( is_array(array()) ); // true
var_dump( is_array(array(1, 2, 3)) ); // true
var_dump( is_array($_SERVER) ); // true
?>
Read more in How to check if a variable is an array in PHP?
Functions
<?php
/**
* Is Array?
* #param mixed $x
* #return bool
*/
function isArray($x) : bool {
return !isAssociative($x);
}
/**
* Is Associative Array?
* #param mixed $x
* #return bool
*/
function isAssociative($x) : bool {
if (!is_array($array)) {
return false;
}
$i = count($array);
while ($i > 0) {
if (!isset($array[--$i])) {
return true;
}
}
return false;
}
Example
<?php
$arr = [ 'foo', 'bar' ];
$obj = [ 'foo' => 'bar' ];
var_dump(isAssociative($arr));
# bool(false)
var_dump(isAssociative($obj));
# bool(true)
var_dump(isArray($obj));
# bool(false)
var_dump(isArray($arr));
# bool(true)
Since PHP 7.1 there is a pseudo-type iterable for exactly this purpose. Type-hinting iterable accepts any array as well as any implementation of the Traversable interface. PHP 7.1 also introduced the function is_iterable(). For older versions, see other answers here for accomplishing the equivalent type enforcement without the newer built-in features.
Fair play: As BlackHole pointed out, this question appears to be a duplicate of Iterable objects and array type hinting? and his or her answer goes into further detail than mine.
I'm looking to create an array or list with elements of a certain type (eg objects the implement a certain interface). I know I can create an object that does the same thing implementing Traversable and Iterator, or override ArrayObject. But maybe there's another way I have missed.
Do you mean something like:
$array=Array();
foreach ($itemsToAdd as $item) {
if ($item instanceof NameOfwantedInterface) {
Array_push($array,$item);
}
}
If you don't, them I'm sorry - it's just that your question isn't too clear.
I would write a custom class that extended ArrayObject and threw an exception if you tried to assign a variable that wasn't the correct type, there's really no better way to do it that I can think of.
PHP as a lanugage is very flexible in terms of type handling and type conversion. You will probably have to put a manual check in if you want any kind of strong type checking, a simple if statement will do.
The array object is designed to be especially flexible (lazy key assignment, automatic increment, string or integer keys, etc.) so you should probably use a custom object of your own.
You could use type hinting:
<?php
interface Shape
{
function draw();
}
class MyArray
{
private $array = array();
function addValue(Shape $shape) //The hinting happens here
{
$array[] = $shape;
}
}
?>
This example is not perfect, but you'll get the idea.
Basically, you are going to want to do a function that checks if the variable you are inserting into the array is an object.
function add($var)
{
if(is_object($var))
{
$this->array[] = $var;
}
}
If you want to make sure it has a specific class name, you would do something like this (for PHP5):
function add(className $var)
{
$this->array[] = $var;
}
or this for previous PHP versions:
function add($var)
{
if($var instanceOf className)
{
$this->array[] = $var
}
}
You might want to look into array_filter() to do this without building an object.
Looking at that page, I've found that you can use array_filter with common functions like is_object. So doing something like this:
$this->array = array_filter($this->array ,'is_object');
Would filter the array to contain only objects.
I seem to remember that in PHP there is a way to pass an array as a list of arguments for a function, dereferencing the array into the standard func($arg1, $arg2) manner. But now I'm lost on how to do it. I recall the manner of passing by reference, how to "glob" incoming parameters ... but not how to de-list the array into a list of arguments.
It may be as simple as func(&$myArgs), but I'm pretty sure that isn't it. But, sadly, the php.net manual hasn't divulged anything so far. Not that I've had to use this particular feature for the last year or so.
http://www.php.net/manual/en/function.call-user-func-array.php
call_user_func_array('func',$myArgs);
As has been mentioned, as of PHP 5.6+ you can (should!) use the ... token (aka "splat operator", part of the variadic functions functionality) to easily call a function with an array of arguments:
<?php
function variadic($arg1, $arg2)
{
// Do stuff
echo $arg1.' '.$arg2;
}
$array = ['Hello', 'World'];
// 'Splat' the $array in the function call
variadic(...$array);
// 'Hello World'
Note: array items are mapped to arguments by their position in the array, not their keys.
As per CarlosCarucce's comment, this form of argument unpacking is the fastest method by far in all cases. In some comparisons, it's over 5x faster than call_user_func_array.
Aside
Because I think this is really useful (though not directly related to the question): you can type-hint the splat operator parameter in your function definition to make sure all of the passed values match a specific type.
(Just remember that doing this it MUST be the last parameter you define and that it bundles all parameters passed to the function into the array.)
This is great for making sure an array contains items of a specific type:
<?php
// Define the function...
function variadic($var, SomeClass ...$items)
{
// $items will be an array of objects of type `SomeClass`
}
// Then you can call...
variadic('Hello', new SomeClass, new SomeClass);
// or even splat both ways
$items = [
new SomeClass,
new SomeClass,
];
variadic('Hello', ...$items);
Also note that if you want to apply an instance method to an array, you need to pass the function as:
call_user_func_array(array($instance, "MethodName"), $myArgs);
For sake of completeness, as of PHP 5.1 this works, too:
<?php
function title($title, $name) {
return sprintf("%s. %s\r\n", $title, $name);
}
$function = new ReflectionFunction('title');
$myArray = array('Dr', 'Phil');
echo $function->invokeArgs($myArray); // prints "Dr. Phil"
?>
See: http://php.net/reflectionfunction.invokeargs
For methods you use ReflectionMethod::invokeArgs instead and pass the object as first parameter.