Question

How can I specify the argument type as an array? Say I have a class named 'Foo':

class Foo {}

and then I have a function that accepts that class type as an argument:

function getFoo(Foo $f) {}

When I pass in an array of 'Foo's I get an error, saying:

Catchable fatal error: Argument 1 passed to getFoo() must be an instance of Foo, array given

Is there a way to overcome this issue? maybe something like

function getFoo(Foo $f[]) {}
Was it helpful?

Solution

If you want to ensure you are working with "Array of Foo" and you want to ensure methods receive "Array of Foo", you can:

class ArrayOfFoo extends \ArrayObject {
    public function offsetSet($key, $val) {
        if ($val instanceof Foo) {
            return parent::offsetSet($key, $val);
        }
        throw new \InvalidArgumentException('Value must be a Foo');
    }
}

then:

function workWithFoo(ArrayOfFoo $foos) {
    foreach ($foos as $foo) {
        // etc.
    }
}

$foos = new ArrayOfFoos();
$foos[] = new Foo();
workWithFoo($foos);

The secret sauce is that you're defining a new "type" of "array of foo", then passing that "type" around using type hinting protection.


The Haldayne library handles the boilerplate for membership requirement checks if you don't want to roll your own:

class ArrayOfFoo extends \Haldayne\Boost\MapOfObjects {
    protected function allowed($value) { return $value instanceof Foo; }
}

(Full-disclosure, I'm the author of Haldayne.)


Historical note: the Array Of RFC proposed this feature back in 2014. The RFC was declined with 4 yay and 16 nay. The concept recently reappeared on the internals list, but the complaints have been much the same as levied against the original RFC: adding this check would significantly affect performance.

OTHER TIPS

Old post but variadic functions and array unpacking can be used (with some limitations) to accomplish typed array hinting, at least with PHP7. (I didn't test on earlier versions).

Example:

class Foo {
  public function test(){
    echo "foo";
  }   
};  

class Bar extends Foo {
  //override parent method
  public function test(){
    echo "bar";
  }   
}          

function test(Foo ...$params){
  foreach($params as $param){
    $param->test();
  }   
}   

$f = new Foo();
$b = new Bar();

$arrayOfFoo = [$f,$b];

test(...$arrayOfFoo);
//will output "foobar"


The Limitations:

  1. This isn't technically a solution, as you aren't really passing a typed array. Instead, you use the array unpacking operator1 (the "..." in the function call) to convert your array to a list of parameters, each of which must be of the type hinted in the variadic declaration2 (which also employs an ellipsis).

  2. The "..." in the function call is absolutely necessary (which isn't surprising, given the above). Trying to call

    test($arrayOfFoo)
    

    in the context of the above example will yield a type error, as the compiler expects parameter(s) of foo, not an array. See below for an, albeit hacky, solution to pass in an array of a given type directly, while preserving some type-hinting.

  3. Variadic functions may only have one variadic parameter and it must be the last parameter (since otherwise how might the compiler determine where the variadic parameter ends and the next begins) meaning you couldn't declare functions along the lines of

    function test(Foo ...$foos, Bar ...$bars){ 
        //...
    }
    

    or

    function test(Foo ...$foos, Bar $bar){
        //...
    }
    


An Only-Slightly-Better-Than-Just-Checking-Each-Element Alternative:

The following procedure is better than just checking the type of each element insofar as (1) it guarantees the parameters used in the functional body are of the correct type without cluttering the function with type checks, and (2) it throws the usual type exceptions.

Consider:

function alt(Array $foos){
    return (function(Foo ...$fooParams){

        //treat as regular function body

        foreach($fooParams as $foo){
            $foo->test();
        }

    })(...$foos);
}

The idea is define and return the result of an immediately invoked closure that takes care of all the variadic / unpacking business for you. (One could extend the principle further, defining a higher order function that generates functions of this structure, reducing boilerplate). In the above example:

alt($arrayOfFoo) // also outputs "foobar"

The issues with this approach include:

(1) Especially to inexperienced developers, it may be unclear.

(2) It may incur some performance overhead.

(3) It, much like just internally checking the array elements, treats the type checking as an implementational detail, insofar as one must inspect the function declaration (or enjoy type exceptions) to realize that only a specifically typed array is a valid parameter. In an interface or abstract function, the full type hint could not be encoded; all one could do is comment that an implementation of the above sort (or something similar) is expected.


Notes

[1]. In a nutshell: array unpacking renders equivalent

example_function($a,$b,$c);

and

example_function(...[$a,$b,$c]);


[2]. In a nutshell: variadic functions of the form

function example_function(Foo ...$bar){
    //...
}

can be validly invoked in any of the following ways:

example_function();
example_function(new Foo());
example_function(new Foo(), new Foo());
example_function(new Foo(), new Foo(), new Foo());
//and so on

Sorry, PHP does not work this way. It has been made for quick'n'easy programming, and so you are not bothered with strict typing, which leaves you in dynamic type hell without any help (like a type inferencing compiler). The PHP interpreter is completely clueless about what you have put into your array, so it must iterate over all array entries if it wanted to validate something like the following (which is not working in PHP, of course):

function bar(Foo[] $myFoos)

This is a major impact on performance when the array gets large. I think that's the reason why PHP doesn't offer typed array hints.

The other answers here suggest that you create your strongly typed array wrapper. Wrappers are fine when you have a compiler with generic typing like Java or C#, but for PHP, I disagree. Here, those wrappers are tedious boilerplate code and you need to create one for every typed array. If you want to use array functions from the library, you need to extend your wrappers with type checking delegation functions and bloat your code. This can be done in an academic sample, but in a production system with many classes and collections, where developer time is costly and the MIPS in the web cluster are scarce? I think not.

So, just to have PHP type validation in the function signature, I would refrain from strongly typed array wrappers. I don't believe that the give you enough of a ROI. The PHPDoc support of good PHP IDEs help you more, and in PHPDoc Tags, the Foo[] notation works.

On the other hand, wrappers CAN make sense if you can concentrate some good business logic into them.

Perhaps the day will come when PHP is extended with strongly typed arrays (or more precise: strongly typed dictionaries). I would like that. And with those, they can provide signature hints that don't punish you.

function getFoo()

Generally, you would then have an add method that would typehint to Foo

function addFoo( Foo $f )

So, the getter would return an array of Foo's and the add method can ensure you only had Foo's to the array.

EDIT Removed the argument from the getter. I don't know what I was thinking, you don't need an argument in a getter.

EDIT just to display the a full class example:

class FooBar
{
    /**
     * @return array
     */
    private $foo;

    public function getFoo()
    {
        return $foo;
    }

    public function setFoo( array $f )
    {
        $this->foo = $f;

        return $this;
    }

    public function addFoo( Foo $f )
    {
        $this->foo[] = $f;

        return $this;
    }
}

You generally, and probably shouldn't, have the setter method since you have the add method to help ensure $foo is an array of Foo's but it helps illustrate what is going on in the class.

class Foo {
    /**
     * @param Foo[] $f
     */
    function getFoo($f) {

    }
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top