phpDocumentor

Extend api docs

This feature is still under development and only available in the nightly docker builds.

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 to do_action() (or a variant) while the element being reflected, on top of the context stack, can hold metadata.
  • doCreate() adds a Hooks object 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 %}
                &mdash; {{ action.docBlock.summary }}
            {% endif %}
        </li>
    {% endfor %}
</ul>

The template receives two variables:

metadata
The metadata object of your extension, in our case the Hooks object.
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.twig in the template path of the extension.
  • template path is not a directory : check that getTemplatePath() points to an existing directory.

Search results