phpaopcross-cutting-concerns

Can we inject some more lines in a function by extending it with PHP?


I have an idea for the event system I'm developing for my custom framework.

Imagine a pseudo function like this.

class Test
{
    public function hi()
    {
        Event::add(__FUNCTION__ . 'is about to run.');
        return "hi";
    }
}

Imagine you need to do the same for some more functions. (Maybe you want to log which functions ran at the runtime and want to log them in a separate file.)

Instead of doing this and adding Events into functions manually, can we do something like this?

class Test
{
    public function hi()
    {
        return "hi";
    }
}

// events.php (It's a pseudo code so may not work.)
// Imagine extend's purpose is to inject codes into target function

Event::bind('on', $className, $methodName, function() use ($className, $methodName) 
{
    return $className->$methodName->extend('before', Event::add(__FUNCTION__ . 'is about to run.'));
});

The idea is to inject hi() function which is inside Test class and injecting whatever we pass in extend function by outside. 'before' means injection has to be at the first line of target function.

Finally, the events and event bindings are kept completely abstracted away from the functions. I want to be able to bind custom things without altering the functions.

I have a feeling that we can do this by hacking around with eval() or by toying with call_user_func(). I'm not sure, though. Using eval() sounds pretty bad already.

My question is;

  1. Is it a possible thing to do with PHP?
  2. Does it has a name in OOP/OOP Principles so I can read further?
  3. Does it make any sense or is it a bad idea?

Solution

  • Yes, you can. You can use AOP using GO! AOP framework which works on annotations.

    For example you want to log every public method calling. Instead of adding to every function line like this.

    namespace Acme;
    
    class Controller
    {
        public function updateData($arg1, $arg2)
        {
            $this->logger->info("Executing method " . __METHOD__, func_get_args()); 
            // ...
        }    
    }
    

    You can use one Aspect for all public methods of all classes of Acme namespace like this:

    use Go\Aop\Aspect;
    use Go\Aop\Intercept\MethodInvocation;
    use Go\Lang\Annotation\Before;
    
        class LoggingAspect implements Aspect
        {
            /** @var null|LoggerInterface */
            protected $logger = null;
    
            /** ... */
            public function __construct($logger) 
            {
                $this->logger = $logger;
            }
    
            /**
             * Method that should be called before real method
             *
             * @param MethodInvocation $invocation Invocation
             * @Before("execution(public Acme\*->*())")
             */
            public function beforeMethodExecution(MethodInvocation $invocation)
            {
                $obj    = $invocation->getThis();
                $class  = is_object($obj) ? get_class($obj) : $obj;
                $type   = $invocation->getMethod()->isStatic() ? '::' : '->';
                $name   = $invocation->getMethod()->getName();
                $method = $class . $type . $name;
    
                $this->logger->info("Executing method " . $method, $invocation->getArguments());
            }
        }    
    

    It looks more complicated but it's more flexible.