Print name or definition of callable in PHP - php

Callables in PHP can be in a lot of forms, like an object, an array, or a string containing a function name.
If I got a callable like this in a variable how can I print some user friendly "definition" of it in the log.
Think of this code:
call_user_func($callable);
$logger->log("Provided callable " . (string) $callable . " called");
Problem is, this raises error, for example array to string conversion error. What is the best way to print out something useful about that callable?

Very old question but since i just used this to log some info, i thought it could use some clarification.
The comment by #Fabian Picone is a bit misleading.
The type hint actually works with string (arrays too), but the string MUST be an existing method or function (if you add function foo() {} in your code it will work). And that is, it must actually be callable. The error message is not that intuitive.
See this answer too https://stackoverflow.com/a/63289789/7409925.
This is my take that i'm using for logging (expanded from #Seb's), adding support for invokables and removing unnecessary trim's:
function getCallableName(callable $callable) {
switch (true) {
case is_string($callable) && strpos($callable, '::'):
return '[static] ' . $callable;
case is_string($callable):
return '[function] ' . $callable;
case is_array($callable) && is_object($callable[0]):
return '[method] ' . get_class($callable[0]) . '->' . $callable[1];
case is_array($callable):
return '[static] ' . $callable[0] . '::' . $callable[1];
case $callable instanceof Closure:
return '[closure]';
case is_object($callable):
return '[invokable] ' . get_class($callable);
default:
return '[unknown]';
}
}

Something like this should work:
function getCallableName($callable) {
if (is_string($callable)) {
return trim($callable);
} else if (is_array($callable)) {
if (is_object($callable[0])) {
return sprintf("%s::%s", get_class($callable[0]), trim($callable[1]));
} else {
return sprintf("%s::%s", trim($callable[0]), trim($callable[1]));
}
} else if ($callable instanceof Closure) {
return 'closure';
} else {
return 'unknown';
}
}

Inspired by an answer of #Bigdot https://stackoverflow.com/a/68113840/6916271 I created 2 methods, which could be useful when we need to retrieve the context of callable.
I used result together with Monolog, but it is also possible to use it with print_r(), json_encode(), var_dump() or var_export() if you need to convert it to string.
The main difference here compared to the answer above is extended information about closure, which might be needed during an investigation.
/**
* Retrieve the context of callable for debugging purposes
*
* #param callable $callable
* #return array
*/
private function getCallableContext(callable $callable): array
{
switch (true) {
case \is_string($callable) && \strpos($callable, '::'):
return ['static method' => $callable];
case \is_string($callable):
return ['function' => $callable];
case \is_array($callable) && \is_object($callable[0]):
return ['class' => \get_class($callable[0]), 'method' => $callable[1]];
case \is_array($callable):
return ['class' => $callable[0], 'static method' => $callable[1]];
case $callable instanceof \Closure:
try {
$reflectedFunction = new \ReflectionFunction($callable);
$closureClass = $reflectedFunction->getClosureScopeClass();
$closureThis = $reflectedFunction->getClosureThis();
} catch (\ReflectionException $e) {
return ['closure' => 'closure'];
}
return [
'closure this' => $closureThis ? \get_class($closureThis) : $reflectedFunction->name,
'closure scope' => $closureClass ? $closureClass->getName() : $reflectedFunction->name,
'static variables' => $this->formatVariablesArray($reflectedFunction->getStaticVariables()),
];
case \is_object($callable):
return ['invokable' => \get_class($callable)];
default:
return ['unknown' => 'unknown'];
}
}
/**
* Format variables array for debugging purposes in order to avoid huge objects dumping
*
* #param array $data
* #return array
*/
private function formatVariablesArray(array $data): array
{
foreach ($data as $key => $value) {
if (\is_object($value)) {
$data[$key] = \get_class($value);
} elseif (\is_array($value)) {
$data[$key] = $this->formatVariablesArray($value);
}
}
return $data;
}
In we use logger
try {
\call_user_func($callable);
} catch (\Throwable $e) {
$logger->log(
'Error occurred',
['exception' => $e, 'callable' => $this->getCallableContext($callable)]
);
//In case we can use string only
$logger->log('Error occurred: ' . \print_r($this->getCallableContext($callable), true));
}

Related

Laravel: model id goes to true before update

I've got quite a bizarre situation. I've got a piece of code that never gave any issues in the past. Since last night it behaves like this.
Before updating a model the id of that model goes to true. The function below is from a controller and gets called with a POST request. The request gets validated and when the model has not been exported it gets exported to another system. If the export is successful, the model gets updated with the appropriate values. The id does not get set in any stage of this process.
I've added comments to the code to give you an idea where, what happens.
public function export(Request $request, VeniceService $service, Invoice $invoice)
{
$invoice = $invoice->load([
'user', 'customer', 'extension.referenceValues.definition', 'lines'
]);
$this->enforce([
new CheckCstNum($invoice->customer),
new CheckReferences($invoice->extension),
], 432);
if ($invoice->to_export) {
DB::beginTransaction();
try {
var_dump($invoice->id); // returns the id
$data = $service->export($invoice);
var_dump($invoice->id); // returns the true
$invoice->book_date = Carbon::now();
$invoice->doc_num = $data['doc_num'];
$invoice->sys_num = $data['sys_num'];
$invoice->tsm_num = $data['tsm_num'];
$invoice->to_export = false;
$invoice->is_ticked = false;
var_dump($invoice->id); // This returns true
var_dump($invoice); // All the values are correct, except the id, this is set to true
$invoice->save(); // With the id as true, this throws an exception. Duplicate entries for PRIMARY key id, '1'
DB::commit();
$service->attachPdf($invoice, Printer::print($invoice)->output());
} catch (VeniceException $e) {
DB::rollBack();
return $e->render($request);
} catch (\Exception $e) {
DB::rollBack();
return response()->json($e->getMessage(), 500);
}
}
return new InvoiceResource($invoice->refresh()); // returns the invoice, but the id is still true
}
$this->service->export() resolves to this function. Before this happens, the id is still the original id of the model.
public function export($model)
{
return $this->call($model, __FUNCTION__);
}
protected function call($model, $function)
{
$class = $this->getClassName($model);
$method = "{$function}{$class}";
return $this->$method($model);
}
public function exportInvoice($invoice)
{
var_dump($invoice->id); // Returns the id
$veniceInvoice = (new VeniceInvoiceResource($invoice))->toArray(request());
var_dump($invoice->id); // Returns true...
return $this->request('POST', 'venice/invoices/' . $this->bookSales, [
RequestOptions::JSON => $veniceInvoice
]);
}
$veniceInvoice = (new VeniceInvoice($invoice))->toArray(request()); After this line the id is set as true. This really does not make any sense as it has always worked, and the model does not get manipulated in any way.
One last bit of code. But I do not think this has anything to do with the issue.
VeniceInvoiceResource.php
public function toArray($request)
{
$pdf = Printer::print($this->resource)->output();
$lines = $this->customer->standard_base == 10 ? VeniceInvoiceLineIC::collection($this->lines) : VeniceInvoiceLine::collection($this->lines);
$refs = $this->extension->referenceValues->map(function ($item) {
return [
'index' => 0,
'quantity' => 0,
'unit_price' => 0,
'description' => $item->definition->name . ' ' . $item->value,
'vat_code' => 0,
'ic_code' => 0,
];
})->toArray();
$details = array_merge($refs, $lines->toArray($request));
return [
'cst_num' => $this->customer->cst_num,
'book' => ($this->book === 'VKPCN') ? $this->book : config('venice.config.sales.book'),
'amount' => $this->total,
'vat_amount' => $this->total,
'exp_date' => carbon(config('venice.config.sales.date'))->addDays($this->customer->exp_term)->format('d/m/Y'),
'doc_date' => carbon(config('venice.config.sales.date'))->format('d/m/Y'),
'vat_system' => $this->customer->vat_system,
'bf_code' => $this->customer->bf_code,
'doc_type' => ($this->doc_type === 'slsCreditnote') ? 1 : 0,
'pdf' => base64_encode($pdf),
'pdfName' => $this->date->format('Ym') . '-' . $this->id . '.pdf',
'remark' => 'Clockwork ' . $this->date->format('Y F') . ' ' . $this->user->name,
'details' => $details,
];
}
For now I've added a temporary fix to mitigate the issue. I've created a clone of the $invoice. later I set the id of the original invoice to the cloned invoice id.
...
$invoice_copy = clone $invoice;
if ($invoice->to_export) {
DB::beginTransaction();
try {
$data = $service->export($invoice);
$invoice->book_date = Carbon::now();
$invoice->doc_num = $data['doc_num'];
$invoice->sys_num = $data['sys_num'];
$invoice->tsm_num = $data['tsm_num'];
$invoice->to_export = false;
$invoice->is_ticked = false;
$invoice->id = $invoice_copy->id;
$invoice->save();
DB::commit();
...
After a lot of debugging I have pinpointed where the id is set to true. I still don't know why.
In VeniceInvoiceResource $this->id before the PDF generation, the id is still the original invoice id. After the Printer, the id istrue.
If I look at the contructor for the resources, found in Illuminat\Http\Resources\JsonResource (Resource extends JsonResource) I see that $this->resource is set to the incomming value, in this case the $invoice.
/**
* Create a new resource instance.
*
* #param mixed $resource
* #return void
*/
public function __construct($resource)
{
$this->resource = $resource;
}
While in VeniceInvoiceResource $this->resource gets passed to the Printer instance. In the resource $this also has the values of the invoice.
/**
* Load items to print.
*
* #param $items
* #return $this
* #throws \Illuminate\Contracts\Filesystem\FileNotFoundException
*/
public function print($items, $toPrint = null)
{
$items = is_array($items) || $items instanceof Collection ? $items : func_get_args();
foreach ($items as $item) {
if ($item instanceof Printable) {
foreach ($item->printData($toPrint) as $key => $data) {
switch($key) {
case 'merge':
$this->mergeOutput($data);
break;
case 'mergeFile':
$this->mergeFile($data);
break;
default:
$this->toPrint[] = $this->view->make($key, $data)->render();
}
}
} elseif ($item instanceof Renderable) {
$this->toPrint[] = $item->render();
} elseif (is_string($item)) {
$this->toPrint[] = $item;
} else {
throw new \InvalidArgumentException('Invalid argument');
}
}
return $this;
}
In the print method, $this->toPrint[] = $this->view->make($key, $data)->render(); gets used in this case. The output method looks like this.
/**
* Get the output as string.
*
* #return string
* #throws \iio\libmergepdf\Exception
*/
public function output()
{
return $this->generate();
}
/**
* Generate and merge.
*
* #return string
* #throws \iio\libmergepdf\Exception
*/
protected function generate()
{
$data = !empty($this->toPrint) ? $this->toPrint : [''];
$result = $this->pdf->getOutputFromHtml($data);
if (!empty($this->toMerge)) {
$this->merger->addRaw($result);
foreach ($this->toMerge as $item) {
$this->merger->addRaw($item);
}
$result = $this->merger->merge();
}
$this->toPrint = null;
$this->toMerge = null;
return $result;
}
In the print service nothing gets manipulated, it simply prints collections and items to a PDF format.
The last edit, because I found the line that caused all this. But I don't fully understand why it sets the id to true.
In Printer::print there is a call to a method on the model, printData() this method has an if statement to solve a problem we had with two invoices that needed some special treatment. There was not much time so we decided a simple if statement was sufficient enough for this situation.
if ($this->id = 4128 || $this->id === 4217) {
$vat_amount = 0;
$vat_label = '';
}
if you look closely you see that the first condition is not a condition... There is the problem, and the fix was simple. Remove this if statement as we don't need it any more. The invoices 4128 & 4217 already got printed and are archived. They do not need to be processed anymore.
Looks like you found the issue in your printData() method.
For why id ends up as true, it's due to the differing operator precedences.
The comparison operators (===) have a higher precedence than the logical operator (||), so the comparisons are done before the logical comparison. So, if the comparison operator had been correct, this is what would have been run (parens added for clarity):
($this->id === 4128) || ($this->id === 4217)
However, because the first operator was actually an assignment instead of a comparison, this changed the order of operation. The comparion and logical operators have a higher precedence than the assignment operator, so they are executed first. This is what was actually run (parens added for clarity):
$this->id = (4128 || $this->id === 4217)
So, id got assigned to the result of the logical comparison. Since all non-zero numbers evaluate to true, the logical comparison evaluated to true, and therefore id got set to true.

How can I convert an array into a php statement?

I have an array like this...
[Summary] => Array
(
[0] => yearManufactured
[1] => &&
[2] => make
[3] => ||
[4] => model
)
how can I convert this array into function calls and operators and then use it to make a comparision, for example turn it into this...
if( $this->yearManufactured() && $this->make() || $this->model() ) {
// do something
} else {
// do something else
}
Methods in class..
public function yearManufactured() {
return true;
}
public function make() {
return false;
}
public function model() {
return true;
}
This seems like something that could actually be a valid use for eval. You can verify that each array item is either an operator or a valid method name, and convert the result of the method call to a boolean string. Putting those things together should result in a string that you can safely eval without worrying about it doing something nasty, other than maybe causing a parse error, which can be caught in PHP 7.
If you find anything in the array that isn't supposed to be there, or the expression doesn't parse, you can return null, or throw an exception, however you want to handle it.
public function evaluateExpressionArray(array $expression) {
// build the expression
$expr = '$result =';
foreach ($expression as $part) {
if ($part == '||' || $part == '&&') {
$expr .= " $part ";
} elseif (method_exists($this, $part)) {
$expr .= $this->$part() ? 'true' : 'false';
} else {
return null;
}
}
// try to evaluate it
try {
eval("$expr;");
} catch (ParseError $e) {
return null;
}
return $result;
}
Be very careful with eval, though. Don't ever put anything into it unless you know exactly what it is.
Here's an example to mess with.

How to make a PHP structure, that doesn't complain about requests of undefined properties?

I have a bunch of optional settings and I'm sick of checking for isset and property_exists.
In Laravel, if I ask for a property that does not exist on a model or request, I get null and no complaints (errors). How can I do the same for my data structure.
If I try array, I can't do simple $settings['setting13'], I have to either pre-fill it all with nulls or do isset($settings['setting13']) ? $settings['setting13'] : '' or $settings['setting13'] ?? null. If I try an object (new \stdClass()), $settings->setting13 still gives me a warning of Undefined property.
How can I make a class such that it responds null or an empty string whenever it is asked for a property that it doesn't have?
Simply do what Laravel does, create a class that deals with your data structure which returns a value if key exists, and something else if it doesn't.
I'll illustrate with an example class (this class supports the "dot notation" of accessing array keys):
class MyConfigClass
{
protected $data;
public function __construct(array $data)
{
$this->data = $data;
}
public function get($path = '', $default = null)
{
if(!is_string($path))
{
return $default;
}
// There's a dot in the path, traverse the array
if(false !== strpos('.', $path))
{
// Find the segments delimited by dot
$segments = explode('.', $path);
$result = $this->data;
foreach($segments as $segment)
{
if(isset($result[$segment]))
{
// We have the segment
$result = $result[$segment];
}
else
{
// The segment isn't there, return default value
return $default;
}
}
return $result;
}
// The above didn't yield a result, check if the key exists in the array and if not - return default
return isset($this->data[$path]) ? $this->data[$path] : $default;
}
}
Use:
$my_structure = [
'url' => 'www.stackoverflow.com',
'questions' => [
'title' => 'this is test title'
]
];
$config = new MyConfigClass($my_structure);
echo $config->get('url'); // echoes www.stackoverflow.com
echo $config->get('questions.title'); // echoes this is test title
echo $config->get('bad key that is not there'); // returns null
There is also a possibility to create wrapper as Jon Stirling mentioned in a comments. This approach will allow to keep code clean and also add functionality via inheritance.
<?php
class myArray implements ArrayAccess {
private $container;
function __construct($myArray){
$this->container = $myArray;
}
public function offsetSet($offset, $value) {
if (is_null($offset)) {
$this->container[] = $value;
} else {
$this->container[$offset] = $value;
}
}
public function offsetExists($offset) {
return isset($this->container[$offset]);
}
public function offsetUnset($offset) {
unset($this->container[$offset]);
}
public function offsetGet($offset) {
return isset($this->container[$offset]) ? $this->container[$offset] : null;
}
}
$settings = array("setting1"=>1,"setting2"=>2,"setting3"=>3);
$arr = new myArray($settings);
echo $arr['setting1'];
echo "<br>";
echo $arr['setting3'];
echo "<br>";
echo $arr['setting2'];
echo "<br>";
echo "------";
echo "<br>";
echo $arr['setting4'] ?:"Value is null";
!empty($settings['setting13']) ? $settings['setting13'] : ''
can be replaced with
$settings['setting13'] ?: ''
as long as whatever you want to print and whatever you want to check exists is the same expression. It's not the cleanest thing ever - which would be to check the existence of anything - but it's reasonably clear and can be chained :
echo ($a ?: $b ?: $c ? $default ?: '');
However, you are not the first who are "sick of checking for isset and property_exists, it's just that we still have to do it, or else we get unexpected results when we expect it the least.
It's not about saving time typing code, it's about saving time not debugging.
EDIT : As pointed in the comments, I wrote the first line with isset() instead of !empty(). Since ?: returns the left operand if it's equal to true, it's of course uncompatible with unchecked variables, you have at least to check for existence beforehand. It's emptiness that can be tested.
The operator that returns its left operand if it exists and is different from NULL is ??, which can be chained the same way ?: does.
Admittedly not the best way to do this, but you can use the error suppressor in php like this:
$value = #$settings['setting13'];
This will quitely set$value to NULL if $settings['setting13'] is not set and not report the undefined variable notice.
As for objects, you should just calling for attributes that are not defined in class.

PHP/Laravel disappearing variable

I have some very strange behaviour in an app I am working on. In the example below there are 2 functions.
public function updatePhpVhostVersions(Request $r) {
$data = array(
'hostname' => $r['hostname'],
'username' => $r['username'],
'vhost' => $r['vhost'],
'php_version' => $r['php_version']
);
$result = Cpanel::setPhpVhostVersions($data)->data;
if($r->ajax()){
return Response::json($result);
}
return $result;
}
public function getInstalledPhpVersions(Request $r) {
$data = array(
'hostname' => $r['hostname'],
'username' => $r['username']
);
$result = Cpanel::getInstalledPhpVersions($data)->data;
if($r->ajax()){
return Response::json($result);
}
return $result;
}
The two functions contain Cpanel:: ... ($data)->data; this gets handeled by a __call method. In this method I use a function that prepares some vars, caching, etc. to simplify things I have combined some functions into one.
private function prepare($function, $arguments) {
// Dettirmine what will be used
$this->function = $function;
$nameSplit = preg_split('/(?=\p{Lu})/u', $function);
$this->class = 'App\\Phase\\Cpanel\\' . $this->folder($nameSplit) . '\\' . $this->file($nameSplit);
// Cache True/False
if(isset($this->arguments['cache'])) {
$this->cache = $this->arguments['cache'];
}
// Get the provided arguments
// these are used in the API post
if(isset($arguments[0]['hostname'], $arguments[0]['username'])) {
$arguments = $arguments[0];
}
$this->arguments = $arguments;
// Flush the cache when needed
if (!$this->cache || in_array($nameSplit[0], array('create', 'delete', 'add', 'install', 'set'))) {
try {
Cache::tags(['cpanel', $this->function . $arguments['username']])
->flush();
} catch (Exception $e) {
dd($arguments);
}
}
}
The prepare method is used every time the __call method gets used. In Cpanel::setPhpVhostVersion() the $arguments somehow get empty after the following if statement.
// Flush the cache when needed
if (!$this->cache || in_array($nameSplit[0], array('create', 'delete', 'add', 'install', 'set'))) {
try {
Cache::tags(['cpanel', $this->function . $arguments['username']])
->flush();
} catch (Exception $e) {
dd($e);
}
}
Before Cache::tags() the $arguments contains an array with some user information. But when Cache::tags()->flush() gets called it throws an exception that $arguments['username'] is empty. Now if I dd($arguments) after this, it returns an empty array. If I dd() before this the array still has the information. This only happens with Cpanel::setPhpVhostVersion() not with the 37 other possible Cpanel:: ... () what could be causing this?
EDIT
After some playing around with the code, I noticed that $arguments gets empty everytime after it gets used. It does not matter where, it just gets empty. (But only with setPhpVhostVersions)
Example
if(!isset($this->arguments['username'])) {
return Api::respondUnauthenticated('Account username is a required parameter');
}
Before the !isset() the $arguments['username'] exists, during and after the !isset() I get the exception:
ErrorException: Undefined index: username

Should i force memoization?

I see myself doing this a lot:
function getTheProperty()
{
if (! isset($this->theproperty)) {
$property = // logic to initialise thepropery
$this->theproperty = $property;
}
return $this->theproperty;
}
This is good, since it avoids the epxensive logic used to initialise the value. however, the downside so far as i can see is that i cant determine exactly how clients will use this and this may be confusing.
Is this a good pattern to use? What considerations should be taking when doing this?
How about adding a parameter - $forceNew for example that bypasses the memoization?
Magic Methods. Something like:
Class MyMagic {
private $initinfo = array(
'foo' => array('someFunction', array('arg1', 'arg2'))
'bar' => array(array($this, 'memberFunction'), array('arg3'))
);
public function __get($name) {
if( ! isset($this->$name) ) {
if( isset($this->initinfo[$name]) ) {
$this->$name = call_user_func_array(initinfo[$name][0], initinfo[$name][1]);
} else {
throw new Exception('Property ' . $name . 'is not defined.');
}
} else {
return $this->$name;
}
}
}
$magic = new MyMagic();
echo $magic->foo; //echoes the return of: someFunction('arg1', 'arg2')
echo $magic->bar; //echoes the return of: $this->memberFunction('arg3')

Categories