This feature is still under development and only available in the nightly docker builds.
Extend api docs
phpDocumentor can attach extra information, called metadata , to the elements it reflects. An extension can produce that metadata and provide a template that shows it in the generated documentation.
In this tutorial we build an extension that finds WordPress do_action()
calls and lists them on the page of the
method or function that fires them:
class Post
{
public function save(): void
{
/** Fires after a post was saved. */
do_action('save_post');
}
}
The page of Post::save()
will get a "Hooks" section that contains 'save_post'
and its summary.
Read first how to setup a phpDocumentor extension before you continue this guide.
Metadata is rendered on the pages of classes, interfaces, traits, enums, functions, methods and files. Namespaces are not supported.
The extension consists of two parts: producing the metadata while the code is reflected, and rendering it while the documentation is written. Start with the manifest and the extension class:
<?xml version="1.0" encoding="utf-8" ?>
<phar xmlns="https://phar.io/xml/manifest/1.0">
<contains name="phpDocumentor\WordpressHooks" version="1.0" type="extension">
<extension for="phpdocumentor/phpdocumentor" compatible="^3.5"/>
</contains>
<copyright>
<author name="You" email="[email protected]"/>
<license type="MIT" url="https://example.com/LICENSE"/>
</copyright>
<requires>
<php version="^8.1"/>
</requires>
<bundles>
<component name="phpDocumentor\WordpressHooks\Extension" version="1.0" />
</bundles>
</phar>
<?php
declare(strict_types=1);
namespace phpDocumentor\WordpressHooks;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
class Extension extends \phpDocumentor\Extension\Extension
{
public function getAlias(): string
{
return 'wordpress-hooks';
}
public function getTemplatePath(): string|null
{
return __DIR__ . '/Resources/templates';
}
/** @return string[] */
public function getMetadataKeys(): array
{
return ['wp_hooks'];
}
public function load(array $configs, ContainerBuilder $container)
{
$loader = new PhpFileLoader($container, new FileLocator(__DIR__ . '/Resources/config'));
$loader->load('services.php');
}
}
The alias returned by getAlias()
is also the name of the Twig namespace of your templates, so use only letters,
numbers and -
. We come back to getTemplatePath()
and getMetadataKeys()
in step 2.
Step 1: Produce the metadata
Metadata is an object that implements phpDocumentor\\Reflection\\Metadata\\Metadata
. Its key()
identifies the kind of metadata. The format of the rest of the object is up to you. Ours holds a list of actions:
<?php
declare(strict_types=1);
namespace phpDocumentor\WordpressHooks\Reflection\Php;
use phpDocumentor\Reflection\Metadata\Metadata;
final class Hooks implements Metadata
{
/** @var Action[] */
private array $actions = [];
public function key(): string
{
return 'wp_hooks';
}
public function addAction(Action $action): void
{
$this->actions[] = $action;
}
/** @return Action[] */
public function getActions(): array
{
return $this->actions;
}
}
<?php
declare(strict_types=1);
namespace phpDocumentor\WordpressHooks\Reflection\Php;
use phpDocumentor\Reflection\DocBlock;
final class Action
{
public function __construct(private readonly string $name, private readonly DocBlock $docBlock)
{
}
public function getName(): string
{
return $this->name;
}
public function getDocBlock(): DocBlock
{
return $this->docBlock;
}
}
Next we need something that finds do_action()
calls and adds the metadata to the element that contains them. This
is a reflection strategy
. It receives the parsed PHP nodes, and decides with matches()
whether it handles one:
<?php
declare(strict_types=1);
namespace phpDocumentor\WordpressHooks\Reflection\Php\Factory;
use phpDocumentor\Reflection\DocBlock;
use phpDocumentor\Reflection\DocBlockFactoryInterface;
use phpDocumentor\Reflection\Metadata\MetaDataContainer;
use phpDocumentor\Reflection\Php\Factory\AbstractFactory;
use phpDocumentor\Reflection\Php\Factory\ContextStack;
use phpDocumentor\Reflection\Php\StrategyContainer;
use phpDocumentor\WordpressHooks\Reflection\Php\Action;
use phpDocumentor\WordpressHooks\Reflection\Php\Hooks;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\PrettyPrinter\Standard as PrettyPrinter;
use function in_array;
final class ActionFactory extends AbstractFactory
{
private const ACTION_NAMES = ['do_action', 'do_action_ref_array', 'do_action_deprecated'];
public function __construct(DocBlockFactoryInterface $docBlockFactory, private readonly PrettyPrinter $printer)
{
parent::__construct($docBlockFactory);
}
public function matches(ContextStack $context, object $object): bool
{
return $object instanceof FuncCall
&& $context->peek() instanceof MetaDataContainer
&& in_array($object->name->toString(), self::ACTION_NAMES, true);
}
protected function doCreate(ContextStack $context, object $object, StrategyContainer $strategies): object|null
{
$container = $context->peek();
$hooks = $container->getMetadata()['wp_hooks'] ?? null;
if ($hooks === null) {
$hooks = new Hooks();
$container->addMetadata($hooks);
}
$action = new Action(
$this->printer->prettyPrintExpr($object->args[0]->value),
$this->createDocBlock($object->getDocComment(), $context->getTypeContext()) ?? new DocBlock(),
);
$hooks->addAction($action);
return $action;
}
}
matches()accepts a function call todo_action()(or a variant) while the element being reflected, on top of the context stack, can hold metadata.doCreate()adds aHooksobject to that element, if it does not have one yet, and records the action in it. The docblock above the call becomes the description of the action.
Register the strategy in the container with the phpdoc.reflection.strategy
tag:
<?php
declare(strict_types=1);
use phpDocumentor\WordpressHooks\Reflection\Php\Factory\ActionFactory;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return static function (ContainerConfigurator $container): void {
$container->services()
->defaults()
->autowire()
->autoconfigure()
->set(ActionFactory::class)
->tag('phpdoc.reflection.strategy');
};
Step 2: Render the metadata
phpDocumentor renders metadata with a template that belongs to the extension. For this the extension class needs two things, which you already saw above:
getTemplatePath()returns the directory that contains your templates. phpDocumentor makes it available as the Twig namespace@wordpress-hooks, named after the alias of the extension.getMetadataKeys()lists the keys of the metadata your extension renders. Metadata with a key that no extension lists is not rendered.
Each metadata object is rendered with the template @<alias>/metadata/<key>.html.twig
. Our key is wp_hooks
, so
the template is Resources/templates/metadata/wp_hooks.html.twig
:
<h4 class="phpdocumentor-wp-hooks__title">Hooks</h4>
<ul class="phpdocumentor-wp-hooks">
{% for action in metadata.actions %}
<li>
<code>{{ action.name }}</code>
{% if action.docBlock.summary %}
— {{ action.docBlock.summary }}
{% endif %}
</li>
{% endfor %}
</ul>
The template receives two variables:
metadata- The metadata object of your extension, in our case the
Hooksobject. node- The descriptor that the metadata is attached to, such as the method.
The output of all templates is placed in a <section class="phpdocumentor-metadata">
on the page. When nothing
is rendered, the section is left out.
Try it
Install the extension, run phpDocumentor on code that calls do_action()
and open the page of the method. You
will see the "Hooks" section below the other information of the method.
Troubleshooting
phpDocumentor logs a warning for each metadata object that is not rendered, so run it with -v
to see them:
- no extension declares this key
: add the key to
getMetadataKeys(). - extension has no template
: create
metadata/<key>.html.twigin the template path of the extension. - template path is not a directory
: check that
getTemplatePath()points to an existing directory.