Named arguments in PHP 5 and 7

Spoiler: Unlike many languages, PHP (before version 8) does not allow to pass arguments by their name. Here is a generic adapter to add this possibility to all your classes. We will define an adapter which will do the translation between the calling code with a single array and your methods and their (many) arguments.

A while ago I was rereading a piece of code and came across an original way to pass arguments, using an array. Something like this:

$res = $obj->method(["foo" => "bar"]) ;
jplenio @ pixabay

Since this is the first time that I have encountered this idiom in this code, I go to see the called method which I find easily and basically works as follows:

class SomeClass
{

    public function method($params)
    {
        // Get Parameters
        $foo = $params["foo"] ?: "default" ;
        $bar = $params["bar"] ?: "default" ;

        // Compute result
        return $foo . $bar ;
    }

}

The real method was actually 500 lines long, with if / else rather than the?: operator and used about ten arguments, not very photogenic. There, I made you a more readable version.

I’m amazed that this function doesn’t just declare two arguments with their types and defaults and I’m already imagining all the maintenance risks it implies…

So, because I guess the author of these lines had good reasons for writing them the way he did, I go hunting for information and ended up finding the reason for this idiom:

It is to pass them by names, as in python. I would prefer to write method(foo: "bar") but php does not allow it.

Tintin (not his real name)

And yes, unlike other languages like python, C# and others, PHP (5 and 7) does not allow to pass arguments by names. A feature we miss a few times.

So without further introduction, here is a method to add this feature to PHP without touching the called functions!

Example of class to adapt

Before we start, here is an example of a PHP class that we are going to adapt. Since the only purpose of this class is to serve as an example, its methods do nothing useful.

class Sample
{

    public static function logMsg($foo = "foo", $bar = "bar", $method = null)
    {
        echo ($method ?: "logMsg") ;
        echo " - foo => $foo" ;
        echo " - bar => $bar\n" ;
    }
    
    public $foo ;
    public $bar ;
    
    public function __construct($foo = "foo", $bar = "bar")
    {
        $this->foo = $foo ;
        $this->bar = $bar ;
        self::logMsg($foo, $bar, "__construct") ;
    }

    public function doStuff($foo = "foo", $bar = "bar")
    {
        self::logMsg($foo, $bar, "doStuff") ;
    }

    public function __toString()
    {
        return "{foo : {$this->foo}, bar : {$this->bar}}" ;
    }

}

A methods to adapt them all

So let’s start with the method common to all cases, and which takes care of the heart of the matter: managing the array in arguments and call the method you want.

For that, we will use the PHP reflection API and in particular, the ReflectionMethod class which allows you to manipulate the methods as if they were objects.

function invokeWithArray($method, $instance, $parameters)
{
    $args = [] ;
    
    foreach ($method->getParameters() as $param) {
        $name = $param->getName() ;
        if (isset($parameters[$name])) {
            $args[$name] = $parameters[$name] ;
            unset($parameters[$name]) ;
        } else if ($param->isDefaultValueAvailable()) {
            $args[$name] = $param->getDefaultValue() ;
        } else {
            throw new \Exception("parameter missing") ;
        }
    }
    
    if (count($parameters) > 0) {
        throw new \Exception("Too many arguments") ;
    }
    
    // Appel de la méthode avec les paramètres
    return $method->invokeArgs($instance, $args) ;
}

Of course, we could imagine improving this function by adding type management (with something like instance of $params->getType()) but that weighs down the example and will be done anyway during the call to invokeArgs ().

With this you can already call any method. You get the corresponding ReflectionMethod then make your call.

$refClass  = new ReflectionClass(get_class($obj) ;
$refMethod = $refClass->getMethod("method") ;
$res       = invokeWithArray($refMethod, $obj, ["foo" => "bar"]) ;

But that’s not enough yet. If the goal was to simplify, it is a bit of a failure… We will therefore improve the system.

A class to adapt them all

To easily adapt any class, we will define an abstract class which will serve as a basis for all the others. And to avoid having to define all the possible methods, we use the magic methods of the PHP, namely:

abstract class ArrayAdapter
{

    abstract public static function getReflectionClass() ;

    private $target ;

    public function __construct($args)
    {
        $class = static::getReflectionClass() ;
        $this->target = $class->newInstanceWithoutConstructor() ;

        $ctor = $class->getConstructor() ;
        invokeWithArray($ctor, $this->target, $args) ;
    }

    public function __call($name, $args)
    {
        if (count($args) != 1) {
            throw new \Exception("Wrong number of arguments") ;
        }
        $class = static::getReflectionClass() ;
        $method = $class->getMethod($name) ;
        return invokeWithArray($method, $this->target, $args[0]) ;
    }

    public static function __callStatic($name, $args)
    {
        if (count($args) != 1) {
            throw new \Exception("Wrong number of arguments") ;
        }
        $class = static::getReflectionClass() ;
        $method = $class->getMethod($name) ;
        return invokeWithArray($method, null, $args[0]) ;
    }
    
    public function __invoke()
    {
        return $this->target ;
    }

}

To build an adapter for a class (i.e. the example one), it suffices to create a class which extends the ArrayAdapter and to define the getReflectionClass() method. Something like this:

class AdaptedSample extends \ArrayAdapter
{
    public static function getReflectionClass() {
        return new \ReflectionClass("Sample") ;
    }
}

It’s already better, but if we have to create a new class every time, it’s not going to clean the code. This is where PHP goes beyond a lot of languages, you can automate all of that.

A loader to make it magic

To adapt any class without having to write any code, we are going to use three other very practical features of PHP:

  1. Namespaces,
  2. Automatic class loading,
  3. Execution of arbitrary code.

eva () Often considered (very) dangerous for security, we can still use it sparingly to add magic to our codes…

The idea is therefore to define a namespace dedicated to adapters (e.g. \\Adapted\\ in our case) in which the classes will be built automatically when needed.

Again, this code is simplified. In fact, I make a check that the class to adapt exists and rather than “eval()uate” a string, I write the template in a separate file that I include while retrieving the result via a timeout output. But let’s keep it simple for now.

spl_autoload_register(function ($name) {

    if (strpos($name, "Adapted") !== 0) {
        return ;
    }

    $parts = explode("\\", $name) ;
    $classname = array_pop($parts) ;
    $namespace = implode("\\", $parts) ;
    $targetClass = str_replace("Adapted\\", "", $name) ;

    // Here be dragons
    eval("
        namespace $namespace ;
        class $classname extends \ArrayAdapter
        {
            public static function getReflectionClass() {
                return new \ReflectionClass('$targetClass') ;
            }
        }
        ") ;

}) ;

With that, you don’t even have to write code to adapt your methods anymore, the autoloader does it for you :).

use Adapted\Sample ;

$a = new Sample(["bar" => "new"]) ;
// __construct - foo => foo - bar => new

$a->doStuff(["bar" => "new"]) ;
// doStuff     - foo => foo - bar => new

Sample::logMsg(["bar" => "new"]) ;
// logMsg      - foo => foo - bar => new

echo $a() . "\n";
// {foo : foo, bar : new}

And after ?

The interest of languages interpreted on compiled ones is, among other things, their ease of introspection. As seen here, it is possible to write code that will automatically adapt to any code.

In this case, this allows us to add a missing functionality to the language, passing parameters by names. We can then pass them in the order we want and the calling code will be more readable since the name of the parameter is indicated directly.

But this possibility of named parameters is dangerous for me because it masks a deeper problem of maintainability by making it easier to define methods with a large number of parameters. This type of programming should therefore only be used if no cleaning of the code is possible.