PHPでContentfulのRichTextをレンダリングする

備忘録です。

## 環境
Laravel 8

## 主な手順
1. composerでrich-text.phpを追加
1. NodeRendererInterfaceを実装したクラスを用意
1. RendererクラスにpushNodeRendererメソッドを使って[2]で用意したレンダラーを登録
1. Renderer::renderメソッドにAPIで取得したRichTextNodeを渡す

## composerでrich-text.phpを追加

公式から配布されているリッチテキストレンダリング用のライブラリをcomposerでインストール

```shell
composer require contentful/rich-text
```

## まずどうやってレンダリングされるのか
```php
// エントリーの取得
$entry = $this->client->getEntry('Entry ID');

// レンダラーのインスタンス化
$renderer  = new Renderer();
// 用意したレンダリングクラスを登録
$renderer->pushNodeRenderer(new BlogPostEmbeddedEntryBlock);
$renderer->pushNodeRenderer(new BlogPostEmbeddedEntryInline);

// レンダリング
return $renderer->render($entry->get('body'));
```

### $renderer->render()

```php:vendor\contentful\rich-text\src\Renderer.php
    public function render(NodeInterface $node, array $context = []): string
    {
        foreach ($this->nodeRenderers as $renderer) {
            if ($renderer->supports($node)) {
                return $renderer->render($this, $node, $context);
            }
        }

        throw new \InvalidArgumentException(\sprintf('Structured text renderer could not find NodeRenderer instance which supports node of class "%s".', \get_class($node)));
    }
```
`$this->nodeRenderers`には`NodeRendererInterface`を実装したクラス群が登録されており、
それぞれHTMLのNode(例外有)ごとに振る舞いが決定されています。
#### Renderer::NodeRenderers
```php
array:24 [▼
  0 => App\Contentful\RichText\NodeRenderer\BlogPostEmbeddedEntryInline {#440}
  1 => App\Contentful\RichText\NodeRenderer\BlogPostEmbeddedEntryBlock {#439}
  2 => Contentful\RichText\NodeRenderer\AssetHyperlink {#417}
  3 => Contentful\RichText\NodeRenderer\Blockquote {#418}
  4 => Contentful\RichText\NodeRenderer\Document {#419} // RichTextを取得するとこれになる
  // etc.
]
```
このクラス群を順に回し、`supports()` を実行し引数として渡されたNodeがレンダリング対象か判定し、
Trueだった場合、`render()`が実行され結果が返されます。

```php
$renderer->pushNodeRenderer(new BlogPostEmbeddedEntryBlock);
```
上記のように`Renderer::pushNodeRenderer`で登録した場合`array_unshift`で配列の頭に登録されるので
デフォルトで用意されたレンダラーより先に実行されます。

RichTextを`getEntry()`で取得するとDocumentクラスが結果として返ります。

```
Contentful\RichText\Node\Document {#378 ▼
  #content: array:8 [▼
    0 => Contentful\RichText\Node\Paragraph {#379 ▶}
    1 => Contentful\RichText\Node\Paragraph {#381 ▶}
    2 => Contentful\RichText\Node\Paragraph {#384 ▶}
    3 => Contentful\RichText\Node\EmbeddedEntryBlock {#386 ▶}
    4 => Contentful\RichText\Node\Paragraph {#389 ▶}
    5 => Contentful\RichText\Node\Paragraph {#395 ▶}
    6 => Contentful\RichText\Node\EmbeddedAssetBlock {#397 ▶}
    7 => Contentful\RichText\Node\Paragraph {#410 ▶}
  ]
}
```
Documentクラスは各Nodeを配列で持っており、`NodeRenderer\Document::render()`で`renderCollection()`を実行することで各Nodeのレンダリング結果を結合したものを返します。

```php:Controllerとか
return $renderer->render($entry->get('body'));
```


```php:vendor\contentful\rich-text\src\NodeRenderer\Document.php
    public function render(RendererInterface $renderer, NodeInterface $node, array $context = []): string
    {
        /* @var NodeClass $node */
        if (!$node instanceof NodeClass) {
            throw new \LogicException(\sprintf('Trying to use node renderer "%s" to render unsupported node of class "%s".', static::class, \get_class($node)));
        }

        return $renderer->renderCollection($node->getContent(), $context);
    }
```

```php:vendor\contentful\rich-text\src\Renderer.php
    public function renderCollection(array $nodes, array $context = []): string
    {
        return \implode('', \array_map(function (NodeInterface $node) use ($context): string {
            return $this->render($node, $context);
        }, $nodes));
    }
```

以上が一連の処理になります。

## NodeRendererInterfaceを実装したクラスを用意

基本的には既存のレンダラーの上書きになると思うので
`vendor\contentful\rich-text\src\`から対象のクラスをコピーして、`render()`の中身を変更すれば事足りると思います。
以下は`EmbeddedEntryInline`のレンダラーです。

```php:app\Contentful\RichText\NodeRenderer\BlogPostEmbeddedEntryBlock.php
<?php

// ファイルの位置は適当です
namespace App\Contentful\RichText\NodeRenderer;


use Contentful\RichText\Node\EmbeddedEntryInline;
use Contentful\RichText\Node\NodeInterface;
use Contentful\RichText\RendererInterface;
use Contentful\RichText\NodeRenderer\NodeRendererInterface;

class BlogPostEmbeddedEntryInline implements NodeRendererInterface
{

    /**
     * @inheritDoc
     */
    public function supports(NodeInterface $node): bool
    {
        return $node instanceof EmbeddedEntryInline;
    }

    /**
     * @inheritDoc
     */
    public function render(RendererInterface $renderer, NodeInterface $node, array $context = []): string
    {
        /* @var EmbeddedEntryInline $node */

        // Aタグにして返す
        $entry = $node->getEntry();
        return sprintf('<a href="%s">%s</a>', route('post', ['id' => $entry->getId()]), $entry->title);
    }
}

```

## 用意したクラスをRendererクラスに渡す
```php
$renderer = new Renderer();
$renderer->pushNodeRenderer(new BlogPostEmbeddedEntryBlock);
```

`__construct()`で複数一括登録もできます

```php
$renderer = new Renderer([
    new BlogPostEmbeddedEntryBlock,
    new BlogPostEmbeddedEntryInline
]);
```

## レンダリング実行
```php
return $renderer->render($entry->get('body'));
```

### Bladeを使う
```php
    public function render(RendererInterface $renderer, NodeInterface $node, array $context = []): string
    {
        /* @var EmbeddedEntryBlock $node */
        /* @var Entry $entry */
        $entry = $node->getEntry();
        return view('contentful.embedded_entry_block', compact('entry'));
    }
```