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.
Related
I'm trying to filter an array of objects implementing a specific interface (which simply defines the isComplete(): bool method) based on the result of that method. array_filter doesn't work because it can't call a method on each object to determine whether to filter it (or can it?). I've tried writing a function that takes the splatted array as an argument by reference, this doesn't work either:
function skipIncomplete(CompletableObjectInterface &...$objects): array {
$skipped = [];
foreach ($objects as $index => $item) {
if (!$item->isComplete()) {
$skipped[] = $item->id ?? $index;
unset($objects[$index]);
}
}
return $skipped;
}
The original elements passed in simply don't end up getting unset.
I'm looking for a way that doesn't include creating an entirely new Collection class to hold my CompletableObjects for complexity reasons. I really want to keep the type hint so no one can pass in a generic array, causing runtime errors when the function tries to call $item->isComplete.
Is there any way I can achieve this in PHP 7.3.15?
Added a filter, please comment as to what is wrong with this type of approach:
<?php
interface CompletableObjectInterface {
public function isComplete() : bool;
}
class Foo implements CompletableObjectInterface
{
public function isComplete() : bool
{
return false;
}
}
class Bar implements CompletableObjectInterface
{
public function isComplete() : bool
{
return true;
}
}
$foo = new Foo;
$bar = new Bar;
$incomplete = array_filter([$foo, $bar], function($obj) { return !$obj->isComplete();});
var_dump($incomplete);
Output:
array(1) {
[0]=>
object(Foo)#1 (0) {
}
}
Looks like you got a bit hung up on a wrong understanding of the ... syntax for a variable number of arguments.
You are passing in one array, and the $objects parameter will therefore contain that array in the first index, i.e. in $objects[0].
So in theory you could just change your line
unset($objects[$index]);
to
unset($objects[0][$index]);
However, I do not really see why the variable number of arguments syntax is used at all, since you apparently are just expecting one array of values (objects in this case) as an argument to the function. Therefore I'd recommend you just remove the ... from the argument list and your solution does what you wanted.
Alternatively you can of course add an outer foreach-loop and iterate over all passed "arrays of objects", if that is an use case for you.
I'm trying to achieve the following task in php:
Setting dynamic arrays in session variables.
Adding a prefix named order_ to these session variables.
Looping through the session variables that starts with the prefix order_.
Here's the code I have so far:
foreach($array as $subarray) {
foreach($subarray as $subset) {
$nomInput = $subset['Nom'];
$inputArray=[
1=>[
['Nom'=>$input->get($nomInput, null, 'string'),
'LabelFr'=>$subset['LabelFr'],
'LabelEn'=>$subset['LabelEn']]
]
];
$session->set('order_'.$nomInput, $inputArray);
}
}
With this code, I'm able to set the variables correctly with the prefix.
However, I can't find a way to loop through the results with a foreach loop.
Can somebody give me some pointers on how to manipulate only the session variables that have the prefix order_ with a foreach loop?
Thanks a bunch!
According to the Joomla JSession documentation the JSession class does provide a getIterator method returning an ArrayIterator.
As a reusable approach, you could implement your own FilterIterator class, only iterating over items having a specific prefix and optionally stripping the prefix from keys.
In your code you get the iterator by
$sessionArrayIter = $session->getIterator();
Since I do not well know Joomla and do not have any installation running, I will spoof that part:
$sessionArray = ['aa_test1' => 1, 'bb_test2' => 2, 'aa_test3' => 3, 'cc_test4' => 4];
$sessionArrayIter = new ArrayIterator($sessionArray);
Class Implementation
Then we implement the PrefixFilterIterator class extending PHP's abstract FilterIterator class.
class PrefixFilterIterator extends FilterIterator
{
private
$_prefix,
$_prefixLength,
$_strip_prefix
;
public function __construct(Iterator $iterator, string $prefix, bool $strip_prefix = false)
{
parent::__construct($iterator);
$this->set_prefix($prefix, $strip_prefix);
}
public function set_prefix(string $prefix, ?bool $strip_prefix = null) : void
{
$this->_prefix = $prefix;
$this->_prefixLength = strlen($prefix);
if(null !== $strip_prefix)
$this->_strip_prefix = $strip_prefix;
}
// conditionally remove prefix from key
public function key() /* : mixed scalar */
{
return $this->_strip_prefix ? substr(parent::key(), $this->_prefixLength) : parent::key();
}
// accept prefixed items only
public function accept() : bool
{
return 0 === strpos(parent::key(), $this->_prefix);
}
}
Usage
To iterate over the filtered items, we create a new instance of our iterator.
$prefixIter = new PrefixFilterIterator($sessionArrayIter, 'aa_', true);
foreach ($prefixIter as $k => $v)
echo "$k => $v", PHP_EOL;
Output
test1 => 1
test3 => 3
live demo
Remarks, Limits, ToDo:
The code above runs on PHP >= 7.1
To support PHP 7.0, type hints must be adapted. :void is not supported in PHP<7.1 and must be removed, likewise ?bool must be changed into bool.
This is a simple implementation focussing the problem in the question to reduce the 'noise' in the answer. mbstring is a non-default extension to PHP. Therefore I did not use multibyte string functions. However, array keys may include multibyte charactersets. To support such keys, a conditional implementation of some string function wrappers would be required, using appropriate functions if they are installed. The preg_* functions with the u modifier could be an alternative to support multibyte unicode keys.
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.
In JS you could have something like this:
var a = [1, 2, 3];
a.myProp = false;
This is possible because in js array are actually objects.
What I'm trying to do is to implement the same, but in PHP.
The closest solution I've found is to use the ArrayObject class, but the main problem of this "solution" is that it is object and when you check with is_array or try to use other specific array method it will return false or throw error.
Is there any magic php method like __construct or sth similar for arrays?
In general - I want to add some kind of a flag separated from the array values. For example:
$a = [1, 2, 3];
$a.alreadyIterated = false;
foreach ($a as $item) {
...
}
$a.alreadyIterated = true;
...
if (!$a.alreadyIterated) {
foreach($a as $item) {...}
}
This is somewhat possible via creating your own class:
class PropArray implements ArrayAccess, Iterator {
public $alreadyIterated = false;
protected $a = []; # internal array for iterable values
# add required methods from the interfaces, pretty easy
}
Instances will still be objects, but can be iterated like arrays and tested via is_array($obj) || $obj instanceof Traversable.
I need to write a simple script that loads data from multiple files and merges it somehow. However, given the fact that the files might be quite huge I'd like to load data partially. To do so I decided to use yield. And according to examples I found I could use following construction for single generator:
$generator = $someClass->load(); //load method uses yield so it returns generator object
foreach($generator as $i) {
// do something
}
But what if I want to use two generators at once?
$generatorA = $someClass1->load(); //load method uses yield so it returns generator object
$generatorB = $someClass2->load(); //load method uses yield so it returns generator object
foreach($generatorA as $i) {
// how can I access to resultSet from generatorB here?
}
Generators in PHP implement the Iterator interface, so you can merge / combine multiple Generators like you can combine multiple Iterators.
If you want to iterate over both generators one after the other (merge A + B), then you can make use of the AppendIterator.
$aAndB = new AppendIterator();
$aAndB->append($generatorA);
$aAndB->append($generatorB);
foreach ($aAndB as $i) {
...
If you want to iterate over both generator at once, you can make use of MultipleIterator.
$both = new MultipleIterator();
$both->attachIterator($generatorA);
$both->attachIterator($generatorB);
foreach ($both as list($valueA, $valueB)) {
...
Example for those two incl. examples and caveats are in this blog-post of mine as well:
Iterating over Multiple Iterators at Once
Otherwise I don't understand what you've been asking for. If you could clarify, I should be able to give you more directions.
From https://www.php.net/manual/en/language.generators.syntax.php#control-structures.yield.from
Generator delegation via yield from
In PHP 7, generator delegation allows you to yield values from another
generator, Traversable object, or array by using the yield from keyword.
The outer generator will then yield all values from
the inner generator, object, or array until that is no longer valid,
after which execution will continue in the outer generator.
So it's possible to combine two (or more) generators using yield from.
/**
* Yield all values from $generator1, then all values from $generator2
* Keys are preserved
*/
function combine_sequentially(Generator $generator1, Generator $generator2): Generator
{
yield from $generator1;
yield from $generator2;
};
Or something more fancy (here, it's not possible to use yield from):
/**
* Yield a value from $generator1, then a value from $generator2, and so on
* Keys are preserved
*/
function combine_alternatively(Generator $generator1, Generator $generator2): Generator
{
while ($generator1->valid() || $generator2->valid()) {
if ($generator1->valid()) {
yield $generator1->key() => $generator1->current();
$generator1->next();
}
if ($generator2->valid()) {
yield $generator2->key() => $generator2->current();
$generator2->next();
}
}
};
While AppendIterator works for Iterators, it has some issues.
Firstly it is not so nice to need to construct a new object rather than just calling a function. What is even less nice is that you need to mutate the AppendIterator, since you cannot provide the inner iterators in its constructor.
Secondly AppendIterator only takes Iterator instances, so if you have a Traversable, such as IteratorAggregate, you are out of luck. Same story for other iterable that are not Iterator, such as array.
This PHP 7.1+ function combines two iterable:
/**
* array_merge clone for iterables using lazy evaluation
*
* As with array_merge, numeric elements with keys are assigned a fresh key,
* starting with key 0. Unlike array_merge, elements with duplicate non-numeric
* keys are kept in the Generator. Beware that when converting the Generator
* to an array with a function such as iterator_to_array, these duplicates will
* be dropped, resulting in identical behaviour as array_merge.
*
*
* #param iterable ...$iterables
* #return Generator
*/
function iterable_merge( iterable ...$iterables ): Generator {
$numericIndex = 0;
foreach ( $iterables as $iterable ) {
foreach ( $iterable as $key => $value ) {
yield is_int( $key ) ? $numericIndex++ : $key => $value;
}
}
}
Usage example:
foreach ( iterable_merge( $iterator1, $iterator2, $someArray ) as $k => $v ) {}
This function is part of a small library for working with iterable, where it is also extensively tested.
If you want to use Generators with AppendIterator you'll need to use NoRewindIterator with it:
https://3v4l.org/pgiXB
<?php
function foo() {
foreach ([] as $foo) {
yield $foo;
}
}
$append = new AppendIterator();
$append->append(new NoRewindIterator(foo()));
var_dump(iterator_to_array($append));
Trying to traverse a bare Generator with AppendIterator will cause a fatal error if the Generator never actually calls yield:
https://3v4l.org/B4Qnh
<?php
function foo() {
foreach ([] as $foo) {
yield $foo;
}
}
$append = new AppendIterator();
$append->append(foo());
var_dump(iterator_to_array($append));
Output:
Fatal error: Uncaught Exception: Cannot traverse an already closed generator in /in/B4Qnh:10
Stack trace:
#0 [internal function]: AppendIterator->rewind()
#1 /in/B4Qnh(10): iterator_to_array(Object(AppendIterator))
#2 {main}
thrown in /in/B4Qnh on line 10
Process exited with code 255.
You can use yield from
function one()
{
yield 1;
yield 2;
}
function two()
{
yield 3;
yield 4;
}
function merge()
{
yield from one();
yield from two();
}
foreach(merge() as $i)
{
echo $i;
}
An example Reusable function
function iterable_merge( iterable ...$iterables ): Generator {
foreach ( $iterables as $iterable ) {
yield from $iterable;
}
}
$merge=iterable_merge(one(),two());
Something like:
$generatorA = $someClass1->load(); //load method uses yield so it returns generator object
$generatorB = $someClass2->load(); //load method uses yield so it returns generator object
$flag = true;
$i = 0;
while($flag === false) {
if ($i >= count($generatorA) || $i >= count($generatorB)) {
$flag = true;
}
// Access both generators
$genA = $generatorA[$i];
$genB = $generatorB[$i];
$i++;
}
Try this:
<?php
foreach($generatorA as $key=>$i) {
$A=$i;//value from $generatorA
$B=$generatorB[$key];//value from $generatorB
}