This question already has answers here:
Replace preg_replace() e modifier with preg_replace_callback
(3 answers)
Closed 8 years ago.
I have this Deprecated warning after switching my php to 5.5.8,
Deprecated: preg_replace(): The /e modifier is deprecated, use
preg_replace_callback instead in C:\wamp\www...Curly.php on
line 28
This is the function in my class of Curly,
public function replace ($input, $options = array()) {
return preg_replace("/\{{2}(([a-z\_]+\|.+)|([a-z\_]+))\}{2}/Ue",'$this->_replace("\\1",$options)',$input);
}
so if I use preg_replace_callback instead,
return preg_replace_callback("/\{{2}(([a-z\_]+\|.+)|([a-z\_]+))\}{2}/Ue",'$this->_replace("\\1",$options)',$input);
Then I get this error,
Warning: preg_replace_callback(): Requires argument 2,
'$this->_replace("\1",$options)', to be a valid callback
in C:\wamp\www...Curly.php on line 28
Any ideas how can I fix this?
EDIT:
class Curly extends CoreModel
{
// Set the property/ variable of this class
public $constant = null;
/**
* Extend the parent class property.
*/
public function __construct($connection){
// Extend parent's.
parent::__construct($connection);
$this->constant = new Constant($connection);
}
/**
* Replace the curly in an input string
* #param string $input
* #return string
*/
public function replace ($input, $options = array()) {
//return preg_replace("/\{{2}([a-z]+\|.+)\}{2}/Ue",'$this->_replace("\\1")',$input);
//return preg_replace("/\{{2}(([a-z\_]+\|.+)|([a-z\_]+))\}{2}/Ue",'$this->_replace("\\1",$options)',$input);
return preg_replace_callback(
"/\{\{([a-z_]+(?:\|.+)?)\}\}/U",
function($m) { return $this->_replace($m[1], $options); },
$input
);
}
/**
* Run the replacement code on a given macro string
* #param string $input
* #return string
*/
private function _replace ($input,$options) {
// Set local vars.
$defaults = array();
// Call internal method to process the array.
$array = parent::arrayMergeValues($defaults,$options);
//print_r($array);
// Convert array to object.
$property = parent::arrayToObject($array);
// type-checking comparison operator is necessary.
if (strpos($input, '|') !== false) {
//VERTICAL SIGN FOUND
list ($name,$params) = explode("|",$input);
if (method_exists($this,$name)) {
return $this->$name($params);
}
throw new Exception ("Unrecognised macro: {$name}.",500);
} else {
// Get the input string and request the data from constant table.
$value = $this->constant->getRow($input)->value;
// If there is a value returned from the contstant table.
if($value !== null) {
// Return the what is returned from the the constant table.
return $value;
} else if(isset($property->$input)) { // If there is a customised value from the developer.
// Return what is customised by the developer.
return $property->$input;
} else { // Nothing is found.
// Return what is from the input.
return "{{{$input}}}";
}
}
}
/**
* Replaces a YouTube curly
* #param string $params
* #return string
*/
private function youtube ($params) {
parse_str($params);
// set defaults
if (!isset($id)) { $id = "ykwqXuMPsoc"; }
if (!isset($width)) { $width = 560; }
if (!isset($height)) { $height = 315; }
// output the final HTML
return "<iframe width=\"{$width}\" height=\"{$height}\" src=\"http://www.youtube.com/embed/{$id}\" frameborder=\"0\" allowfullscreen></iframe>";
}
}
How about:
public function replace ($input, $options = array()) {
return preg_replace_callback(
"/\{\{([a-z_]+(?:\|.+)?)\}\}/U",
function($m) use($options) { return $this->_replace($m[1], $options); },
$input
);
}
Otherwise with call_user_func_array:
return preg_replace_callback(
// RegExpr
"/\{{2}(([a-z\_]+\|.+)|([a-z\_]+))\}{2}/Ue",
// Callback
array($this, '_replace'),
// Data
$input
);
Related
Disclaimer: I would have preferred a more generic question like "How do I keep track of the state of a recursive method?" on the code review stack exchange site, as that better describes where the problem is currently at. But the constraint on that board is that code must first be working
Background: I have a container that
can recursively hydrate and inject constructor arguments
concretes can be provided to it for use to circumvent hydration
provision context exist in two states: for classes being auto-wired, and for classes using the container as a service locator
Three of these target behaviours function as expected, save when the last 2 are combined.
Main problem: when 3a is followed by 3b, container uses an incorrect context, and I don't know how to inspect that particular state since the hydrator/service locator is recursive. Unit testing the individual methods all work correctly. Integration test of either of the two works. But at the level of multi layer hydration, I can't mock out any of the involved parts, thus, I have no way of determining what context is used there
I feel the problem is more of a philosophical one, where more than one answer is applicable. But instead of down-voting, kindly migrate to the appropriate stack exchange site
The code is in PHP, but if you aren't conversant with it, pseudo-code or a verbal solution is welcome. An acceptable solution may even be a test that demonstrates how to simulate and observe container state after hydration and service location. I already have this, but it fails
public function test_hydrated_class_with_getClass_correctly_uses_needs () {
$ourB = new BCounter; // given
$this->container->provideSelf();
$this->container->whenTypeAny()->needsAny([
BCounter::class => $ourB
]);
$this->assertSame( // then
$this->container->getClass($this->aRequires)
->getInternalB(), // when
$this->ourB
);
}
Relevant parts of the container below
<?php
use ReflectionMethod, ReflectionClass, ReflectionFunction, ReflectionType, ReflectionFunctionAbstract, ReflectionException;
class Container {
const UNIVERSAL_SELECTOR = "*";
private $provisionedNamespaces = [], // NamespaceUnit[]
$hydratingForStack = [], // String[]. Doubles as a dependency chain. #see [lastHydratedFor] for main usage
$internalMethodHydrate = false, // Used when [getMethodParameters] is called directly without going through instance methods such as [instantiateConcrete]
$hydratingArguments = false,
$constructor = "__construct",
$externalHydrators = [], $externalContainerManager,
$interfaceHydrator,
$provisionContext, // the active Type before calling `needs`
$provisionSpace; // same as above, but for namespaces
protected $provisionedClasses = []; // ProvisionUnit[]
public function __construct () {
$this->initializeUniversalProvision();
}
public function initializeUniversalProvision ():void {
$this->provisionedClasses[self::UNIVERSAL_SELECTOR] = new ProvisionUnit;
}
public function getInterfaceHydrator ():InterfaceHydrator {
return $this->interfaceHydrator;
}
/**
* Looks for the given class in this order
* 1) pre-provisioned caller list
* 2) Provisions it afresh if an interface or recursively wires in its constructor dependencies
*
* #param {includeSub} Regular provision: A wants B, but we give C sub-class of B. Sub-classes of A can't obtain B unless this parameter is used
*
* #return A class instance, if found
*/
public function getClass (string $fullName, bool $includeSub = false) {
$concrete = $this->decorateProvidedConcrete($fullName);
if (!is_null($concrete)) return $concrete;
if ($includeSub && $parent = $this->hydrateChildsParent($fullName))
return $parent;
$externalManager = $this->externalContainerManager;
if (
!is_null($externalManager) &&
$concrete = $externalManager->findInManagers($fullName)
) {
$this->saveWhenImplements($fullName, $concrete);
return $concrete;
}
return $this->initializeHydratingForAction($fullName, function ($className) {
if ($this->getReflectedClass($className)->isInterface())
return $this->provideInterface($className);
return $this->instantiateConcrete($className);
});
}
public function decorateProvidedConcrete (string $fullName) {
$freshlyCreated = $this->initializeHydratingForAction($fullName, function ($className) {
return new HydratedConcrete(
$this->getProvidedConcrete($className),
$this->lastHydratedFor()
);
});
if (!is_null($freshlyCreated->getConcrete()))
return $this->getDecorator()->scopeInjecting(
$freshlyCreated->getConcrete(),
$freshlyCreated->getCreatedFor()
); // decorator runs on each fetch (rather than only once), since different callers result in different behavior
}
public function getProvidedConcrete (string $fullName) {
$context = $this->getRecursionContext();
if ($context->hasConcrete($fullName))
return $context->getConcrete($fullName);
$globalContext = $this->provisionedClasses[self::UNIVERSAL_SELECTOR];
if ($globalContext->hasConcrete($fullName)) // current provision doesn't include this class. check in global
return $globalContext->getConcrete($fullName);
}
/**
* Switches unit being provided to universal if it doesn't exist
* #return currently available provision unit
*/
public function getRecursionContext ():ProvisionUnit {
$hydrateFor = $this->lastHydratedFor();
if (!array_key_exists($hydrateFor, $this->provisionedClasses))
$hydrateFor = self::UNIVERSAL_SELECTOR;
return $this->provisionedClasses[$hydrateFor];
}
/**
* This tells us the class we are hydrating arguments for
*/
public function lastHydratedFor ():?string {
$stack = $this->hydratingForStack;
if(empty($stack) ) return null;
$index = $this->hydratingArguments ? 2: 1; // If we're hydrating class A -> B -> C, we want to get provisions for B (who, at this point, is indexed -2 while C is -1). otherwise, we'll be looking through C's provisions instead of B
$length = count($stack);
return $stack[$length - $index];
}
/**
* Not explicitly decorating objects from here since it calls [getClass]
*/
private function hydrateChildsParent (string $fullName) {
$providedParent = $this->getProvidedParent($fullName);
if (!is_null($providedParent))
return $this->getClass($providedParent);
}
/**
* #return the first provided parent of the given class
*/
private function getProvidedParent (string $class):?string {
$allSuperiors = array_keys($this->provisionedClasses);
$classSuperiors = array_merge(
class_parents($class, true),
class_implements($class, true)
);
return current(
array_intersect($classSuperiors, $allSuperiors)
);
}
private function saveWhenImplements (string $interface, $concrete):void {
if (!($concrete instanceof $interface))
throw new InvalidImplementor($interface, get_class($concrete));
$this->storeConcrete( $interface, $concrete);
}
private function storeConcrete (string $fullName, $concrete):ProvisionUnit {
return $this->getRecursionContext()->addConcrete($fullName, $concrete);
}
private function getReflectedClass (string $className):ReflectionClass {
try {
return new ReflectionClass($className);
}
catch (ReflectionException $re) {
$message = "Unable to hydrate ". $this->lastHydratedFor() . ": ". $re->getMessage();
$hint = "Hint: Cross-check its dependencies";
throw new HydrationException("$message. $hint");
}
}
/**
* Wrap any call that internally attempts to read from [lastHydratedFor] in this i.e. calls that do some hydration and need to know what context/provision they're being hydrated for
*/
public function initializeHydratingForAction (string $fullName, callable $action) {
$this->initializeHydratingFor($fullName);
$result = $action($fullName);
$this->popHydratingFor($fullName);
return $result;
}
/**
* Tells us who to hydrate arguments for
*/
protected function initializeHydratingFor (string $fullName):void {
$isFirstCall = is_null($this->lastHydratedFor());
$hydrateFor = $isFirstCall ? $this->lastCaller(): $fullName;
$this->pushHydratingFor($hydrateFor);
}
private function lastCaller ():string {
$stack = debug_backtrace (2 ); // 2=> ignore concrete objects and their args
$caller = "class";
foreach ($stack as $execution)
if (array_key_exists($caller, $execution) && $execution[$caller] != get_class()) {
return $execution[$caller];
}
}
/**
* Updates the last element in the context hydrating stack, to that whose provision dependencies should be hydrated for
*/
protected function pushHydratingFor (string $fullName):void {
$this->hydratingForStack[] = $fullName;
}
/**
* #param {completedHydration} To guarantee push-pop consistency. When the name of what is expected to be removed doesn't match the last item in stack, it indicates we're currently hydrating an interface (where its name differs from concretes involved). When this happens, we simply ignore popping our list since those concretes were not the ones that originally got pushed
*/
private function popHydratingFor (string $completedHydration):void {
if (end($this->hydratingForStack) == $completedHydration)
array_pop($this->hydratingForStack);
}
/**
* #throws InvalidImplementor
*
* #return Concrete of the given [Interface] if it was bound
*/
protected function provideInterface (string $interface) {
$caller = $this->lastHydratedFor();
if ($this->hasRenamedSpace($caller)) {
$newIdentity = $this->relocateSpace($interface, $caller);
$concrete = $this->instantiateConcrete($newIdentity);
}
else {
$concrete = $this->getInterfaceHydrator()->deriveConcrete($interface);
if (!is_null($concrete))
$this->saveWhenImplements($interface, $concrete);
}
if (is_null($concrete))
throw new InvalidImplementor($interface, "No matching concrete" );
return $concrete;
}
/**
* A shorter version of [getClass], but neither checks in cache or contextual provisions. This means they're useful to:
* 1) To hydrate classes we're sure doesn't exist in the cache
* 2) In methods that won't be called more than once in the request cycle
* 3) To create objects that are more or less static, or can't be overidden by an extension
*
* All objects internally derived from this trigger decorators if any are applied
*/
public function instantiateConcrete (string $fullName) {
$freshlyCreated = $this->initializeHydratingForAction ($fullName, function ($className) {
if (!method_exists($className, $this->constructor))
return new HydratedConcrete(new $className, $this->lastHydratedFor() );
return $this->hydrateConcreteForCaller($className);
});
$this->storeConcrete($fullName, $freshlyCreated->getConcrete());
return $this->getDecorator()->scopeInjecting(
$freshlyCreated->getConcrete(),
$freshlyCreated->getCreatedFor()
);
}
public function hydrateConcreteForCaller (string $className):HydratedConcrete {
$dependencies = $this->internalMethodGetParameters(function () use ($className) {
return array_values($this->getMethodParameters($this->constructor, $className));
});
return new HydratedConcrete(
new $className (...$dependencies),
$this->lastHydratedFor()
);
}
public function internalMethodGetParameters (callable $action) {
$this->internalMethodHydrate = true;
$result = $action();
$this->internalMethodHydrate = false;
return $result;
}
/**
* Fetch appropriate dependencies for a callable's arguments
*
* #param {callable}:string|Closure
* #param {anchorClass} the class the given method belongs to
*
* #return {Array} associative. Contains hydrated parameters to invoke given callable with
*/
public function getMethodParameters ( $callable, string $anchorClass = null):array {
$context = null;
if (is_null($anchorClass))
$reflectedCallable = new ReflectionFunction($callable);
else {
$reflectedCallable = new ReflectionMethod($anchorClass, $callable);
if (!$this->internalMethodHydrate)
$this->initializeHydratingFor($anchorClass);
$context = $this->getRecursionContext();
}
$dependencies = $this->populateDependencies($reflectedCallable, $context);
if (is_null($anchorClass)) return $dependencies;
elseif (!$this->internalMethodHydrate)
$this->popHydratingFor($anchorClass);
return $this->getDecorator()->scopeArguments( $anchorClass, $dependencies, $callable);
}
public function populateDependencies (ReflectionFunctionAbstract $reflectedCallable, ?ProvisionUnit $callerProvision):array {
$dependencies = [];
foreach ($reflectedCallable->getParameters() as $parameter) {
$parameterName = $parameter->getName();
$parameterType = $parameter->getType();
if (!is_null($callerProvision) )
$dependencies[$parameterName] = $this->hydrateProvidedParameter($callerProvision, $parameterType, $parameterName);
elseif (!is_null($parameterType))
$dependencies[$parameterName] = $this->hydrateUnprovidedParameter($parameterType);
elseif ($parameter->isOptional() )
$dependencies[$parameterName] = $parameter->getDefaultValue();
else $dependencies[$parameterName] = null;
}
return $dependencies;
}
/**
* Pulls out a provided instance of a dependency when present, or creates a fresh one
*
* #return object matching type at given parameter
*/
private function hydrateProvidedParameter (ProvisionUnit $callerProvision, ReflectionType $parameterType, string $parameterName) {
if ($callerProvision->hasArgument($parameterName))
return $callerProvision->getArgument($parameterName);
$typeName = $parameterType->getName();
if ($callerProvision->hasArgument($typeName))
return $callerProvision->getArgument($typeName);
return $this->hydrateUnprovidedParameter($parameterType);
}
private function hydrateUnprovidedParameter (ReflectionType $parameterType) {
$typeName = $parameterType->getName();
if ( $parameterType->isBuiltin()) {
$defaultValue = null;
settype($defaultValue, $typeName);
return $defaultValue;
}
if (!in_array($typeName, $this->hydratingForStack)) {
$this->hydratingArguments = true;
$concrete = $this->getClass($typeName);
$this->hydratingArguments = false;
return $concrete;
}
if ($this->getReflectedClass($typeName)->isInterface())
throw new HydrationException ("$typeName's concrete cannot depend on its dependency's concrete");
trigger_error("Circular dependency detected while hydrating $typeName", E_USER_WARNING);
}
public function whenType (string $toProvision):self {
if (!array_key_exists($toProvision, $this->provisionedClasses))
$this->provisionedClasses[$toProvision] = new ProvisionUnit;
$this->provisionContext = $toProvision;
return $this;
}
public function whenTypeAny ():self {
return $this->whenType(self::UNIVERSAL_SELECTOR);
}
public function needs (array $dependencyList):self {
if (is_null ($this->provisionContext))
throw new HydrationException("Undefined provisionContext");
$this->provisionedClasses[$this->provisionContext]->updateConcretes($dependencyList);
return $this;
}
public function needsAny (array $dependencyList):self {
$this->needs($dependencyList)
->needsArguments($dependencyList);
$this->provisionContext = null;
return $this;
}
public function needsArguments (array $argumentList):self {
if (is_null ($this->provisionContext))
throw new HydrationException("Undefined provisionContext");
$this->provisionedClasses[$this->provisionContext]->updateArguments($argumentList);
return $this;
}
public function provideSelf ():void {
$this->whenTypeAny()->needsAny([get_class() => $this]);
}
protected function getDecorator ():DecoratorHydrator {
return $this->decorator;
}
}
?>
The relevant parts are:
decorateProvidedConcrete (who I want to assert was called twice, but by mocking, getClass can no longer function)
public function test_hydrated_class_with_getClass_reads_provision () {
// given
$container = $this->positiveDouble(Container::class, [
"getDecorator" => $this->stubDecorator(),
"getProvidedConcrete" => $this->returnCallback(function ($subject) {
return $this->positiveDouble($subject, []); // return a stub
})
], [
"getProvidedConcrete" => [2, [
$this->callback(function ($subject) {
return BCounter::class == $subject; // this obviously won't work, since method attempts to hydrate other classes, as well
})
]] // then 1
]);
$this->bootContainer($container);
$this->entityBindings();
// when
$container->getClass($this->aRequires)->getInternalB();
}
getRecursionContext, who I have no way of observing until getClass returns. But I would like to know what context it's working with by the time we're doing the service location. And, that's difficult to figure out since getClass is recursive
Finally, ARequiresBCounter. I want to DI this class with provided BCounter. Then when getInternalB runs, it equally uses the provided BCounter
class ARequiresBCounter {
private $b1, $container, $primitive;
public function __construct (BCounter $b1, Container $container, string $primitive) {
$this->b1 = $b1;
$this->container = $container;
$this->primitive = $primitive;
}
public function getConstructorB ():BCounter {
return $this->b1;
}
public function getInternalB ():BCounter {
return $this->container->getClass(BCounter::class);
}
}
PHP 7.2 - WordPress 5.3.2
I'm trying to use a WP plugin from a couple of years ago but I get this error:
Fatal error: Cannot use "self" when no class scope is active in /home/... on line 36
Here is the code: functions.php
<?php
if (!function_exists('is_teatime_simple_error')) {
/**
* Checks if the object is an instance of a simple error
* #param mixed $object The object to check against.
* #return boolean Returns true if the object is a simple error
*/
function is_teatime_simple_error($object) {
if (is_a($object, 'Teatime_Simple_Error')) {
return true;
}
return false;
}
}
if (!function_exists('teatime_spintax')) {
/**
* Assembles a random spintax string.
* #var str Text containing our {spintax|spuntext}
* #return str Text with random spintax selections
*/
function teatime_spintax($s) {
preg_match('#{(.+?)}#is', $s, $m);
if (empty($m))
return $s;
$t = $m[1];
if (strpos($t, '{') !== false) {
$t = substr($t, strrpos($t, '{') + 1);
}
$parts = explode("|", $t);
$s = preg_replace("+{" . preg_quote($t) . "}+is", $parts[array_rand($parts)], $s, 1);
/** Error line 36 */ return self::spintax($s);
}
}
According to the PHP documentation on Strict Typing found here
By default, PHP will coerce values of the wrong type into the expected scalar type if possible. For example, a function that is given an integer for a parameter that expects a string will get a variable of type string.
I'm curious if there is a way to override this functionality to customize the way that the coercion is done.
For example
function getResponse() : \Namespace\Response {
return []; // Ideally, this would be coerced into a Response object.
}
. . .
namespace Namespace;
class Response {
public $data;
public function __construct(array $arr)
{
$this->data = $arr;
}
public static function __coerce($value)
{
if (! is_array($value)) {
throw new \TypeError('Wrong type during coercion.');
}
return new self($value);
}
}
This is not possible as this is evalue on language level on compile time.
Only thing you can do is override parent return type:
public function getResponse(): [] // though parent has "Reponse" type
{
return [];
}
I've written my own implementation to do this in PHP, since none existed. This is how it works.
These are the two base functions.
The multiReturnFunction function. (Used for calling global functions and anon functions)
/**
* Call a global function and use type coercion
* for non-registered return types.
*
* #param closure $closure The function to execute.
* #param string $returnType The registered return type.
* #param array $params The parameters to pass to the function.
*
* #return mixed The result of the function to execute.
*/
function multiReturnFunction($closure, $returnType, ...$params)
{
$val = $closure(...$params);
if (gettype($val) === 'object') {
if (get_class($val) != $returnType) {
if (method_exists($returnType, '__coerce')) {
$val = $returnType::__coerce($val);
} else {
throw new \Exception(
'Returned value does not match the return type defined, '.
'and no __coerce function is visible.'
);
}
}
} else if (gettype($val) != $returnType) {
if (method_exists($returnType, '__coerce')) {
$val = $returnType::__coerce($val);
} else {
throw new \Exception(
'Returned value does not match the return type defined, '.
'and no __coerce function is visible.'
);
}
}
return $val;
}
The multiReturnFunction function will call a closure and use the __coerce function of the return type class to coerce the return type if the resulting return type does not match.
An Example of the multiReturnFunction function
Define the class that we will be using, and be sure to give it a __coerce function.
Note: __coerce functions take a single variable for the object that we will be trying to coerce into this class type. The function must be declared static.
class MyClass
{
private $data;
public function __construct(array $value)
{
$this->data = $value;
}
public static function __coerce($value)
{
if (! is_array($value)) {
throw new \Exception(
'Returned value does not match the return type defined.'
);
}
return new self($value);
}
}
Next, you will need to call the multiReturnFunction function using your class and an anonymous function.
$resultingMyClass = multiReturnFunction (
// Multi return type function.
function($name, $age) {
// Here you can return either a MyClass instance, or an array.
// All other types will throw an exception.
return [$name, $age];
},
// Return Type, any other type will be coerced through this class.
MyClass::class,
// Function parameters.
'Nathan', 23
);
The multiReturnMethod function. (Used for calling class methods)
/*
* Call a class method and use type coercion
* for non-registered return types.
*
* #param object $obj The object to call the method on.
* #param string $method The method to call on the object.
* #param string $returnType The registered return type.
* #param array $params The parameters to pass to the method.
*
* #return mixed The result of the method to execute.
*/
function multiReturnMethod($obj, $method, $returnType, ...$params)
{
$val = $obj->{$method}(...$params);
if (gettype($val) === 'object') {
if (get_class($val) != $returnType) {
if (method_exists($returnType, '__coerce')) {
$val = $returnType::__coerce($val);
} else {
throw new \Exception(
'Returned value does not match the return type defined, '.
'and no __coerce function is visible.'
);
}
}
} else if (gettype($val) != $returnType) {
if (method_exists($returnType, '__coerce')) {
$val = $returnType::__coerce($val);
} else {
throw new \Exception(
'Returned value does not match the return type defined, '.
'and no __coerce function is visible.'
);
}
}
return $val;
}
The multiReturnMethod function will call a class method and use the __cooerce function of the return type class to coerce the return type if the resulting return type does not match.
An Example of the multiReturnMethod function
Note: We will use the MyClass class we created before as a return type.
Define a class that we can use to call the class method on.
class MRFClass {
function callMe($name)
{
return [$name, 1, 2, 3];
}
}
Call the multiReturnMethod function using your class and an anonymous function.
$resultingMyClass = multiReturnMethod (
// The object that we will be calling the
// class method on.
new MRFClass(),
// The class method to call, by name.
'callMe',
// The return type to coerce to.
MyClass::class,
// The function parameters.
'Nathan'
);
Both of these methods can be used to force strict return type functions, that accept multiple return types that can be coerced to the return type on a per-class implementation.
This is the error message generated:
Strict Standards: call_user_func_array() expects parameter 1 to be a
valid callback, non-static method
ModCareercoachoccupationsHelper::getRelated() should not be called
statically in
/customers/f/0/0/studiomitchell.agency/httpd.www/libraries/joomla/cache/controller/callback.php
on line 152
<?php
/**
* #package Joomla.Platform
* #subpackage Cache
*
* #copyright Copyright (C) 2005 - 2016 Open Source Matters, Inc. All rights reserved.
* #license GNU General Public License version 2 or later; see LICENSE
*/
defined('JPATH_PLATFORM') or die;
/**
* Joomla! Cache callback type object
*
* #since 11.1
*/
class JCacheControllerCallback extends JCacheController
{
/**
* Executes a cacheable callback if not found in cache else returns cached output and result
*
* Since arguments to this function are read with func_get_args you can pass any number of arguments to this method
* as long as the first argument passed is the callback definition.
*
* The callback definition can be in several forms:
* - Standard PHP Callback array see <https://secure.php.net/callback> [recommended]
* - Function name as a string eg. 'foo' for function foo()
* - Static method name as a string eg. 'MyClass::myMethod' for method myMethod() of class MyClass
*
* #return mixed Result of the callback
*
* #since 11.1
*/
public function call()
{
// Get callback and arguments
$args = func_get_args();
$callback = array_shift($args);
return $this->get($callback, $args);
}
/**
* Executes a cacheable callback if not found in cache else returns cached output and result
*
* #param mixed $callback Callback or string shorthand for a callback
* #param array $args Callback arguments
* #param mixed $id Cache ID
* #param boolean $wrkarounds True to use wrkarounds
* #param array $woptions Workaround options
*
* #return mixed Result of the callback
*
* #since 11.1
*/
public function get($callback, $args = array(), $id = false, $wrkarounds = false, $woptions = array())
{
// Normalize callback
if (is_array($callback))
{
// We have a standard php callback array -- do nothing
}
elseif (strstr($callback, '::'))
{
// This is shorthand for a static method callback classname::methodname
list ($class, $method) = explode('::', $callback);
$callback = array(trim($class), trim($method));
}
elseif (strstr($callback, '->'))
{
/*
* This is a really not so smart way of doing this... we provide this for backward compatability but this
* WILL! disappear in a future version. If you are using this syntax change your code to use the standard
* PHP callback array syntax: <https://secure.php.net/callback>
*
* We have to use some silly global notation to pull it off and this is very unreliable
*/
list ($object_123456789, $method) = explode('->', $callback);
global $$object_123456789;
$callback = array($$object_123456789, $method);
}
if (!$id)
{
// Generate an ID
$id = $this->_makeId($callback, $args);
}
$data = $this->cache->get($id);
$locktest = new stdClass;
$locktest->locked = null;
$locktest->locklooped = null;
if ($data === false)
{
$locktest = $this->cache->lock($id);
if ($locktest->locked == true && $locktest->locklooped == true)
{
$data = $this->cache->get($id);
}
}
$coptions = array();
if ($data !== false)
{
$cached = unserialize(trim($data));
$coptions['mergehead'] = isset($woptions['mergehead']) ? $woptions['mergehead'] : 0;
$output = ($wrkarounds == false) ? $cached['output'] : JCache::getWorkarounds($cached['output'], $coptions);
$result = $cached['result'];
if ($locktest->locked == true)
{
$this->cache->unlock($id);
}
}
else
{
if (!is_array($args))
{
$referenceArgs = !empty($args) ? array(&$args) : array();
}
else
{
$referenceArgs = &$args;
}
if ($locktest->locked == false)
{
$locktest = $this->cache->lock($id);
}
if (isset($woptions['modulemode']) && $woptions['modulemode'] == 1)
{
$document = JFactory::getDocument();
$coptions['modulemode'] = 1;
if (method_exists($document, 'getHeadData'))
{
$coptions['headerbefore'] = $document->getHeadData();
}
}
else
{
$coptions['modulemode'] = 0;
}
ob_start();
ob_implicit_flush(false);
$result = call_user_func_array($callback, $referenceArgs);
$output = ob_get_clean();
$coptions['nopathway'] = isset($woptions['nopathway']) ? $woptions['nopathway'] : 1;
$coptions['nohead'] = isset($woptions['nohead']) ? $woptions['nohead'] : 1;
$coptions['nomodules'] = isset($woptions['nomodules']) ? $woptions['nomodules'] : 1;
$cached = array(
'output' => ($wrkarounds == false) ? $output : JCache::setWorkarounds($output, $coptions),
'result' => $result,
);
// Store the cache data
$this->cache->store(serialize($cached), $id);
if ($locktest->locked == true)
{
$this->cache->unlock($id);
}
}
echo $output;
return $result;
}
/**
* Generate a callback cache ID
*
* #param callback $callback Callback to cache
* #param array $args Arguments to the callback method to cache
*
* #return string MD5 Hash
*
* #since 11.1
*/
protected function _makeId($callback, $args)
{
if (is_array($callback) && is_object($callback[0]))
{
$vars = get_object_vars($callback[0]);
$vars[] = strtolower(get_class($callback[0]));
$callback[0] = $vars;
}
return md5(serialize(array($callback, $args)));
}
}
The site retrieves the data but with this text error also above
the listing.
The callback.php line 152 is as follows:
$result = call_user_func_array($callback, $referenceArgs);
Any help would be great. Thanks
You'll get this error if you define the method non-statically:
class Foo {
public function bar() {
}
}
And then define $callback as either a string:
$callback = 'Foo::bar';
Or an array of strings:
$callback = ['Foo', 'bar'];
And then use it as an argument to call_user_func_array():
call_user_func_array($callback, []);
To avoid this, you can either define the method statically:
public static function bar() { ... }
Or use an instantiated object in your callback:
$callback = [new Foo(), 'bar'];
I'm trying to write a memoization function, and I just realized this solution does not work when the callback is not a simple string. For example, PHP can accept a callback in the form array($this,'memberFunc'), which is not amenable to serialization.
Then I realized that we don't really want to hash/serialize the whole callback function/object anyway, we just need a unique ID for it so we can check for reference equality. I thought spl_object_hash would do the trick, but it doesn't work on arrays.
Is there another way to generate a unique reference ID for a callable?
You can always cast to object for hashing purposes:
<?php
class Foo{
public function __construct(){
$foo = array($this,'memberFunc');
var_dump( spl_object_hash((object)$foo) );
var_dump( spl_object_hash((object)$foo) );
}
}
new Foo;
string(32) "00000000532ba9fd000000000cc9b0a5"
string(32) "00000000532ba9fd000000000cc9b0a5"
I wrote this function to get a hash for callables specifically:
/**
* #param callable $func
* #return string Unique string identifier for callable instance
* #throws Exception
*/
private static function callable_hash($func) {
if(!is_callable($func)) throw new Exception("Function must be a callable");
if(is_string($func)) {
return $func;
} elseif(is_array($func)) {
if(count($func) !== 2) throw new Exception("Array-callables must have exactly 2 elements");
if(!is_string($func[1])) throw new Exception("Second element of array-callable must be a string function name");
if(is_object($func[0])) return spl_object_hash($func[0]).'::'.$func[1];
elseif(is_string($func[0])) return implode('::',$func);
throw new Exception("First element of array-callable must be a class name or object instance");
} elseif(is_object($func)) {
return spl_object_hash($func);
}
throw new Exception("Unhandled callable type");
}
But if Alvaro's solution works... that's much simpler and more flexible.
Based on Alvaro's answer, I came up with these two functions:
function memoized() {
static $cache = array();
$args = func_get_args();
$func = array_shift($args);
$key = sha1(serialize(array_merge(array(spl_object_hash((object)$func)),$args)));
if(!isset($cache[$key])) {
$cache[$key] = call_user_func_array($func, $args);
}
return $cache[$key];
}
function memoized_func($func) {
return function() use ($func) {
static $cache = array();
$args = func_get_args();
$key = sha1(serialize($args));
if(!isset($cache[$key])) {
$cache[$key] = call_user_func_array($func, $args);
}
return $cache[$key];
};
}
Usage:
$f = function($n) {
for($i=0; $i<$n; ++$i);
return $n;
};
$result = memoized($f,50000);
$func = memoized_func($f);
$result2 = $func(50000);