PowerCMS Xを動作させるためのnginx設定例

PowerCMS Xをnginx + php-fpm環境で動作させるための設定を作成しました。2018年12月に私のブログで公開した情報を2023年1月に検証・アップデートしたものです。

nginxの設定を書く目的は以下の2点です。

1. 拡張子が.phpへのアクセスをphp-fpmで処理させるため（＝ミドルウェアの設定）
1. nginxは通常.htaccessを解釈しないことから、PowerCMS Xで必要なリライト処理をnginxの設定に書く必要があるため（ダイナミック生成やライブプレビュー等使用時）

## 前提

1. PowerCMS Xのコードは`/var/www/vhosts/example.com/app`に配置する
1. RESTfulAPIを利用可能にする
1. 次の場合は`/pt-view.php`でダイナミック生成を行う
    - Cookieに`pt-live-preview`を名前とする情報がある場合（LivePreviewプラグイン）
    - HTTPリクエストメソッドが`GET`以外の場合
    - クエリストリングがある場合
1. `pt-view.php`以外のPHPを「サイト・パス」に配置しても実行しない
1. 3\.〜4\.に該当しない場合は次のように処理をする
    - 静的ファイルが存在する場合はそのファイルを配信する
    - 静的ファイルが存在しない場合は`/pt-view.php`でダイナミック生成を行う

## nginx設定例

```nginx
server {
    server_name example.com;
    root /var/www/vhosts/example.com/public_html;

    listen 443 ssl http2;
    listen [::]:443 ssl http2;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

    location /app {
        alias /var/www/vhosts/example.com/app;

        location ~ /app/(?!api|assets|plugins|theme|index\.php|pt-.*\.php|favicon).* {
            return 404;
        }

        location ^~ /app/api {
            fastcgi_split_path_info ^/app/api/(.+?\.php/)?(.*)$;
            include fastcgi_params;
            fastcgi_param SCRIPT_FILENAME /var/www/vhosts/example.com/app/api/index.php;
            fastcgi_param REDIRECT_URL $fastcgi_path_info;
            fastcgi_pass unix:/run/php-fpm/www.sock;
        }

        location ~ \.php$ {
            include fastcgi_params;
            fastcgi_param SCRIPT_FILENAME $request_filename;
            fastcgi_param PATH_INFO $fastcgi_path_info;
            fastcgi_buffering off;
            fastcgi_intercept_errors off;
            fastcgi_pass unix:/run/php-fpm/www.sock;
        }
    }

    location / {
        set $use_ptview 0;

        if ($http_cookie ~* "pt-live-preview") {
            set $use_ptview 1;
        }
        if ($request_method !~ "GET") {
            set $use_ptview 1;
        }
        if ($query_string ~ "=") {
            set $use_ptview 1;
        }

        if ($use_ptview) {
            rewrite ^ /pt-view.php last;
        }

        index index.html;
        try_files $uri $uri/ /pt-view.php;
    }

    location ~ pt-view\.php$ {
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_pass unix:/run/php-fpm/www.sock;
    }

    # プラグインのappディレクトリにあるプログラムを利用する例
    location = /pt-members.php {
        rewrite . /app/plugins/Members/app/pt-members.php last;
    }
}
```

### 気を付けるポイント

管理画面のPHP設定では以下に気を付けます。

- fastcgi_buffering off;
- fastcgi_intercept_errors off;

上記がオンの場合、PowerCMS X システムチェック（`pt-check.php`）で「システム出力バッファをフラッシュ(flush())することができませんでした。」の警告が表示されました。また、php-fpmのpm(process manager)の設定値によっても表示されることがありました。

![PowerCMS X システムチェックにおいて問題がない状態になった画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6914/321560cb-82f9-6aa8-5880-f15feff04d40.png)

## 検証

### 検証環境

- Amazon Linux 2
- nginx 1.22.1
- PHP 8.0.25
- PowerCMS X 3.x
    - API・LivePreviewプラグインを有効化
    - LivePreviewプラグインの「挿入するHTML」に`<mt:livepreviewdate format_ts="Y-m-d H:i:s">`を含むHTMLを設定

検証のために`expose_php = On`のままにしてあります。検証終了後は`Off`にします。

### 検証内容

1. `/app/index.php`で管理画面が表示されること
1. `/app/pt-check.php`で出力バッファのフラッシュに関するエラーが表示されないこと
1. `https://example.com/app/api/index.php/v1/0/entry/list`（RESTfulAPI）で記事一覧JSONが表示されること
1. 記事ページを開くと表示されること（ステータスコード`200`・レスポンスヘッダーに`x-powered-by: PHP/8.x.y`がないこと）
1. 記事ページのURLに`?test=1`を付けて開くと表示されること（ステータスコード`200`・レスポンスヘッダーに`x-powered-by: PHP/8.x.y`があること）
1. システムスコープのライブプレビューを設定後に記事ページを開き、設定した日時が表示されること
1. ワークスペースのライブプレビューを設定後（システムスコープと異なる日時にする）に記事ページを開き、設定した日時が表示されること
1. URLマップでファイル出力を「ダイナミック」にしたページが表示されること
1. `/app/config.json`にアクセスできないこと（`pt-check.php`でも確認できる）

## [非公式] pt-view.phpのカスタマイズ

※PowerCMS X ver.3.2から同等の機能が製品に実装されました。

> 環境変数「dynamic_ignore_scope」を追加しました。ダイナミック・パブリッシングでpt-view.phpのworkspace_idを考慮せずにルート相対パスからURLを解決します。

2023年1月現在、非公開ページのプレビューやLivePreviewプラグイン使用時などダイナミック・パブリッシングが必要な場合はワークスペース毎に`pt-view.php`と`.htaccess`を用意する必要があります。しかしnginxでは通常`.htaccess`が利用できないためワークスペース毎に`location`ディレクティブを使用して設定を追記していく必要がありますがそれは手間なため、一つの`/pt-view.php`でまかなうことができないか検討してみました。その結果`pt-view.php`（Bootstrapper）のテンプレートをカスタマイズすることで実現できました。

カスタマイズした部分を抜粋して掲載します。

```php
if ( $app->request_method != 'GET' || $app->query_string ) {
    $app->force_dynamic = true;
}

// ここからカスタマイズ
require_once '<mt:var name="application_dir">/db-config.php';
require_once '<mt:var name="application_dir">/lib/PADO/class.pado.php';
$pado = new PADO();
$pado->prefix = 'mt_';
$pado->colprefix = '<model>_';
$pado->init();
$request_uri = $app->base . $app->request_uri;
$terms = [
    'url' => $request_uri,
    'delete_flag' => [ 'IN' => [ 0, 1 ] ],
];
$args = [
    'sort' => [
        'delete_flag' => 'ascend',
        'id' => 'descend'
    ],
];
$urlinfos = $pado->model( 'urlinfo' )->load( $terms, $args, 'id,workspace_id' );
if ( $urlinfos ) {
    $workspace_id = (int) $urlinfos[0]->workspace_id;
}
// ここまでカスタマイズ

require_once LIB_DIR . 'Prototype' . DS . 'class.PTViewer.php';
```

これで先に記述した検証内容が正しく動作することを確認していますが、URLオブジェクトの状態によっては上手くいかないURLがでるかもしれません。また、いずれはカスタマイズすることなく一つの`pt-view.php`で済むように改良されているかもしれません。

## 参考文献

- [nginx実践入門](https://amzn.to/2GdfEq6)