{"id":20114904,"url":"https://github.com/marcuskober/php-attributes","last_synced_at":"2025-09-21T01:32:45.509Z","repository":{"id":157374560,"uuid":"621205439","full_name":"marcuskober/php-attributes","owner":"marcuskober","description":"In this tutorial, I will demonstrate how to utilize WordPress hooks with PHP attributes. Although it may not be necessary for every simple plugin, employing PHP attributes can be particularly useful in plugins with a large codebase.","archived":false,"fork":false,"pushed_at":"2023-04-11T10:51:52.000Z","size":25,"stargazers_count":8,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2024-11-13T18:44:28.484Z","etag":null,"topics":["hooks","php","php-attribute","php-attributes","php8","wordpress","wordpress-development","wordpress-oop","wordpress-oop-php","wordpress-plugin","wp","wp-plugin","wp-plugins"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/marcuskober.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-03-30T07:44:06.000Z","updated_at":"2024-11-06T15:48:01.000Z","dependencies_parsed_at":null,"dependency_job_id":"ceb227a3-3d5d-4042-aa7e-5f57d3a24a88","html_url":"https://github.com/marcuskober/php-attributes","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/marcuskober%2Fphp-attributes","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/marcuskober%2Fphp-attributes/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/marcuskober%2Fphp-attributes/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/marcuskober%2Fphp-attributes/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/marcuskober","download_url":"https://codeload.github.com/marcuskober/php-attributes/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":233700501,"owners_count":18716387,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["hooks","php","php-attribute","php-attributes","php8","wordpress","wordpress-development","wordpress-oop","wordpress-oop-php","wordpress-plugin","wp","wp-plugin","wp-plugins"],"created_at":"2024-11-13T18:32:57.200Z","updated_at":"2025-09-21T01:32:40.241Z","avatar_url":"https://github.com/marcuskober.png","language":"PHP","readme":"# 📖 Tutorial: Registering WordPress hooks using PHP attributes\n\nIn this tutorial, I will demonstrate how to utilize WordPress hooks with PHP attributes. Although it may not be necessary for every simple plugin, employing PHP attributes can be particularly useful in plugins with a large codebase.\n\nNote: The techniques presented in this tutorial are exclusively available in PHP 8! Therefore, make sure to adjust the 'Requires PHP' field in the plugin header accordingly.\n\n## Why is registering hooks problematic?\n\nUsing an object-oriented programming (OOP) approach for WordPress plugins has often caused difficulties when it comes to hook registration. There are various methods available, and I will present three commonly used options.\n\n### Option 1: Using the constructor\n\n```php\nclass MyClass\n{\n  public function __construct()\n  {\n    add_filter('body_class', [$this, 'addBodyClass']);\n  }\n\n  public function addBodyClass(array $classes): array\n  {\n    $classes[] = 'my-new-class';\n\n    return $classes;\n  }\n}\n\nnew MyClass();\n```\n\nUsing the constructor of a class for registering hooks can create issues when adding unit testing. Moreover, it can be considered a misuse of the constructor, which should solely be used for initializing an object's properties upon its creation.\n\n### Option 2: Register from outside of the class\n\n```php\nclass MyClass\n{\n  public function addBodyClass(array $classes): array\n  {\n    $classes[] = 'my-new-class';\n\n    return $classes;\n  }\n}\n\n$myClass = new MyClass();\nadd_filter('body_class', [$myClass, 'addBodyClass']);\n```\n\nThat's an improvement - we no longer misuse the constructor of the class. However, in large plugins, we may end up with a long list of class instantiations, and in large classes, the hook registration can be far from the corresponding method, which can be inconvenient and unclear.\n\n### Option 3: Register from static method\n\n```php\nclass MyClass\n{\n  public static function register(): void\n  {\n    $self = new self();\n\n    add_filter('body_class', [$self, 'addBodyClass']);\n  }\n\n  public function addBodyClass(array $classes): array\n  {\n    $classes[] = 'my-new-class';\n\n    return $classes;\n  }\n}\n\nMyClass::register();\n```\n\nThis is how I used hook registration before switching to PHP attributes. I appreciate that the registration takes place inside the class and we don't need to use the constructor. However, the registration is still decoupled from the method.\n\n---\n## Leveraging PHP attributes\n\nPHP attributes provide the capability to add structured metadata to classes, methods, functions, and more. These attributes are machine-readable and can be inspected during runtime using the Reflection API.\n\nIf you are not familiar with PHP attributes, please refer to [the documentation](https://www.php.net/manual/en/language.attributes.overview.php).\n\nTo utilize PHP attributes for hook registration, we need to complete three tasks:\n\n1. Define the attribute class.\n2. Use the attribute on the method.\n3. Scan the classes with hooks.\n\nIn practice, we follow this sequence to achieve our goal. To gain a better understanding of the concept, we will begin with step 2, move on to step 1, and then complete step 3.\n\n---\n### 1️⃣ The class with the hook\n\nLet's say we want to add a classname to the body tag, as seen in the examples above. We take the pure class without any hook registration:\n\n```php\nclass MyClass\n{\n  public function addBodyClass(array $classes): array\n  {\n    $classes[] = 'my-new-class';\n\n    return $classes;\n  }\n}\n```\n\nThe attribute declaration begins with `#[` and ends with `]`. Inside, the attribute is listed. We place the attribute declaration right before the method:\n\n```php\nclass MyClass\n{\n  #[Filter('body_class')]\n  public function addBodyClass(array $classes): array\n  {\n    $classes[] = 'my-new-class';\n\n    return $classes;\n  }\n}\n```\n\nObserve how elegantly the attribute is connected to its respective method. By using this approach, you can easily make changes to the priority or number of arguments passed to the function in one central location.\n\nHere is an example of how to change the priority:\n\n```php\nclass MyClass\n{\n  #[Filter('body_class', 1)]\n  public function addBodyClass(array $classes): array\n  {\n    $classes[] = 'my-new-class';\n\n    return $classes;\n  }\n}\n```\n\nWe'll cover that in more detail later on.\n\n---\n\n### 2️⃣ The attribute class\n\nIn order for the code above to work, we need to define the corresponding attribute class.\n\nWe need the constructor to take the properties `hook`, `priority` and `acceptedArgs`, as the `add_filter` function needs these too. We leverage the [constructor property promotion](https://www.php.net/manual/en/language.oop5.decon.php#language.oop5.decon.constructor.promotion) of PHP 8.\n\n```php\nclass Filter\n{\n    public function __construct(\n        public string $hook,\n        public int $priority = 10,\n        public int $acceptedArgs = 1\n    )\n    {\n    }\n}\n```\n\nNow we need to transform this regular class into an attribute class. As previously mentioned, attributes can also be added to classes. To indicate that this class is an attribute, we must include the #[Attribute] definition immediately preceding the class definition:\n\n```php\n#[Attribute]\nclass Filter\n{\n    public function __construct(\n        public string $hook,\n        public int $priority = 10,\n        public int $acceptedArgs = 1\n    )\n    {\n    }\n}\n```\n\nNow, we want to incorporate the call to `add_filter()` within our class:\n\n```php\n#[Attribute]\nclass Filter\n{\n    public function __construct(\n        public string $hook,\n        public int $priority = 10,\n        public int $acceptedArgs = 1\n    )\n    {\n    }\n\n    public function register(callable|array $method): void\n    {\n        add_filter($this-\u003ehook, $method, $this-\u003epriority, $this-\u003eacceptedArgs);\n    }\n}\n```\n\nThat's our attribute class. The next step is to make it functional.\n\n---\n\n### 3️⃣ Scanning our hooked classes\n\nNow that our attribute class is ready and we have another class that uses this attribute, it's time to scan the classes for `Filter` attributes.\n\nn order to scan a class for attributes, we need to utilize the [Reflection API](https://www.php.net/manual/en/language.attributes.reflection.php) of PHP.\n\nLet's assume that we have a main class for our plugin:\n\n```php\nclass App\n{\n  public static function init(): void\n  {\n  }\n}\n\nApp::init();\n```\n\nWe will create a method called `registerHooks()` and for simplicity's sake, we will hardcode the list of classes to scan directly into the method. However, in production, it's recommended to use other techniques. You can refer to another approach in the plugin inside this repository.\n\n```php\nclass App\n{\n  public static function init(): void\n  {\n    $self = new self();\n    $self-\u003eregisterHooks();\n  }\n\n  private function registerHooks(): void\n  {\n    $hookedClasses = [\n      'MyClass',\n    ];\n  }\n}\n\nApp::init();\n```\n\nNote: You need to use the qualified class name here (see [src/Main/App.php](https://github.com/marcuskober/php-attributes/blob/142568d438c493051b97a002fee7f9479a99e137/src/Main/App.php)).\n\nOur goal now is to retrieve all the methods from the classes and check if they have any Filter attributes. To achieve this, we need to iterate through the classes and create a ReflectionClass instance of each class:\n\n```php\nclass App\n{\n  public static function init(): void\n  {\n    $self = new self();\n    $self-\u003eregisterHooks();\n  }\n\n  private function registerHooks(): void\n  {\n    $hookedClasses = [\n      'MyClass',\n    ];\n\n    foreach ($hookedClasses as $hookedClass) {\n      $reflectionClass = new ReflectionClass($hookedClass);\n    }\n\n  }\n}\n\nApp::init();\n```\n\nNow that we have the reflection class, we can retrieve all its methods:\n\n```php\n$methods = $reflectionClass-\u003egetMethods();\n```\n\nThis will return an array of `ReflectionMethod` objects. We can then loop through each method and get all `Filter` attributes, if any. The `getAttributes()` method returns an array filled with the `Filter` attribute objects or an empty array if no methods with filter attributes are found. We can then loop through each filter attribute object using a foreach loop:\n\n```php\nforeach ($methods as $method) {\n    $filterAttributes = $method-\u003egetAttributes(Filter::class);\n\n    foreach ($filterAttributes as $filterAttribute) {\n      // do the magic\n    }\n}\n```\n\nIn the next step, we can instantiate the `Filter` attribute class using the `newInstance` method:\n\n```php\nforeach ($methods as $method) {\n    $filterAttributes = $method-\u003egetAttributes(Filter::class);\n\n    foreach ($filterAttributes as $filterAttribute) {\n      $filter = $filterAttribute-\u003enewInstance();\n    }\n}\n```\n\nLet's recall what property we need for the `register` method of our `Filter` class. We need the method in the form of an array (`[$className, $method]`). To get the required method, we first need to instantiate the class with the hooks:\n\n```php\nforeach ($methods as $method) {\n    $filterAttributes = $method-\u003egetAttributes(Filter::class);\n\n    foreach ($filterAttributes as $filterAttribute) {\n      $hookedClassObject = new $hookedClass();\n      \n      $filter = $filterAttribute-\u003enewInstance();\n      $filter-\u003eregister([$hookedClassObject, $method-\u003egetName()]);\n    }\n}\n```\n\nAnd because a method is allowed to have multiple attributes and the hooked class may have more than one method, we need to ensure that the hooked class is instantiated only once. Here's the full `App` class for you to better understand the code:\n\n```php\nclass App\n{\n  private array $instances = [];\n\n  public static function init(): void\n  {\n    $self = new self();\n    $self-\u003eregisterHooks();\n  }\n\n  private function registerHooks(): void\n  {\n    $hookedClasses = [\n      'MyClass',\n    ];\n\n    foreach ($hookedClasses as $hookedClass) {\n      $reflectionClass = new ReflectionClass($hookedClass);\n\n      foreach ($reflectionClass-\u003egetMethods() as $method) {\n        $filterAttributes = $method-\u003egetAttributes(Filter::class);\n\n          foreach ($filterAttributes as $filterAttribute) {\n            if (! isset($this-\u003einstances[$hookedClass])) {\n              $this-\u003einstances[$hookedClass] = new $hookedClass();\n            }\n\n            $filter = $filterAttribute-\u003enewInstance();\n            $filter-\u003eregister([$this-\u003einstances[$hookedClass], $method-\u003egetName()]);\n          }\n      }\n    }\n  }\n}\n```\n\n### 4️⃣ Extending our code to register actions too\n\nThe `Action` attribute class looks very similar to the `Filter` class:\n\n```php\n#[Attribute]\nclass Action\n{\n    public function __construct(\n        public string $hook,\n        public int $priority = 10,\n        public int $acceptedArgs = 1\n    )\n    {\n    }\n\n    public function register(callable|array $method): void\n    {\n        add_action($this-\u003ehook, $method, $this-\u003epriority, $this-\u003eacceptedArgs);\n    }\n}\n```\n\nTo be able to search for `Filter` and `Action` attributes using the `getAttributes()` method, we create a simple interface for our hook classes:\n\n```php\ninterface HookInterface\n{\n    public function register(callable|array $method): void;\n}\n```\n\nOur `Filter` and `Action` attribute classes must implement this interface:\n\n```php\n#[Attribute]\nclass Filter implements HookInterface\n{\n  // Class code\n}\n\n#[Attribute]\nclass Action implements HookInterface\n{\n  // Class code\n}\n```\n\nNow we can easily use our exiting code of the `registerHooks()` method to support filters and actions:\n\n```php\nprivate function registerHooks(): void\n{\n  $hookedClasses = [\n    'MyClass',\n  ];\n\n  foreach ($hookedClasses as $hookedClass) {\n    $reflectionClass = new ReflectionClass($hookedClass);\n\n    foreach ($reflectionClass-\u003egetMethods() as $method) {\n      $hookAttributes = $method-\u003egetAttributes(HookInterface::class, ReflectionAttribute::IS_INSTANCEOF);\n\n        foreach ($hookAttributes as $hookAttribute) {\n          if (! isset($this-\u003einstances[$hookedClass])) {\n            $this-\u003einstances[$hookedClass] = new $hookedClass();\n          }\n\n          $hook = $hookAttribute-\u003enewInstance();\n          $hook-\u003eregister([$this-\u003einstances[$hookedClass], $method-\u003egetName()]);\n        }\n    }\n  }\n}\n```\n\nFor `getAttributes()` to accept the interface as a class name, we need to set the flag `ReflectionAttribute::IS_INSTANCEOF` (see [documentation](https://www.php.net/manual/en/reflectionfunctionabstract.getattributes.php)).\n\n---\n\n## End of tutorial\n\nI hope you found this tutorial helpful. If you have any further questions, feel free to ask.\n\nHere's a summary of what we covered:\n\n- We learned how to create attribute classes in PHP 8 and how to apply them to our code.\n- We used attributes to create Filter and Action hooks in our WordPress plugin.\n- We used the Reflection API of PHP to scan our code for hooks and to register them automatically.\n\nThank you for reading, and happy coding!\n\nFeel free to download this WordPress plugin and experiment with it: [https://github.com/marcuskober/php-attributes/archive/refs/heads/main.zip](https://github.com/marcuskober/php-attributes/archive/refs/heads/main.zip)\n\n---\n\n# About me\n\nHey there, I'm Marcus, and I'm a passionate full time WordPress developer who's dedicated to crafting high-quality, well-structured plugins. For me, coding is more than just a job; it's a creative outlet where I can constantly challenge myself to find new and better solutions.\n\nWhen I'm not working on WordPress projects, you can find me hanging out in Cologne, Germany, with my lovely wife and two wonderful kids.\n\nDon't hesitate to get in touch with me at [hello@marcuskober.de](mailto:hello@marcuskober.de). I'm always open to new ideas and collaborations!","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmarcuskober%2Fphp-attributes","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmarcuskober%2Fphp-attributes","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmarcuskober%2Fphp-attributes/lists"}