PHP autoloader pointing to a dynamic method name - php

For testing/debugging/evaluation purposes I want to write wrappers for selected entries of the PHP autoloader chain. As autoloader methods are restricted to a single argument with a given value (the class name) the only way to tell the wrapper what to do seems to be the method name. I thus iterate over spl_autoload_functions(), replace the selected items with dynamically created calls of methods like Wrapper::handler_xxxx and save the original information in that class. Wrapper has a method public static function __callStatic() which should evaluate the incoming requests, do its testing/debugging/evaluating work and otherwise redirect to the (saved) original handler. However, this does not work but leads to errors like this one:
PHP Fatal error: Uncaught LogicException: Passed array does not specify an existing method (class 'Wrapper' does not have a method 'autoload_mdGVzdG1ldGhvZAo') in [somewhere]
Seems that the autoloader needs real methods and does not work with PHP-magic. Whether this is a bug or a feature (if the latter: why?), is there any way to work around it?
Edit: some code example upon request, I am not sure if this helps understanding the problem; this is by far not finished, but just an attempt to get the basic idea up and running. Calling the magic method manually (by the name given above) works fine - it is just that the autoloader-logic is not willing to call it.
class Wrapper {
const AUTOLOAD_PREFIX = 'autoload';
public static function __callStatic(String $name, Array $arguments) {
if (strpos($name, self::AUTOLOAD_PREFIX) === 0) {
try {
# valid signature found
if (preg_match(sprintf('/^%s(_c([^_]+))?_m([^_]+)$/', self::AUTOLOAD_PREFIX), $name, $matches)) {
# call via class/method
$method = self::decodeMethodName($matches[3]);
if ($matches[2]) {
$class = self::decodeMethodName($matches[2]);
$class::$method($arguments);
}
else {
# call global function
$method($arguments);
}
}
else {
# invalid name
throw new \Exception(sprintf('Invalid static call of %s::%s', __CLASS__, $name));
}
}
catch (\Throwable $e) {
echo "I got you.";
die;
}
}
else {
throw new \Exception(sprintf('Invalid static call of %s::%s', __CLASS__, $name));
}
return;
}
private static function encodeMethodName(String $method) : String {
# protect and encode method names
return str_replace(array('+', '/', '='), array(0x81, 0x82, ''), base64_encode($method));
}
private static function decodeMethodName(String $method) : String {
# reconstruct method names
return base64_decode(str_replace(array(0x80, 0x81), array('+', '/'), $method));
}
public function protectAutoloader(?String $originalClass, String $originalMethod) : Void {
$stack = array();
$autoLoaders = spl_autoload_functions();
while (!$done && count($autoLoaders) > 0) {
$item = array_pop($autoLoaders);
if (
is_string($item) &&
is_null($originalClass) &&
($originalMethod === $item)
) {
# global function
$replacement = array(
__CLASS__,
sprintf('%s_m%s', self::AUTOLOAD_PREFIX, self::encodeMethodName($item)),
);
$done = true;
}
elseif (
is_array($item) &&
($item[0] === $originalClass) &&
($item[1] === $originalMethod)
) {
# static method
$replacement = array(
__CLASS__,
sprintf('%s_c%s_m%s', self::AUTOLOAD_PREFIX, self::encodeMethodName($item[0]), self::encodeMethodName($item[1])),
);
$done = true;
}
else {
# don't touch anything else (closures)
$replacement = $item;
}
# remove item and push to the stack
spl_autoload_unregister($item);
array_push($stack, $replacement);
}
# restore autoloader chain
while (count($stack) > 0) {
$item = array_pop($stack);
spl_autoload_register($item, true);
}
return;
}
I'd activate this for a specific autoloader:
$wrapper->protectAutoloader(NULL, 'testmethod');
Now testmethod() (if existing in the chain) will be replaced by Wrapper::autoload_mdGVzdG1ldGhvZAo() and if e.g. a syntactically broken file were autoloaded the message "I got you" would be printed.

Related

If Class exists && If Method exists PHP / Laravel

PHP/Laravel
Hey, I'm moving into abstraction in php and am attempting to validate and store values based on whatever has been submitted, where I expect that the methods should neither know what to validate against and/or which class and method to use to do so -
What I've got works but I can see that there would be issues where classes/methods do not exist. Here lays my question.
If I were to call a method in the following format, which way would be best to 'check' if class_exists() or the method exists()?
public function store(Request $request)
{
$dataSet = $request->all();
$inputs = $this->findTemplate();
$errors = [];
$inputValidators = [];
foreach ($inputs as $input) {
$attributes = json_decode($input->attributes);
if (isset($attributes->validate)) {
$inputValidators[$input->name] = $input->name;
}
}
foreach ($dataSet as $dataKey => $data) {
if (array_key_exists($dataKey, $inputValidators)) {
$validate = "validate" . ucfirst($dataKey);
$validated = $this->caseValidator::{$validate}($data);
if ($validated == true) {
$inputValidators[$dataKey] = $data;
} else {
$errors[$dataKey] = $data;
}
} else {
$inputValidators[$dataKey] = $data;
}
}
if (empty($errors)) {
$this->mapCase($dataSet);
} else {
return redirect()->back()->with(['errors' => $errors]);
}
}
public function mapCase($dataSet)
{
foreach($dataSet as $dataKey => $data) {
$model = 'case' . ucfirst($dataKey);
$method = 'new' . ucfirst($dataKey);
$attribute = $this->{$model}::{$method}($dataKey);
if($attribute == false) {
return redirect()->back()->with(['issue' => 'error msg here']);
}
}
return redirect()->back->with(['success' => 'success msg here'])'
}
For some additional context, an input form will consist of a set of inputs, this can be changed at any time. Therefore I am storing all values as a json 'payload'.
When a user submits said form firstly the active template is found, which provides details on what should be validated $input->attributes, once this has been defined I am able to call functions from caseValidator model as $this->caseValidator::{$validate}($data);.
I do not think that any checks for existence will be needed here as the validation parameters are defined against an input, thus if none exist this check will be skipped using if (array_key_exists($dataKey, $inputValidators))
However, I am dispersing some data to other tables within the second block of code using mapCase(). This is literally iterating over all array keys regardless of if a method for it exists and thus the initial check cannot be made as seen in the first block. I've attempted to make use of class_exists() and method_exists but logically it does not fit and I cannot expect them to work as I'd like, perhaps my approach in mapCase is not correct? I guess if I'm defining a class for each key I should instead use one class and have methods exist there, which would remove the need to check for the class existing. Please advise
Reference:
$attribute = $this->{$model}::{$method}($dataKey);
Solved the potential issue by using class_exists(), considering I know the method names as they are the same as the $dataKey.
public function mapCase($dataSet)
{
foreach($dataSet as $dataKey => $data) {
$model = 'case' . ucfirst($dataKey);
if (class_exists("App\Models\CaseRepository\\" . $model)) {
$method = 'new' . ucfirst($dataKey);
$attribute = $this->{$model}::{$method}($dataKey);
}
if($attribute == false) {
return redirect()->back()->with(['issue' => 'error msg here']);
}
}
return redirect()->back->with(['success' => 'success msg here'])'
}

Optional parameters in PHP function without considering order

Is there a way in PHP to use a function which has optional parameters in its declaration where I do not have to pass an optional arguments which already have values declared and just pass the next argument(s) which have different values that are further down the parameter list.
Assuming I have a function that has 4 arguments, 2 mandatory, 2 optional. I don't want to use null values for the optional arguments. In usage, there are cases where I want to use the function and the value of the 3rd argument is the same as the default value but the value of the 4th argument is different.
I am looking for a not so verbose solution that allows me to just pass the argument that differs from the default value without considering the order in the function declaration.
createUrl($host, $path, $protocol='http', $port = 80) {
//doSomething
return $protocol.'://'.$host.':'.$port.'/'.$path;
}
I find myself repeating declaring variables so that I could use a function i.e to use $port, I redeclare $protocol with the default value outside the function scope i.e
$protocol = "http";
$port = 8080;
Is there any way to pass the 2nd optional parameter($port) without passing $protocol and it would "automatically" fill in the default value of $protocol i.e
getHttpUrl($server, $path, $port);
This is possible in some languages like Dart in the form of Named Optional parameters.See usage in this SO thread. Is their a similar solution in PHP
You could potentially use a variadic function for this.
Example:
<?php
function myFunc(...$args){
$sum = 0;
foreach ($args as $arg) {
$sum += $arg;
}
return $sum;
}
Documentation:
http://php.net/manual/en/functions.arguments.php#functions.variable-arg-list
PHP doesn't allow at this state to call functions parameters in the order we want.Maybe in the future it will.However you can easily achieve your purpose by using an associative array as the only argument, and then define, the default parameter in the function.For the call you will need to pass an array with only the values which interest you.This array will be merged with the default array.You can even implement required parameters and call them in any order you want.
example:
function mysweetcode($argument){
$required=['first'];//specify required parameters here
$default=['first'=>0,'second'=>1,'third'=>2];//define all parameters with their default values here
$missing=[];
if(!is_array($argument)) return false;
$argument=array_intersect_key($argument,$default);
foreach($required as $k=>$v){//check for missing required parameters
if(!isset($argument[$v]))
$missing[]=$v;
}
if(!empty($missing)){// if required are missing trigger or throw error according to the PHP version
$cm=count($missing);
if (version_compare(PHP_VERSION, '7.0.0') < 0) {
trigger_error(call_user_func_array('sprintf',
array_merge(array('Required '.(($cm>1)?'parameters:':'parameter:').
str_repeat('%s,',$cm).(($cm>1)?' are':' is').' missing'),$missing)),
E_USER_ERROR);
}else{
throw new Error(call_user_func_array('sprintf',array_merge(
array('Required '.(($cm>1)?'parameters:':'parameter:').
str_repeat('%s',$cm).(($cm>1)?' are':' is').' missing'),$missing)));
}
}
$default=array_merge($default,$argument);//assign given values to parameters
extract($default);/*extract the parameters to allow further checking
and other operations in the function or method*/
unset($required,$missing,$argument,$default,$k,$v);//gain some space
//then you can use $first,$second,$third in your code
return $first+$second+$third;
}
var_dump(mysweetcode(['first'=>9,'third'=>8]));//the output is 18
var_dump(mysweetcode(['third'=>8]));//this throws Error on PHP7 and trigger fatal error on PHP5
You can check a live working code here
Well, this should work:
function myFunc($arg1, $arg2, $arg3=null, $arg4= null){
if ( is_null( $arg3 ) && is_null( $arg4 ) {
$arg3 = 3;
$arg4 = 4;
} else if ( is_null( $arg4 ) ) {
$arg4 = $arg3;
$arg3 = 3;
}
echo $arg1 + $arg2 + $arg3 + $arg4;
}
However I suggest you to rethink your problem (as a whole) because this is not a very good idea.
You could refactor this to use a parameter object; this way, you could include the default parameters in this object and set them in any order (with a trade-off of more verbose code). As an example using your above code,
<?php
class AdditionParameters
{
private $arg1 = 0;
private $arg2 = 0;
private $arg3 = 3;
private $arg4 = 4;
public function getArg1() { return $this->arg1; }
public function getArg2() { return $this->arg2; }
public function getArg3() { return $this->arg3; }
public function getArg4() { return $this->arg4; }
public function setArg1($value) { $this->arg1 = $value; return $this; }
public function setArg2($value) { $this->arg2 = $value; return $this; }
public function setArg3($value) { $this->arg3 = $value; return $this; }
public function setArg4($value) { $this->arg4 = $value; return $this; }
}
From there, you could simply call the function while passing in this new object.
function myFunc(AdditionParameters $request) {
return $request->getArg1()
+ $request->getArg2()
+ $request->getArg3()
+ $request->getArg4();
}
echo myFunc((new AdditionParameters)->setArg1(1)->setArg2(2)->setArg4(6));
// or echo myFunc((new AdditionParameters)->setArg1(1)->setArg4(6)->setArg2(2));
Otherwise, PHP doesn't allow you to have named optional parameters. (e.g. myFunc(1, 2, DEFAULT, 4);)
You have the response in your question, you can declare your function like
function myFunc($arg1, $arg2, $arg3 = null, $arg4 = null){
//here you check if the $arg3 and $arg4 are null
}
then you call your function using
myFunc($arg1, $arg2);
There is no such way in PHP(like in python for example).
You have to use some tricks in order to do that but will not always work.
For example:
// creates instances of a class with $properties.
// if $times is bigger than 1 an array of instances will be returned instead.(this is just an example function)
function getInstance($class, $properties = [], $times = 1){
//my code
}
$user = getInstance("User", ["name" => "John"]); // get one instance
$users = getInstance("User", ["name" => "John"],2); // get two instances.
If you want to use the function without passing the $parameters argument, like this:
$users = getInstance("User",2);
you can change the function to:
// creates instances of a class with $properties.
// if times is bigger than 1 an array of instances will be returned instead.
function getInstance($class, $properties = [], $times = 1){
if(is_numberic($properties)){
$times = $properties;
$properties = [];
}
//my code
}
Of course, this strategy will work only if you parameters have different types.
PS. This method is use in the Laravel Framework a lot. From there I got the inspiration.
This is modified from one of the answers and allows arguments to be added in any order using associative arrays for the optional arguments
function createUrl($host, $path, $argument = []){
$optionalArgs = [
'protocol'=>'http',
'port'=>80];
if( !is_array ($argument) ) return false;
$argument = array_intersect_key($argument,$optionalArgs);
$optionalArgs = array_merge($optionalArgs,$argument);
extract($optionalArgs);
return $protocol.'://'.$host.':'.$port.'/'.$path;
}
//No arguments with function call
echo createUrl ("www.example.com",'no-arguments');
// returns http://www.example.com:80/no-arguments
$argList=['port'=>9000];
//using port argument only
echo createUrl ("www.example.com",'one-args', $argList);
//returns http://www.example.com:9000/one-args
//Use of both parameters as arguments. Order does not matter
$argList2 = ['port'=>8080,'protocol'=>'ftp'];
echo createUrl ("www.example.com",'two-args-no-order', $argList2);
//returns ftp://www.example.com:8080/two-args-no-order
As of version 8.0, PHP now has named arguments. If you name the arguments when calling the function, you can pass them in any order and you can skip earlier default values without having to explicitly pass a value for them.
For example:
function createUrl($host, $path, $protocol = 'http', $port = 80)
{
return "$protocol://$host:$port/$path";
}
createUrl(host: 'example.com', path: 'foo/bar', port: 8080);
// returns: "http://example.com:8080/foo/bar"

declaring and calling PHP object functions with period in name

I'm using an API that returns different values, and I want to dynamically tell my class to run a function with the same name (so i won't need a huge switch or if/else mess).
How can I declare the object method song.pause?
How can I use the object method song.pause?
This might be an XY Problem, so is there a better way to do this? One alternative i thought of is to always call str_replace('.','_',$type) and set functions without the periods. Thoughts?
Example:
<?php
class MyClass {
...
...
$type = "song.pause"; // This value is returned from another API I can't control
if (property_exists('MyClass', $type)) {
$success = $this->{$type}(); // ???
} else {
header("HTTP/1.1 400 Invalid type: $type);
exit;
}
public function 'song.pause'() { // obviously incorrect :)
???
}
Given a return of song.pause, conceptually song should be the class name and pause should be the method, consider this possibility:
class MyClass {
protected $classes = array();
function processResponse($response) {
// $response example is "song.pause"
list($class, $method) = explode('.', $response);
if(!class_exists($class)) {
// Class doesn't exist
die("Class name {$class} doesn't exist! Exiting...");
}
// Instantiate class
$this->classes[$class] = new $class();
if(!method_exists($this->classes[$class], $method)) {
// Method doesn't exist within specified class
die("Method name {$method} doesn't exist within {$class}. Exiting...");
}
// Call method
$result = $this->classes[$class]->{$method}();
return $result;
}
}
Your logic implementation would be something like this:
class song {
public function pause() {
return 'foobar';
}
}
Here's an example.
Unfortunately, what you're generally asking is not supported. From the manual:
Function names follow the same rules as other labels in PHP. A valid
function name starts with a letter or underscore, followed by any
number of letters, numbers, or underscores. As a regular expression,
it would be expressed thus: [a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*.
This also applies to class methods.
As the walkaround, you may follow the way that you suggested yourself:
$type = 'song.pause';
$type = str_replace('.', '_', $type);
$this->{$type}(); // will call song_pause()
OR use "dark" magic:
<?php
// header('Content-Type: text/plain; charset=utf-8');
class Test {
function __call($method, $args){
// do redirect to proper processing method here
print_r($method);
echo PHP_EOL;
print_r($args);
}
}
$x = new Test();
$x->{'song.pause'}(1,2,3);
?>
Shows:
song.pause // < the method
Array // < arguments
(
[0] => 1
[1] => 2
[2] => 3
)
However, the "long" and really transparent way, which I completely agree with, is suggested by #scrowler.
#HAL9000 is right: what you want is not supported. One potential workaround is:
Define the handlers:
$typeHandlers = array();
$typeHandlers['song.pause'] = function () {
echo 'Pause!'; // or whatever...
};
Call the appropriate handler:
$typeHandlers[$type]();

$this in Array of Closures in Class

Say there is a class for objects, let's use a User as an example. The User class contains it's own Rules to validate it's data before submitting. Before saving to the database, the Rules will be checked and any errors will be returned. Otherwise the update will run.
class User extends DBTable // contains $Rules, $Data, $Updates, and other stuff
{
public __construct($ID)
{
parent::__construct($ID);
// I'll only list a couple rules here...
$this->Rules['Email'] = array(
'Empty' => 'ValidateEmpty', // pre-written functions, somewhere else
'Invalid' => 'ValidateBadEmail', // they return TRUE on error
'Duplicate' => function($val) { return existInDatabase('user_table', 'ID_USER', '`Email`="'. $val .'" AND `ID_USER`!='. $this->ID);}
);
$this->Rules['Password'] = array(
'Empty' => 'ValidateEmpty',
'Short' => function($val) { return strlen($val) < 8; }
);
this->Rules['PasswordConfirm'] = array(
'Empty' => 'ValidateEmpty',
'Wrong' => function($val) { return $val != $this->Updates['Password']; }
);
}
public function Save(&$Errors = NULL)
{
$Data = array_merge($this->Data, $this->Updates);
foreach($this->Rules as $Fields => $Checks)
{
foreach($Checks as $Error => $Check)
{
if($Check($Data[$Field])) // TRUE means the data was bad
{
$Errors[$Field] = $Error; // Say what error it was for this field
break; // don't check any others
}
}
}
if(!empty($Errors))
return FALSE;
/* Run the save... */
return TRUE; // the save was successful
}
}
Hopefully I posted enough here. So you'll notice that in the Duplicate error for Email, I want to check that their new email does not exist for any other user excluding themselves. Also PasswordConfirm tries to use $this->Updates['Password'] to make sure they entered the same thing twice.
When Save is run, it loops through the Rules and sets any Errors that are present.
Here is my problem:
Fatal error: Using $this when not in object context in /home/run/its/ze/germans/Class.User.php on line 19
This error appears for all closures where I want to use $this.
It seems like the combination of closures in an array and that array in a class is causing the problem. This Rule array thing works fine outside of a class (usually involving "use") and AFAIK closures are supposed to be able to use $this in classes.
So, solution? Work-around?
Thanks.
The problem is with the Wrong validator. The validation method is called from here:
if($Check($Data[$Field])) // TRUE means the data was bad
This call is not made in an object context (the lambda is not a class method). Therefore $this inside the body of the lambda causes an error because it only exists when in an object context.
For PHP >= 5.4.0 you can solve the problem by causing the capture of $this:
function($val) { return $val != $this->Updates['Password']; }
In this case you will be able to access Updates no matter what its visibility is.
For PHP >= 5.3.0 you need to make a copy of the object reference and capturing that instead:
$self = $this;
function($val) use($self) { return $val != $self->Updates['Password']; }
In this case however, you will only be able to access Updates if it is public.

Excluding objects when print_r()ing variables in a custom logger

I have Logger-Class which is logging everything. Objects will be logged with print_r to a human-readable state. My Problem is that I have a big MVC-Object. Everytime an Exception or Error occurs, the MVC-Object will also be printed in the Log by print_r. This results in a very very long Logfile that is not really friendly to read.
I tried to set a __toString() method to my MVC-Class but this don't work. I also get the complete MVC-Object in Log. The MVC is a Singleton and is referenced on every Object. So to simple exclude the Object before it cames into the print_r is not that easy.
Is there any way to exclude Objects from print_r?
My Methods:
LOG-Class errorHandler-Method:
public static function errorHandler($errno, $errstr, $errfile, $errline, $vars) {
//If # is set, don't do anything!
if(error_reporting() === 0) {
return;
}
//Get StackTrace with low memory usage ;)
$e = new Exception();
$stackStr = $e->getTraceAsString();
//Build ErrorMessage for Log
$message = 'File: '.$errfile.' - L: '.$errline."\n".
'Code: '.$errno."\n".
'Message: '.$errstr."\n".
'Vars: '.print_r($vars, true)."\n".
'Stacktrace: '.$stackStr;
self::error($message);
}
LOG-Class exceptionHandler-Method:
public static function exceptionHandler(Exception $e) {
$message = get_class($e).': '.$e->getMessage()."\n".
'File: '.$e->getFile().' - L: '.$e->getLine()."\n".
'Code: '.$e->getCode()."\n".
'Message: '.$e->getMessage()."\n".
'Stacktrace: '.$e->getTraceAsString();
self::error($message);
}
LOG-Class error-Method:
public static function error($data, $file='system.log') {
$config = Megaira_PropertyConfiguration::getInstance();
switch($config->get('LOG_MODE')) {
case'DEEPDEBUG':
case'ERROR':
case'WARNING':
case'INFO':
case'DEBUG':
self::writeToLog('ERROR', $data, $file);
break;
}
}
LOG-Class writeToLog-Method:
private static function writeToLog($mode='', $text='', $file=''){
if(!is_string($text) && !is_numeric($text)) {
$text = print_r($text, true);
}
$config = Megaira_PropertyConfiguration::getInstance();
if(!$config->get('LOGGINGACTIVE')) { return; }
self::writeLineToFile($mode, $text, $file);
}
Setup the Error- and Exception-handler:
//Set Error and Exception Handler
set_error_handler(array(new LOG(), 'errorHandler'));
set_exception_handler(array(new LOG(), 'exceptionHandler'));
Thanks
Some Testing:
public static function print_r_filtered($object, $ret=false) {
$filtered = array(
'Megaira_MVC'
);
$text = print_r($object, true);
foreach($filtered as $filter) {
$search = '#('.$filter.'\sObject)\n(\s+)\).*?\n\2\)\n#s';
$replace = "$1";
$text = preg_replace($search, $replace, $text);
}
if($ret)
return $text;
echo $text;
}
Did not work. Maybe RegEx fail?
Solution:
It was a Design flaw. The errorHandler is Logging all Objects that are used on the place the error occurs. So in the index.php are the following code:
$mvc = Megaira_MVC::getInstance();
So this peace of code produced a logging of the Var $mvc with print_r through the errorHandler in LOG-Class.
Conclusion for me: Don't use Variables on big Singleton-Objects or use unset() if the Var is not needed anymore.
__toString() is called, when the object is casted to a string. You may try something like
$objectString = method_exists($object, '__toString')
? (string) $object
: print_r($object, true);
Use is_object() to find out, if a value is an object or not.
At all
$string = !is_object($value) || method_exists($value, '__toString')
? (string) $value
: print_r($value, true);
You could wrap print_r with your own function that checks whether the provided data includes any objects, using the is_object() function. Similarly, you could use is_a() if you only want to exclude certain classes of objects.
as a html solution you can use:
<pre><?=print_r(log_content);?></pre>
to display your log file better. I am displaying the log files in this way.
if (is_object( $foo ))
{
print_r( $foo->__toString() );
} else {
print_r( $foo );
}
If you can alter the Logger class, you can check for the class befpre you print it:
if(!($var instanceof HeavyMVCObject)){
print_r($var);
}

Categories