In this tutorial, learn how to create an automated documentation pipeline in PHP with Autodocs, Minicli, and GitHub Actions
Autodocs is a PHP library designed to facilitate building highly customizable automated docs based on markdown templates. Combined with a Minicli application, it provides a layer of abstraction and structure on top of which you can create your own documentation factory.
I created this project to help me keep up with the documentation needs for Chainguard Images , which now helps keeping more than a thousand doc pages up-to-date in a GitHub-based workflow with nightly runs.
Pages are defined as classes that follow a known interface. Data sources can be autoloaded as JSON cache files, a design that facilitates distributed setups where data comes from varied sources. This distributed approach facilitates integrating Autodocs with existing workflows and pipelines, especially when using GitHub Actions.
In this tutorial we'll create a demo Autodocs application to generate personal GitHub READMEs. If you'd prefer to skip the tutorial and go straight to the code, you can check the demo repository on GitHub .
Let's start by creating the demo application. We're using the Minicli application template to provide more resources and organization when building out the app.
composer create-project minicli/minicli autodocs-demo
Once Composer is finished installing the new application dependencies, you should be able to enter the directory and run a quick test:
cd autodocs-demo
./minicli
You should get output like this, indicating that the Minicli application runs as expected:
❯ ./minicli
███╗ ███╗██╗███╗ ██╗██╗ ██████╗██╗ ██╗
████╗ ████║██║████╗ ██║██║██╔════╝██║ ██║
██╔████╔██║██║██╔██╗ ██║██║██║ ██║ ██║
██║╚██╔╝██║██║██║╚██╗██║██║██║ ██║ ██║
██║ ╚═╝ ██║██║██║ ╚████║██║╚██████╗███████╗██║
╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝╚═╝ ╚═════╝╚══════╝╚═╝
Minimalist, dependency-free framework for building CLI-centric PHP applications
Now you can rename the executable toautodocsor a different name if you'd like:
mv minicli autodocs
Next, we want to include theautodocslibrary as a dependency within Composer:
composer require erikaheidi/autodocs
You now have all requirements set up and can proceed to configuring Autodocs.
Create a new configuration file to hold Autodocs settings. This should go in the config
folder of your Minicli applicaiton:
touch config/autodocs.php
Autodocs expects an autodocs
configuration entry with a few required settings:
templates_dir
: where to find .tpl
files that may be used as page templates. The use of templates is not mandatory, but it can be helpful to facilitate content layout updates without having to change page classes.cache_dir
: .json
files in this directory will be automatically registered as aDataFeedobject within the main Autodocs service.output
: location where to save built pages.pages
: an array with classes that should be registered as Reference Pages. These are the documentation pages that will be built at each run.storage
: (Optional) a class that implements StorageInterface
, able to handle filesystem access to save and retrieve files. The default FileStorage
is used when no option is defined.For our demo, let's create a storage
directory for templates, content, and cache:
mkdir -p storage/cache storage/templates storage/content
Next, open the file we created previously, config/autodocs.php
, using your PHP editor of choice. Copy the following configuration to your file:
<?php
declare(strict_types=1);
use Autodocs\Page\ExamplePage;
return [
'autodocs' => [
// Pages to Build
'pages' => [
ExamplePage::class
],
// Build Output Folder
'output' => envconfig('AUTODOCS_OUTPUT', __DIR__.'/../storage/content'),
// Cache Folder - where to look for cache json files
'cache_dir' => envconfig('AUTODOCS_CACHE', __DIR__.'/../storage/cache'),
// Templates directory
'templates_dir' => envconfig('AUTODOCS_TEMPLATES', __DIR__.'/../storage/templates')
]
];
Save the file. Notice we registered an ExamplePage
that is included for tests. We'll update this later on to include our custom pages.
With the config in place, you can now register the Autodocs service within the Minicli application.
Open the config/services.php
file and make sure it looks like this, with the line including the Autodocs service class registered as autodocs
:
<?php
declare(strict_types=1);
use Autodocs\Service\AutodocsService;
return [
/****************************************************************************
* Application Services
* --------------------------------------------------------------------------
*
* The services to be loaded for your application.
*****************************************************************************/
'services' => [
'autodocs' => AutodocsService::class,
],
];
Save the file. You can now start working on your build command, which we'll see in the next step.
We'll now create a command to build our custom automated docs. This will build all pages defined in the pages
autodocs config, which for now should be set to just include the ExamplePage. Once this simple example page builds, we can start working on our actual doc pages.
In Minicli, creating a command is just a matter of setting up some classes and directories in a pre-defined format. Start by creating a directory for your commands inside app/Commands
:
mkdir app/Command/Build
Next, we'll create the command controller that will handle building the docs.
Create a new file called DefaultController.php
inside the newly created app/Command/Build
directory. In this class, we'll set up a method that will be called whenever we run ./autodocs build
.
touch app/Command/Build/DefaultController.php
Open this file on your PHP editor of choice. You can start by copying the following skeleton for your controller class:
<?php
declare(strict_types=1);
namespace App\Command\Build;
use Minicli\Command\CommandController;
class DefaultController extends CommandController
{
public function handle(): void
{
//build command action
}
}
From the handle
method, we can access the application, the configuration container, and all registered services. The logic here will depend on your documentation needs.
To build the included ExamplePage
, update your handle
method to the following code:
public function handle(): void
{
/** @var AutodocsService $autodocs */
$autodocs = $this->getApp()->autodocs;
$this->info("Starting Build...");
$autodocs->buildPages($this->getParam('pages') ?? "all");
$this->info("Build finished. Output saved to " . $autodocs->config['output']);
}
This will build the example page and save it in the location defined by output
in Autodocs config.
Now you should be able to run the build command with:
./autodocs build
You should get output like the following:
❯ ./autodocs build
Starting Build...
Build finished. Output saved to /home/erika/Projects/autodocs-demo/config/../storage/content
Check the output folder. You should find an example.md
page there.
In the previous step we created a build command and used it to build the includedExamplePage, which is a really simple page that basically outputs a title and a description. After having a look at how pages are defined, we'll create a custom page for our demo app.
Reference Pages are models that represent a document and how it should be built. They should extend from the ReferencePage
class (or implement ReferencePageInterface
) and implement the following methods:
loadData
- this method receives an optional $parameters
array and should load any additional data required to build the page.getName
- must return a unique identifier that can be used later on to build only this type of page.getContent
- the actual content that will be saved.getSavePath
- the path where to save this file, to be used by the page builder.It is easier to see how it works in practice, so this is what the ExamplePage
looks like:
<?php
declare(strict_types=1);
namespace Autodocs\Page;
use Autodocs\DataFeed\JsonDataFeed;
class ExamplePage extends ReferencePage
{
public JsonDataFeed $dataFeed;
public function loadData(array $parameters = []): void
{
$this->dataFeed = new JsonDataFeed();
$this->dataFeed->loadFromArray([
'title' => 'example',
'description' => 'description'
]);
}
public function getName(): string
{
return "example";
}
public function getContent(): string
{
return $this->dataFeed->json['title'].' - '.$this->dataFeed->json['description'];
}
public function getSavePath(): string
{
return 'example.md';
}
}
This example creates a JsonDataFeed
and loads it with static data. Instead, you could use JSON data files and have them automatically loaded by Autodocs - you just need to place the files in the path defined by the cache
configuration option.
Next, we'll create our own custom documentation page using a simple JSON data source. For this demo, we'll generate a markdown page that can be used as your personal README on GitHub (the one that is rendered in your profile).
ReadmePage
The following ReadmePage class loads data from a JsonDataFeed that was pre-loaded into the application container. We'll set that JSON file in the next step.
The getContent
method uses Stencil to render a template called readme.tpl
with values obtained from the JSON data source.
<?php
declare(strict_types=1);
namespace App;
use Autodocs\DataFeed\JsonDataFeed;
use Autodocs\Page\ReferencePage;
class ReadmePage extends ReferencePage
{
public JsonDataFeed $dataFeed;
public function loadData(array $parameters = []): void
{
$this->dataFeed = $this->autodocs->getDataFeed('profile.json');
}
public function getName(): string
{
return "readme";
}
public function getContent(): string
{
return $this->autodocs->stencil->applyTemplate('readme', [
'title' => $this->dataFeed->json['user'],
'about' => $this->dataFeed->json['bio'],
'projects_list' => $this->getProjects(),
]);
}
public function getProjects(): string
{
$content = "";
foreach ($this->dataFeed->json['projects'] as $project => $info) {
$content .= "- [{$project}]({$info['link']}): {$info['description']}\n"; // returns Markdown list
}
return $content;
}
public function getSavePath(): string
{
return 'README.md';
}
}
When you're ready, copy these contents into your own app/ReadmePage.php
and save the file. But don't run the build just yet; we still need to set up the JSON data source and the template file used by the ReadmePage
class.
profile.json
Data SourceNext, we'll create a simple JSON file with some data to use when building the Readme page.
Create a new file called profile.json
in your cache_path
:
touch storage/cache/profile.json
Open the file in your editor of choice. Copy the following JSON skeleton and update the data with your own info:
{
"user": "Erika Heidi",
"bio": "Software and Documentation Engineer",
"projects": {
"minicli/minicli": {
"description": "CLI framework for PHP",
"link": "https://docs.minicli.dev"
},
"erikaheidi/autodocs": {
"description": "Tiny framework for automating documentation",
"link": "https://github.com/erikaheidi/autodocs/wiki"
}
}
}
Save the file when you're finished.
readme.tpl
TemplateNext, create the template file:
touch storage/templates/readme.tpl
Copy the following content to your template:
## {{ title }}
{{ about }}
## My Projects
{{ projects_list }}
Save the file when you're done.
ReadmePage
within Autodocs ConfigurationNow you just need to register your custom page within Autodocs configuration. Edit the config/autodocs.php
file, and change the pages
entry to include the new page. You can also remove the ExamplePage
while you're at it. It should look like this when you're finished:
'autodocs' => [
// Pages to Build
'pages' => [
ReadmePage::class
],
Save the file when you're finished.
ReadmePage
With the new page registered within the documentation, you can now proceed to build the ReadmePage
:
./autodocs build
You should get output indicating that the page was successfully built. If should now have a readme.md
file in your output folder with contents similar to this:
## Erika Heidi
Software and Documentation Engineer
## My PHP Projects
- [minicli/minicli](https://docs.minicli.dev): CLI framework for PHP
- [erikaheidi/autodocs](https://github.com/erikaheidi/autodocs/wiki): Tiny framework for automating documentation
We'll now create a workflow to run this demo on GitHub Actions. This is optional and requires you to set up your own repository with a copy of the project. You can also fork the original demo to your GitHub account and change the JSON data file to have your own info. Once you have your repository set up, go to Settings
-> Actions
-> General
, scroll to the bottom of the page where you'll find the Workflow permissions
section.
Then, enable "Read and Write permissions" and "Allow GitHub Actions to send Pull requests", so that the workflow can make changes and create a PR with the generated doc(s).
Our workflow will:
name: Build Docs
on:
workflow_dispatch:
env:
AUTODOCS_OUTPUT: "${{ github.workspace }}"
AUTODOCS_CACHE: "${{ github.workspace }}/storage/cache"
AUTODOCS_TEMPLATES: "${{ github.workspace }}/storage/templates"
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Cache Composer packages
id: composer-cache
uses: actions/cache@v2
with:
path: vendor
key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-php-
# Install Dependencies
- name: Install dependencies
run: composer install --prefer-dist --no-progress
# Build Docs
- name: Run Autodocs
run: ./autodocs build
# Send a Pull Request
- name: Create a PR
uses: peter-evans/create-pull-request@v5
id: cpr
with:
path: "${{ github.workspace }}"
commit-message: Update content
title: "[AutoDocs] Updated README"
body: "README auto-update"
labels: |
documentation
automated
You can save this file to .github/workflows/autodocs.yaml
. Don't forget to commit and push it to your repository. Once the file is there, go to Actions
and select the Build Docs
action on the left. This workflow is not set to run on schedule, instead it uses the workflow_dispatcher
trigger which can only be triggered from the repository page.
Click on the "Run Workflow" button on the right, then confirm to run the workflow for the first time. If the build passes as it should, you'll get a new pull request on your repository:
Based on this simplified demo, you can elaborate your workflow to auto-generate your profile.json
data and have your README always fresh. Maybe pull your latest posts from DEV or a blog feed? Show off your favorite projects, or highlight your sponsors? The only limit is your imagination :)
Documentation is a living organism, it changes and evolves as projects grow. After quite a few years working in the field of technical writing and documentation, I believe good documentation needs flexibility, context, and human input. These are hard to combine in a one-size-fits-all software to magically create docs.
Autodocs doesn't magically generate docs for you, that was never the intention. Instead, it provides a very thin layer of abstractions to give you enough structure to engineer your super custom docs and also to scale up your documentation using a distributed approach to data sourcing.
If you want to see a more complex implementation to have an idea of what Autodocs is capable, check Chainguard's Images Autodocs project on GitHub.