PHP equivalent for a python decorator?
Asked Answered
H

6

16

I want to be able to wrap a PHP function by another function, but leaving its original name/parameter list intact.

For instance:

function A() {
    print "inside A()\n";
}

function Wrap_A() {
    print "Calling A()\n";
    A();
    print "Finished calling A()\n";
}

// <--- Do some magic here (effectively "A = Wrap_A")

A();

Output:

Calling A()
inside A()
Finished calling A()
Hindustani answered 15/9, 2009 at 5:36 Comment(1)
Your example of Wrap_A is a little misleading to somebody who doesn't already know how Python decorator's work, since your implementation of Wrap_A explicitly references A. The idea of a decorator is that you can decorate any function with it, but in your example you clearly couldn't use Wrap_A to wrap some other function B. Would you mind if I edited your question to make it a more accurate representation of what function decoration is?Envoy
M
7

Apparently runkit might help you.

Also, you can always do this the OO way. Put the original fun in a class, and the decorator into an extended class. Instantiate and go.

Machute answered 15/9, 2009 at 6:12 Comment(3)
How would you use it as easily and generically as in python - here you add the decorate just above the function definition (similar constructs seen in .NET and Java).Telex
The links in this answer are now broken.Neisa
I believe they should now be php.net/manual/en/book.runkit7.php and php.net/manual/en/function.runkit7-function-redefine.php respectivelyProfundity
C
9

Here is my method of mimicking decorators from python in php.

function call_decorator ($decorator, $function, $args, $kwargs) {

    // Call the decorator and pass the function to it
    $decorator($function, $args, $kwargs);
}

function testing ($args, $kwargs) {
    echo PHP_EOL . 'test 1234' . PHP_EOL;
}

function wrap_testing ($func, $args, $kwargs) {

    // Before call on passed function
    echo 'Before testing';

    // Call the passed function
    $func($args, $kwargs);

    // After call on passed function
    echo 'After testing';
}

// Run test
call_decorator('wrap_testing', 'testing');

Output:

Before testing
testing 1234
After testing

With this implementation you can also do something like this with an anonymous function:

// Run new test
call_decorator('wrap_testing', function($args, $kwargs) {
    echo PHP_EOL . 'Hello!' . PHP_EOL;
});

Output:

Before testing
Hello!
After testing

And lastly you can even do something like this, if you are so inclined.

// Run test
call_decorator(function ($func, $args, $kwargs) {
    echo 'Hello ';
    $func($args, $kwargs);
}, function($args, $kwargs) {
    echo 'World!';
});

Output:

Hello World!

With this construction above, you can pass variables to the inner function or wrapper, if need be. Here is that implementation with an anonymous inner function:

$test_val = 'I am accessible!';

call_decorator('wrap_testing', function($args, $kwargs){
    echo $args[0];
}, array($test_val));

It will work exactly the same without an anonymous function:

function test ($args, $kwargs) {
    echo $kwargs['test'];
}

$test_var = 'Hello again!';

call_decorator('wrap_testing', 'test', array(), array('test' => $test_var));

Lastly, if you need to modify the variable inside either the wrapper or the wrappie, you just need to pass the variable by reference.

Without reference:

$test_var = 'testing this';
call_decorator(function($func, $args, $kwargs) {
    $func($args, $kwargs);
}, function($args, $kwargs) {
    $args[0] = 'I changed!';
}, array($test_var));

Output:

testing this

With reference:

$test_var = 'testing this';
call_decorator(function($func, $args, $kwargs) {
    $func($args, $kwargs);
}, function($args, $kwargs) {
    $args[0] = 'I changed!';

// Reference the variable here
}, array(&$test_var));

Output:

I changed!

That is all I have for now, it is a pretty useful in a lot of cases, and you can even wrap them multiple times if you want to.

Crosby answered 4/12, 2015 at 19:4 Comment(5)
$kwargs? that doesnt exist in php.Laundromat
It does if you write code to support it? kwargs in terms of syntax is no different than an array of key value pairs in php.Crosby
This approach fails if the function call is out of your control, for example in test scripts called by PHPunit.Spiller
@Spiller can you provide more context for this? If you are calling a function or method that is not within the current scope, then that wouldn't work regardless of how you want to call it. My answer was from a naive perspective and is not how I would solve this today.Crosby
PHPUnit calls all public function test* on itself. I'd like to add a Python-like @decorator preparing and cleaning up things for each test. I can't change PHPUnit for calling my call_decator function.Spiller
M
7

Apparently runkit might help you.

Also, you can always do this the OO way. Put the original fun in a class, and the decorator into an extended class. Instantiate and go.

Machute answered 15/9, 2009 at 6:12 Comment(3)
How would you use it as easily and generically as in python - here you add the decorate just above the function definition (similar constructs seen in .NET and Java).Telex
The links in this answer are now broken.Neisa
I believe they should now be php.net/manual/en/book.runkit7.php and php.net/manual/en/function.runkit7-function-redefine.php respectivelyProfundity
V
3

maybe you’re looking for call_user_func_array:

function wrapA() {
  $args = func_get_args();
  return call_user_func_array('A', $args);
}

since PHP 5.3 you could even say:

return call_user_func_array('A', func_get_args());

after you’ve edited your question i would say, no, this is not possible, but there are some ways, see this question: how to implement a decorator in PHP?

Visa answered 15/9, 2009 at 5:54 Comment(1)
This is not at all related to what I'm looking for. I edited my question to add clarification.Hindustani
W
3

You can use Aspect Oriented Programming. But you need some framework that support AOP.

For example Symfony. This is one of implementations of AOP for php https://github.com/goaop/goaop-symfony-bundle

There is also the Nette Framework (I use this one)

In principle it works like this: let's say you want to throw an exception if user is not logged in and tries to access some method.

This is just "fictive" code to show how it works.

You have some aspect method like this:

/** @AroundMethod(annotataion="isUserLogged()") */
public function checkUser(Method $method) {
    if ($this->user->isLogged()) {
        return $method->call();
    } else {
        throw new \Exception('User must be logged');
    }
}

And then you can use the annotation @isUserLogged in your services which will be probably registered in some DI container.

/**
 * @isUserLogged()
 */
public function changeUserInfo() {

}
Wolffish answered 27/12, 2019 at 19:15 Comment(0)
E
1

You can't do this with functions in PHP. In other dynamic languages, such as Perl and Ruby, you can redefine previously defined functions, but PHP throws a fatal error when you attempt to do so.

In 5.3, you can create an anonymous function and store it in a variable:

<?php
    $my_function = function($args, ...) { ... };
    $copy_of_my_function = $my_function;
    $my_function = function($arg, ...) { /* Do something with the copy */ };
?>

Alternatively, you can use the traditional decorator pattern and/or a factory and work with classes instead.

Estebanesteem answered 15/9, 2009 at 6:11 Comment(0)
W
1

Here is my implementation of python decorators by using php attributes

use Sagittaracc\PhpPythonDecorator\Decorator;

class Calc
{
    use Decorator;

    #[Timer]
    function sum($a, $b)
    {
        sleep(1);
        return $a + $b;
    }
}
#[Attribute]
class Timer
{
    public function main($func, ...$args)
    {
        $time_start = microtime(true);

        $result = $func($args);

        $time_end = microtime(true);

        return sprintf(
            "Total execution: %f; Result: %d",
            $time_end - $time_start,
            $result
        );
    }
}

You can check it out here - https://github.com/sagittaracc/php-python-decorator

Wheelwork answered 16/1, 2023 at 8:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.