Sorting argument array for dynamically called method - php

I'm using reflection to dynamically call methods.
$object = new $class;
$reflector = new ReflectionMethod($class, $method);
$reflector->invokeArgs($object, $arguments);
The $arguments array looks like:
Array
(
[fooparam] => false
[id] => 238133
)
The method called could be:
class MyClass
{
public function myMethod ($id, $fooParam)
{
// Whatever
}
}
The problem is that everything comes from frontend designers, depending on data-* attributes, href... so $arguments array has arbitrary sorting.
How can I sort this array to match method parameters?
O maybe, is there a better solution? Named parameters?

Use ReflectionMethod::getParameters() to get a list of arguments and filter map them to their corresponding position, e.g.:
$sorted_args = array_map(function($param) use($arguments) {
$name = $param->getName();
if (!isset($arguments[$name]) && !$param->isOptional())
throw new BadMethodCallException("Argument '{$name}' is mandatory");
return isset($arguments[$name]) ? $arguments[$name] : $param->getDefaultValue();
}, $reflector->getParameters());
You could also use a simple foreach loop, it's up to you.
Then invoke the method with $sorted_args instead:
$reflector->invokeArgs($object, $sorted_args);

Related

PHP - create strict typed Map dynamically (multidimensional)?

I need to be able to create strict typed maps dynamically. Like this:
$map = new Map( 'string,array<string,int>', [
'foo' => [
'bar' => 1
]
];
I have seen a lot of solutions for separate cases. All guides are teaching to create a class for each map, like Users_Map (to keep users there), Products_Map (to keep products there), Comments_Map (to keep comments there), etc.
But I don't want to have 3 classes (dozens in fact - for a big project) for each type of the map. I want to create a single class Map and then use it like this:
$users = new Map( 'User', {users data goes here} );
$products = new Map( 'int,Product', {products data goes here} );
$comments = new Map( 'User,array<Comment>', {comments data goes here} );
I would appreciate if somebody can advice me any existing repos. Otherwise I'll probably implement this on my own and will put here a link to my solution as an answer.
What you're looking for is called generics. PHP doesn't support this, although there has been an RFC calling for support for a few years.
If you really want to enforce strict typing on a custom map, you'd have to build it yourself. You could, for example, do something like this:
class Map {
private string $keyType;
private string $valueType;
private array $items;
public function __construct(string $keyType, string $valueType) {
$this->keyType = $keyType;
$this->valueType = $valueType;
}
public function set($key, $value) {
if (gettype($key) !== $this->keyType && !($key instanceof $this->keyType)) {
throw new TypeError("Key must be of type " . $this->keyType);
}
if (gettype($value) !== $this->valueType && !($value instanceof $this->valueType)) {
throw new TypeError("Value must be of type " . $this->valueType);
}
$this->items[$key] = $value;
}
public function get($key) {
if (gettype($key) !== $this->keyType) {
throw new TypeError("Key must be of type " . $this->keyType);
}
return $this->items[$key] ?? null;
}
public function all() {
return $this->items;
}
}
(of course, this particular implementation uses a regular array internally, so keyType is limited to types that are valid array keys. If you want to support other object types, some more interesting logic might be required)
The combination of gettype and instanceof will ensure this works for both simple and complex types. For example:
$map = new Map("string", "array");
$map->set("name", ["Boris", "Johnson"]);
print_r($map->all());
/*
Array
(
[name] => Array
(
[0] => Boris
[1] => Johnson
)
)
*/
$map->set("job", "Prime Minister");
// Fatal error: Uncaught TypeError: Value must be of type array
Or with a class as value type:
class User {
public string $firstName;
public string $lastName;
}
$user = new User();
$user->firstName = "Boris";
$user->lastName = "Johnson";
$map = new Map("string", User::class);
$map->set("pm", $user);
print_r($map->all());
/*
Array
(
[pm] => User Object
(
[firstName] => Boris
[lastName] => Johnson
)
)
*/
If you also want to support nested generics, like in your example array<string,int>, that becomes more complicated. In that case, as soon as someone passes an array as a value, you'd have to manually check all items in the array to ensure all array keys are strings and all array values are integers. It's possible, but for larger arrays it will be a significant performance hit.
Although you could use a nested Map like this one if you extend it to enforce the types:
class StringIntMap extends Map {
public function __construct() {
parent::__construct("string", "integer");
}
}
$map = new Map("string", StringIntMap::class);

PHP - Create instance of class (array param)

For now, I'm creating an instance like this:
function newInstance($clazz, $parameters = []) {
// Do other stuff before
if(!is_array($parameters)) {
$parameters = [$parameters];
}
// Do other stuff before
return (new ReflectionClass($clazz))->newInstanceArgs($parameters)
}
The problem is:
Using an array as a single parameter, interprets it as an array of parameters instead of a single one. I also thought about using "func_get_args" or adding a third optional parameter which defines if there's an array as parameter or the given array contains all parameters, but I don't like that.
For example:
newInstance('clazzname', ['my', 'array']);
// should interpreted as:
function __construct($firstString, $secondString$) {}
// and sometimes as (depends on the class):
function __construct($myArray) {}
Anyone have an idea?
I'd use variable-length arguments to handle this:
function newInstance($clazz, ...$parameters) {
// Do other stuff before
return (new ReflectionClass($clazz))->newInstanceArgs($parameters);
}
newInstance('clazzname', 'my', 'array');
// new clazzname('my', 'array');
newInstance('clazzname', ['my', 'array']);
// new clazzname(['my', 'array']);

Using custom functions dynamically in Twig?

I have the following class method for creating a Twig environment object.
public function getView($filename,
array $customFunctions = null,
array $customFunctionArgs = null,
$debug = false) {
$loader = new \Twig_Loader_Filesystem('/App/Views/Templates/Main');
$twig = new \Twig_Environment($loader);
if (isset($customFunctions)) {
foreach ($customFunctions as $customFunction) {
$customFunction['name'] = new \Twig_SimpleFunction($customFunction['name'],
function ($customFunctionArgs) {
return $customFunction['method']($customFunctionArgs);
});
$twig->addFunction($customFunction['name']);
}
}
// Check debugging option
if ($debug == true && !$twig->isDebug()) {
$twig->enableDebug();
$twig->addExtension(new \Twig_Extension_Debug());
} elseif (!$debug && $twig->isDebug()) {
$twig->disableDebug();
}
$template = $twig->load($filename);
return $template;
}
Problem is, I don't understand how to pass values in order to make this work dynamically and keep all the objects in context and scope. For instance, here is how I'm trying to use it but can't pass the variables as a reference I guess?
$customFunctions = ['name' => 'customFunctionName',
'method' => $Class->method($arg)];
$customFunctionArgs = [$arg];
$template = $View->getView('template.html.twig', $customFunctions, $customFunctionArgs, true);
My environment is PHP 5.6 & Twig 1.35.0. I suppose this is not a Twig specific question per se, but more of how to use class objects within other classes/methods.
FĂ©lix Gagnon-Grenier's answer helped me find a solution to this problem. However, I feel the need to post an answer with all the missing pieces to the puzzle for anyone that needs a solution for this.
I believe it will make more sense if I start at the end and explain to the beginning. When creating your array, there are several things to consider.
Any class objects that are needed for the function have to be declared inside a use() with the closure.
Any arguments for the custom function must be declared as a function parameter for the closure. This will allow you to declare them later.
I ended up adding a sub-array with the arguments I needed for each custom function, that way I don't need to iterate over them separately.
$customFunctions = [
[
'name' => 'customFunction',
'method' => function($arg1, $arg2) use($Class) {
return $Class->customFunction($arg1, $arg2);
},
'arguments' =>
[
'arg1', 'arg2'
]
]
];
$template = $View->getView(
'template.html.twig',
true,
$customFunctions
);
echo $View->renderView($template);
Based on this code (reflective of question above), I had to make some notable modifications.
if (isset($customFunctions)) {
foreach ($customFunctions as $index => $customFunction) {
if (isset($customFunctions['arguments'])) {
$arguments = $customFunctions['arguments'];
} else {
$arguments = [];
}
$twigFunction = new \Twig_SimpleFunction(
$customFunction['name'],
function (...$arguments) use ($customFunction) {
return $customFunction['method'](...$arguments);
});
$twig->addFunction($twigFunction);
}
}
You can do this whatever way works for you, but there are important things to consider which I struggled with. Once again, your arguments MUST go into the function parameters. function (...$arguments) use ($customFunction). Your custom function will be passed in the use(). In order to actually pass the arguments in the closure, you must use ... to unpack them (as an array). This applies to PHP 5.6+. It allows the arguments to be dynamically expanded to the correct amount, otherwise you will get missing argument errors.
There are slight flaws in how you construct the custom functions data array and the loop that injects them into the template.
The custom functions should be a three dimensional array
$customFunctions = [
[ // notice the extra level, allowing you to access the name
'name' => 'customFunctionName',
'method' => function() { return 'wat'; }
// you need to pass a callable, not the result of a call
]
];
The scope is not inherited like you seem to think it is, you need to use() variables you intend to access. I personnally would not overwrite the 'name' value of the array, but that's uncanny paranoia of internal side effects, it seems to work in practice.
if (isset($customFunctions)) {
foreach ($customFunctions as $customFunction) {
$customFunction['name'] = new \Twig_SimpleFunction(
$customFunction['name'],
function () use ($customFunctionArgs, $customFunction) {
return $customFunction['method']($customFunctionArgs);
});
$twig->addFunction($customFunction['name']);
}
}
You might need to add looping over $args so that the correct args are sent to the correct function (send $args[0] to $customFunctions[0] etc.).
Note that this prevents you from sending a parameter into your custom function unless you add it in the loop:
function ($templateArg) use ($customFunctionArgs, $customFunction) {
return $customFunction['method']($customFunctionArgs, $templateArg);
}
Here is a gist with tests if you're interested.

PHP Method chaining with dynamic names

I was wondering if it's possible to create a method chaining using the values (or keys) of an array as the dynamic names of the methods.
For example, I have an array:
$methods = ['first', 'second', 'third']
Is it possible to create the following call ?
first()->second()->third();
This is untested. Something along the lines of:
$object = null; // set this to an initial object to call the methods on
foreach ($methods as $value)
{
$object = $object->$value();
}
Note that each method you call should return an object that has a method to be called next. If it's an object of the same class - then it can just return itself with each chainable method.
You can also use eval function.
Example:
$object = new SomeClass(); // first, second, third
$methods = ['first', 'second', 'third'];
$callStr = 'return $object->';
foreach($methods as $method){
$callStr.= $method . '()->';
}
$callStr = substr($callStr, 0, -2);
$callStr.= ';'; // return $object->first()->second()->third();
$result = eval($callStr); // return result of call - $object->first()->second()->third();

Dynamically call Class with variable number of parameters in the constructor

I know that it is possible to call a function with a variable number of parameters with call_user_func_array() found here -> http://php.net/manual/en/function.call-user-func-array.php . What I want to do is nearly identical, but instead of a function, I want to call a PHP class with a variable number of parameters in it's constructor.
It would work something like the below, but I won't know the number of parameters, so I won't know how to instantiate the class.
<?php
//The class name will be pulled dynamically from another source
$myClass = '\Some\Dynamically\Generated\Class';
//The parameters will also be pulled from another source, for simplicity I
//have used two parameters. There could be 0, 1, 2, N, ... parameters
$myParameters = array ('dynamicparam1', 'dynamicparam2');
//The instantiated class needs to be called with 0, 1, 2, N, ... parameters
//not just two parameters.
$myClassInstance = new $myClass($myParameters[0], $myParameters[1]);
You can do the following using ReflectionClass
$myClass = '\Some\Dynamically\Generated\a';
$myParameters = array ('dynamicparam1', 'dynamicparam2');
$reflection = new \ReflectionClass($myClass);
$myClassInstance = $reflection->newInstanceArgs($myParameters);
PHP manual: http://www.php.net/manual/en/reflectionclass.newinstanceargs.php
Edit:
In php 5.6 you can achieve this with Argument unpacking.
$myClass = '\Some\Dynamically\Generated\a';
$myParameters = ['dynamicparam1', 'dynamicparam2'];
$myClassInstance = new $myClass(...$myParameters);
I implement this approach a lot when function args are > 2, rather then end up with an Christmas list of arguments which must be in a specific order, I simply pass in an associative array. By passing in an associative array, I can check for necessary and optional args and handle missing values as needed. Something like:
class MyClass
{
protected $requiredArg1;
protected $optionalArg1;
public function __construct(array $options = array())
{
// Check for a necessary arg
if (!isset($options['requiredArg1'])) {
throw new Exception('Missing requiredArg1');
}
// Now I can just localize
$requiredArg1 = $options['requiredArg1'];
$optionalArg1 = (isset($options['optionalArg1'])) ? $options['optionalArg1'] : null;
// Now that you have localized args, do what you want
$this->requiredArg1 = $requiredArg1;
$this->optionalArg1 = $optionalArg1;
}
}
// Example call
$class = 'MyClass';
$array = array('requiredArg1' => 'Foo!', 'optionalArg1' => 'Bar!');
$instance = new $class($array);
var_dump($instance->getRequiredArg1());
var_dump($instance->getOptionalArg1());
I highly recommend using an associative array, however it is possible to use a 0-index array. You will have to be extremely careful when constructing the array and account for indices that have meaning, otherwise you will pass in an array with offset args and wreck havoc with your function.
You can do that using func_get_args().
class my_class {
function __construct( $first = NULL ) {
$params = func_get_args();
if( is_array( $first ) )
$params = $first;
// the $params array will contain the
// arguments passed to the child function
foreach( $params as $p )
echo "Param: $p\n";
}
}
function my_function() {
$instance = new my_class( func_get_args() );
}
echo "you can still create my_class instances like normal:";
$instance = new my_class( "one", "two", "three" );
echo "\n\n\n";
echo "but also through my_function:";
my_function( "one", "two", "three" );
Basically, you simply pass the result of func_get_args to the constructor of your class, and let it decide whether it is being called with an array of arguments from that function, or whether it is being called normally.
This code outputs
you can still create my_class instances like normal:
Param: one
Param: two
Param: three
but also through my_function:
Param: one
Param: two
Param: three
Hope that helps.
I've found here
Is there a call_user_func() equivalent to create a new class instance?
the example:
function createInstance($className, array $arguments = array())
{
if(class_exists($className)) {
return call_user_func_array(array(
new ReflectionClass($className), 'newInstance'),
$arguments);
}
return false;
}
But can somebody tell me if there is an example for classes with protected constructors?

Categories