Grav CMS の Flex Objects で、ミニブログを作る

## GravとFlex Objectsについて

[Grav](https://getgrav.org/)とは、PHPのSymfony製のflat-file CMSです。

MySQLなどのRDBを使わず、マークダウンファイルだけでサイトが作れます。
その気軽さから、大変評価の高いCMSのひとつです。

なお、静的サイトジェネレータではなく、動的サイトであるため、記事の予約投稿など、動的処理が可能です。

一方、RDBを使わないため、1000ページを超えるようなサイトだと、実用に向かなくなるそうです。

このデメリットを克服するため（だと思うのですが）、json やyaml でデータ管理できるようにしたのが、Flex Objects というプラグインのようです。

作者によれば、Flex Objects を使えば、[5万ページ以上でも使える](https://getgrav.org/blog/grav-1.7-released) そうです。

Flex Objects は、Grav 1.7から追加されました。

今回は、このFlex Objects を使って、ミニブログ（microblog）を作りたいと思います。

## 環境

Grav 1.7.46
Flex Objects v1.3.6

## 参考サイト

Gravについて：

- [10分で理解する Grav](https://qiita.com/Chanmoro/items/46b496091082be45e44c)
- [Grav でサイトを作ろう](https://grav.maruyuki.org/)

Flex Objectsについて：

- [Flex objects を使ってみた。- GRAV -](https://www.mayonakanouta.com/gravcms/grav15)
- [公式ドキュメント](https://learn.getgrav.org/17/advanced/flex)

Flex Objectでblogを作っている例：

- [grav-plugin-news](https://github.com/bitstarr/grav-plugin-news)

## プラグインをつくる

ミニブログ用の独自プラグインをつくります。
今回は、

- 独自プラグイン名を`mblo`にし、
- Flex Directory名を`microblog`にしました

なお、[Flex Directory](https://learn.getgrav.org/17/advanced/flex/using/directory)というのは、Flex Objectsを使うときの、データのまとまりのようなイメージです。

手順は、[公式ドキュメント](https://learn.getgrav.org/17/plugins/plugin-flex)のとおりです。

プラグインをつくるには、CLIから入力が必要です。
SSH接続していないレンタルサーバーの場合、いったんローカル環境でプラグインを作成してから、FTPなどでアップロードすることになるのではないかと思います。

```bash
bin/gpm install devtools
bin/plugin devtools new-plugin
```

これを実行すると、いろいろ聞かれますが、とりあえず、以下2点に気をつける必要があります。

- オプションでflexを選択することと、
- ストレージはfolderを選択すること

※ストレージは、folderではなく、fileを選んでも良いかもしれません。
しかしsimpleを選ぶと、画像のアップロードができない（かもしれない）ので、注意してください。
（厳密に言えば、このあと操作する`use/plugins/【独自プラグイン】/blueprints/flex-objects/【Flex Directory】.yaml` ファイルの、form.fields.images.destinationに、保存先ディレクトリを指定すれば、画像をアップロードできなくはないのかもしれません。
なんとなく、推奨されていなさそうだったので、今回はsimpleをさけました。）

その後、できた独自プラグインのディレクトリにcdして、`composer update` してください。

## 必要な入力フィールドを考える

ミニブログなので、投稿本文は必須です。
将来的にはmarkdownにしたくなるかもしれませんが、難しくなりそうだったので、今回はプレーンなテキストエリアで作ります。

また、投稿日時は、自動入力したいです。
画像も、ツイッター（X）みたいに4枚以内くらいにはアップロードしたいです。
一方、各投稿にタイトルやカテゴリー分けは不要です。

よって、以下のようなフィールドを想定しました。

```yaml
form:
    validation: loose
    fields:
        log:
            type: textarea
            label: '投稿'
            validate:
                required: true
        time:
            type: datetime
            label: '投稿日'
            format: 'Y-m-d H:i:s'
            data-default@: '\Grav\Plugin\【独自プラグイン名】Plugin::currentTimeDataDefault'
            validate:
                required: true
        images:
            type: file
            label: '画像'
            multiple: true
            limit: 4
            accept:
                - image/*
  
```

Gravでは、設定をyamlファイルでするのですが、formの設定については、[公式ドキュメント](https://learn.getgrav.org/17/forms/blueprints/advanced-features)が参考になります。

`form.fields.time.data-default@` は、投稿日時の自動入力に必要です。
独自プラグインのPHPファイル（`user/plugins/【独自プラグイン名】/【独自プラグイン名】.php`）に、以下のような関数を追加しておきます。
現在時刻を返すだけの関数ですが、data-default@に設定しておくと、デフォルト値として自動入力してくれます。

```php
public static function currentTimeDataDefault(): string
{
    return date('Y-m-d H:i:s');
}
```

## blueprintを作る

上記のformを含む設定ファイルを作成します。
このような設定ファイルを、Gravではblueprintと呼んでいます。

設定ファイルは、独自プラグインディレクトリの、`blueprints/flex-objects/`フォルダに入っています。
とても長いですが、ほとんどは自動で出力されるものです。

```yaml
title: 'Microblog'
description: 'Gravでつくるミニブログ'
type: flex-objects

# Flex Configuration
config:
    # Administration Configuration
    admin:
        # Admin router (optional)
        router:
            path: '/microblog'

        # Admin menu (optional)
        menu:
            list:
                route: '/microblog'
                title: 'Microblog'
                icon: fa-address-card-o
                # Authorization to collection admin
                authorize: ['admin.microblog.list', 'admin.super']
                # Priority -10 .. 10 (highest goes up)
                priority: 2

        # Admin template type / folder
        template: default

        # Permissions
        permissions:
            # Primary permissions
            admin.microblog:
                type: crudpl
                label: 'Microblog'

        # List view
        list:
            title: log
            fields:
                time:
                log:
                    link: edit
                #images:
            options:
                per_page: 10
                order:
                    by: time
                    dir: desc

        # Edit View
        edit:
            title:
                template: ''

        # Preview View
        preview:
            enabled: false
            route:
            #template: '/plugins/flex-objects/directory:contacts'

        # Data Export
        export:
            enabled: true
            method: 'jsonSerialize'
            formatter:
                class: 'Grav\Framework\File\Formatter\YamlFormatter'
            filename: 'microblog'

    # Site Configuration
    site:
        templates:
            collection:
                # Lookup for the template layout files for collections of objects
                paths:
                    - 'flex/{TYPE}/collection/{LAYOUT}{EXT}'
            object:
                # Lookup for the template layout files for objects
                paths:
                    - 'flex/{TYPE}/object/{LAYOUT}{EXT}'
            defaults:
                # Default template {TYPE}; overridden by filename of this blueprint if template folder exists
                type: 'microblog'
                # Default template {LAYOUT}; can be overridden in render calls (usually Twig in templates)
                layout: default

    # Data Configuration
    data:
        # Object class to be used, allowing custom methods for the object
        object: 'Grav\Plugin\Mblo\Flex\Types\Microblog\MicroblogObject'
        # Collection class to be used, allowing custom methods for the collections
        collection: 'Grav\Plugin\Mblo\Flex\Types\Microblog\MicroblogCollection'
        # Index class to be used, works as a quick database-like lookup index
        index: 'Grav\Common\Flex\Types\Generic\GenericIndex'
        storage:
            # Storage class, use single file storage (does not support images and assets)
            class: 'Grav\Framework\Flex\Storage\FolderStorage'
            options:
                formatter:
                    # File formatter class, in this case the file is stored in markdown
                    class: 'Grav\Framework\File\Formatter\JsonFormatter'
                # JSON file where all the objects will be stored
                folder: user-data://microblog
        ordering:
            time: DESC 
        search:
            # Search options
            options:
                contains: 1
            # Fields to be searched
            fields:

form:
    validation: loose
    fields:
        log:
            type: textarea
            label: '投稿'
            validate:
                required: true
        time:
            type: datetime
            label: '投稿日'
            format: 'Y-m-d H:i:s'
            data-default@: '\Grav\Plugin\mbloPlugin::currentTimeDataDefault'
            validate:
                required: true
        images:
            type: file
            label: '画像'
            multiple: true
            limit: 4
            accept:
                - image/*
```

flex-objectsのyaml設定については、[公式ドキュメント](https://learn.getgrav.org/17/advanced/flex/custom-types/blueprint)が参考になります。
公式ドキュメントによると、細かい設定もできるようです。

## 管理画面で確認してみる

この時点で、管理画面から操作できるようになります。
左側のメニューリストに、カードのようなアイコンが増えているはずなので、そこから操作できます。

初期設定のままだと、日付表示が、外国の表示形式なので、日本で馴染み深い`年-月-日 時間:分` にします。
まず、`user/plugins/【独自プラグイン名】/【独自プラグイン名】.php`を開きます。
この中の、`getSubscribedEvents()`という関数の戻り値が配列になっているので、この配列の中に、以下を追記します。

```php
'onTwigTemplatePaths' => ['onTwigTemplatePaths', 1]
```

そして、`onTwigTemplatePaths()`関数を作ります。

```php
public function onTwigTemplatePaths() : void
{
  $this->grav['twig']->twig_paths[] = __DIR__ . '/templates';
  if ($this->isAdmin())
  {
    $this->grav['twig']->twig_paths[] = __DIR__ . '/admin/templates';
  }
}

```

なお、`/templates`フォルダは、後ほど作ります。

次に、`user/plugins/【独自プラグイン】/admin/templates/forms/fields/datetime/edit_list.html.twig`ファイルを作ります。
このtwigファイルには、以下を記入します。

```twig
{{ value ? value|date(Y-m-d G:i)|e }}
```

おそらくこれで、adminプラグインのtwigファイルが上書きされ、日付が日本の表示になるはずです。

## ミニブログ用のページを準備する

管理画面から、ページを追加します。
ページの追加方法は、[記事を書く](https://grav.maruyuki.org/write-with-grav/write-an-article)などが参考になります。
frontmatterを書くのですが、Flex Objectsの場合、以下のように書きます。

```markdown
---
title: home
flex:
    directory: microblog
    collection:
        title: '{{ directory.title }}'
        layout: default
        object:
            layout: default
    object:
        title: 'object'
        layout: default
---
# flex microblogging
```

frontmatterの書き方は、[公式ドキュメント](https://learn.getgrav.org/17/advanced/flex/using)を参考にしてください。

## Twigを用意する

先ほど書いたfrontmatterで、layoutをdefaultにしていましたが、このdefaultのレイアウトを作成します。

collection（記事一覧）用のdefaultレイアウトと、object（個別の記事）用のdefaultレイアウトの、2種類あります。

作成場所は、`user/plugins/【独自プラグイン名】/templates/flex/【ディレクトリ名】/collection/defaul.html.twig`と、
`user/plugins/【独自プラグイン名】/templates/flex/【ディレクトリ名】/object/defaul.html.twig`の2ヶ所です。

Twigでは、いろいろできそうですが、さしあたり日付降順表示となるように、以下のように書きました。

```twig
<div style="max-width:30rem;margin-inline:auto;">
{%- for object in collection -%}
  <div style="margin-block:3rem;padding:1rem;border:thin solid #aaa;">
  {%- render object layout: object_layout with object_context -%}
  <a href="./id:{{ object.getStorageKey() }}">個別記事ページへ</a>
  </div>
{%- endfor -%}
</div>
```

```twig
<div>
<p style="font-size:1.125rem;">{{ object.log|nl2br }}</p>

{%- if object.images -%}
<div style="display:flex;flex-wrap:nowrap;justify-content:center;gap:5px;">
  {% for image_data in object.images %}
    {% set image = object.media.all[image_data.name] %}
  <div style="max-height:200px;">
    {{ image.cropResize(200, 100).html()|raw }}
  </div>
  {% endfor %}
</div>
{%- endif -%}
<p style="border-top:thin solid #aaa;color:#777;font-size:.95;text-align:right;margin:0;">
  {{ object.time }}
</p>
</div>
```

この状態で、先ほど作成したページにアクセスすると、ミニブログが一覧表示されるはずです。

## 感想

ここまでで、とりあえず、最低限のものはできたと思います。
が、リンクを張ったり、ページネーションを設定したりと、まだまだ不完全です。

また、Generic indexというClassをextendすることで、データを探すときの索引が作れるようです。
データが多くなれば、仕方ないことかもしれませんが、調整しないといけないことがどんどん増えていきます。

他のサイトでも指摘されていることですが、Gravは、基本的には、1000ページ以下の、小規模なサイトの運用に向いている気がします。
どうしてもGravで大量のデータが扱いたい場合のみ、Flex Objectsを使うのがよいと思いました。