CakePHP4でJavaScriptからfetchされた複数ファイルを受け取ってみる
SPA(Single Page Application)でグループウェアを作っていて複数ファイルをアップロードする必要が出たので調査してみようと思います。グループウェアのフロントエンドはAlpine.jsで作成しているのでそれも使っていこうと思います。

フロントエンドの実装をします
ひとまずComposerでアプリを作成しましょうかね。
composer self-update
composer create-project --prefer-dist cakephp/app:"4.*" test-appHTMLは実験なのでレイアウトとか気にせず作ります。
<?php
$this->Html->script('https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js', [
'block' => true,
'defer',
])
?>
<div x-data="data()">
<input class="form-control" type="file" multiple id="files" @change="files = Object.values($event.target.files)">
<span x-text="files ? files.map(file => file.name).join(', ') : 'Choose multiple files...'"></span>
<button class="btn btn-primary" @click="send">送信</button>
</div>
<script>
let data = function() {
return {
files: null,
send: function() {
const data = new FormData();
this.files.forEach(file => {
data.append("files[]", file, file.name);
});
fetch('<?= $this->Url->build('/test/upload') ?>', {
method: 'POST',
headers: {
'X-CSRF-Token': '<?= $this->getRequest()->getAttribute('csrfToken') ?>',
'X-Requested-With': 'XMLHttpRequest'
},
body: data
})
.then(res => {
if (res.ok) {
alert('OK');
} else {
alert('NG');
}
});
}
};
};
</script>バックエンドの実装をします
Controllerを実装します。情報が取れているかをLogで確認しようと思うので、必要と思うところにLogを埋め込んでいきます。
<?php
namespace App\Controller;
use Cake\Log\Log;
use Cake\Http\Client;
use Cake\Routing\Router;
class TestController extends AppController
{
public function initialize(): void
{
parent::initialize();
}
public function index()
{
return $this->render("index");
}
public function upload()
{
$files = $this->request->getData('files');
Log::info('upload:' . print_r($files, true));
if (!file_exists(WWW_ROOT . 'temp')) {
mkdir(WWW_ROOT . 'temp', 0777);
}
$temp = [];
foreach ($files as $file) {
$filePath = WWW_ROOT . 'temp' . DS . base64_encode($file->getClientFilename());
$file->moveTo($filePath);
$temp[] = fopen($filePath, 'r');
}
$httpClient = new Client();
$httpClient->post('http://localhost:8888/test/api', ['files' => $temp]);
return $this->response->withStatus(200);
}
public function api()
{
$files = $this->request->getData('files');
Log::info('api:' . print_r($files, true));
foreach ($files as $file) {
Log::info('base64_decode: ' . base64_decode($file->getClientFilename()));
}
return $this->response->withStatus(200);
}
}APIへの転送もできるか確認したかったのでapiメソッドも追加しました。はじめpost先を単純に”/test/api”にしていて30秒ほど固まったので何かなと思ったら、同じプロセスなもんで前回のリクエストを処理するまで次のリクエストが開始されなくて、post先でpost元が終わるのを待ってる状態になっていました。同じソースコードを別ポートで起動して回避しました。サーバーによっては同時アクセスとか普通に許容してると思うのですが、「bin/cake server」で立ち上げた簡易サーバーだったので。。
ファイル名はuploadで受けた方は日本語が正しく取れていたのですが、apiの方はなぜか日本語がアルファベット化していた(文字化けの方が理解できるのですが。。)のでbase64でエンコードしてしまいました。使うときにデコードすれば戻せるので。
# 抜粋
2024-01-03 04:10:06 info: upload:Array
(
[0] => Laminas\Diactoros\UploadedFile Object
[clientFilename:Laminas\Diactoros\UploadedFile:private] => スクリーンショット (1).png
2024-01-03 04:10:07 info: api:Array
(
[0] => Laminas\Diactoros\UploadedFile Object
[clientFilename:Laminas\Diactoros\UploadedFile:private] => sukurinshotto (1).png動作確認
サーバーを2つ立ち上げて画面を表示し画像を複数送信してみたところすぐにOKと表示されたので、たぶん大丈夫でしょう。
bin/cake server
# 別のコマンドプロンプトで
bin/cake server -p 8888
ログを見てみると、uploadとapiでそれぞれファイルが取れているようでした。
2024-01-03 04:30:40 info: upload:Array
(
[0] => Laminas\Diactoros\UploadedFile Object
(
[error:Laminas\Diactoros\UploadedFile:private] => 0
[file:Laminas\Diactoros\UploadedFile:private] => C:\temp\php1EB6.tmp
[moved:Laminas\Diactoros\UploadedFile:private] =>
[stream:Laminas\Diactoros\UploadedFile:private] =>
[size:Laminas\Diactoros\UploadedFile:private] => 1894639
[clientFilename:Laminas\Diactoros\UploadedFile:private] => スクリーンショット (1).png
[clientMediaType:Laminas\Diactoros\UploadedFile:private] => image/png
)
[1] => Laminas\Diactoros\UploadedFile Object
(
[error:Laminas\Diactoros\UploadedFile:private] => 0
[file:Laminas\Diactoros\UploadedFile:private] => C:\temp\php1EB7.tmp
[moved:Laminas\Diactoros\UploadedFile:private] =>
[stream:Laminas\Diactoros\UploadedFile:private] =>
[size:Laminas\Diactoros\UploadedFile:private] => 183592
[clientFilename:Laminas\Diactoros\UploadedFile:private] => スクリーンショット 2023-09-11 221721.png
[clientMediaType:Laminas\Diactoros\UploadedFile:private] => image/png
)
)
2024-01-03 04:30:41 info: api:Array
(
[0] => Laminas\Diactoros\UploadedFile Object
(
[error:Laminas\Diactoros\UploadedFile:private] => 0
[file:Laminas\Diactoros\UploadedFile:private] => C:\temp\php2315.tmp
[moved:Laminas\Diactoros\UploadedFile:private] =>
[stream:Laminas\Diactoros\UploadedFile:private] =>
[size:Laminas\Diactoros\UploadedFile:private] => 1894639
[clientFilename:Laminas\Diactoros\UploadedFile:private] => 44K544Kv44Oq44O844Oz44K344On44OD44OIICgxKS5wbmc=
[clientMediaType:Laminas\Diactoros\UploadedFile:private] => image/png
)
[1] => Laminas\Diactoros\UploadedFile Object
(
[error:Laminas\Diactoros\UploadedFile:private] => 0
[file:Laminas\Diactoros\UploadedFile:private] => C:\temp\php2316.tmp
[moved:Laminas\Diactoros\UploadedFile:private] =>
[stream:Laminas\Diactoros\UploadedFile:private] =>
[size:Laminas\Diactoros\UploadedFile:private] => 183592
[clientFilename:Laminas\Diactoros\UploadedFile:private] => 44K544Kv44Oq44O844Oz44K344On44OD44OIIDIwMjMtMDktMTEgMjIxNzIxLnBuZw==
[clientMediaType:Laminas\Diactoros\UploadedFile:private] => image/png
)
)
2024-01-03 04:30:41 info: base64_decode: スクリーンショット (1).png
2024-01-03 04:30:41 info: base64_decode: スクリーンショット 2023-09-11 221721.pngまとめ
CakePHP4で複数ファイルのアップロードからAPIへの転送までとりあえずできそうな感じです。
