Hooks provided by PHP

PHP and the Zend Engine provide many different hooks for extensions that allow
extension developers to control the PHP runtime in ways that are not available
from PHP userland.

This chapter will show various hooks and common use-cases for hooking into them
from an extension.

The general pattern for hooking into PHP functionality are extensions
overwriting function pointers that the PHP core provides. The extension
function then often performs their own work and calls the original PHP core
function. Using this pattern different extensions can overwrite the same hook
without causing conflicts.

The execution of userland and internal functions are handled by two functions
within the Zend engine that you can replace with your own implementations.
The primary use-case for extensions to overwrite this hook is generic
function-level profiling, debugging and aspect oriented programming.

If you want to overwrite these function pointers, then you must do this in
MINIT, because other decisions inside the Zend Engine are made early based on
the fact if the pointers are overwritten or not.

One downside of overwriting zend_execute_ex is that it changes the Zend
Virtual Machine runtime behavior to use recursion instead of handling calls
without leaving the interpreter loop. In addition a PHP engine without
overwritten zend_execute_ex can also generate more optimized function call
opcodes.

These hooks are very performance sensitive depending on the complexity of code
that wraps the original functions.

While overwriting the execute hooks an extension can record every function
call, you can also overwrite individual function pointers of userland, core and
extension functions (and methods). This has much better performance
characteristics if an extension only needs access to specific internal function
calls.:

#if PHP_VERSION_ID < 70200typedefvoid(*zif_handler)(INTERNAL_FUNCTION_PARAMETERS);#endifzif_handleroriginal_handler_var_dump;ZEND_NAMED_FUNCTION(my_overwrite_var_dump){// if we want to call the original functionoriginal_handler_var_dump(INTERNAL_FUNCTION_PARAM_PASSTHRU);}PHP_MINIT_FUNCTION(my_extension){zend_function*original;original=zend_hash_str_find_ptr(EG(function_table),"var_dump",sizeof("var_dump")-1);if(original!=NULL){original_handler_var_dump=original->internal_function.handler;original->internal_function.handler=my_overwrite_var_dump;}}

When overwriting a class method, the function table can be found on the
zend_class_entry.:

When PHP 7 compiles PHP code it converts it into an abstract syntax tree (AST)
before finally generating Opcodes that are persisted in Opcache. The
zend_ast_processhook is called for every compiled script and allows you to
modify the AST after it is parsed and created.

This is one of the most complicated hooks to use, because it requires perfect
understanding of the AST possibilities. Creating an invalid AST here can cause
weird behavior or crashes.

Whenever a user script calls include/require or their counterparts
include_once/require_once PHP core calls the function at the pointer
zend_compile_file to handle this request. The argument is a file handle
and the result is a zend_op_array.:

There are two extensions in PHP core that implement this hook: dtrace and
opcache.

If you start the PHP script with the environment variable USE_ZEND_DTRACE
and compiled PHP with dtrace support, then dtrace_compile_file is used
from Zend/zend_dtrace.c.

Opcache stores op arrays in shared memory for better performance, so that
whenever a script is compiled its final op array is served from a cache and
not re-compiled. You can find this implementation in
ext/opcache/ZendAccelerator.c.

The default implementation called compile_file is part of the scanner
code in Zend/zend_language_scanner.l.

Use cases for implementing this hook are Opcode Accelerating, PHP code
encrypting/decrypting, debugging or profiling.

You can replace this hook whenever you want in the execution of a PHP process
and all PHP scripts compiled after the replacement will be handled by your
implementation of the hook.

It is very important to always call the original function pointer, otherwise
PHP cannot compile scripts anymore and Opcache will not work anymore.

The extension overwriting order here is also important as you need to be aware
whether you want to register your hook before or after Opcache, because Opcache
does not call the original function pointer if it finds an opcode array entry
in its shared memory cache. Opcache registers their hook as a post startup
hook, which runs after the minit phase for extensions, so by default your hook
will not be called anymore when the script gets cached.

If you implement this hook be aware that this hook is called regardless of
whether the exception is caught or not. It can still be useful to temporarily
store the exception here and then combine this with an implementation of the
Error Handler hook to check if the exception was uncaught and caused the script
to halt.

Use-cases to implement this hook include debugging, logging and exception
tracking.

PHPs eval is not an internal function but a special language construct. As
such you cannot hook into it through zend_execute_internal or by
overwriting its function pointer.

Use cases for hooking into eval are not that many, you can use it for profiling
or for security purposes. If you change its behavior be aware that other extensions
may need eval. One example is Xdebug that uses it to execute breakpoint conditions.

PHPs Garbage Collector can be triggered explicitly when gc_collect_cycles()
is called or implicitly by the engine itself when the number of collectable
objects reaches a certain theshold.

To allow understanding of how the garbage collector works or to profile its
performance, you can overwrite the function pointer hook that performs the
garbage collection operation. Theoretically you can implement your own garbage
collection algorithm here, but given other changes to the engine would probably
be necessary this probably is not really feasible.

The interrupt handler is called once when the executor global
EG(vm_interrupt) is set to 1. This is checked at regular checkpoints during
the execution of userland code. The engine uses this hook to implement the PHP
execution timeout via a signal handler that sets the interrupt to 1 after the
timeout duration is reached.

This can be helpful to defer signal handling to a later stage of the runtime
execution, when it is safer to clean up or to implement your own timeout
handling. By setting this hook you cannot accidently disable the timeout check
of PHP, because it has customized handling that has higher priority than any
overwrite of zend_interrupt_function.