Sami で PHP プロジェクトの API ドキュメントを出力しよう

[Sami](https://github.com/FriendsOfPHP/Sami) は Symfony の作者である Fabien Potencier さん作の API ドキュメント生成ツールです。
個人的には [phpDocumentor2](https://github.com/phpDocumentor/phpDocumentor2)  よりもお手軽な気がしてます。
[Symfony の API ドキュメント](http://api.symfony.com/master/index.html) も、このツールで出力されています。

例として、 EC-CUBE の API ドキュメントを出力してみます。

## 下準備

アプリケーションを git clone しておきます。
タグごとや、ブランチごとドキュメントを出力することも可能です。
git clone したら、アプリケーションのディレクトリに移動します。

```
git clone git@github.com:EC-CUBE/ec-cube.git
cd ec-cube
```

## 設定ファイルの定義

`sami_config.php` を用意します。設定内容は以下の通りです。

- Resource, cache, vendor の各ディレクトリを除いた `*.php` ファイルが出力対象
- Git のタグごとにドキュメントを出力する
- 3.0 ブランチ, master ブランチのドキュメントを出力する
- api-theme 以下のディレクトリに、オーバーライドするテンプレートを格納
- api-specifications 以下のディレクトリにドキュメントを出力する
- cache 以下のディレクトリにドキュメントのキャッシュファイルを出力する

```php:sami_config.php
<?php

use Sami\Sami;
use Sami\Version\GitVersionCollection;
use Sami\RemoteRepository\GitHubRemoteRepository;
use Symfony\Component\Finder\Finder;

// Symfony\Finder で探索したファイルが出力対象となる
$iterator = Finder::create()
    ->files()
    ->name('*.php')
    ->exclude('Resource')
    ->exclude('cache')
    ->exclude('vendor')
    ->in($dir = __DIR__);

// ブランチや、タグごとにドキュメントを出力する場合の設定
$versions = GitVersionCollection::create($dir)
    ->addFromTags('3.0.*')
    ->add('3.0', '3.0 branch')
    ->add('master', 'master branch')
;

// 出力ディレクトリや、テンプレートの設定
return new Sami($iterator, array(
    'theme' => 'eccube',
    'title' => 'EC-CUBE3 API Specifications',
    'versions' => $versions,
    'build_dir' => __DIR__.'/api-specifications/%version%',
    'cache_dir' => __DIR__.'/cache/%version%',
    'template_dirs' => array(__DIR__.'/api-theme')
));
```

必要に応じて、オーバーライドするテーマを用意します

`template_dirs` のディレクトリに `manifest.yml` を置くことで、独自のテーマを作成できます。

```yaml:api-theme/manifest.yml
name:   eccube
parent: default
```

template_dirs は config.php で指定できます。

```php:config.php
$templates = $sami['template_dirs'];

$templates[] = __DIR__ . '/../themes/';


$sami['template_dirs'] = $templates
```

```twig:api-theme/layout/layout.twig
{% extends "layout/base.twig" %}

{% block content %}
    <div id="content">
        <div id="left-column">
            {{ block('control_panel') }}
            {{ block('leftnav') }}
        </div>
        <div id="right-column">
            {{ block('menu') }}
            {% block below_menu '' %}
                <div id="page-content">
                    {% block page_content '' %}
                </div>
                {{ block('footer') }}
        </div>
    </div>
{% endblock %}

{% block menu %}
    <nav id="site-nav" class="navbar navbar-default" role="navigation">
        <div class="container-fluid">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#navbar-elements">
                    <span class="sr-only">Toggle navigation</span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <a class="navbar-brand" href="{{ path('index.html') }}">{{ project.config('title') }}</a>
            </div>
            <div class="collapse navbar-collapse" id="navbar-elements">
                <ul class="nav navbar-nav">
                    <li><a href="{{ path('classes.html') }}">Classes</a></li>
                    {% if has_namespaces %}
                        <li><a href="{{ path('namespaces.html') }}">Namespaces</a></li>
                    {% endif %}
                    <li><a href="{{ path('interfaces.html') }}">Interfaces</a></li>
                    <li><a href="{{ path('traits.html') }}">Traits</a></li>
                    <li><a href="{{ path('doc-index.html') }}">Index</a></li>
                    <li><a href="{{ path('search.html') }}">Search</a></li>
                </ul>
            </div>
        </div>
    </nav>
{% endblock %}

{% block leftnav %}
    <div id="api-tree"></div>
{% endblock %}

{% block control_panel %}
    <div id="control-panel">
        {% if project.versions|length > 1 %}
            <form action="#" method="GET">
                <select id="version-switcher" name="version">
                    {% for version in project.versions %}
                        <option value="{{ path('../' ~ version ~ '/index.html') }}" data-version="{{ version }}">{{ version.longname }}</option>
                    {% endfor %}
                </select>
            </form>
        {% endif %}
        <script>
            $('option[data-version="'+window.projectVersion+'"]').prop('selected', true);
        </script>
        <form id="search-form" action="{{ path('search.html') }}" method="GET">
            <span class="glyphicon glyphicon-search"></span>
            <input name="search"
                   class="typeahead form-control"
                   type="search"
                   placeholder="Search">
        </form>
    </div>
{% endblock %}

{% block footer %}
    <div id="footer">
        Generated by <a href="http://sami.sensiolabs.org/">Sami, the API Documentation Generator</a>.
        <address>(c) EC-CUBE</address>
    </div>
{% endblock %}

```

## ドキュメントの出力

以下のコマンドを実行することで、 API定義を出力可能です。

```
curl -O http://get.sensiolabs.org/sami.phar
php sami.phar parse sami_config.php
php sami.phar render sami_config.php
```

[こんな感じ](http://ec-cube.github.io/api-specifications/master/index.html) の API ドキュメントが出力できます！

2回目からは、前回の内容がキャッシュされているので、 update オプションで更新できます。

```
php sami.phar update sami_config.php
```