Can I automatically modify/add statement to all user-defined functions in PHP?

Viewed 153

I want to modify or add functionality to all the PHP user defined functions in a request. Like, to add a snippet to a function body that will log all arguments passed to a function. At execution time.

I presume that this is more of a lisp-macro thing but maybe there is a way.

In some kind of pseudocode:

foreach (user-defined-function-in-call-stack){
    prepend-to-body($function){
        log(funct_get_args(), $function->name);
    }
}
4 Answers

While AOP and source code rewriting are closest to the implementation you describe, a debugging extension is probably the most appropriate tool for the specific use case you mention in the question and in comments.

The difference is that rather than having to modify the PHP code itself, an extension can integrate with the run-time engine at a lower level, and observe everything that's happening directly.

Specifically, the popular and open source XDebug extension has a function tracing mode built for exactly the use case you describe:

Those so-called "function traces" can be a help for when you are new to an application or when you are trying to figure out what exactly is going on when your application is running. The function traces can optionally also show the values of variables passed to the functions and methods, and also return values.

Installing the extension will depend on how you've installed PHP, but is often as simple as installing the appropriate package (e.g. sudo apt install php-xdebug). There is a page in the XDebug manual with instructions for various scenarios.

XDebug has a lot of functionality, and therefore a lot of configuration options, but these have been significantly simplified in XDebug 3, so the to get started, you should just be able to set:

  • xdebug.mode=trace to enable the feature and set appropriate defaults for other settings
  • xdebug.start_with_request=yes to trace all calls without needing a special trigger
  • xdebug.output_dir set to some directory the web server process can write to

See the documentation for the other options you can use to adjust the behaviour and output.

If I understand right you want to have cross-cutting concerns (such as logging and etc) separately from the main code. There is Aspect-oriented programming to achieve this goal. Unfortunately, PHP doesn't have native tools for that. But you can install AOP-framework via composer and use it to implement cross-cutting concerns without modification of source code.

What you need is a program transformation system (PTS).

This is a tool that accepts source-to-source rewrite rules of the general form,

 if you see syntax pattern x, replace it by constructor syntax pattern y

that can be applied to structured constructs ("syntax") such as code. Simple string replacement doesn't work for this because it doesn't honor the boundaries of the constructs.

You can sometimes find a PTS that is wired to work on single programming language, such as Spoon (a PTS for legacy Java code). I've never heard of one for PHP.

The really useful ones can be configured to accept the description of a programming language, and provide a PTS for the described language. Stratego, Rascal MPL, and DMS Software Reengineering Toolkit (the commercial PTS I have been working on for 25 years) all do this.

DMS can already be obtained with an appropriate description of PHP and so can be used to accomplish this purpose using DMS's rule specification language.

DMS rules have the general form of

 rule rulename ( metavariables ):  syntaxcategory -> syntaxcategory
      lefthandsidepattern -> righthandsidepattern ;

A simplified version of what you want can be written as a single DMS rewrite rule:

 rule insert_entry_probe(name:IDENTIFIER,args:arguments,body:statement_list)
               :function-> function
   " function \name ( \args ) { \body } "
    ->  "function \name ( \args ) {  log(\"entered\",\makestring\(\name\)) \body } ";

The metaquotes " are used to distinguish code/syntax of the targeted language, from the code/syntax of the rule specification language. name:syntaxtype allows one to specify named metavariables that match complex structures of a particular syntax-type defined by the target grammar. "\" is an escape used inside metaquotes. \name allows one match/use a complex substructures (this is where string matching isnt powerful enough). \name(...) is a metacall to an auxiliary procedure or constructor patterns that computes a complex substructure; in the example, we want to convert the function identifier into a text string. (Forgive my PHP syntax as being rusty).

To handle passing the args to the log function, you'll need more rules to transform the complex structure represented by the args int a list of argument names, with a list of argument values, to be passed to a log function.

Using DMS to process a PHP file, and handing it this rule, will cause it to insert probes tracking function entry in every function found in that file. It should be obvious how to make another rule to track every function exit... but you'll need two rules, one to handle the implicit exit at the end of the function body, and one to handle exits that occur at return statements.

Using such rules, DMS implements test coverage tools for PHP by inserting appropriate instrumentation. That has about 100 rules in it because of the many different bits of PHP syntax that affect control flow and therefore where you want test coverage probes, but one can code those rules from scratch in about a day if you understand how DMS works, and you use the PHP definition as a reference grammar.

A PTS gives you the power to make arbitrarily complex changes to source code. They are far more powerful than you might guess. We use DMS to do fully automated migrations from million-line mainframe COBOL programs to maintainable Java or C#. No, you can't write the rules for this in an afternoon, but 1-2 engineers can do this in 3-6 months. There's no way to rewrite a million lines of COBOL code by hand in that time frame.

Per function? Not that I'm aware. But you use constructor to run code every time the class is called. You could then do things such as find which method is being called, what the params are, or anything else you need to do.

class Parent
{
    public function __construct()
    {
        // Code here to run every time class is called
    }
}

This ticket might be relevant to finding the method being called from the constructor.

PHP: how do you get the method called from the constructor?

If you need a line of code to run for literally every method, you could have every class include only one method/function, then have all your classes inherit a parent class that has the needed code inside of the constructor. This all sounds very overkill and would make the codebase much messier. Depends on what you're actually trying to do. If you just want to log requests and stack traces, there's much easier ways to do that.

Related