A lightweight, WordPress-style hooks system for PHP. Add actions and filters with priority support to create extensible, event-driven applications.
- ✅ WordPress-inspired API (
addAction,addFilter,doAction,applyFilter) - ✅ Priority-based execution order (with per-hook priority support)
- ✅ Multiple callbacks per hook
- ✅ Multiple hook names in a single call
- ✅ Supports all PHP callable types (closures, functions, static methods, instance methods, invokables)
- ✅ Optimized sorting (sorted once, cached until modified)
- ✅ Type-safe with strict types
- ✅ Interface-based design (
HookInterface) - ✅ Optional type hint enforcement for callbacks
- ✅ Priority introspection (
getMinPriority,getMaxPriority) - ✅ Built-in
FIRSTandLASTpriority constants - ✅ Zero dependencies
composer require kristos80/hookuse Kristos80\Hook\Hook;
$hook = new Hook();
// Add a filter
$hook->addFilter('format_title', function(string $title) {
return strtoupper($title);
});
// Apply the filter
$result = $hook->applyFilter('format_title', 'hello world');
echo $result; // HELLO WORLDLower priority numbers run first (default is 10):
$hook->addFilter('modify_value', function(int $value) {
return $value * 2;
}, 10);
$hook->addFilter('modify_value', function(int $value) {
return $value + 5;
}, 5); // Runs first
$result = $hook->applyFilter('modify_value', 10);
echo $result; // 30 (first: 10 + 5 = 15, then: 15 * 2 = 30)Actions are filters that don't return values:
$hook->addAction('user_login', function() {
error_log('User logged in');
});
$hook->doAction('user_login');$hook->addFilter('format_name', function(string $name, string $prefix) {
return $prefix . ' ' . $name;
});
$result = $hook->applyFilter('format_name', 'John', 'Mr.');
echo $result; // Mr. JohnRegister the same callback to multiple hooks at once:
$hook->addAction(['init', 'startup', 'boot'], function() {
// Initialization logic
});
$hook->doAction('init'); // Executes callback
$hook->doAction('startup'); // Executes callback
$hook->doAction('boot'); // Executes callbackWhen registering multiple hook names, you can assign a different priority to each hook by passing an array of priorities. Each hook name is mapped to the priority at the same index, with a fallback to the first priority if the index doesn't exist:
// Different priority per hook: 'init' gets priority 1, 'save' gets priority 20
$hook->addFilter(['init', 'save'], function(string $data) {
return $data;
}, [1, 20]);
// Partial array: 'init' gets priority 5, 'save' and 'cleanup' fall back to index 0 (priority 5)
$hook->addAction(['init', 'save', 'cleanup'], function() {
// ...
}, [5]);
// Single int still works as before — all hooks get the same priority
$hook->addFilter(['init', 'save'], $callback, 10);The library accepts any valid PHP callable:
// Closure
$hook->addFilter('my_filter', function(string $value) {
return strtoupper($value);
});
// Function name (string)
$hook->addFilter('my_filter', 'strtoupper');
// Static method (string)
$hook->addFilter('my_filter', 'MyClass::transform');
// Static method (array)
$hook->addFilter('my_filter', [MyClass::class, 'transform']);
// Instance method (array)
$formatter = new TextFormatter();
$hook->addFilter('my_filter', [$formatter, 'format']);
// Invokable object
class MyTransformer {
public function __invoke(string $value): string {
return strtoupper($value);
}
}
$hook->addFilter('my_filter', new MyTransformer());Use the requireTypedParameters named argument to enforce that all callback parameters have type hints:
$hook->addFilter('process_data', function(array $data): array {
return array_map('strtoupper', $data);
});
// This will work - callback has typed parameters
$result = $hook->applyFilter('process_data', ['hello'], requireTypedParameters: true);
// Register an untyped callback
$hook->addFilter('other_filter', function($value) {
return $value;
});
// This will throw MissingTypeHintException
$hook->applyFilter('other_filter', 'test', requireTypedParameters: true);The requireTypedParameters argument is stripped and never passed to callbacks. This feature helps enforce stricter contracts when the hook owner wants to ensure all registered callbacks follow type safety conventions.
Use HookInterface::FIRST and HookInterface::LAST to guarantee a callback runs before or after all others:
use Kristos80\Hook\HookInterface;
// Guaranteed to run before any other callback
$hook->addFilter('process', function(string $data) {
return trim($data);
}, HookInterface::FIRST);
// Guaranteed to run after any other callback
$hook->addFilter('process', function(string $data) {
return htmlspecialchars($data);
}, HookInterface::LAST);The constants are also accessible via Hook::FIRST and Hook::LAST. Multiple callbacks at the same constant priority follow FIFO order, consistent with the rest of the priority system.
Query the lowest or highest registered priority for a given hook:
$hook->addFilter('my_filter', function(string $v) { return $v; }, 5);
$hook->addFilter('my_filter', function(string $v) { return $v; }, 20);
$hook->addFilter('my_filter', function(string $v) { return $v; }, 12);
$hook->getMinPriority('my_filter'); // 5
$hook->getMaxPriority('my_filter'); // 20
// Returns null for hooks with no registered callbacks
$hook->getMinPriority('nonexistent'); // null
$hook->getMaxPriority('nonexistent'); // nullAdd a filter callback to one or more hooks.
$hookNames- Hook name(s) to attach to$callback- Callable to execute$priority- Execution priority (lower = earlier, default: 10). Pass an array to assign a different priority per hook name (falls back to index 0 for missing indices)
Alias for addFilter(). Use for hooks that don't return values.
Note: The
$acceptedArgsparameter exists for backwards compatibility but is deprecated and no longer used. PHP natively handles argument count validation.
Execute all callbacks registered to a filter hook.
$hookName- Hook name to execute...$arg- Arguments to pass to callbacksrequireTypedParameters: bool- Named argument to enforce type hints on callbacks (default: false)- Returns the filtered value
- Throws
MissingTypeHintExceptionifrequireTypedParametersis true and a callback has untyped parameters
Execute all callbacks registered to an action hook.
$hookName- Hook name to execute...$arg- Arguments to pass to callbacksrequireTypedParameters: bool- Named argument to enforce type hints on callbacks (default: false)- Throws
MissingTypeHintExceptionifrequireTypedParametersis true and a callback has untyped parameters
Priority constants for guaranteed earliest (PHP_INT_MIN) and latest (PHP_INT_MAX) execution. Also accessible as Hook::FIRST / Hook::LAST.
Get the lowest registered priority for a hook.
$hookName- Hook name to query- Returns the minimum priority value, or
nullif the hook has no registered callbacks
Get the highest registered priority for a hook.
$hookName- Hook name to query- Returns the maximum priority value, or
nullif the hook has no registered callbacks
The Hook class implements HookInterface, providing several benefits:
- Dependency Injection - Type-hint against
HookInterfacein your constructors and methods, making dependencies explicit and swappable - Testability - Easily mock or stub the hook system in unit tests by creating test doubles that implement
HookInterface - Decoupling - Your code depends on an abstraction rather than a concrete implementation, following the Dependency Inversion Principle
- Extensibility - Create alternative implementations (e.g., a
NullHookfor disabled hooks, or aLoggingHookdecorator) without modifying existing code - Contract Guarantee - The interface defines a clear API contract, ensuring any implementation provides the expected methods
// Type-hint against the interface for better architecture
class UserService {
public function __construct(
private HookInterface $hooks
) {}
public function createUser(array $data): User {
$data = $this->hooks->applyFilter('user_data', $data);
// ...
}
}./vendor/bin/pestMIT
Christos Athanasiadis - [email protected]