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
Related
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'])'
}
Hi tried refreshing the modification cache cache in opencart and since then i am getting a blank page in front end with this error message.
public function trigger($event, array $args = array()) {
foreach ($this->data as $value) {
if (preg_match('/^' . str_replace(array('\*', '\?'), array('.*', '.'), preg_quote($value['trigger'], '/')) . '/', $event)) {
$result = $value['action']->execute($this->registry, $args);
if (!is_null($result) && !($result instanceof Exception)) {
return $result;
}
}
}
}
Thanks,
It seems your have an OC version 3.0.2.x or above.
In your $this->data of the Event Class, you have an event registered that is missing an action parameter.
$this->data[] = array(
'trigger' => $trigger,
'action' => $action, // <-- this must be an Action Object with a method execute()
'priority' => $priority
);
All events are registered via the register() method which explicitly requests that an Action object is being passed as a parameter.
Since the error is pointing to "Call to undefined method Action::execute()", I can assume, you have an issue with the action class.
Most likely you need to check the Modifications of the system/engine/action.php file in your system/storage/modifications.
It could be that the method execute() is either missing or somehow corrupt.
Debug
try to var_dump the $value to see what is there:
public function trigger($event, array $args = array()) {
foreach ($this->data as $value) {
//log out the $value before the error to see if the Action object is actually there and see what trigger causes this.
var_dump($value);
if (preg_match('/^' . str_replace(array('\*', '\?'), array('.*', '.'), preg_quote($value['trigger'], '/')) . '/', $event)) {
$result = $value['action']->execute($this->registry, $args);
if (!is_null($result) && !($result instanceof Exception)) {
return $result;
}
}
}
}
Hope this helps
Hi tried refreshing the modification cache cache in opencart and since then i am getting a blank page in front end with this error message.
public function trigger($event, array $args = array()) {
foreach ($this->data as $value) {
if (preg_match('/^' . str_replace(array('\*', '\?'), array('.*', '.'), preg_quote($value['trigger'], '/')) . '/', $event)) {
$result = $value['action']->execute($this->registry, $args);
if (!is_null($result) && !($result instanceof Exception)) {
return $result;
}
}
}
}
Thanks,
It seems your have an OC version 3.0.2.x or above.
In your $this->data of the Event Class, you have an event registered that is missing an action parameter.
$this->data[] = array(
'trigger' => $trigger,
'action' => $action, // <-- this must be an Action Object with a method execute()
'priority' => $priority
);
All events are registered via the register() method which explicitly requests that an Action object is being passed as a parameter.
Since the error is pointing to "Call to undefined method Action::execute()", I can assume, you have an issue with the action class.
Most likely you need to check the Modifications of the system/engine/action.php file in your system/storage/modifications.
It could be that the method execute() is either missing or somehow corrupt.
Debug
try to var_dump the $value to see what is there:
public function trigger($event, array $args = array()) {
foreach ($this->data as $value) {
//log out the $value before the error to see if the Action object is actually there and see what trigger causes this.
var_dump($value);
if (preg_match('/^' . str_replace(array('\*', '\?'), array('.*', '.'), preg_quote($value['trigger'], '/')) . '/', $event)) {
$result = $value['action']->execute($this->registry, $args);
if (!is_null($result) && !($result instanceof Exception)) {
return $result;
}
}
}
}
Hope this helps
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));
}
I'm a little bit baffled by this, so I'm hoping someone can shed some light on it for me.
I have a function which is meant to return a column Status from one of my tables, visit.
function someFunction($visitId = null) {
$visit = VisitQuery::create()
->select(array('Status'))
->findPk($visitId);
}
If I var_dump($visit), when calling the function for the first time, it outputs:
string '1' (length=1)
Subsequent, identical calls to the function however seem to return an entire object:
object(Visit)[30]
protected 'startCopy' => boolean false
protected 'id' => int 362
protected 'job_id' => int 351
protected 'company_id' => int 2
protected 'type_id' => int 1
protected 'visit_date' => string '2013-08-23 00:00:00' (length=19)
protected 'status' => string '1' (length=1)
...
I'm calling the function for the first time with an (int) $visitId passed via a posted form:
var_dump($visitId); // int 362
Subsequent calls are made with an (int) $visitId which is returned from another function, saveVisit() (which uses Propel to save the record - I believe this may have something to do with it).
$visitId = saveVisit($visitId);
var_dump($visitId); // int 362
I tried to do some debugging, and for some reason the query issued to MySQL is different between the first function call and subsequent ones:
var_dump($con->getLastExecutedQuery());
SELECT visit.STATUS AS "Status" FROM `visit` WHERE visit.ID=362 // 1st call
SELECT `ID`, `JOB_ID`, `COMPANY_ID`, `TYPE_ID`, `VISIT_DATE`, `STATUS`, `REMIND`, `PRINTED` FROM `visit` WHERE `ID` = 362 // 2nd call
SELECT `ID`, `JOB_ID`, `COMPANY_ID`, `TYPE_ID`, `VISIT_DATE`, `STATUS`, `REMIND`, `PRINTED` FROM `visit` WHERE `ID` = 362 // 3rd call
Can anyone tell me why or how this is happening?
I'm using Propel 1.6.
Propel's create() method:
public static function create($modelAlias = null, $criteria = null)
{
if ($criteria instanceof VisitQuery) {
return $criteria;
}
$query = new VisitQuery();
if (null !== $modelAlias) {
$query->setModelAlias($modelAlias);
}
if ($criteria instanceof Criteria) {
$query->mergeWith($criteria);
}
return $query;
}
Propel's findPk() method:
public function findPk($key, $con = null)
{
if ($con === null) {
$con = Propel::getConnection($this->getDbName(), Propel::CONNECTION_READ);
}
// As the query uses a PK condition, no limit(1) is necessary.
$this->basePreSelect($con);
$criteria = $this->isKeepQuery() ? clone $this : $this;
$pkCols = $this->getTableMap()->getPrimaryKeyColumns();
if (count($pkCols) == 1) {
// simple primary key
$pkCol = $pkCols[0];
$criteria->add($pkCol->getFullyQualifiedName(), $key);
} else {
// composite primary key
foreach ($pkCols as $pkCol) {
$keyPart = array_shift($key);
$criteria->add($pkCol->getFullyQualifiedName(), $keyPart);
}
}
$stmt = $criteria->doSelect($con);
return $criteria->getFormatter()->init($criteria)->formatOne($stmt);
}
My function to retrieve the visit status:
function getVisitStatus($visitId = null) {
if (empty($visitId)) {
return false;
}
try {
$visit = VisitQuery::create()
->select(array('Status'))
->findPk($visitId);
} catch (Exception $e) {
echo $e->getMessage(); exit;
}
if (is_null($visit)) {
return false;
}
return $visit;
}
The function which saves a visit record:
function saveVisit($data = null) {
if (empty($data)) {
return false;
}
try {
$visit = (!empty($data['visit_id'])) ? VisitQuery::create()->findPk($data['visit_id']) : new Visit();
if (!is_object($visit)) {
return false;
}
$visitDataMap = array(
'JobId' => 'job_id',
'TypeId' => 'type_id',
'VisitDate' => 'visit_date',
'Status' => 'status',
'CompanyId' => 'engineer_id',
'Remind' => 'remind'
);
$visitData = array();
foreach ($visitDataMap as $column => $value) {
if (!empty($data[$value])) {
$visitData[$column] = $data[$value];
}
}
$visit->fromArray($visitData);
$visit->save();
return $visit->getId();
} catch (PropelException $e) {
echo $e->getMessage(); exit;
}
}
It seems that on the first call will grab your data but then put a copy of the full object in to the instance pool. I am not sure if this is a bug or valid behaviour (your code would suggest the former, but I'd love to hear from someone who knows more about Propel such as a dev) but you can stop it happening with:
VisitPeer::clearInstancePool();
Bear in mind you're losing a bit of caching from other queries you might have done here with the visit relation.
You will be able to confirm this by adding an echo in the BaseVisitPeer.php file. You will see something like this:
public static function getInstanceFromPool($key)
{
if (Propel::isInstancePoolingEnabled()) {
if (isset(VisitPeer::$instances[$key])) {
return VisitPeer::$instances[$key];
}
}
return null; // just to be explicit
}
If you add an echo somewhere inside the if (isset(VisitPeer::$instances[$key])) { statement, you should see it appear only after the second and subsequent calls. If you were to comment this whole if statement out then you would get the same result back each time - the one you correctly get back from your original call.
Hope that helps!