ユーザーにファイルをアップロードさせてサーバー側に保存する。非常によくある処理です。
公式のSymfony cookbookには既にそのものズバリなタイトルのドキュメントがあります。
How to Handle File Uploads with Doctrine
http://symfony.com/doc/current/cookbook/doctrine/file_uploads.html
日本語訳:Doctrine でファイルアップロードを扱う方法
http://docs.symfony.gr.jp/symfony2/cookbook/doctrine/file_uploads.html
日本語版は原文に比べて多少内容が古いですが、説明されている実装方法は同じです。
最近、Symfony歴の浅いプログラマにSymfonyの使い方を説明する機会がなぜか多く、そのうち一人にファイルアップロードのケースを書いてもらおうと思ったのですが、上記ページを見せて説明していたら心中「なんか違う…」という違和感がフツフツと湧いてきたのです。
まだまだsymfony1癖が抜けずSymfony2初心者だった頃の私にとっては「素晴らしい!」と思えたのに、なんでだろう?
というわけで、どこが違和感(不便…メンテしづらい…もやもや…)なのか、今の私が同じ処理を書くとしたらどうなるか考えてみました。
まず、公式の「Doctrine でファイルアップロードを扱う方法」がイケてないと思う点は、下記の2点です。(あくまで個人的に、です。)
1. Doctrine2のLifecycleCallback(PrePersist, PreUpdate, PreRemove)を利用しているため、実際の処理(アップロードファイルが一時保存先から恒久的な保存先に移動)がいつ呼ばれるのかわかりにくい。
2. アップロード先パスがEntityにベタ書きでテスト/変更しづらい。
アップロード先のディレクトリ(Document::getUploadRootDir())を直接書かざるを得ません。特にサンプルコードのように__DIR__を使って書かれていると、Symfony2 standard editionの中に置いた状態(あるいは同じディレクトリ構造を無理やり作る)じゃないとテストが失敗してしまいます。せっかく再利用可能なバンドルを書いても単独でテストできないことになります。
それに、ファイルの保存先を設定ファイルで指定することもできなくなります。一見、Document::setUploadRootDir()のようなメソッドと$uploadRootDirプロパティを設定すればいいじゃん、と思うかもしれません。でも、そうすると「エンティティのupload()メソッドを呼ぶ前には必ずsetUploadRootDir()を呼ばなければならない」という決まりが必要になるわけです。データ保存のタイミングで必ず呼ばれるDoctrineのLifecycleCallbackを使う意味が失われてしまいます(せっかくアプリケーション内のどこから$documentを保存してもファイルアップロード周りの処理が行われるようになっているのに、アプリケーション内のどこでも$documentを保存しようとしたらまず先にsetUploadRootDir()を呼ぶ必要が出てくる)。
#こういう実装ルールって、後々うっかりミスでトラブルになる臭いがしませんか?
では、「Doctrineでファイルアップロード」をどう実装するか?
答え:専用のサービスを作りましょう
アップロードしたファイルの扱いで何が一番面倒かと考えたとき、データベース上のレコードとファイルシステム上のファイルの同期ですよね。ファイル保存したのにレコード保存してない、とか。レコード削除したのにファイル削除してない、とか。
それ「だけ」を担当するサービスを作ってしまえば、同期の心配はそこにお任せできるわけです。
サービスで実装した版がこちら
実際のアップロードファイル移動処理はサービスで行い、コントローラーからはサービスのメソッドを呼び出すだけです。
ファイルの恒久的保存先が新規Documentの保存後じゃないと決まらない場合(例えば、auto_increment/serialなidを使って保存先ディレクトリを決定する場合。id=1のdocumentのファイルをuploads/documents/1/hoge.jpgやuploads/documents/1.jpgのように保存したいとき)も、処理の順番を書き換えればいいだけです。なんならpersist()→flush()→ファイル移動処理→パス(保存されたファイル名)を変更flush()と2回保存もできちゃいます。LifecycleCallbackのトリッキーな使い方をググる必要はありませんw
見るからにテストも書きやすいです。
#そもそも昨今、アップロードしたファイルの恒久的保存先が同じファイルシステム内とも限らない訳で、追加要件が出てきたときにサクッと変更できるのは圧倒的にサービス方式だと思います。
まぁこれが正解とか公式が間違っているというわけではなく、こういう方法もあるよ(こっちのほうが私はしっくりくるよ)という話でした。