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.json
の functions
の項目で指定した api/index.php
で src/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-app
か prod-hal-app
を決定していますが、
Vercelではビルトインサーバでphpを実行しているため、常に hal-app
になってしまいます。
そこで api/index.php
では VERCEL_ENV
を利用するようにしました。
VERCEL_ENV
には実行環境(production, preview, development)が入っているので、それを基に prod-hal-app を使用するかを適切に判断できます。
まとめ
- PHPプロジェクトでも
package.json
が必要
- vercel.json で
functions
と routes
の設定が必要
./var/tmp
は使えないので /tmp
に書き込むように修正
- prod環境かどうかの判定は
VERCEL_ENV
環境変数を利用
最後に
まだデータベース(DB)周りは試していませんが、 pdo_pgsql は有効になっているので、VercelでPostgreSQL(Neon)を使えるかもしれません。
簡単なアプリケーションの運用には、Vercelが良い選択肢になりそうです。