views:

795

answers:

3

Is there a way I can set up callbacks on (or automataically log) method parameters, entries, and exits without making explicit calls within each method? I basically want to log this information to my logger class (which is static) without having to do it manually for each method.

Right now I have to call Logger::logEntry() and Logger::logExit() in every method to accomplish this. I would love to not have to do this:

class TestClass {
    public function tester($arg) {
        Logger::logEntry();
        Logger::info('Parameter $arg => ' . $arg);

        // Do some stuff...

        Logger::logExit();
    }
}
A: 

you could use the magic function __call. It gets called when no functions match that name. Rename your methods to be prefixed with something, (eg: underscore), and optionally set them to private/protected.

class TestClass {
    public function __call($function, $args) {
        Logger::logEntry();
        Logger::info('Parameters: ' . implode(", ", $args);

        $localFunc = "_" . $function;
        $return = $this->$localFunc($args);

        Logger::logExit();

        return $return;
    }

    private function _tester() {
        // do stuff...
        return "tester called";
    }
}

 $t = new TestClass();
 echo $t->tester();
 // "tester called"
nickf
Prefixing the user's functions is really bad style.
tuergeist
it's not changing anyone else's code - you'd have to change your own. Alternatively, just change the methods to private or protected.
nickf
+3  A: 

If you want to do function logging for the sake of debugging you may want to look into the Xdebug extension. There's no good way to intercept function calls at at runtime, and any automated interception will add great runtime overhead.

Using XDebug you could instead turn it on as-needed, as well as get lots of other stuff

( XDebug is thus used with PHPUnit to do unit testing and coverage analysis. )

The Problem with __call

__call may look to be a fun solution to the problem, but there are 3 problems with this, namely

  • Significant Execution Overhead. your doing __call --> call_user_func_array , which will literally add not one, but two function calls to every execution.

  • Backtraces become indecipherable: The actual function you were trying to call gets lost in a sea of __call and call_user_func_array making backtracing exceedingly hard, especially if your backtraces come with their arguent lists included.

  • Stupid Hidden Functions: You're going back to PHP4 style "hiding" of functions by prefixing them with _ to stop the user directly calling it or seeing it, because if the function name happens to be named what they wan't, the __call wont trigger, so you have already got a whole class full of really horrible function names, which developers will be tempted to call directly anyway in various places. ( And if you want to get rid of __call later, you will have to rename all these functions as to not break the code! )

Thus, if you are utilising php code to implement this will result in epically horrible code, that any future user of your codebase will NOT want to work with. You are far better getting something ( like Xdebug ) that can be added transparently when you need it, and save greatly polluting your code.

Kent Fredric
definitely agree, however if it has to be done (for whatever reason) i'd prefer the method which does not tie the logger to the classes it's trying to log.
Owen
+8  A: 

use a wrapper class. this method has the following benefits:

  • no need to change your underlying class structure / method signatures
  • change logging? just update this class
  • update object calls vs inserting code into every class you want to log

.

class LogWatch {
    function __construct($class)    {
        $this->obj  =   $class;
    }

    function __call($method, $args) {
        if (in_array($method, get_class_methods($this->obj) ) ) {
            Logger::logEntry();
            Logger::info('Parameter '.implode(', ', $args) );

            call_user_func_array(array($this->obj, $method), $args);

            Logger::logExit();

        } else {
            throw new BadMethodCallException();
        }
    }
}

$test = new LogWatch(new TestClass() );
$test->tester();

// you can use instances of `LogWatch()` just like your watched class
// including passing appropriate params:
$test->tester($param1, $param2);
Owen