Laravel – アップロードファイルの保存

概要

フォームからアップロードされたファイルをサーバーの場所を指定して保存する手順について整理する。

フォームから取得したUploadedFileオブジェクトを介して、一時保存されたファイルの実体を、store()メソッドやstoreAs()メソッドで所定の場所に保存する。

ファイル取得

ここでは画像ファイルのアップロードを題材とし、アップロードファイルのUploadedFileオブジェクトを取得済みとする。

アップロードファイルの取得についてはこちらを参照。

ファイル保存の流れ

フォームでファイルを選択・アップロードし、それをサーバーに保存する流れは以下の通り。

  • フォームのname属性値からUploadedFileオブジェクトを取得
  • store()メソッドやstoreAs()メソッドで保存
  • メソッドの戻り値から、保存されたファイルのパスを取得
  • パスをデータベースに保存するなどの処理を実行

実装例

アップロードファイル取得のコード中、storeアクションの内容を以下の様に書き換える。ここでは得られたファイルパスを表示させて、そこで実行を停止している。

store()/storeAs()

UploadedFile::store()メソッド

標準形

  • 引数にファイルを保存するディレクトリーとディスクを指定する
    →省略不可
  • 指定した場所にアップロードファイルが保存される
  • 戻り値として、ランダム文字列化されたファイル名を含むパスが返される

引数が''の場合

ファイルパスの内容。

保存されたファイルの確認。

引数にディレクトリーを指定

ここでは引数に'test'というディレクトリーを指定している。

戻り値のパスは、testディレクトリー下にファイルが位置している。

storage/appの下にtestディレクトリーが存在すればその下に、存在しなければ新たにtestディレクトリーが作成されてその下にファイルが保存される。

第2引数にディスクを指定

以下の例では、store()の第2引数に'public'を指定している。filesystems.phpでのpublicディスクの定義から、この場合のアップロードファイルはstorage/app/public下のtestディレクトリーの下に保存される。

store('ディスク内のディレクトリー', 'ディスク');

戻り値のパス。ディスク内のtestディレクトリーに続いてランダム文字列でファイル名が生成されている。

確認すると、確かにstorage/app/public/testの下にファイルが保存されている。なおtestディレクトリーが存在しないときは、新たに作成される。

UploadedFile::storeAs()メソッド

store()メソッドはアップロードファイルに新たにランダム文字列でファイル名をつけるが、特定の名前を与えて保存したいときにはstoreAs()メソッドを使う。使い方や戻り値はstore()と同じ。

第3引数は省略可能で、その場合はstore()で第2引数を省略した時と同じ場所にファイルが保存される。

以下の例ではpublicディスクを指定しているが、その場所はfilesystems.phpstorage/app/publicと定義されている

戻り値のファイルパス。ディレクトリーの後に指定したファイル名が続いている。

指定された場所にファイルが保存されているのが確認できる。

ファイルのパスのデータベース登録

store()/storeAs()の戻り値で得られたパスをデータベースに保存することで、画像表示などに活用できる。

具体的には、以下の例の様にパス名を登録するカラムにstore()の戻り値がセットされた$pathの内容を登録する。

 

Laravel – publicディスクの場所

Laravelでアップロードファイルを保存するときなどに、UploadedFileクラスのstore()storeAs()を使う。

これらの第2引数や第3引数でディスクを指定することができ、通常は’public’が指定される。

このpublicディスクがどの場所を指しているかは、アプリケーションディレクトリー下のconfig/filesystems.phpに書かれている。

この中で'root' => storage_path('app/public')というのがあるが、storage_path()はアプリケーションのstorageディレクトリーからの相対パスを指定してパスを返すので、publicディスクの場所はstorage/app/publicということになる。

 

Laravel – アップロードファイルの取得

概要

ファイル選択要素を持つフォームでファイルを取得する手順を整理する。概要は以下のような流れ。

  • ファイル選択要素を持ったフォームを含むページを表示する
  • ユーザーがファイルを選択してPOSTリクエスト
  • リクエストを介して、サーバーにアップロード・一時保存されたファイルを取得する
  • バリデーションが必要な場合、フォームリクエストを作成して実装

基本例

機能

アップロードファイル取得の機能を、シンプルなコントローラーとビューで確認する。例として、画像ファイルの取得を目的として組み立てていく。

ファイル取得に限って言えば、要点は次の2点。

  • ビューでは、ファイル選択要素を持つフォームでファイルを選択、送信
  • コントローラーでは、アップロードされたファイルをフォームのリクエストから取得

ルーティング

例として以下のようにルーティングを設定する。

設定されるルーティングは以下のとおり。

  • /imagesへのGETリクエストでコントローラーのindexアクションが呼ばれる
    • このアクションでフォームページを表示する
  • フォームからのPOSTリクエストは/imagesに送られ、コントローラーのstoreアクションが呼ばれる
    • 本来はファイル保存を想定したアクションだが、ここではファイルの内容確認のみ行う

コントローラー~フォーム表示

indexアクションではフォームを含むページを表示するだけ。

ビュー~フォームとファイル選択要素の設定

表示するページのテンプレートは以下の通りで以下の2点が要点。

  • FORM要素でenctype="multipart/form-data"を指定している
  • INPUT要素でtype="file"としている

HTML-input-fileを参照

フォームで選択されたファイルの取得

アップロードされたファイルは、メソッド・インジェクションで得られたRequestオブジェクトのfile()メソッドで得られる。file()メソッドの引数にINPUT要素のname属性値を指定することでファイルのオブジェクトを得られる。

以下のアクションでは選択されたファイルの内容をdd()で表示させ、そこで実行を止めている。

ローカルのファイルを選択して送信ボタンを押した結果、上記のdd()で表示された内容を示したのが下記。この例ではJPEGファイルをアップロードしている。

アップロードされたファイルの実体が一時的に保存されるのは/tmpディレクトリーで、php.iniupload_tmp_dirで設定されるようだが、コメントアウトされていた。この場合はルート直下の/tmpになると思われる。

バリデーション

フォームリクエスト

フォーム入力からファイルアップロード時にバリデーションをかける場合、フォームリクエストを使うとコントローラーで行うよりも明快になる。

フォームリクエストはartisanで作成する。

フォームリクエストファイルはapp/Http/Requestsディレクトリーに作成され、このファイルにバリデーションの条件を記述する。条件は配列で書くか、文字列内で'|'で連結してもよい。

フォームリクエスト生成時にはauthorize()の戻り値はfalseになっているが、ここでは無条件にtrueとしている。

ここでは画像ファイルを想定したバリデーションをかけているが、画像以外のファイルを扱う場合は、MIMEタイプをpdfdocなど様々なファイル形式で指定。

また、ファイルサイズをmax:xxxのようにKB単位で制限することもできる。

なおここではファイルがアップロードされていないときは$imagenullとなるが、ファイル指定を必須とする場合はrequiredを指定し、エラーメッセージでファイル指定を促すなどの対応をする。

エラー表示

フォームリクエストでのバリデーションの結果エラーがあると、検証結果が$errorsに記録され、もとのビューにリダイレクトされる。

ここでは、以下のようにフォーム入力のテンプレートで@foreachを使って$errorsの内容を全て表示させている。$errorsの使い方についてはこちらを参照。

 

Laravel – リレーション

 

概要

Laravelでは、テーブル間のリレーションを設定することで、外部キーで関連付けられた複数のモデルを(外部キーを意識することなく)一貫して扱うことができる。

リレーションの種類と定義方法

リレーションはモデル定義で設定し、以下の3種類の何れかを設定する。

hasOne
1対1のリレーションの親のモデルで設定し、親データに属する子データのモデルとidを指定する。これにより親データから子データやそのプロパティーの参照が可能になる。
hasMany
1対nの親のモデルで設定し、親データに属する子データのモデルとidを指定する。これにより親データから子データのコレクション、要素インスタンスやそのプロパティーの参照が可能になる。
belongsTo
1対1、1対nとも子のモデルで設定し、子データが属する親データを指す外部キーと親のモデルを指定する。これにより子データから親データやそのプロパティーが参照可能になる。

いずれについてもモデルでの記述方法は同じで、たとえばhasManyを例にとると以下の様に書く。

子の参照名はモデル名のスモールケースで、hasOneの場合は単数形、hasManyの場合は複数形。

例えば親テーブルcustomersのデータが子テーブルemailsのデータを1つだけ持つ場合は以下の様に書く。

また、親テーブルcustomersのデータが子テーブルordersの複数のデータを持つ場合は以下の様に書く。

子テーブルのデータが親テーブルのデータに属することを設定するbelongsToでは、メソッド名は単数形。

リレーション設定後の参照方法

リレーションを設定すると、そのメソッド名と同じ名前のプロパティーによって親から子、子から親のモデルインスタンスを参照できるようになる。

先のCustomerEmailOrderの場合、以下の様に参照する。

子データが得られれば、以下の例の様にそのプロパティーも得ることができる。

挙動確認

準備

設定

挙動確認のため、3つのモデルとテーブルを作成する。

  • Customer
    • Auto Increment(AI)のid、顧客名、作成・更新日時を持つ
    • e-mailアドレスを1つだけ持つ
    • 複数の注文を持つ
  • Email
    • AIのid、e-mailアドレスを持つ顧客のid、アドレス、作成・更新日時を持つ
  • Order
    • AIのid、注文した顧客のid、注文品、作成・更新日時を持つ

モデル作成

artisanでCustomerEmailOrderの3つのモデルを、マイグレーションファイルとともに作成する。

timestampsの生成の抑制

本筋ではないが、ここではテーブル構造をシンプルにするため、デフォルトのtimestampsの生成を抑制している。

マイグレーションファイル編集

マイグレーションファイルを編集し、各テーブルの要素を追加する。なおタイムスタンプを省略するため、各マイグレーションファイルに自動で記述される$table->timestamps()は削除している。

customersテーブルには顧客名を追加記述する。

emailsテーブルには以下を追加記述する。

  • 親のcustomersテーブルのidを参照する外部キーcustomer_idと、メールアドレスemail
  • customer_id外部キー設定

ordersには以下を追加記述する。

  • 親のcustomersテーブルのidを参照する外部キーcustomer_idと、注文品名item
  • customer_id外部キー設定

マイグレーション

マイグレーションを実行する。

テーブル構造

生成された3つのテーブルは以下のとおり。

customersテーブル

emailsテーブル。

ordersテーブル。

リレーション設定

リレーションを各モデルで設定する。なおタイムスタンプを省略するため、各モデルで$timestampsfalseにセットする行を追加している。

Customerモデルでは、1対1でEmailを子に持つこと、1対nでOrderを子に持つことを設定。

EmailモデルではCustomerを親に持つことを設定。

OrderモデルではCustomerを親に持つことを設定。

テストデータ作成

tinkerでテストデータを作成し、データベースに登録する。

Customer

まずCustomerのデータを登録する。

3人の顧客データを登録。

Email

Emailのデータを、Customerの各データに関連付けながら登録する。ここではcustomer1customer2に紐づくデータを登録し、customer3のアドレスは未登録とする。

登録後のテーブルは以下のとおり。

Order

Orderのデータを、Customerの各データに関連付けながら登録する。ここではcustomer1に1つの、customer2に2つの注文があり、customer3からの注文はない状態とする。

登録後のテーブルは以下のとおりで、customer1はネジのオーダー1つ、customer2はゴムシートとプラスチック板で2つのオーダー。

リレーションを使った操作

親データを取得

まず3人の顧客のデータを取得する。

親からhasOneを取得

それぞれの顧客データから、hasOneで関連付けられたemailのインスタンスを取得する。$customer3emailは未登録なのでnullとなる。

親からhasManyを取得

それぞれの顧客データから、hasManyで関連付けられたorderのデータを取得する。

  • hasManyリレーションのデータは配列で得られる
  • データが1つの場合は要素数1の配列
  • 子データがない場合は空の配列

hasManyの場合、子データは配列で得られるので、要素指定やforeachなどで個々のデータを取り出す。

子データのプロパティーの取得

hasOnehasManyの子データが親データから得られれば、そのプロパティーを得ることもできる。

hasOneの子から親を取得

hasOneEmailが属するCustomerを取得する例。ここでは変数を介さず、生成したインスタンスから直接プロパティーを参照している。

hasManyの子から親を取得

hasManyで関連付けられた子のOrderから親のCustomerを取得する例。

親データのプロパティーの取得

子データから親データが得られれば、そのプロパティーも得ることができる。

 

Laravel – timestampsを抑制する

概要

Laraveのデフォルトでモデルとマイグレーションファイルを作成すると、マイグレーションファイルには$table->timestampsが自動的に記述され、マイグレートするとcreated_atupdated_atの2つのカラムがtimestamp型で定義される。

ここでは、これらのタイムスタンプを生成させない方法をまとめる。

  • マイグレーションファイルの該当文を削除orコメントアウト
  • モデルでタイムスタンプを生成させないよう記述を追加

マイグレーションファイルの編集

マイグレーションファイル生成時に自動で記述される$table->timestamps()を削除するかコメントアウトする。

モデルへのタイムスタンプ抑制の記述

マイグレーションファイルからタイムスタンプの行を消しても、モデルをデータベースに登録しようとするとモデル側でタイムスタンプを生成し、データベースに対応するカラムがないとしてエラーになる。

そこで、モデルにタイムスタンプの生成を抑制する記述を追加する。

 

Laravel – timestampsの2038年問題

マイグレーションファイルでのtimestamp

Laravelでマイグレーションファイルを生成させると、timestampが自動的に設定されている。たとえばusersテーブルを生成するマイグレーションファイルは以下のとおり。

生成されるテーブルの構造は以下のとおりで、引数を与えないtimestamps()メソッドはcreated_atupdated_atの2つのカラムを生成する。

ところがMySQLのtimestampは2038年問題を回避できないので、安易にこのまま使うののではなく、これらをdatetime型に変更しておいた方が安全。

timestamp datetime
UTC内部表現で保持 タイムゾーン文字列で保持
1970-01-01 00:00:01UTC~2038-01-19 03:14:07UTC 1000-01-01 00:00:00~9999-12-31 23:59:59

マイグレーションファイルの修正

timestamps()を削除して、必要なら以下の2行に入れ替えるのが一方法。

このように設定すると、Laravelの方で作成日時と更新日時を自動的に更新してくれる(tinkerでのモデル操作を参照)。

Laravel – データ削除

delete~データの削除

データを削除するルーティングとしてdeleteが準備されている。

前提

モデルデータの操作で準備した枠組みを使う。ユーザー認証機能を持ち、ユーザーごとに入力されたシンプルな投稿記事Postを扱う。

流れ

  1. DELETEメソッドでリクエスト
  2. posts.deleteルートでPostsControllerdestroyアクションを実行
  3. destroyアクションで指定されたデータを削除

destroyアクションの呼び出し

リソースルーティングで以下のように設定されていて、DELETEメソッドによるリクエストの場合、ドメイン/posts/(id)からPostControllerdestroyアクションにルーティングされる。ルーティング名はposts.delete

このルーティングにリクエストする場合、たとえばページの中にフォーム要素とINPUT(type="submit")要素を配置することが考えられる。

ここでルーティングに従えばDELETEメソッドを明示する必要があるが、現在のブラウザーではGETとPOSTしか認識しない。

フォームからDELETEメソッドをリクエストする場合、以下のようにフォームのmethodではPOSTを指定し、Laravelの@methodディレクティブでDELETEを指定する。

この例では、フォームのaction先にroute()メソッドを使い、第一引数でposts.daestroyのルーティング名を、第2引数で削除対象のデータ$postを渡している。これによってDELETEメソッドでURIとしてドメイン名/posts/(id)が渡される

なお@methodディレクティブは、以下のようなhidden要素を生成する。

コントローラー~destroyアクション

ルーティングに従って、PostControllerdestroyアクションを以下のように記述する。

ここでの要点は以下の通り。

  • destroy()メソッドの引数で、URLに埋め込まれたid$idとして受け取られている
  • Postクラスのfind()メソッドの引数に$idを渡して、指定したidの記事インスタンスを$postで参照している
  • 削除対象記事のインスタンス$postdelete()メソッドの引数に、更新対象の要素を渡している
  • データ削除を反映した一覧表示のため、indexにリダイレクトしている

まとめ

以上の設定により、たとえばフォームに配置された削除ボタンを押すと、その記事のidがURLに埋め込まれてDELTEメソッドでリクエストされ、destroyアクションでデータが削除される。

 

Laravel – データ編集・更新

create, store~データの入力と登録

リソースルーティングでは、以下を意図している。

edit
データを編集する。フォームで編集する場合、フォームを持つビューを表示する。
update
編集後の内容でデータベースのデータを更新する。editがフォームでの編集の場合、フォームのaction先をこのルートにする。

前提

モデルデータの操作で準備した枠組みを使う。ユーザー認証機能を持ち、ユーザーごとに入力されたシンプルな投稿記事Postを扱う。

流れ

  1. 編集する記事の$idを指定してドメイン名/posts/(id)/editをGETリクエスト
  2. posts.editルートでPostsControllereditアクションを実行
  3. editアクションで編集フォームを含むビューを表示
  4. 編集フォームのactionからposts.updateへルーティング
    methodPATCH
  5. posts.updateルートでPostControllerupdateアクションを実行
  6. updateアクションでデータベース上のデータを更新

editアクションの呼び出し

リソースルーティングで以下のように設定されていて、ドメイン/posts/(id)/editからPostControllereditアクションにルーティングされる。ルーティング名はposts.edit

編集対象の記事を指定してposts.editにGETリクエストでルーティングする例として、以下のようなアンカー要素がある。

route()メソッドの第2引数でモデルデータを指定すると、自動的にルーティングに沿った形でデータのidがURLに埋め込まれる。

上記の例ではルーティングがposts.editでURLがposts/{post}/editとなっているので、例えば記事idが3の記事の場合は以下のようなURLとなり、これがブラウザーにも表示される。

ドメイン名/posts/3/edit

コントローラー~editアクション

コントローラーでは、ビューの$titleと編集対象の記事のインスタンスを設定してresources/views/posts/edit.blade.phpを表示させる。

editアクションに来る際にURLに埋め込まれたidが、editアクションメソッドの引数$idとして受け取られる。この$idPostモデルのfind()メソッドの引数に与えて、指定したidの記事データを取得する。

編集フォームビュー

bladeテンプレート

投稿を編集するフォームはposts/edit.blade.phpに以下の要領で記述。

ここでHTTPのメソッドはPOSTだが、ルーティングではPATCHメソッドを期待しているので、@method('patch')ディレクティブが必要。

actionで指定しているroute('posts.update', $post)はデータベースの更新アクションupdateへのルーティング。

PATCHに対するルーティングのURLはドメイン名/posts/(id)となる。

フォームリクエスト~バリデーション

編集フォームのバリデーションは入力フォームのところで作成したフォームリクエストを使う。

コントローラー~updateアクション

use指定

PostモデルとPostRequestに対するuse指定はフォーム入力で指定済み

updateアクション

入力フォームのaction先で指定されたアクションメソッドで、編集後の内容でデータベースを更新する。

ここでの要点は以下の通り。

  • フォームリクエストのバリデーションを有効にするため、update()メソッドのインジェクションをPostRequestにしている
  • update()メソッドの第2引数で、URLに埋め込まれたid$idとして受け取られている
  • Postクラスのfind()メソッドの引数に$idを渡して、指定したidの記事インスタンスを$postで参照している
  • 編集対象記事のインスタンス$postupdate()メソッドの引数に、更新対象の要素を渡している
    • 更新対象の要素は、引数のリクエストで得られたフォーム入力のcommentonly()で限定している
  • 更新後のデータを含めた一覧表示のため、indexにリダイレクトしている

モデルでの$fillable設定

update()メソッドでのフォームリクエストによる登録はマスアサインメントになるので、モデルに$fillableの定義が必要(フォーム入力のところで定義済み)。

確認

ユーザーログイン状態で<a href="{{ route('posts.edit', $post) }}">編集</a>などでGETリクエストすると編集フォームのページが表示され、更新ボタンを押すとデータが更新される。

 

Laravel – データ表示

index~全データ表示

データの一覧を表示させるルーティングとしてindexを使う。

前提

モデルデータの操作で準備した枠組みを使う。ユーザー認証機能を持ち、ユーザーごとに入力されたシンプルな投稿記事Postを扱う。

流れ

  1. ドメイン名/postsをGETリクエスト
  2. posts.indexルートでPostsControllerindexアクションを実行
  3. indexアクションでログインユーザーの投稿記事を表示

indexアクションの呼び出し

リソースルーティングで以下のように設定されていて、GETメソッドによるリクエストの場合、ドメイン/postsからPostControllerindexアクションにルーティングされる。ルーティング名はposts

コントローラー~indexアクション

ルーティングに従って、PostControllerindexアクションを以下のように記述する。

all()メソッドは、Postモデルのデータが保存されているpostsテーブルから全データを取得して配列として返す。つまりこの段階では、すべてのログインユーザーが他のユーザーのものも含めた全投稿を見られることになる。

Postクラスや継承元のModelクラスはall()メソッドを持たないが、store()アクションで見たのと同じようにModelクラスのマジックメソッドから呼び出していると思われる。

投稿記事の配列はresource/posts/index.blade.php'posts'として渡され、これがindexビューで$postsとして利用される。

表示ビュー

投稿表示はindex.blade.phpに以下の要領で記述。

ここでは@forelseディレクティブ$postsに格納された投稿記事群を一つずつ取り出して、commentcreated_at(datetime型)を表示させている。

$postsindexアクションで読み込まれた投稿記事を格納した配列。

@forelseを使っているので、$postsが空の場合、つまり投稿が一つもされていない場合は、それに対応したメッセージが表示される。

Laravel – モデルデータの操作

概要

ここではモデルを使った基本の流れを整理する。

  1. フォームで入力したデータをデータベースに登録
  2. データベースのデータを表示
  3. 登録されたデータを編集・更新
  4. 登録されたデータの削除

例として、短いコメントを書き込む掲示板を想定する。

設定

ユーザー認証機能を実装。

データとしてユーザーが入力した投稿記事を扱う。

  • モデルは投稿記事を表すPost
  • コントローラーはPostController
  • postsに対するリソースルーティングを使う

準備

モデル

artisanでモデルを作成

マイグレーションファイルを編集し、マイグレート。

  • 投稿本文はcommentとし、200文字の文字列
  • 自動生成されるtimestampsdatetime型のcreated_atupdated_atに変更(2038問題対応)
  • user_idusersidを外部キーとして持つ
    外部キー参照

コントローラー

artisanでコントローラーを作成

ルーティング

postsに対してPostControllerを割り当てるリソースルーティングを記述。

Postモデルに関するリソースルーティングは以下の通り。

アクセス制限

ログインユーザーのみページを見ることができるようアクセス制限をかける場合は、PostControllerのコンストラクターでミドルウェアを指定する。

データの操作