💡

PHPでMicroCMSブログを作ってみる!

2023/02/23に公開

MicroCMSとは日本製のヘッドレスCMSになります。
https://microcms.io/

PHPのCMSと言えば、みなさんご存知WordPressですが、昨今ではApiベースのヘッドレスCMSというものが選択肢のひとつになってきました。
ただ、Zennの記事で「PHPでMicroCMSを使ってみた」みたいな記事が少なく、ほとんどの記事が「Jamstack」や「NextJS」などの、JS/TSでのMicroCMSの記事ばかりです・・・

どうしても「Webフロント=JS/TSフレームワーク」みたいなイメージがあるのですが、PHPerの自分としては、前から「PHPでも全然MicroCMSという選択肢ありだぜ!」という考えがあったので、今回試しにMicroCMS+PHPで簡単なブログアプリのお試し実装をしました。

https://github.com/takemo101/egg-microcms
https://egg-microcms-33ckyjcmyq-uc.a.run.app/

注釈! HTMLコーディングがクソなのは目をつぶってください・・・

利用パッケージ

今回は、普通にLaravelなどのメジャーなフレームワークではなく、今まで使った事ないパッケージも使ってみたかったので「自作フレームワーク+使った事ないパッケージ」という構成で開発してみました。

ということで、主に以下のPHPパッケージを利用しました。

パッケージ 説明
Egg 自作のオレオレWebフレームワーク
Latte テンプレートエンジン
CycleORM ORM(データベース操作)
MicroCMS PHP SDK MicroCMSのApiアクセス

Latte

Latteは安全性が高く、直感的に扱える構文が用意されていて、非常にテンプレートの作成が捗るエンジンになります。

https://latte.nette.org/ja/

以下のように、とてもシンプルなテンプレートを定義できます。

トップページ.latte.html
{layout '@layout.layout'}

{block content}
	<h1 n:block="title">Home</h1>

	<ul class="mx-auto mt-16 grid max-w-2xl grid-cols-1 gap-6 text-sm sm:mt-20 sm:grid-cols-2 md:gap-y-10 lg:max-w-none lg:grid-cols-2">
        <li n:foreach="$blogs as $blog" class="rounded-2xl border border-gray-200 p-8 bg-white">
            <a href="{route('blog.show', ['id' => $blog->id])}">
                {if $blog->eyecatch}
                    <img src="{$blog->eyecatch}" class="w-64 rounded mb-6">
                {/if}
                <h3 class="font-semibold text-gray-900">
                    {$blog->title}
                </h3>
            </a>
        </li>
    </ul>
{/block}
記事詳細.latte.html
{layout '@layout.layout'}
{block title}{$blog->title}{/block}
{block content}

    <div class="flex items-center md:justify-start mb-3 lg:mb-5 text-xs md:text-sm leading-6 -tracking-[0.3px] lg:tracking-normal">
        <a href="{route('home')}" class="text-gray-750">Home</a>
        {if $blog->category}
            <i class="fas fa-chevron-right text-gray-750 mx-[7px]" aria-hidden="true"></i>
            <a href="{route('category.show', ['id' => $blog->category->id])}" class="text-gray-750">{$blog->category->name}</a>
        {/if}
        <i class="fas fa-chevron-right text-gray-750 mx-[7px]" aria-hidden="true"></i>
        <a href="javascript:void(0)" class="text-black">{$blog->title}</a>
    </div>

    <div class="mx-auto max-w-2xl text-center">
        <h1 class="text-3xl font-medium tracking-tight text-gray-900">
            {$blog->title}
        </h1>
        <div class="m-3">
            <span class="blog-date">{$blog->publishedAt|date:'Y.m.d'}</span>
        </div>
        {if $blog->eyecatch}
            <img src="{$blog->eyecatch}" class="rounded">
        {/if}
    </div>

    <div class="mx-auto mt-16 w-full">
        <section class="overflow-hidden rounded-3xl md:p-12 p-6 shadow-lg shadow-gray-900/5 bg-white">
            <article class="prose">
                {$blog->content|noescape}
            </article>
        </section>
    </div>
{/block}

CycleORM

CycleORMは、Spiral Frameworkで採用されているORMになります。
普段業務でPHPを使うときは、よくLaravelを使って開発をするので、Eloquentを多用しています。
ただ、Eloquentモデルは、ひとつのクラスでの責務が多すぎるし(データクラスやデータベース操作など責務が色々・・・)タイプヒントが中々出せなかったり、個人的に不満がありました。

CycleORMを利用すると、データベース操作・データクラスなどの責務がある程度分離できて、テストがしやすい印象があったので、今回使ってみました。

https://cycle-orm.dev/

以下のようなEntityクラスを定義しました。

Blog.php
<?php

namespace App\Entity;

use App\Repository\BlogRepository;
use Cycle\Annotated\Annotation\Entity;
use Cycle\Annotated\Annotation\Column;
use Cycle\Annotated\Annotation\Relation\BelongsTo;
use DateTimeInterface;

#[Entity(
    role: 'blog',
    repository: BlogRepository::class,
    table: 'blogs',
)]
class Blog
{
    public function __construct(
        #[Column(type: 'string', primary: true)]
        public string $id,
        #[Column(type: 'text', nullable: true)]
        public ?string $eyecatch,
        #[Column(type: 'string')]
        public string $title,
        #[Column(type: 'longText')]
        public string $content,
        #[Column(type: 'timestamp', name: 'created_at')]
        public DateTimeInterface $createdAt,
        #[Column(type: 'timestamp', name: 'updated_at')]
        public DateTimeInterface $updatedAt,
        #[Column(type: 'timestamp', name: 'published_at')]
        public DateTimeInterface $publishedAt,
        #[BelongsTo(target: Category::class, nullable: true)]
        public ?Category $category = null,
    ) {
        //
    }
}
Category.php
<?php

namespace App\Entity;

use App\Repository\CategoryRepository;
use Cycle\Annotated\Annotation\Entity;
use Cycle\Annotated\Annotation\Column;
use DateTimeInterface;

#[Entity(
    role: 'category',
    repository: CategoryRepository::class,
    table: 'categories',
)]
class Category
{
    public function __construct(
        #[Column(type: 'string', primary: true)]
        public string $id,
        #[Column(type: 'string')]
        public string $name,
        #[Column(type: 'timestamp', name: 'created_at')]
        public DateTimeInterface $createdAt,
        #[Column(type: 'timestamp', name: 'updated_at')]
        public DateTimeInterface $updatedAt,
    ) {
        //
    }
}

CycleORMだと、アノテーションを使えたりするので、非常にシンプルにデータクラスを実装できます!

注釈! 今回テスト実装なのでプロパティをpublicしているのですが、本来はprivateにした方が良いので注意してください

MicroCMS PHP SDK

MicroCMSへのApiアクセスを簡単にしてくれるパッケージです。

https://github.com/microcmsio/microcms-php-sdk

以下のような設定をすれば、すぐ利用できます。

<?php

use Microcms\Client;

// クライアントのApi設定をする
$client = new Client(
    config('setting.microcms.domain'), // MicroCMS Api サブドメイン
    config('setting.microcms.api-key'), // MicroCMS Api キー
),

// 記事のリストを取得する
$content = $client->list('blogs');

SSG or SSR

記事内容の出力については、SSG(静的サイトジェネレート)にするかSSR(サーバーサイドレンダリング)にするか悩みました。
今回、更新頻度が少ないブログサイトを想定しているので、テンプレートレンダリングでのSSGにしても良かったのですが、PHPをフルに利用できるようなサイトにしたかったので、SSRにしました。

ただ、ページにアクセスするたびにMicroCMSのApiにアクセスして、そのデータを直接テンプレートで表示するようなサイトも嫌だなーと思っていたので、以下のような設計を考えました。

  1. MicroCMSの公開記事データを、SQLiteのデータベースにインポートする。
  2. 記事を表示する時に、事前にSQLiteにインポートしたデータを参照して、テンプレートで表示する。
  3. 記事データが公開された場合は、再度SQLiteのデータベースにインポートする。

つまり、SSGのように記事が更新されたら静的HTMLを全て出力するように、記事が更新されたらデータベースにデータをインポートして、SSRでデータベースのデータを表示するシステムにしました。
更新が多いサイトだと、この方法では厳しいと思います(笑)

設置サーバー

システムの設置先サーバーをどうするかについてですが、格安のLolipopやXserverでも良かったのですが、テスト実装だし、あまりお金をかけたくなかったので、GoogleCloudRunでシステムを設置することにしました。
CloudRunへのデプロイについては、Github Actionsのワークフローで行なっています。

以下は、ワークフローの定義となります。

name: Cloud Run Deploy

on:
  repository_dispatch:
    types: [update_post]

env:
  GCP_PROJECT: ${{ secrets.GCP_PROJECT }}
  GCP_SERVICE: ${{ secrets.GCP_SERVICE }}
  GCP_REGION: us-central1
  MICROCMS_DOMAIN: ${{ secrets.MICROCMS_DOMAIN }}
  MICROCMS_API_KEY: ${{ secrets.MICROCMS_API_KEY }}
  DOTENV_PATH: ./.gcr/.deploy.env

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v2
        with:
          fetch-depth: 0
      - run: git checkout main

      ## ソースからのデプロイになり.コンテナの細かい設定はできないので
      ## .envファイルを書き換えてコンテナをビルドするようにした
      - name: Copy Environment
        run: |
          cp ./.gcr/.example.env $DOTENV_PATH
          sed -i -e "s/=\${MICROCMS_DOMAIN}/=${MICROCMS_DOMAIN}/g" ${DOTENV_PATH}
          sed -i -e "s/=\${MICROCMS_API_KEY}/=${MICROCMS_API_KEY}/g" ${DOTENV_PATH}

      - name: GCloud Auth
        uses: 'google-github-actions/auth@v1'
        with:
          credentials_json: '${{ secrets.GCP_SA_KEY }}'

      - name: Deploy to Cloud Run
        id: deploy
        uses: google-github-actions/deploy-cloudrun@v1
        with:
          service: ${{ env.GCP_SERVICE }}
          project_id: ${{ env.GCP_PROJECT }}
          region: ${{ env.GCP_REGION }}
          source: ./

      - name: Show Output
        run: echo ${{ steps.deploy.outputs.url }}

記事公開タイミングでのデータ更新について

記事が公開されたタイミングでのデータ更新をどうするか?についてですが、MicroCMSではWebhookを設定できるので、記事公開タイミングでのデータ更新処理を簡単にフックすることができます。

今回は、MicroCMSの管理画面から、GithubのWebhookを設定して、記事公開タイミングでGithub Actionsのワークフローでデータ更新処理&CloudRunへのデプロイを実行するようにしました。

開発してみての感想

PHPでもMicroCMSいけるやん!
って思いました(笑)

私自身、普段使っている技術が非常にレガシーなものになるので、最近のJamstackやフロントフレームワークを使いこなせないというのもあるのですが、成熟した言語であるPHPを利用すると、学習コスト低めでサクッとページが作成できたりします。
なので、MicroCMSなどのヘッドレスCMSを利用する場合、PHPという選択肢は全然アリだと思いました。

開発内容については、結構無理やりな実装方法ではあったかなーと思うのですが、みなさんあまり利用しないであろう、LatteやCycleORMを紹介できたので、開発としては満足です。

今回説明の中に入れたソースコードの内容についてですが、部分的な内容だったので非常にわかりにくかったと思います(申し訳ない)、、、興味のある方は是非リポジトリを見てください。

ありがとうございました。

Discussion