Reputation: 8335
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.
Upvotes: 11
Views: 10115
Reputation: 402
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.
This solution creates two major problems:
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.
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.
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.
Upvotes: 0
Reputation: 15053
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();
Upvotes: 9