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"]) ;
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…
- How can I easily know which arguments are valid since there is no documentation? How to ensure that the documentation is up to date elsewhere?
- How to ensure that the codes calling this method are up to date if this list of parameters were to change?
- What should happen if a caller adds unexpected parameters?
- If type checks are necessary, they must be added, which complicates the code…
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:
__call()
which is called when a method called on an object does not exist. The first parameter is the name of the method, the second is the array containing the parameters.__callStatic()
which does the same thing but for static class methods.__invoke()
which allows to use the object as a function. I’m using it to dereference the adapter and get the inner object, just in case.
abstract class ArrayAdapter
{
abstract public static function getReflectionClass() ;
private $target ;
public function __construct($args)
{$class = static::getReflectionClass() ;
$this->target = $class->newInstanceWithoutConstructor() ;
$ctor = $class->getConstructor() ;
$ctor, $this->target, $args) ;
invokeWithArray(
}
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
:
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.