Featured image of post VercelでBEAR Sundayを動かしてみた

VercelでBEAR Sundayを動かしてみた

Twitter ツイート Hatena Bookmark ブックマーク

Vercelは、Next.jsなどのフロントエンドアプリケーションのproduction環境としてよく使われますが、実はPHPも動かすことができます。
vercel-community/php

PHPerとしては、何かPHPのアプリケーションをVercelで動かしたいと思い、今回はBEAR SundayをVercelで動かしてみました。

今回のサンプルについて

今回動かしたサンプルは以下のリポジトリです。
polidog/Polidog.VercelApp

package.jsonが必要

PHPのプロジェクトでも package.json が必要になります。これがないとデプロイ時にエラーが発生します。
engines だけ指定すれば大丈夫なので、以下のように package.json を用意します。

1
2
3
4
5
{
  "engines": {
    "node": "18.x"
  }
}

vercel.jsonを用意する

VercelでPHPを動かすために vercel.json が必要です。ここでは functions と routes の2つの項目を設定します。

functions

Serverless Functionsのパスを設定する項目で、ここでは api/index.php を指定します。
また、runtimeの指定もします。 [email protected] を指定すると、PHP 8.3系が利用できます。

詳しくは、vercel-community/phpのVersionsの項目を参照してください。

1
2
3
4
5
6
7
{
    "functions": {
    "api/index.php": {
      "runtime": "[email protected]"
    }
  },
}

routes

routes では、すべてのリクエストが api/index.php にルーティングされるように設定します。

1
2
3
4
5
{
  "routes": [
    { "src": "/(.*)",  "dest": "/api/index.php" }
  ]
}

var/tmpにファイルが書き込まれない問題について

Vercel上でBEAR Sundayを動かす際、 ver/tmp ディレクトリにファイルが書き込めない問題が発生しました。
BEAR Sundayはデフォルトで var/tmp/{context} に様々なファイルを書き込みまが、そのため NotWritableException が発生します。

1
Fatal error: Uncaught BEAR\AppMeta\Exception\NotWritableException: /var/task/user/var/tmp/hal-app in /var/task/user/vendor/bear/app-meta/src/Meta.php:30 Stack trace: #0 /var/task/user/vendor/bear/package/src/Injector.php(26): BEAR\AppMeta\Meta->__construct('Polidog\\VercelA...', 'hal-app', '/var/task/user') #1 /var/task/user/src/Injector.php(23): BEAR\Package\Injector::getInstance('Polidog\\VercelA...', 'hal-app', '/var/task/user') #2 /var/task/user/src/Bootstrap.php(29): Polidog\VercelApp\Injector::getInstance('hal-app') #3 /var/task/user/public/index.php(8): Polidog\VercelApp\Bootstrap->__invoke('hal-app', Array, Array) #4 /var/task/user/api/index.php(8): require('/var/task/user/...') #5 {main} thrown in /var/task/user/vendor/bear/app-meta/src/Meta.php on line 30

Vercelでは /tmp ディレクトリなら書き込めるので、コードを変更して対応します。

BEAR Sundayではどのようにファイルを書き込むディレクトリを決定しているのか?

BEAR Sundayでは、 BEAR\AppMeta\Meta クラスがファイルを書き込むディレクトリを決定します。
https://github.com/bearsunday/BEAR.AppMeta/blob/9361d5ecd33befbfbeb12ca52ba20d7de97433dd/src/Meta.php#L28

このMetaクラスのインスタンスは BEAR\Package\Injector::getInstance() で生成します。
https://github.com/bearsunday/BEAR.Package/blob/ced1e3bfaee1d7607b207bbbebd8b22ac282b450/src/Injector.php#L26

Injector::getInstance()src/Injector.php にあるInjectorクラスから実行されます。
そしてInjectorクラスは src/Bootstrap.php で定義されているBootstrapクラスから実行されています。

つまりMetaクラスを新たに作って、src/Injector.php を修正すれば var/tmp/{context}/tmp/{context} という形に変更できます。

実際に修正する

Vercel固有の問題なので、私は src/Vercel ディレクトリにVercel用の Bootstrap, Injector, Meta クラスを作成し、問題を解決しました。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?php

// src/Vercel/Bootstrap.php
namespace Polidog\VercelApp\Vercel;

use BEAR\Resource\ResourceObject;
use BEAR\Sunday\Extension\Application\AppInterface;
use Polidog\VercelApp\Module\App;
use Throwable;

final class Bootstrap
{
    public function __invoke(string $context, array $globals, array $server): int
    {
        $app = Injector::getInstance($context)->getInstance(AppInterface::class);
        assert($app instanceof App);
        if ($app->httpCache->isNotModified($server)) {
            $app->httpCache->transfer();

            return 0;
        }

        $request = $app->router->match($globals, $server);
        try {
            $response = $app->resource->{$request->method}->uri($request->path)($request->query);
            assert($response instanceof ResourceObject);
            $response->transfer($app->responder, $server);

            return 0;
        } catch (Throwable $e) {
            $app->throwableHandler->handle($e, $request)->transfer();

            return 1;
        }
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php

// src/Vercel/Injector.php
namespace Polidog\VercelApp\Vercel;

use BEAR\Package\Injector\PackageInjector;
use Ray\Di\AbstractModule;
use Ray\Di\InjectorInterface;
use Ray\PsrCacheModule\LocalCacheProvider;

final class Injector
{
    /** @codeCoverageIgnore */
    private function __construct()
    {
    }

    public static function getInstance(string $context): InjectorInterface
    {
        $appName = 'Polidog\VercelApp';
        $appDir = dirname(__DIR__, 2);
        $meta = new Meta($appName, $context, $appDir);

        $cacheNamespace = str_replace('/', '_', $appDir) . $context;
        $cache ??= (new LocalCacheProvider($meta->tmpDir . '/injector', $cacheNamespace))->get();

        return PackageInjector::getInstance($meta, $context, $cache);
    }

    public static function getOverrideInstance(string $context, AbstractModule $overrideModule): InjectorInterface
    {
        $appName = __NAMESPACE__;
        $appDir = dirname(__DIR__);

        return PackageInjector::factory(new Meta($appName, $context, $appDir), $context, $overrideModule);
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?php

// src/Vercel/Meta.php
namespace Polidog\VercelApp\Vercel;

use BEAR\AppMeta\AbstractAppMeta;
use BEAR\AppMeta\Exception\AppNameException;
use BEAR\AppMeta\Exception\NotWritableException;

final class Meta extends AbstractAppMeta
{
    public function __construct(string $name, string $context = 'app', string $appDir = '')
    {
        $this->name = $name;
        $this->appDir = $appDir ?: $this->getAppDir($name);
        $this->tmpDir = '/tmp/' . $context; // ここでVercelようにディレクトリを変更する。
        if (! file_exists($this->tmpDir) && ! @mkdir($this->tmpDir, 0777, true) && ! is_dir($this->tmpDir)) {
            throw new NotWritableException($this->tmpDir);
        }

        $this->logDir = $this->tmpDir . '/log/' . $context;
        if (! file_exists($this->logDir) && ! @mkdir($this->logDir, 0777, true) && ! is_dir($this->logDir)) {
            throw new NotWritableException($this->logDir); // @codeCoverageIgnore
        }
    }

    private function getAppDir(string $name): string
    {
        $module = $name . '\Module\AppModule';
        if (! class_exists($module)) {
            throw new AppNameException($name);
        }

        return dirname((string) (new \ReflectionClass($module))->getFileName(), 3);
    }
}

api/index.php

vercel.jsonfunctions の項目で指定した api/index.phpsrc/Vercel/Bootstrap を呼び出すようにします。

1
2
3
4
5
6
7
8
<?php
// api/index.php
declare(strict_types=1);

use Polidog\VercelApp\Vercel\Bootstrap;

require dirname(__DIR__) . '/autoload.php';
exit((new Bootstrap())(getenv('VERCEL_ENV') !== 'production' ? 'hal-app' : 'prod-hal-app', $GLOBALS, $_SERVER));

これでVercelでもBEAR Sundayが実行できるようになります。

prod問題について

通常の public/index.php では PHP_SAPI === 'cli-server' のチェックで hal-appprod-hal-app を決定していますが、 Vercelではビルトインサーバでphpを実行しているため、常に hal-app になってしまいます。

そこで api/index.php では VERCEL_ENV を利用するようにしました。
VERCEL_ENV には実行環境(production, preview, development)が入っているので、それを基に prod-hal-app を使用するかを適切に判断できます。

まとめ

  • PHPプロジェクトでも package.json が必要
  • vercel.json で functionsroutes の設定が必要
  • ./var/tmp は使えないので /tmp に書き込むように修正
  • prod環境かどうかの判定は VERCEL_ENV 環境変数を利用

最後に

まだデータベース(DB)周りは試していませんが、 pdo_pgsql は有効になっているので、VercelでPostgreSQL(Neon)を使えるかもしれません。
簡単なアプリケーションの運用には、Vercelが良い選択肢になりそうです。

comments powered by Disqus
Built with Hugo
テーマ StackJimmy によって設計されています。