【PHP】PHPフレームワーク「Flow」を試してみる～ファイルアップロード編～

# 初めに
前回は[こちら](https://qiita.com/nnhkrnk/items/f0e779ed60bff2791a76)の記事で独自Exceptionの設定方法をまとめました。
今回はFlowを用いてファイルアップロードとダウンロードの実装方法についてまとめます。

# 事前知識
今回の記事はFlowにおける基本的なDB操作ができることを前提に記載しています。
以下の記事を読んでから読むことを推奨します。
[【PHP】マイナーフレームワーク「Flow」を試してみる～API作成編～](https://qiita.com/nnhkrnk/items/2b9f647d6130a098345f)
[【PHP】マイナーフレームワーク「Flow」を試してみる～DB接続＆データ登録編～](https://qiita.com/nnhkrnk/items/739ac12036c858525e23)

# Flowにおけるファイル管理
前提として、Flowではファイルを保存する領域が2つ存在します。

|領域|用途|
|--|--|
|Storage|アップロードされたファイルが保存される領域|
|Target|外部から参照可能なファイルが保存されている領域|

今回はアップロード編ということで、Storage領域についてのみ解説します。

## Storage
API経由でアップロードされたファイルが格納される領域です。
ファイル名はsha1でHash化された状態でここに保存されます。

Storageを使うにはSettings.yamlに以下の設定を加える必要があります。
```yaml:Settings.yaml
Neos:
  Flow:
    resource:
      storages:
        defaultPersistentResourcesStorage:
          storage: 'Neos\Flow\ResourceManagement\Storage\WritableFileSystemStorage'
          storageOptions:
            path: '%FLOW_PATH_DATA%Persistent/Resources/'
```

storageに設定してある```WritableFileSystemStorage```はFlowに標準で用意されているStorage管理用のクラスです。```WritableStorageInterface```というインターフェースを継承しており、保存するときの処理などが書かれています。

pathは実際にStorageとして利用するフォルダを指定します。```%FLOW_PATH_DATA%```はプロジェクト直下にあるDataディレクトリのことで、実行中のログなどが保存されているディレクトリです。今回はその中にファイルを保存します。
以下のようなイメージです。
```
Project
  └ Data
     └ Persistent
        └ Resources
           └ 45a8c7...d8f      ← アップロードしたファイル（Hash化）
  ```

### ファイルの識別方法
Storage領域に保存されたファイルの名前はHash化されているため、この状態ではどれがどのファイルなのか判別ができません。
そのため、Hash化されたファイル名と、その識別子をペアでDBに保存することで解決をします。


### ファイルを識別子と紐づける方法
アップロードされたファイルと識別子はDBに紐づけるのですが、そのためには2つのTBLが必要になります。

* **①ファイル情報を保存するTBL**
* **②ファイル情報と識別子をペアで保存するTBL**

#### ①ファイル情報を保存するTBL
Flowではファイル情報を保存するTBLをあらかじめ用意してくれています。

Flowでは初回のmigrate時に4つのTBLが作成されます。（[こちらの記事](https://qiita.com/nnhkrnk/items/739ac12036c858525e23#doctrinemigrate%E3%81%AE%E5%AE%9F%E8%A1%8C)参照）
その中の```neos_flow_resourcemanagement_persistentresource```というTBLがファイル情報を格納するTBLになっています。

以下のカラムを持っています。

| カラム名                      | 和名                                 | データ型                      |
|-----------------------------|--------------------------------------|-------------------------------|
| persistence_object_identifier | オブジェクト識別子                | varchar(40)                  |
| sha1                        | SHA1ハッシュ                        | varchar(40)                  |
| filename                    | ファイル名                         | varchar(255)                 |
| collectionname              | コレクション名                    | varchar(255)                 |
| mediatype                   | メディアタイプ                    | varchar(100)                 |
| relativepublicationpath     | 相対パブリケーションパス        | varchar(255)                 |
| filesize                    | ファイルサイズ                    | decimal(20,0)                |

:::note 
infopersistence_object_identifierというカラムはdoctrineがテーブル管理に用いている主キーです。全テーブルにあります。
:::

<details><summary>詳しいTBL情報はこちら</summary>

```sql
mysql> show create table neos_flow_resourcemanagement_persistentresource\G
*************************** 1. row ***************************
       Table: neos_flow_resourcemanagement_persistentresource
Create Table: CREATE TABLE `neos_flow_resourcemanagement_persistentresource` (
  `persistence_object_identifier` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL,
  `sha1` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL,
  `filename` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
  `collectionname` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
  `mediatype` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
  `relativepublicationpath` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
  `filesize` decimal(20,0) NOT NULL,
  PRIMARY KEY (`persistence_object_identifier`),
  KEY `IDX_35DC14F03332102A` (`sha1`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
1 row in set (0.00 sec)
```
</details>

#### ②ファイル情報と識別子をペアで保存するTBL
このTBLについては開発者自身で作成する必要があります。
以下の二つのカラムを用意してください。

* ファイル情報TBLの```persistence_object_identifier```を外部キーとするカラム
* ファイルの識別子（ファイル名やIDなど）


# 試してみた
それではファイルアップロードを試してみましょう。
今回は以下のようなAPIを作成します。

* アップロードするファイルと識別子をリクエストで受け取る
* ファイルをStorage領域に保存
* ファイル情報と識別子をDBに保存


また、今回のプロジェクト構成は以下です。
※上述のSettings.yamlへの記載は割愛してます。
```
Project/
  └ Packages/
      ├ Application/
      |    └ Neos.Welcome/
      |         └ Classes/
      |              ├ Controller/
      |              |    └ ImageController.php（★）
      |              |
      |              └ Domain/
      |                   ├ Model
      |                   |    └ Image.php（★）
      |                   |
      |                   └ Repository/
      |                        └ ImageRepository.php（★）
      |
      ├ Framework/
      └ Libraries/
```

## 1. Modelを作成し、TBLを自動生成する

ファイルと識別子をペアで保存するためのTBLを作成します。
カラムは以下です。
|カラム名|詳細|備考|
|--|--|--|
|originalResource|ファイル情報を保存する。|※1|
|fileKey|ファイル識別子|

※1：ファイル情報を保存するTBL（```neos_flow_resourcemanagement_persistentresource```）の外部キー



ということで以下のような```Image```クラスを作成しました。

```php:item
<?php
namespace Neos\Welcome\Domain\Model;

use Neos\Flow\Annotations as Flow;
use Doctrine\ORM\Mapping as ORM;

/**
 * @Flow\Entity
 * @ORM\Table(name="image")
 */
class Image {

   /**
    * @var string
    */
   protected $fileKey;

   /**
    * @var \Neos\Flow\ResourceManagement\PersistentResource
    * @ORM\OneToOne
    */
   protected $originalResource;

   // setterの記載は割愛します。
   // 動かす際はsetterが必要です。
}
```

:::note info
以下のように書くことでカラムに外部キー制約を付与することができます。Flowが使用してるORM（doctrine）では、```@ORM\OneToOne```をつけると```@var```で指定したクラスに対応するTBLを外部キーの主テーブルとして設定します。

```php
   /**
    * @var \Neos\Flow\ResourceManagement\PersistentResource
    * @ORM\OneToOne
    */
   protected $originalResource;
```
:::


Modelを作成したので、```./flow doctrine:update```コマンドでTBLを作成しました。
できあがったTBLが以下です。
```sql
mysql> show create table image\G
*************************** 1. row ***************************
       Table: image
Create Table: CREATE TABLE `image` (
  `persistence_object_identifier` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL,
  `originalresource` varchar(40) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `filekey` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
  PRIMARY KEY (`persistence_object_identifier`),
  UNIQUE KEY `UNIQ_C53D045F4E59BB9C` (`originalresource`),
  CONSTRAINT `FK_C53D045F4E59BB9C` FOREIGN KEY (`originalresource`) REFERENCES `neos_flow_resourcemanagement_persistentresource` (`persistence_object_identifier`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
1 row in set (0.00 sec)

```

## 2. TBLにアクセスするためのRepositoryを作成する

作成したTBLにアクセスするための```ImageRepository```クラスを作成します。
親クラスで定義してあるメソッドを利用できるため、自前でメソッドを用意する必要はありません。

```php:ImageRepository
<?php
namespace Neos\Welcome\Domain\Repository;

use Neos\Flow\Annotations as Flow;
use Neos\Flow\Persistence\Repository;

/**
 * @Flow\Scope("singleton")
 */
class ImageRepository extends Repository
{}
```

## 3. Controller作成
APIの口を用意します。

```php: ImageController
<?php
namespace Neos\Welcome\Controller;


use Neos\Flow\Annotations as Flow;
use Neos\Flow\Mvc\Controller\ActionController;
use Neos\Welcome\Domain\Model\Image;
use \Neos\Flow\ResourceManagement\PersistentResource;

class ImageController extends ActionController
{
    /**
     * @Flow\Inject
     * @var \Neos\Welcome\Domain\Repository\ImageRepository
     */
    protected $imageRepository;
    
    /**
     * @Flow\Inject
     * @var \Neos\Flow\Mvc\View\JsonView
     */
    protected $view;

    /**
     * Uploads a file
     */
    public function uploadFileAction(string $fileKey, PersistentResource $originalResource) {
        $newImage = new Image();
        $newImage->setFileKey($fileKey);
        $newImage->setOriginalResource($originalResource);
        $this->imageRepository->add($newImage);
    }
}
```

アップロードしたファイルは```PersistentResource```型で扱います。
これは、ファイル情報を保存するTBLのModelクラスです。
リクエストを受け取った時にフレームワークの中で変換しているようです。

## 4. 動作確認
作成したAPIを呼び出してみましょう。
ローカルにある画像をアップロードしてみました。

```sh
$ curl -i -X POST ^
More?    -H "Content-Type:multipart/form-data" ^
More?    -F "originalResource=@\"./vermeer.png\";type=image/png;filename=\"vermeer.png\"" ^
More?    -F "fileKey=key" ^
More?  "http://localhost:8081/Neos.Welcome/Image/uploadFile"
HTTP/1.1 200 OK
Host: localhost:8081
Date: Thu, 28 Dec 2023 04:28:14 GMT
Connection: close
X-Powered-By: PHP/8.1.25
Content-Type: application/json
X-Flow-Powered: Flow/8.3
Content-Length: 4

null
```

無事に200が返ってきましたね。
続いてDBを覗いてみます。

```sql
mysql> select * from image\G
*************************** 1. row ***************************
persistence_object_identifier: ae625d6e-085d-43a1-9c31-3e313b0db6d0
             originalresource: fa5ab961-f714-4d7f-aab0-e8d8252d8f71
                      filekey: key
1 row in set (0.00 sec)

mysql>
mysql>
mysql> select * from neos_flow_resourcemanagement_persistentresource\G
*************************** 1. row ***************************
persistence_object_identifier: fa5ab961-f714-4d7f-aab0-e8d8252d8f71
                         sha1: fd9398696168cb57325e84cfef875f013e4d8c94
                     filename: vermeer.png
               collectionname: persistent
                    mediatype: image/png
      relativepublicationpath:
                     filesize: 558377
1 row in set (0.00 sec)

mysql>
```
DBにも画像の情報が登録されてました。
最後にStorageを覗いてみます。

```
$ pwd
C:\[プロジェクトまでのパス]\Quickstart\Data\Persistent\Resources\f\d\9\3

$ ls
Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----        2023/12/28     13:07         558377 fd9398696168cb57325e84cfef875f013e4d8c94
```

こちらにもhash化されたファイル名が保存されてることが確認できました。
Hash値の頭4文字と同じ名前のディレクトリが作成され、その中にファイルが保存されるようですね。

# 終わりに
今回はFlowにおけるファイルアップロードの方法を調べました。
実際にはファイルの格納場所にはS3などが使われることが多いでしょう。
また、sha1は脆弱性やHash値の衝突の恐れもあるため、運用するためにはもう少し検討が必要そうです。

次回はファイルのダウンロードについてまとめる予定なのでそちらのほうもぜひ読ご覧ください。
ここまでご覧いただきありがとうございました！

# 追記（2024/1/7）
* アップロードされたファイルはHash化されると思っていたのですが、実際はファイル名のみがHash化されるようでした。訂正しています。


# 参考
* https://flowframework.readthedocs.io/en/stable/TheDefinitiveGuide/PartIII/ResourceManagement.html#
* https://zenn.dev/spiegel/articles/20201025-sayonara-sha1