Passing __call() parameters by reference fails. Any work around? - php

I have written a fairly simple lazy loading proxy class, which I have documented in the past over at http://blog.simonholywell.com/post/2072272471/logging-global-php-objects-lazy-loading-proxy
Now as I convert another project to work with it I have been tripped up by proxying a method, which has one of its parameters passed to it by reference. When this goes through the __call method of my proxy class I get:
Fatal error: Method LazyLoader::__call() cannot take arguments by reference in /home/simon/file/name.php
Any clever ideas of how this might be solved or worked around. Preferably without refactoring the code that requires the pass by reference if possible.
The lazy loading proxy class looks like this, but the description in my blog post explains the purpose better:
<?php
/**
* #author Simon Holywell <treffynnon#php.net>
*/
class LazyLoadingProxy {
/**
* Where the instance of the actual class is stored.
* #var $instance object
*/
private $instance = null;
/**
* The name of the class to load
* #var $class_name string
*/
private $class_name = null;
/**
* The path to the class to load
* #var $class_path string
*/
private $class_path = null;
/**
* Set the name of the class this LazyLoader should proxy
* at the time of instantiation
* #param $class_name string
*/
public function __construct($class_name, $class_path = null) {
$this->setClassName($class_name);
$this->setClassPath($class_path);
}
public function setClassName($class_name) {
if(null !== $class_name) {
$this->class_name = $class_name;
}
}
public function getClassName() {
return $this->class_name;
}
public function setClassPath($class_path) {
if(null !== $class_path) {
$this->class_path = $class_path;
}
}
public function getClassPath() {
return $this->class_path;
}
/**
* Get the instance of the class this LazyLoader is proxying.
* If the instance does not already exist then it is initialised.
* #return object An instance of the class this LazyLoader is proxying
*/
public function getInstance() {
if(null === $this->instance) {
$this->instance = $this->initInstance();
}
return $this->instance;
}
/**
* Load an instance of the class that is being proxied.
* #return object An instance of the class this LazyLoader is proxying
*/
private function initInstance() {
Logger::log('Loaded: ' . $class_name);
require_once($this->class_path);
$class_name = $this->class_name;
return new $class_name();
}
/**
* Magic Method to call functions on the class that is being proxied.
* #return mixed Whatever the requested method would normally return
*/
public function __call($name, &$arguments) {
$instance = $this->getInstance();
Logger::log('Called: ' . $this->class_name . '->' . $name . '(' . print_r($arguments, true) . ');');
return call_user_func_array(
array($instance, $name),
$arguments
);
}
/**
* These are the standard PHP Magic Methods to access
* the class properties of the class that is being proxied.
*/
public function __get($name) {
Logger::log('Getting property: ' . $this->class_name . '->' . $name);
return $this->getInstance()->$name;
}
public function __set($name, $value) {
Logger::log('Setting property: ' . $this->class_name . '->' . $name);
$this->getInstance()->$name = $value;
}
public function __isset($name) {
Logger::log('Checking isset for property: ' . $this->class_name . '->' . $name);
return isset($this->getInstance()->$name);
}
public function __unset($name) {
Logger::log('Unsetting property: ' . $this->class_name . '->' . $name);
unset($this->getInstance()->$name);
}
}
Any help greatly appreciated.

The short answer is to not pass by reference. In 99.9% of cases, you don't need it. And in those other 0.1% you can work around the lack of references anyway. Remember, objects are passed by object-reference anyway so you don't need to use variable references for them.
Now, as far as a workaround, I'd personally hardcode that into an adapter. So extend your proxy for that particular class, and include a wrapper for that particular method. Then instantiate that new extended class instead of the core proxy for that class. Is it dirty? Absolutely. But it's your only workaround without refactoring the original class to not take the argument by reference, or refactoring the caller to prevent passing by reference (which is deprecated anyway).

Here is a trick for you, to pass variables by references via __call magic method:
I don't now how long will this works or in witch php versions.
I tested on php 5.3.2
Fitst you cannot pass $arguments variable by $reference in __call function definition. Because php falls with error.
So first:
Here is a code what does not do, what you want with referenced arguments, but nearly good ;)
class A { public $x = array(); }
class Teszt{
private function addElement( &$list ){
$list->x[] = 'new element';
return count($list->x);
}
public function __call($name,$arguments){
if (!preg_match('#^add([0-9]+)Element$#', $name, $matches) || $matches[1]<1){
trigger_error ("Function is not exists teszt::$name", E_USER_ERROR);
}
$ret = null;
for($i=0;$i<$matches[1];$i++){
$ret = call_user_func_array(array($this,'addElement'), $arguments);
}
return $ret;
}
}
$a = new A();
$a->x = array('old element','old element');
$t = new Teszt();
$cnt = $t->add5Element($a);
var_dump($a);
var_dump($cnt);
OUTPUT:
PHP Warning: Parameter 1 to Teszt::addElement() expected to be a reference, value given in /home/gkovacs/callMagicWithReference.php on line 19
PHP Stack trace:
PHP 1. {main}() /home/gkovacs/callMagicWithReference.php:0
PHP 2. Teszt->add2Element() /home/gkovacs/callMagicWithReference.php:27
PHP 3. Teszt->__call() /home/gkovacs/callMagicWithReference.php:0
PHP 4. call_user_func_array() /home/gkovacs/callMagicWithReference.php:19
PHP Warning: Parameter 1 to Teszt::addElement() expected to be a reference, value given in /home/gkovacs/callMagicWithReference.php on line 19
PHP Stack trace:
PHP 1. {main}() /home/gkovacs/callMagicWithReference.php:0
PHP 2. Teszt->add2Element() /home/gkovacs/callMagicWithReference.php:27
PHP 3. Teszt->__call() /home/gkovacs/callMagicWithReference.php:0
PHP 4. call_user_func_array() /home/gkovacs/callMagicWithReference.php:19
object(A)#1 (1) {
["x"]=>
array(2) {
[0]=>
string(11) "old element"
[1]=>
string(11) "old element"
}
}
NULL
ohhh :(((
After a litle 'HACK':
class A { public $x = array(); }
class Teszt{
private function addElement( &$list ){
$list->x[] = 'new element';
return count($list->x);
}
public function __call($name,$arguments){
if (!preg_match('#^add([0-9]+)Element$#', $name, $matches) || $matches[1]<1){
trigger_error ("Function is not exists teszt::$name", E_USER_ERROR);
}
$ret = null;
//Look at my hand, because i will cheat
foreach($arguments as $k=>&$v){ }
//end of cheat
for($i=0;$i<$matches[1];$i++){
$ret = call_user_func_array(array($this,'addElement'), $arguments);
}
return $ret;
}
}
$a = new A();
$a->x = array('old element','old element');
$t = new Teszt();
$cnt = $t->add5Element($a);
var_dump($a);
var_dump($cnt);
Output:
object(A)#1 (1) {
["x"]=>
array(4) {
[0]=>
string(11) "old element"
[1]=>
string(11) "old element"
[2]=>
string(11) "new element"
[3]=>
string(11) "new element"
}
}
int(4)
As we wanted. This works only with objects, but not with array-s.
There is a way with array-s to call-time-pass-by-reference ...
like this: (and of corse you can use this way with objects too)
$cnt = $t->add5Element(&$a);
In this case php generate notices ...
The easiest way to redefine the function if it is possible.
in my code , : private functionaddElement($list) , don't define as reference in parameter list. And as you read in previous message , it will works beacause objects are passed by reference automatically
But sometimes , you can not redefine functions because of implemented interfaces or other reasons ...

Its can. So is hungry.
<?php
class MyClass(){
public function __call($name, $params)
{
var_dump($params);
$params[0][0] = '222';
}
}
$obj = new MyClass();
$info = 3;
var_dump($info);
$obj->setter([&$info]);
var_dump($info);

Related

What, why and how are Laravel service providers PHP

I started to wonder about what exactly is the purpose of service providers in Laravel, and why they work in the way they do. After searching through some articles,
the key points of service providers in my understanding are:
Simplifies object creation (Laravel What is the use of service providers for laravel)
Decoupling your code (r/laravel: When to use service providers?)
Dependency injection
Reduces technical debt
So it basically binds an implementation to an interface, and we can use it by
$app(MyInterface::class)
or something like that, and we can just change the implementation when needed, only in one place, and the rest of our code which depends on it won't break.
But i still can not grasp the concept, why they are the way they are, it seems overcomplicated. I peaked in to the code, it was certainly a ton of work to make Service Providers & Containers work, so there must be a good reason.
So to learn further, i tried to make my own, more simple version of it, which achieves the same goals. (i obviously lack a lot of info on this, and most probably missed some other goals)
My question is, why would this implementation would not satisfy the same use cases?
Service.php
namespace MyVendor;
/**
* Abstract class for creating services
*/
abstract class Service
{
/**
* Holds the instance of the provided service
*
* #var mixed
*/
private static mixed $instance = null;
/**
* Retrieves the instance of the provided service & creates it on-demand
*
* #return mixed
*/
public static function get(): mixed
{
if (self::$instance === null) {
self::$instance = static::instantiate();
}
return self::$instance;
}
/**
* A function which contains the service's object creation logic
*
* #return mixed
*/
abstract protected static function instantiate(): mixed;
}
Example implementation:
For the example, i chose an interface to parse environment variables, as i already had phpdotenv in my project as a dependency
Services/DotenvParser/DotenvParserInterface.php
namespace MyVendor\Services\DotenvParser;
/**
* This is the service interface i want to provide
*/
interface DotenvParserInterface
{
public function parse(string $directory, string $fileName = ".env"): array;
}
Now i will have 2 implementations of this class. I will pretend that a lot of my code already depends on DotenvParserInterface. An old, hacky one which "depends" on another thing, and the replacement for it which uses phpdotenv
A quick fake dependency:
Services/DotenvParser/Dependency.php
namespace MyVendor\Services\DotenvParser;
class Dependency
{
private bool $squeeze;
public string $bar;
public function __construct(string $foo, bool $squeeze)
{
$this->squeeze = $squeeze;
$this->bar = $foo;
if($this->squeeze){
$this->bar .= " JUICE";
}
}
}
Our old code:
Services/DotenvParser/OldDotenvCode.php
namespace MyVendor\Services\DotenvParser;
use BadMethodCallException;
use InvalidArgumentException;
class OldDotenvCode implements DotenvParserInterface
{
/**
* Our fake dependency
*
* #var Dependency
*/
private Dependency $foo;
private string $dir;
private string $fileName;
private string $contents;
private array $result;
public function __construct(Dependency $myDependency)
{
$this->foo = $myDependency;
}
/**
* Implementation of DotenvParserInterface
*
* #param string $directory
* #param string $fileName
* #return array
*/
public function parse(string $directory, string $fileName = ".env"): array
{
try{
$this->setDir($directory)->setFileName($fileName);
}catch(BadMethodCallException $e){
throw new InvalidArgumentException($e->getMessage(), 0, $e);
}
$this->getEnvContents();
$this->contents = $this->getEnvContents();
$this->result = [];
foreach(explode("\n", $this->contents) as $line){
$exploded = explode("=", $line);
$key = $exploded[0];
$value = (isset($exploded[1])) ? trim($exploded[1], "\r") : "";
if($this->foo->bar === "ORANGE JUICE"){
$value = trim($value, "\"");
}
$this->result[$key] = $value;
}
return $this->result;
}
#region Old, bad stuff
public function setDir(string $directory): self{
if(!\is_dir($directory)){
throw new InvalidArgumentException("Directory $directory is not a valid directory");
}
$this->dir = rtrim($directory, "/");
return $this;
}
public function setFileName(string $fileName): self{
if(empty($this->dir)){
throw new BadMethodCallException("Must call method setDir() first with a valid directory path");
}
$fileName = ltrim($fileName, "/");
if(!\file_exists($this->dir . "/" . $fileName)){
throw new InvalidArgumentException("File $fileName does not exist in provided directory {$this->dir}");
}
$this->fileName = $fileName;
return $this;
}
private function getFilePath(): string{
if(empty($this->fileName)){
throw new BadMethodCallException("Must call method setFileName() first");
}
return $this->dir . "/" . $this->fileName;
}
private function getEnvContents(): string{
return \file_get_contents($this->getFilePath());
}
public function setup(): void
{
$this->setDir($directory)->setFileName($fileName);
}
#endregion
}
Now, the phpdotenv version
Services/DotenvParser/phpdotenv.php
namespace MyVendor\Services\DotenvParser;
use Dotenv\Dotenv;
use InvalidArgumentException;
use Dotenv\Dotenv;
use InvalidArgumentException;
class phpdotenv implements DotenvParserInterface
{
public function parse(string $directory, string $fileName = ".env"): array
{
try{
Dotenv::createMutable($directory, $fileName)->load();
}catch(\Dotenv\Exception\InvalidPathException $e){
throw new InvalidArgumentException($e->getMessage(), 0, $e);
}
$result = $_ENV;
$_ENV = []; //Hehe
return $result;
}
}
Our service which we made from extending our Service class
Services/DotenvParser/DotenvParserService.php
namespace MyVendor\Services\DotenvParser;
use MyVendor\Service;
class DotenvParserService extends Service
{
// We can do this to make type hinting for ourselves
public static function get(): DotenvParserInterface
{
return parent::get();
}
protected static function instantiate(): DotenvParserInterface
{
$year = 2022;
// Some condition, to return one or another
if($year < 2022){
$dep = new \MyVendor\Services\DotenvParser\Dependency("ORANGE", true);
return new OldDotenvCode($dep);
}
return new phpdotenv();
}
}
And now, we can use it like this:
$dotenvparser = \MyVendor\Services\DotenvParser\DotenvParserService::get();
$result = $dotenvparser->parse(__DIR__);
var_dump($result);
// Outputs an array of our environment variables, yey!
We can also write tests for our services to see if anything breaks:
namespace MyVendorTest\Services\DotenvParser;
use InvalidArgumentException;
use MyVendor\Services\DotenvParser\DotenvParserInterface;
use MyVendor\Services\DotenvParser\DotenvParserService;
final class DotenvParserServiceTest extends \PHPUnit\Framework\TestCase
{
public function doesInstantiate(): void
{
$testParser = DotenvParserService::get();
$this->assertInstanceOf(DotenvParserInterface::class, $testParser);
}
public function testWorksFromValidDirNFile(): void
{
// The actual contents of a .env file
$testArray = [
"DEV_MODE" => "TRUE",
"BASE_HREF" => "http://localhost:8080/"
];
$testParser = DotenvParserService::get();
// phpdotenv loads every parent .env too and i was having none of it for this quick demonstration
$result = $testParser->parse(__DIR__."/../../../", ".env");
$this->assertEquals($testArray, $result);
}
public function testSetupFromInvalidDir(): void
{
$this->expectException(InvalidArgumentException::class);
$testParser = DotenvParserService::get();
$testParser->parse("i_am_a_dir_which_does_not_exist");
}
public function testSetupFromInvalidFile(): void
{
$this->expectException(InvalidArgumentException::class);
$testParser = DotenvParserService::get();
$testParser->parse(__DIR__, ".notenv");
}
}
So this ended up quite lenghty, but after having that Service class, you basically only need: An interface, at least one implementation of that interface, and a service class which instantiates an implementation of that interface, and optionally some tests for it. And, you can even do dependency injection with it (??) (circular dependencies would get us stuck in an endless loop), like this:
protected static function instantiate(): FooInterface
{
//BarService & AcmeService are extending our Service class
return new FooInterface(BarService::get(), AcmeService::get(), "ORANGE JUICE")
}
I am ready to absorb massive amounts of information
What other things Laravel's Service providers & containers do than i am aware of?
Why and how is it better than a simpler version, like this one?
Does my version really achieve at least those 4 key points i mentioned in the start?

Cannot override final method Exception::getPrevious()

I got CMS and trying to install it, but when i try to login i got error
Cannot override final method Exception::getPrevious()
Fatal error: Cannot override final method Exception::getPrevious() in C:\wamp\www\uis-online\application\exceptions\applicationException.php on line 41
Does anyboy have idea what cause this error
code in this file is
class ApplicationException extends Exception
{
protected $innerException;
/**
* Konstruktor
* #return unknown_type
*/
public function __construct($message, $code = 0, Exception $innerException = null)
{
parent::__construct($message, $code);
if (!is_null($innerException))
{
$this->innerException = $innerException;
}
}
public function getPrevious()
{
return $this->innerException;
}
// custom string representation of object
public function __toString() {
$exceptions_message = "";
if($this->innerException != null && $this->innerException->getMessage() != "") {
$exceptions_message = $this->innerException->__toString();
}
return $exceptions_message .__CLASS__ . ": [{$this->code}]: {$this->message}\n";
}
}
As shown in the documentation the method you're trying to override is a final one.
final public Exception Exception::getPrevious ( void )
http://php.net/manual/en/exception.getprevious.php
According to the PHP manual you can't override final methods.
PHP 5 introduces the final keyword, which prevents child classes from overriding a method by prefixing the definition with final. If the class itself is being defined final then it cannot be extended.
http://php.net/manual/en/language.oop5.final.php

PHP - __call all the time

Say we have a class with several protected and/or public methods.
I need to perform a check each time a method is called. I could do that check each time i call a method :
class Object
{
// Methods
}
$o = new Object();
if($mayAccess) $o->someMethod();
or
if($mayAccess) $this->someMethod();
But i would like developers neither to have to think about it nor to write it. I've thought about using __call to do :
class Object
{
public function __call($methodName, $args)
{
if($mayAccess) call_user_func_array($this->$methodName, $args);
}
}
Unfortunatly, if i call the method from inside the class, __call will not invoked as it only works when a non-visible method is called.
Is there a clean way to hide this check for both internal and external calls ? Again the goal is to make sure a developper won't forget to do it when calling a method.
Thanks in advance :)
EDIT :
I have another way of doing this :
class Object
{
public function __call($methodName, $args)
{
if($mayAccess) call_user_func_array($methodName, $args);
}
}
function someMethod() { }
But i won't be able to use $this anymore, which means no protected methods, which i do need.
No, I dont think so. What you could do though is write a proxy:
class MayAccessProxy {
private $_obj;
public function __construct($obj) {
$this->_obj = $obj;
}
public function __call($methodName, $args) {
if($mayAccess) call_user_func_array(array($this->_obj, $methodName), $args);
}
}
This means you have to instantiate a proxy for every object you want to check:
$obj = new MayAccessProxy(new Object());
$obj->someMethod();
Ofcourse you'd also want the proxy to behave exactly like the object itself. So you also have to define the other magic methods.
To make it a bit easier for the developers you could do something like this:
class Object {
/**
* Not directly instanciable.
*/
private __construct() {}
/**
* #return self
*/
public static function createInstance() {
$obj = new MayAccessProxy(new self());
return $obj;
}
}
$obj = Object::createInstance();
So what if you made all your methods protected or private? (I know this is old and "answered" question)
The __call magic method intercepts all non-existing and non-public methods so having all your methods not public will allow you to intercepts all of them.
public function __call( $func, $args )
{
if ( !method_exists( $this, $func ) ) throw new Error("This method does not exist in this class.");
Handle::eachMethodAction(); // action which will be fired each time a method will be called
return $this->$func( ...$args );
}
Thanks to that you will not need to do anything to your code (expect adding __call and doing quick replace all) and if your classes have common parent then you can just add it to parent and not care anymore.
BUT
This solution creates two major problems:
The protected/private methods automatically will be available to public
The errors will be pointing to __call not the proper file
What can we do?
Custom private/protected
You can add a list of all protected/private methods and check before the call if the method can be return to public:
public function __call( $func, $args )
{
$private = [
"PrivateMethod" => null
];
if ( !method_exists( $this, $func ) ) throw new Error("This method does not exist in this class.");
if ( isset( $private[$func] ) ) throw new Error("This method is private and cannot be called");
Handle::eachMethodAction(); // action which will be fired each time a method will be called
return $this->$func( ...$args );
}
For many this might be deal breaker, but I personally use this approach only in classes with only public methods (which I set to protected). So if you can, you might separate methods into publicClass and privateClass and eliminate this problem.
Custom Errors and Stack
For better errors I have created this method:
/**
* Get parent function/method details
*
* #param int counter [OPT] The counter allows to move further back or forth in search of methods detalis
*
* #return array trace It contains those elements :
* - function - name of the function
* - file - in which file exception happend
* - line - on which line
* - class - in which class
* - type - how it was called
* - args - arguments passed to function/method
*/
protected function getParentMethod( int $counter = 0 ) {
$excep = new \Exception();
$trace = $excep->getTrace();
$offset = 1;
if ( sizeof( $trace ) < 2 ) $offset = sizeof( $trace ) - 1;
return $trace[$offset - $counter];
}
Which will return details about the previous method/function which called protected method.
public function __call( $func, $args )
{
$private = [
"PrivateMethod" => null
];
if ( !method_exists( $this, $func ) ) {
$details = (object) $this->getParentMethod();
throw new Error("Method $func does not exist on line " . $details->line . ", file: " . $details->file . " invoked by " . get_class($this) . $details->type . $func . " () ");
}
if ( isset($private[$func]) ) {
$details = (object) $this->getParentMethod();
throw new Error("Method $func is private and cannot be called on line " . $details->line . ", file: " . $details->file . " invoked by " . get_class($this) . $details->type . $func . " () ");
}
return $this->$func( ...$args );
}
This is not much of a problem but might lead to some confusion while debugging.
Conclusion
This solution allows you to have control over any call of private/protected methods FROM OUTSIDE OF CLASS. Any this->Method will omit __call and will normally call private/protected method.
class Test {
public function __call( $func, $args )
{
echo "__call! ";
if ( !method_exists( $this, $func ) ) throw new Error("This method does not exist in this class.");
return $this->$func( ...$args );
}
protected function Public()
{
return "Public";
}
protected function CallPublic()
{
return "Call->" . $this->Public();
}
}
$_Test = new Test();
echo $_Test->CallPublic(); // result: __call! Call->Public - it uses two methods but __call is fired only once
If you want to add a similar thing to static methods use __callStatic magic method.

How can I make a method wait for a callback, that was called externally, to finish?

The following code is a simplified example of what I'm trying to understand.
I'm using an external library that uses callbacks to process multiple requests. Ultimately I've been trying to figure out how to make Test->inc() call ExternalLib for each array element, then wait for all callbacks to be executed before continuing.
As you can see, the fatal error on line 18 is due to the method being called via call_user_func. How can I do this, or is there perhaps a better method?
class Test {
public $a = array();
public function inc(array $ints){
$request = new ExternalLib();
foreach ($ints as $int) {
$request->increment($int);
}
while( count($this->a) < count($ints) ) {
usleep(500000);
}
$test->dump();
}
public function incCallback($in, $out) {
/* append data to Test class array */
$this->a[] = array($in => out); /* Fatal error: Using $this when not in object context */
}
public function dump() {
/* Print to screen */
var_dump($this->a);
}
}
class ExternalLib {
/* This library's code should not be altered */
public function increment($num) {
call_user_func(array('Test','incCallback'), $num, $num++);
}
}
$test = new Test();
$test->inc(array(5,6,9));
Desired output:
array(3) {
[5]=>
int(6)
[6]=>
int(7)
[9]=>
int(10)
}
This code also available at codepad
The problem isn't a timing/waiting issue. It's a static vs. instantiated issue.
Calling the function using call_user_func(array('Test','incCallback')... is the same as calling Test::incCallback. You can't use $this when making a static call.
You will either need to modify the external lib to use an instantiated object or modify the Test class to use all static data.
Edit
I don't know exactly what you're looking to accomplish, but if making the class operate as a static class is your only option, then that's what you have to do ...
There are a couple other issues with your code:
Based on your desired output, you don't want a[] = array($in, $out) but rather a[$in] = $out
$num++ will not increment until after the function is called ... you want ++$num
Here is a working example ...
class Test {
public static $a;
public function inc(array $ints){
$request = new ExternalLib();
foreach ($ints as $int) {
$request->incriment($int);
}
while( count(self::$a) < count($ints) ) {}
self::dump();
}
public function incCallback($in, $out) {
/* append data to Test class array */
self::$a[$in] = $out;
}
public function dump() {
/* Print to screen */
var_dump(self::$a);
}
}
class ExternalLib {
/* This library's code should not be altered */
public function incriment($num) {
call_user_func(array('Test','incCallback'), $num, ++$num);
}
}
Test::$a = array();
Test::inc(array(5,6,9));

PHP Reflection - Get Method Parameter Type As String

I'm trying to use PHP reflection to dynamically load the class files of models automatically based upon the type of parameter that is in the controller method. Here's an example controller method.
<?php
class ExampleController
{
public function PostMaterial(SteelSlugModel $model)
{
//etc...
}
}
Here's what I have so far.
//Target the first parameter, as an example
$param = new ReflectionParameter(array('ExampleController', 'PostMaterial'), 0);
//Echo the type of the parameter
echo $param->getClass()->name;
This works, and the output would be 'SteelSlugModel', as expected. However, there is the possibility that the class file of the model may not be loaded yet, and using getClass() requires that the class be defined - part of why I'm doing this is to autoload any models that a controller action may require.
Is there a way to get the name of the parameter type without having to load the class file first?
I supposed this is what you are looking for:
class MyClass {
function __construct(AnotherClass $requiredParameter, YetAnotherClass $optionalParameter = null) {
}
}
$reflector = new ReflectionClass("MyClass");
foreach ($reflector->getConstructor()->getParameters() as $param) {
// param name
$param->name;
// param type hint (or null, if not specified).
$param->getClass()->name;
// finds out if the param is required or optional
$param->isOptional();
}
I think the only way is to export and manipulate the result string:
$refParam = new ReflectionParameter(array('Foo', 'Bar'), 0);
$export = ReflectionParameter::export(
array(
$refParam->getDeclaringClass()->name,
$refParam->getDeclaringFunction()->name
),
$refParam->name,
true
);
$type = preg_replace('/.*?(\w+)\s+\$'.$refParam->name.'.*/', '\\1', $export);
echo $type;
getType method can be used since PHP 7.0.
class Foo {}
class Bar {}
class MyClass
{
public function baz(Foo $foo, Bar $bar) {}
}
$class = new ReflectionClass('MyClass');
$method = $class->getMethod('baz');
$params = $method->getParameters();
var_dump(
'Foo' === (string) $params[0]->getType()
);
You could use Zend Framework 2.
$method_reflection = new \Zend\Code\Reflection\MethodReflection( 'class', 'method' );
foreach( $method_reflection->getParameters() as $reflection_parameter )
{
$type = $reflection_parameter->getType();
}
I had similar problem, when checking getClass on reflection parameter when a class was not loaded. I made a wrapper function to get the class name from example netcoder made. Problem was that netcoder code didnt work if it was an array or not an class -> function($test) {} it would return the to string method for the reflection parameter.
Below the way how i solved it, im using try catch because my code requires at some point the class. So if i request it the next time, get class works and doesnt throw an exception.
/**
* Because it could be that reflection parameter ->getClass() will try to load an class that isnt included yet
* It could thrown an Exception, the way to find out what the class name is by parsing the reflection parameter
* God knows why they didn't add getClassName() on reflection parameter.
* #param ReflectionParameter $reflectionParameter
* #return string Class Name
*/
public function ResolveParameterClassName(ReflectionParameter $reflectionParameter)
{
$className = null;
try
{
// first try it on the normal way if the class is loaded then everything should go ok
$className = $reflectionParameter->getClass()->name;
}
// if the class isnt loaded it throws an exception and try to resolve it the ugly way
catch (Exception $exception)
{
if ($reflectionParameter->isArray())
{
return null;
}
$reflectionString = $reflectionParameter->__toString();
$searchPattern = '/^Parameter \#' . $reflectionParameter->getPosition() . ' \[ \<required\> ([A-Za-z]+) \$' . $reflectionParameter->getName() . ' \]$/';
$matchResult = preg_match($searchPattern, $reflectionString, $matches);
if (!$matchResult)
{
return null;
}
$className = array_pop($matches);
}
return $className;
}
This is a better regular expression than the one from that answer. It will work even when the parameter is optional.
preg_match('~>\s+([a-z]+)\s+~', (string)$ReflectionParameter, $result);
$type = $result[1];

Categories