OctoberCMSプラグイン作成：リソースの管理画面からファイルを関連付けてアップロードする

## 概要
例えば、製品やユーザなどのデータに画像ファイルを関連付けたい時、OctoberCMSでは簡単にそのデータの管理ページに画像ファイルのアップロード・表示・管理機能が追加できる。意外にもドキュメントではまとまった説明がなかったのでここに記載する。

<img width="1035" alt="Screen Shot 2018-01-01 at 19.22.31.png" src="https://qiita-image-store.s3.amazonaws.com/0/138322/008baa67-5faf-3b09-896a-7bf9f8019ad9.png">

### ステップ

1. Configを設定
2. モデルにリレーションを定義
3. CRUD画面にファイルアップロードウィジェットを追加

## 前提
テーブル、モデル、[CRUD画面](https://qiita.com/pikanji/items/dc2f5180c4ffe8829752)は既に作成済み。

## Configを設定する
まだ設定していない場合は config/filesystem.php に`disks`を設定する。デフォルトで存在するが、下記に注意。

* `local`を使用する場合、指定ディレクトリにweb serverユーザの書き込み権限がある。
* `s3`などクラウドを使用する場合はクラウドサーバへの書き込みが可能な状態。

下記の例では`env()`を使用しているが、直書きでも問題ない。

```php:config/filesystems.php
    'disks' => [

        'local' => [
            'driver' => 'local',
            'root'   => storage_path('app'),
        ],

        's3' => [
            'driver' => 's3',
            'key'    => env('AWS_ACCESS_KEY_ID'),
            'secret' => env('AWS_SECRET_ACCESS_KEY'),
            'region' => env('AWS_REGION'),
            'bucket' => env('AWS_S3_BUCKET'),
            'visibility' => 'public',
        ],
    ],
```

そして、ファイルアップロード関連の設定は config/cms.php の`storage.uploads`で行う。

```php:config/cms.php
    'storage' => [

        'uploads' => [
            'disk'   => 'local',
            'folder' => 'uploads',
            'path'   => '/storage/app/uploads',
        ],

        // S3へを指定する場合は`path`はバケットへのURLを指定する必要がある
//        'uploads' => [
//            'disk'   => 's3',
//            'folder' => 'uploads',
//            'path'   => 'https://s3-ap-southeast-1.amazonaws.com/my-bucket/uploads',
//        ],

        ...
    ],
```

## 対象データのモデルにリレーションを定義する
OctoberCMSでは添付ファイルの情報は別のテーブルに保存される。ここで説明するように[OctoberCMSの添付ファイルのドキュメント](https://octobercms.com/docs/database/attachments#file-attachments)に従えば、添付ファイルの情報は既存の`system_files`テーブルに保存されるので、新たにテーブルを作成したり修正したりする必要はない。

対象のモデルクラスに`attachOne`または`attachMany`を追加する。下記の例では`Product`というモデルクラスに`$attachMany`フィールドを設けてattachManyリレーションを定義している。リレーション名が`image`でリレーションのモデルクラスを`System\Models\File`としている。

```php:models/Product.php
    public $attachMany = [
        'images' => 'System\Models\File'
    ];
```

## CRUD画面にファイルアップロードウィジェットを追加する
BuilderプラグインのCRUD画面作成画面からでFileUpload controlを追加する。
<img width="1170" alt="Screen_Shot_2018-01-01_at_19_48_42.png" src="https://qiita-image-store.s3.amazonaws.com/0/138322/ffa54c33-4e21-1dc9-c8e4-e65cd3f02835.png">

追加したウィジェットの設定でField nameをモデルクラスで定義したリレーション名に設定する。上記の場合、`images`になる。

ファイルが画像の場合は、Builderで生成されたyamlファイル（デフォルトでは`<plugin_name>/models/<model_name>/fields.yaml`）を開いて追加したフィールドの`mode`を`image`にすると、サムネイルを表示してくれる。

```yaml
fields:
    name:
        label: 'cocci.utility::backend.common.field.name.label'
        oc.commentPosition: ''
        span: auto
        required: true
        type: text
    display_name:
        label: 'cocci.utility::backend.common.field.display_name.label'
        oc.commentPosition: ''
        span: auto
        required: true
        type: text
    description:
        label: 'cocci.utility::backend.common.field.description.label'
        size: large
        oc.commentPosition: ''
        span: auto
        type: textarea
    images:
        label: Images
        oc.commentPosition: ''
        mode: image
        useCaption: true
        thumbOptions:
            mode: crop
            extension: auto
        span: auto
        type: fileupload
```

## アップロードしたファイルの使用方法
データに添付したファイルの使用方法も非常に簡単だ。
例えば上の例だと、下記のようにアップロードしたファイルのURLを取得できる。

```php
$product->images[0]->getPath();
```

また、`attachOne`のリレーションの場合は単純に配列ではなくなる。

```php
$product->image->getPath();
```

Twigテンプレートからは単純にこうだ。

```twig
<img src='{{ product.images[0].getPath() }}'>
```

## バグ対処方法
2018/1/1現在（build 431）、OctoberCMSにバグらしきものがあり、`storage.uploads.disk`が効かない。代わりにfilesystem.phpの`default`が適用されてしまっている。が、修正されるまで比較的簡単・安全な方法で回避できる。

上記の説明では`System\Models\File`をリレーションモデルに使用しているが、このクラスを拡張して修正を仕込む。１行目のnamespaceだけ適宜変更すれば下記を丸コピーでよい。

```php:myplugin/models/File.php
<?php namespace Pikanji\Myplugin\Models;

use Config;
use File as FileHelper;
use Storage;

class File extends \System\Models\File
{
    /**
     * Returns true if storage.uploads.disk in config/cms.php is "local".
     * @return bool
     */
    protected function isLocalStorage()
    {
        return Config::get('cms.storage.uploads.disk') == 'local';
    }

    /**
     * Copy the local file to Storage
     * @return bool True on success, false on failure.
     */
    protected function copyLocalToStorage($localPath, $storagePath)
    {
        $disk = Storage::disk(Config::get('cms.storage.uploads.disk'));
        return $disk->put($storagePath, FileHelper::get($localPath), ($this->isPublic()) ? 'public' : null);
    }
}
```

そして、このクラスをattachOne/Manyで指定した`System\Models\File`の代わりに指定する。これで、`storage.uploads.disk`で指定した先にアップロードしたファイルが保存されるはずだ。

この修正は、`System\Models\File`のベースクラス`October\Rain\Database\Attach\File`のメソッド`isLocalStorage`と`copyLocalToStorage`をオーバーライドしている。元のメソッドは`cms.storage.uploads.disk`ではなく`filesystem.default`を見てしまっているからだ。

この問題は[このissue](https://github.com/octobercms/october/issues/3332)で報告しており、[pull request](https://github.com/octobercms/october/pull/3333)も出しているので、早く取り込んでもらいたい。