- 投稿日:2019-02-11T23:30:14+09:00
Laravelを始めるときのメモ
内容
- Ubuntu上で、Laravelのプロジェクトを生成して、PhpStormで開くまでのメモです
前提
- Ubuntu 18.04
- PHP 7.2
- Laravel Installer 1.3.3
- PhpStorm 2018.3
PHP+Laravelの環境構築
参考にしたページ
https://readouble.com/laravel/5.5/ja/installation.htmlsudo apt install php-zip # ←未インストールの場合 sudo apt install composer composer global require "laravel/installer"laravelコマンドにパスを通す(~/.config/composer/vendor/bin)
プロジェクトの作成
laravel new {YOUR_PROJECT_NAME} cd {YOUR_PROJECT_NAME} composer require barryvdh/laravel-ide-helperエラーが出て、laravel-ide-helperが入れられなかったので、以下で解消。
sudo apt install php-mbstring
初期環境の動作確認
- PhpStormでは、"Run Anything"からphp artisan serveを実行
- PhpStromのRunウィンドウに表示されるURLをクリックするとブラウザで確認できる。
最初に変更するところ
- routes/web.php
- パスが "/"直下に設置される
もしくは
- routes/api.php
- パスが "/api"下に設置される
- 投稿日:2019-02-11T22:27:54+09:00
bladeの変数確認
- 投稿日:2019-02-11T18:11:36+09:00
LaravelとStripeを使って決済機能を持つショッピングサイトを作った
約1年前にWeb開発の業務を担当させて頂きそこからマイペースにLaravelの勉強をしてきました。
勉強してきたことを1つの成果物としてまとめたいなと思い決済機能を持つ簡単なショッピングサイトを作りました。人によっては1ヶ月以内で出来る様な出来で恐縮ですがベーシック認証をかけた状態でherokuにデプロイをしました。
SSLの設定や画像ファイルの格納先の権限やらでまだ完璧にデプロイ出きた訳では無いですが、社内でのみ公開が出来る様にしたいと考えております。ここでは主に画面のキャプチャを用いて画面構成やどの様に作り込みをしたかをまとめていきます。
githubにソースコードを上げているのでソースコードの詳細はそちらをご覧頂ければと存じます。
自習やポートフォリオサイトを作ってみたいという方の助けになれれば幸いです。*決済はStripeを利用したクレジットカード決済となっています。(テストモードです。)
*以前書いた記事と重複している箇所は説明を割愛します。動作環境について
OS:macOS High Sierra
version: 10.13.6Docker(Engine):18.09.1
対象ブラウザ
GoogleChrome:72以上
開発環境
centOS:7.5
Laravel:5.6
PHP:7.2
MySQL:5.7
PostgreSQL:10.6
APache2.4外部サービス
参考資料
【簡単・オシャレ】PHPでstripeの決済を導入する手順【決済システム導入】
【初級編】LaravelでStripeを使って単発決済を実装するTOPページ
現在DBに登録されている商品が表示されています。
キーワード入力による商品の検索も可能です。TOPページでは決済が出来ないようになっています。
管理者は管理者ログインのリンクを、お客様はお客様ログインのリンクを押下して貰うことでログインページに遷移します。
お客様の新規登録の場合は新規登録のリンクを押下することでログインページに遷移します。ログイン・ユーザー画面
管理者と顧客それぞれのログイン関連の処理については以前書いた記事にまとめています。
adminテーブルを使って認証する方法
customerテーブルを使って認証する方法パスワード再設定に使うメールはMailgunを使いますが、送信先が限定されているので本番環境ではsmtpを立てた方が良さそうです。
管理者画面
TOPページと同様に登録されている商品の一覧が表示されています。
TOPページと異なる点は各商品ごとに編集、画像アップロード、削除の処理を行えるボタンを置いていることです。ファイルアップロードについても別の記事でまとめています。
右上のプルダウンを押すとメニュー画面が出てきます。
ここから管理者のホーム画面への遷移、管理者登録、商品登録、ログアウトを行います。新商品登録
商品登録画面では商品名、価格、説明文を入力できます。
上記3項目はバリデーションファイルにて入力条件を定めることで誤ったデータがDBに挿入されることを防いでいます。
public function rules() { return [ 'goods_name' => 'required|string|max:50', // 必須|文字列|最大50文字 'price' => 'required|integer|min:1|digits_between:1,6', // 必須|最小値:1|数値かつ1桁~6桁 'goods_text' => 'nullable|string|max:120', // null可|文字長120文字以内 ]; }入力誤りがあればメッセージが表示されます。
最終的な入力内容に誤りが無いか確認する為に確認画面を設けています。
「登録する」ボタンを押下すると商品の登録が完了します。
商品情報編集
各商品の「編集」ボタンを押下します。
画面構成自体は新規登録時と違いはありません。
商品ごとにDBに登録されている値が予め入力欄に表示されます。編集し終えたら確認をし、最後にデータの更新をかけます。
商品削除
「削除」ボタンを押すと確認のアラートが表示されます。「OK」を押すと即座に商品の削除が実行されます。
顧客画面
TOPページと同様に登録されている商品の一覧が表示されています。
TOPページと異なる点は商品のカートへの格納と削除を行うボタン、最終的な購入手続きを行うボタンがあることです。右上のプルダウンを押すとメニュー画面が出てきます。
ここから顧客のHome画面への遷移、ログアウトを行います。カートに入れる
カートに何も商品を入れないまま購入手続きをしようとしても出来ない様になっています。
「カートに入れる」ボタンを押下することで該当の商品をカートに入れることが出来ます。
1回のボタン押下によりカートに入れることが出来る数量は1つとなっています。カートから外す
「カートから外す」ボタンを押下することで該当の商品をカートから外すことが出来ます。
1回のボタン押下によりカートから外すことが出来る数量は1つとなっています。購入内手続き
ホーム画面から「購入内手続きへ」ボタンを押すと購入内容の確認画面に遷移します。
確認画面ではカートに入れた商品のデータと購入数、合計決済金額が表示されています。
確認という観点からこの画面内では一切の商品や購入数の変更が出来ない様になっています。「購入する」ボタンを押下するとstripeの決済画面が表示されます。(テストモードです。)
メールアドレス、カード番号、利用期限、セキュリティコードを入力して「*を支払う」と書いてあるボタンを押下することでクレジットカード決済が完了します。決済処理後はホーム画面に遷移し、決済完了のモーダルが表示されます。
決済の実装で躓いた箇所がモーダルを表示する為のボタンの実装でした。
stripeのリファレンス等では下記の様な形式で記述すると書いてあります。
scriptタグ内でボタンを表示させていることを理解出来ていなかったのでどうやって任意の位置にボタンを配置すれば良いのだろうと悩んでいました。<form action="{{ route('') }}" method="POST"> <script src="https://checkout.stripe.com/checkout.js" class="stripe-button" data-key="{{ env('STRIPE_PUBLIC_KEY') }}" data-amount=1000 data-name="EC Shop" data-label="決済をする" data-description="Online shopping by Stripe" data-image="https://stripe.com/img/documentation/checkout/marketplace.png" data-locale="auto" data-currency="JPY"> </script> </form>scriptタグの直前に下記のaタグを記述することでキャプチャ通りの配置でボタンを実装することが出来ました。
<a class="btn btn-primary" href="/customer/home">戻る</a>また、Laravelでデフォルトで用意されている。app.jsがstripeの動作を妨げているらしく、モーダル画面が開くことが出来ませんでした。
この画面だけjsの利用を限定させるなどの工夫が必要でした。まとめ
以上が、今回私が作成したショッピングサイトの構成となります。
特に時間のかかった作業は認証、ファイルアップロード、決済機能の実装に関してです。
Laravelではデフォルトでusersテーブルを使った管理者機能を利用出来る様になっていますが、別のテーブルを利用して管理者と顧客両方のログイン機能を実装するのに綿密な調査が必要でした。
ファイルアップロード機能は業務含めて初めての試みだったのでバリデーションを考慮して実装するのに時間がかかりました。
決済機能の実装も初めての試みであり今回はAPIを利用させて頂きましたが、深いところを理解出来ていないのが正直な話です。もっと検討すべき仕様などがございますが時間的な都合で出来なかったことが多々あります。
また、本番公開の段階で発生した修正などもあり、実際に作っている中で自分の力不足を痛感することが多々ございました。今回の反省点を踏まえてもっと品質の良いプロダクトを作れる様に精進したいと思います。
ご覧頂きありがとうございました。
- 投稿日:2019-02-11T18:11:36+09:00
Web開発を初めて1年の成果として決済機能を持つショッピングサイトを作った話
約1年前にWeb開発の業務を担当させて頂きそこからマイペースにLaravelの勉強をしてきました。
勉強してきたことを1つの成果物としてまとめたいなと思い決済機能を持つ簡単なショッピングサイトを作りました。人によっては1ヶ月以内で出来る様な出来で恐縮ですがベーシック認証をかけた状態でherokuにデプロイをしました。
SSLの設定や画像ファイルの格納先の権限やらでまだ完璧にデプロイ出きた訳では無いですが、社内でのみ公開が出来る様にしたいと考えております。ここでは主に画面のキャプチャを用いて画面構成やどの様に作り込みをしたかをまとめていきます。
githubにソースコードを上げているのでソースコードの詳細はそちらをご覧頂ければと存じます。
自習やポートフォリオサイトを作ってみたいという方の助けになれれば幸いです。*決済はStripeを利用したクレジットカード決済となっています。(テストモードです。)
*以前書いた記事と重複している箇所は説明を割愛します。動作環境について
OS:macOS High Sierra
version: 10.13.6Docker(Engine):18.09.1
対象ブラウザ
GoogleChrome:72以上
開発環境
centOS:7.5
Laravel:5.6
PHP:7.2
MySQL:5.7
PostgreSQL:10.6
APache2.4外部サービス
参考資料
【簡単・オシャレ】PHPでstripeの決済を導入する手順【決済システム導入】
【初級編】LaravelでStripeを使って単発決済を実装するTOPページ
現在DBに登録されている商品が表示されています。
キーワード入力による商品の検索も可能です。TOPページでは決済が出来ないようになっています。
管理者は管理者ログインのリンクを、お客様はお客様ログインのリンクを押下して貰うことでログインページに遷移します。
お客様の新規登録の場合は新規登録のリンクを押下することでログインページに遷移します。ログイン・ユーザー画面
管理者と顧客それぞれのログイン関連の処理については以前書いた記事にまとめています。
adminテーブルを使って認証する方法
customerテーブルを使って認証する方法パスワード再設定に使うメールはMailgunを使いますが、送信先が限定されているので本番環境ではsmtpを立てた方が良さそうです。
管理者画面
TOPページと同様に登録されている商品の一覧が表示されています。
TOPページと異なる点は各商品ごとに編集、画像アップロード、削除の処理を行えるボタンを置いていることです。ファイルアップロードについても別の記事でまとめています。
右上のプルダウンを押すとメニュー画面が出てきます。
ここから管理者のホーム画面への遷移、管理者登録、商品登録、ログアウトを行います。新商品登録
商品登録画面では商品名、価格、説明文を入力できます。
上記3項目はバリデーションファイルにて入力条件を定めることで誤ったデータがDBに挿入されることを防いでいます。
public function rules() { return [ 'goods_name' => 'required|string|max:50', // 必須|文字列|最大50文字 'price' => 'required|integer|min:1|digits_between:1,6', // 必須|最小値:1|数値かつ1桁~6桁 'goods_text' => 'nullable|string|max:120', // null可|文字長120文字以内 ]; }入力誤りがあればメッセージが表示されます。
最終的な入力内容に誤りが無いか確認する為に確認画面を設けています。
「登録する」ボタンを押下すると商品の登録が完了します。
商品情報編集
各商品の「編集」ボタンを押下します。
画面構成自体は新規登録時と違いはありません。
商品ごとにDBに登録されている値が予め入力欄に表示されます。編集し終えたら確認をし、最後にデータの更新をかけます。
商品削除
「削除」ボタンを押すと確認のアラートが表示されます。「OK」を押すと即座に商品の削除が実行されます。
顧客画面
TOPページと同様に登録されている商品の一覧が表示されています。
TOPページと異なる点は商品のカートへの格納と削除を行うボタン、最終的な購入手続きを行うボタンがあることです。右上のプルダウンを押すとメニュー画面が出てきます。
ここから顧客のHome画面への遷移、ログアウトを行います。カートに入れる
カートに何も商品を入れないまま購入手続きをしようとしても出来ない様になっています。
「カートに入れる」ボタンを押下することで該当の商品をカートに入れることが出来ます。
1回のボタン押下によりカートに入れることが出来る数量は1つとなっています。カートから外す
「カートから外す」ボタンを押下することで該当の商品をカートから外すことが出来ます。
1回のボタン押下によりカートから外すことが出来る数量は1つとなっています。購入内手続き
ホーム画面から「購入内手続きへ」ボタンを押すと購入内容の確認画面に遷移します。
確認画面ではカートに入れた商品のデータと購入数、合計決済金額が表示されています。
確認という観点からこの画面内では一切の商品や購入数の変更が出来ない様になっています。「購入する」ボタンを押下するとstripeの決済画面が表示されます。(テストモードです。)
メールアドレス、カード番号、利用期限、セキュリティコードを入力して「*を支払う」と書いてあるボタンを押下することでクレジットカード決済が完了します。決済処理後はホーム画面に遷移し、決済完了のモーダルが表示されます。
決済の実装で躓いた箇所がモーダルを表示する為のボタンの実装でした。
stripeのリファレンス等では下記の様な形式で記述すると書いてあります。
scriptタグ内でボタンを表示させていることを理解出来ていなかったのでどうやって任意の位置にボタンを配置すれば良いのだろうと悩んでいました。<form action="{{ route('') }}" method="POST"> <script src="https://checkout.stripe.com/checkout.js" class="stripe-button" data-key="{{ env('STRIPE_PUBLIC_KEY') }}" data-amount=1000 data-name="EC Shop" data-label="決済をする" data-description="Online shopping by Stripe" data-image="https://stripe.com/img/documentation/checkout/marketplace.png" data-locale="auto" data-currency="JPY"> </script> </form>scriptタグの直前に下記のaタグを記述することでキャプチャ通りの配置でボタンを実装することが出来ました。
<a class="btn btn-primary" href="/customer/home">戻る</a>また、Laravelでデフォルトで用意されている。app.jsがstripeの動作を妨げているらしく、モーダル画面が開くことが出来ませんでした。
この画面だけjsの利用を限定させるなどの工夫が必要でした。まとめ
以上が、今回私が作成したショッピングサイトの構成となります。
特に時間のかかった作業は認証、ファイルアップロード、決済機能の実装に関してです。
Laravelではデフォルトでusersテーブルを使った管理者機能を利用出来る様になっていますが、別のテーブルを利用して管理者と顧客両方のログイン機能を実装するのに綿密な調査が必要でした。
ファイルアップロード機能は業務含めて初めての試みだったのでバリデーションを考慮して実装するのに時間がかかりました。
決済機能の実装も初めての試みであり今回はAPIを利用させて頂きましたが、深いところを理解出来ていないのが正直な話です。もっと検討すべき仕様などがございますが時間的な都合で出来なかったことが多々あります。
また、本番公開の段階で発生した修正などもあり、実際に作っている中で自分の力不足を痛感することが多々ございました。今回の反省点を踏まえてもっと品質の良いプロダクトを作れる様に精進したいと思います。
ご覧頂きありがとうございました。
- 投稿日:2019-02-11T15:49:25+09:00
Laravel tinkerコマンドでメール送信する
- 投稿日:2019-02-11T15:09:58+09:00
Laravel5.6でファイルアップロードの実装と躓いたところまとめ
業務でも自習でも初めてファイルアップロード機能を手を動かして実装してみたので手順をまとめておこうと思います。
実装中に躓いた箇所があったのでその対応方法も記載します。動作環境について
OS:macOS High Sierra
version: 10.13.6
Docker(Engine):18.09.1対象ブラウザ
GoogleChrome:72以上
環境
centOS:7.5
Laravel:5.6
PHP:7.2
MySQL:5.7
PostgreSQL:10.6(MySQLから変更しました。)
APache2.4参考資料
Laravelで画像ファイルアップロードをする簡単なサンプル
【Laravel5.6】画像ファイルアップロードについてのポイントまとめ
Laravelのバリデーションで指定できる内容をざっくりまとめ直しました。画面構成について
全然スマートな画面じゃなくて恐縮ですが・・・。
管理画面にある各商品情報の右に「画像」ボタンを配置しています。「画像」ボタンを押下するとファイルアップロードのモーダルが表示されます。
アップロードに成功するとTOPページに各商品ごとの画像が表示される様になります。
*「-------」となっている商品はまだ画像のアップロードを行っていないことを示しています。下記では、この上記画像の動作を実現する方法をまとめています。
View
モーダルのformタグ内は下記の通りになっています。
name属性が「select_id」のinputタグは「画像」ボタン押下時にjavascriptでvalueが入る様になっています。<form method="POST" action="{{ route('admin_upload_image') }}" enctype='multipart/form-data'> {{ csrf_field() }} <input type="hidden" name="select_id" value=""> <div class="form-group row"> <label for="goods_image" class="col-md-3 col-form-label text-md-left">{{ __('イメージ') }}</label> <div class="col-md-7"> <input type="file" name="goods_image" value="" style="border:none;" > <small class="input_condidion">*jpg,png形式のみ</small></br> <small class="input_condidion">*最小画像サイズ:縦横100px</small></br> <small class="input_condidion">*最大画像サイズ:縦横600px</small> @if ($errors->has('goods_image')) <span class="invalid-feedback" role="alert"> <strong>{{ $errors->first('goods_image') }}</strong> </span> @endif </div> </div> <div class="col-md-offset-3 text-center"> <button type="button" class="btn btn-primary" data-dismiss="modal">閉じる</button> <input type="submit" class="btn btn-success" value="登録"> </div> </form>ルーティング
web.phpに下記の通りに記載しています。
Route::group(['prefix' => 'admin'], function(){ . . . Route::post('goods/upload', 'Admin\HomeController@uploadGoodsImage')->name('admin_upload_image'); });Controller
Controllerの処理の前にバリデーションファイルを別途用意する予定でしたが、後述の躓いた箇所の影響もあり、他の人が書いてくださった内容に習わせて頂きました。
拡張子の指定が「mimes」、画像サイズの指定が「dimensions」をキーとして詳細を指定しています。・画像の格納先を指定するstoreAs()について
⇨第1引数:保存するディレクトリ名,第2引数:アップロードするファイル名,第3引数:disksのドライバ名(config/filesystems.phpに記載)となっています。
今回の場合はgoodsディレクトリの中に各商品IDのディレクトリを作成し、その中にアップロードした時の同様のファイル名で格納する様にパラメーターを変数で指定しています。
第3引数は特に変更がなければ「public」のままで良いと思います。
変更する場合は「config/filesystems.php」を編集する必要があります。public function uploadGoodsImage(Request $request) { // 選択した商品IDの取得 $select_id = $request->input('select_id'); // アップロードしたファイル名を取得 $upload_name = $_FILES['goods_image']['name']; // アップロードするディレクトリ名を指定 $up_dir = 'goods/' . $select_id; // アップロードしたファイルのバリデーション設定 $this->validate($request, [ 'goods_image' => [ 'required', 'file', 'image', 'mimes:jpeg,png', 'dimensions:min_width=100,min_height=100,max_width=600,max_height=600', ] ]); //アップロードに成功しているか確認 if ($request->file('goods_image')->isValid([])) { $filename = $request->file('goods_image')->storeAs($up_dir, $upload_name, 'public'); // DBへファイル名登録処理 $goods = \App\Models\Goods::findOrFail($select_id); // $filenameだとパスが含まれてしまう為、basename()で囲う $goods->image_name = basename($filename); // 更新(差分があればDBに登録) $goods->save(); return redirect()->to('admin/home')->with('flashmessage', 'イメージ画像の登録が完了しました。'); } else{ return redirect()->to('admin/home')->with('flashmessage', 'イメージ画像の登録に失敗しました。'); } }シンボリックリンクの作成
下記コマンドで public/storage から storage/app/public にシンボリックリンクを貼ること出来ます。
[root@87c2be02241a shop]# php artisan storage:link The [public/storage] directory has been linked.画像アップロードの確認
各商品のIDを格納先として画像が保存されていることが確認出来ます。
[root@87c2be02241a shop]# ls public/storage/goods/4 sample_goods.png [root@87c2be02241a shop]# ls storage/app/public/goods/4 sample_goods.pngアップロードするファイルを表示させる時
各商品の画像名を取得して、nullでは無い場合はURLを指定して表示させています。
@if($goods->image_name) <img src="{{ asset('storage/goods/' . $goods->id . '/' . $goods->image_name) }}" width="40" height="40" alt="no_goods_image" /> @else <p>--------</p> @endif躓いたところ
アップロードするとhome画面にリダイレクトしてしまう不具合が発生。
バリデーションで何かミスかなと調べてみました。Controller部分内でdd()を使ってファイルが取得出来ているかを確認してみると、ファイルを取得出来ていませんでしrた。
本来下記の形でファイルを取得出来るのですが、nullになるばかり。$_FILESを確認してもアップロードされたファイルが無い状態でした。
$file = $request->file('goods_image'); dd($file)調べていく中でformタグに「enctype」属性が抜けていたのを気づいたので下記の通り追記してみたのですが、これもだめでした。
enctype="multipart/form-data"php.iniの設定が悪いのかと思い確認してみました。
ファイルアップロード関連で関係のありそうなのは下記の設定ですが確認してみるとアップロードしているファイルのサイズ的には問題無さそうでした。upload_max_filesize = 10M file_uploads = On max_file_uploads = 20 post_max_size = 8M⇨念の為「post_max_size」と「upload_max_filesize」の上限を上げてみたのですがこれも結局だめでした。
その後調べを進めてみるとやはり「enctype="multipart/form-data"」が記載されていないのが原因とする記述が多かった為、もしかしてブラウザ上では表示されていないのかなと思いデベロッパーツールで確認してみると、原因がわかりました。
「enctype="multipart/form-data"」と表示されるべきところが下記の様に表示されていました。
enctype=""multipart/form-data""そこでviewファイルにて「enctype='multipart/form-data'」と書き直したところ、無事ファイルアップロードが出来ました。
後日改めてダブルクォーテーションで囲ってみたらブラウザでも「enctype="multipart/form-data"」と表示されていました。
viewファイルの構成を考えながら作っていた為、viewファイルのどこかで記述がミスがあったと思われます。
viewとファイルアップロードを同時平行で作っていたのがアダとなった形です。取り急ぎでファイルアップロードの機能を試したい時に正常に動かない場合、上記対応で動かせることがわかりました。
- 投稿日:2019-02-11T13:10:49+09:00
【Laravel】実行されたSQLを取得する
実行されたSQLを出力します。
SQLだけでなく、プリペアドステートメントや実行時間も出力されるので便利です。ログの出力
// ログを有効化 DB::enableQueryLog(); City::where('Name', '=', 'Kabul')->get(); // ログ出力 dd(DB::getQueryLog()); // ログを無効化 DB::disableQueryLog();出力結果
array:1 [▼ 0 => array:3 [▼ "query" => "select * from `city` where `Name` = ?" "bindings" => array:1 [▼ 0 => "Kabul" ] "time" => 6.46 ] ]
- 投稿日:2019-02-11T12:57:54+09:00
LaravelのDIでサービスコンテナを使って環境ごとにクラスを使いわける
開発環境の時はslackにさせたい(orさせたくない)とか、動作確認を簡単にするために艦初環境ではバリデーションをゆるくしたいなど、環境ごとに処理を分けたいことがあると思います。
直接コードの中に環境ごとの分岐を書くこともできるのですが、テスタブルではなくなるので、外からクラスを渡せるようにコンストラクターインジェクションを行い、環境に応じたクラスを使い分ける方法とテストコードについて書きます。環境ごとに使いたいクラスを用意する
今回はシンプルにproductionの処理とそれ以外の環境での処理を分けるようにします。処理の内容は環境ごとに違う文字列を返すだけです。
production用クラス
Services/Message.php<?php namespace App\Services; class Message { public function makeMessage() { return 'message'; } }その他用のクラス
先ほどのクラスを継承させて、開発環境などで使われるfakeのクラスを作成しました。
Services/FakeMessage.php<?php namespace App\Services; class FakeMessage extends Message { public function makeMessage() { return 'fake message'; } }実際には通知先を変えるとか特定の処理を行わないとかもう少しこのクラス内でやることが変わるはずですがシンプルに。
呼び出し元
コンストラクタの引数にMessageクラスを渡せるようにしておきます。
Services/Notification.php<?php namespace App\Services; class Notification { private $messageService; // インスタンス生成時にMessageクラスを渡せるようにする public function __construct(Message $messageService) { $this->messageService = $messageService; } public function send() { // どこかに通知をしたとする return $this->messageService->makeMessage(); } }サービスコンテナに追加をする
環境ごとにクラスを使い分けたいので、サービスコンテナに記載します。
どのクラスを使うかは、configファイルから取得をします。元のクラスがMessgeクラスでそれを継承しているクラスがFakeMessageなので、デフォルトではMessageクラスを使用して、使うクラスが指定されている場合のみそのクラスを使うようにします。
app/Providers/AppServiceProvider.phppublic function register() { if (!$messageClass = config('app.message')) { // configで指定されていないので、Messageクラスを使う $this->app->bind("App\\Services\\Message", "App\\Services\\Message"); } else { // Messageクラスの代わりにconfigで指定されたクラスを使う $this->app->bind("App\\Services\\Message", "App\\Services\\$messageClass"); } }こちらの部分で、
configディレクトリ
のapp.php
のkeyがmessage
の値を取得しています。config('app.message')config
.envファイルから値を取得します。
config.app.php'message' => env('MESSAGE_CLASS', ''),下記envファイルからその環境で使うクラス名を指定します。(localやstagingなども同様に)
env.testingMESSAGE_CLASS=FakeMessageここまでで環境ごとに使い分けられるようになりました。
各環境ごとにクラスを分ける場合にはenvファイルに追加すれば環境ごとに変えられます。テストコード
そのまま使用をするとtestingの環境として実行されるため、本番環境で動くコードのテストが行えません。
そのためコンストラクタインジェクションで本番環境で実行されるクラスを渡すようにします。test/Unit/NotificationTest.php<?php namespace Tests\Unit; use App\Services\Message; use App\Services\Notification; use Tests\TestCase; class NotificationTest extends TestCase { public function testNotification() { // FakeMessageではなくMessageクラスを使用する $notification = new Notification(new Message()); $this->assertEquals('message', $notification->send()); } }
- 投稿日:2019-02-11T12:41:07+09:00
【Laravel】既存テーブルのマイグレーションファイルを作成する
Laravelで既存のテーブルを使用したい場合は、マイグレーションファイルを自分で用意する必要があります。
それを自動化するパッケージがあったので、ご紹介します。
使用パッケージ
migrations-generator
というパッケージを利用します。参考記事です。
開発環境
- PHP 7.3.2
- Laravel 5.7.25
- Composer 1.8.3
手順
マイグレーションファイルの生成
worldデータベースにある下記テーブルのマイグレーションファイルを生成します。
- city
- country
- countrylanguage
手順は
README
に書かれています。
その通りに実行します。$ composer require --dev "xethron/migrations-generator"Laravel既存のテーブル
users, password_resets
を除いて、マイグレーションファイルを生成します。$ php artisan migrate:generate --ignore="users, password_resets" # 省略 Do you want to log these migrations in the migrations table? [Y/n] : > Y Migration table created successfully. Next Batch Number is: 1. We recommend using Batch Number 0 so that it becomes the "first" migration [Default: 0] : > 0 # 省略これで、以下のマイグレーションファイルが生成されました。合わせて外部キーのマイグレーションファイルも生成されています。
- 2019_02_10_224508_create_city_table.php
- 2019_02_10_224508_create_country_table.php
- 2019_02_10_224508_create_countrylanguage_table.php
- 2019_02_10_224509_add_foreign_keys_to_city_table.php
- 2019_02_10_224509_add_foreign_keys_to_countrylanguage_table.php
Modelの設定
試しにcityテーブルを扱うCityモデルを作成します。
$ php artisan make:model Cityテーブル名が「city」のため、Modelにテーブル名を明示する必要があります。
以下、公式サイトからの引用です。他の名前を明示的に指定しない限り、クラス名を複数形の「スネークケース」にしたものが、テーブル名として使用されます。
参考記事です。
Laravelで「Base table or view not found: 1146 Table」エラーが出るときの対処法
City.php<?php namespace App; use Illuminate\Database\Eloquent\Model; class City extends Model { protected $table = 'city'; }これでテーブルが使用できるようになりました!
- 投稿日:2019-02-11T11:55:40+09:00
【Laravel】allメソッドを使うとtoSqlメソッドが使えない
クエリビルダを使用すると、
toSqlメソッド
で実行されるSQLを取得することができます。しかし、
City::all()
のようにallメソッドを使用した場合は、toSqlメソッドを使うことができません。以下のエラーが出ます。
Method Illuminate\Database\Eloquent\Collection::toSql does not exist.エラーメッセージの通りなのですが、原因を探ってみました。
allメソッドの場合
test.php$sql_all = City::all(); var_dump(get_class($sql_all)); // エラー発生 var_dump($sql_all->toSql());オブジェクトのクラス名を取得すると、
Illuminate\Database\Eloquent\Collection
クラスが使用されていることがわかります。addメソッド自身は、スーパークラスである
Support\Collection
クラスに定義されています。しかし、両クラスともtoSqlメソッドが定義されていません。
これが原因でした。Illuminate\Support\Collection.php/** * Get all of the items in the collection. * * @return array */ public function all() { return $this->items; }さらに、
getメソッド
を使用した場合も同じエラーが出ます。test.php$sql_builder = DB::table('city')->get(); var_dump(get_class($sql_builder)); // エラー発生 var_dump($sql_builder->toSql());上記のコードではEloquentを使用していないので、オブジェクトのクラス名は
Illuminate\Support\Collection
になります。エラーの理由は上記と同じです。
toSqlメソッドの場所
当のtoSqlメソッドが定義されているクラスは、Builderクラスになります。
Illuminate\Database\Query\Builder.php/** * Get the SQL representation of the query. * * @return string */ public function toSql() { return $this->grammar->compileSelect($this); }whereメソッドの場合
whereメソッドを使用する場合は、toSqlメソッドが使用できます。
オブジェクトのクラス名を確認すると、
lluminate\Database\Eloquent\Builder
クラスが使用されていると確認できます。これはtoSqlメソッドが定義されているクラスと同じです。
test.php$sql_where = City::where('Name', '=', 'Kabul'); var_dump(get_class($sql_where)); // 出力される var_dump($sql_where->toSql());コアファイルを読む良いきっかけになりました。
- 投稿日:2019-02-11T02:09:50+09:00
dockerで作った環境(nginx+php-fpm+mysql)にLaravelを入れたときの設定メモ
nignxとphp-fpmとmysqlの連携環境をdocker-composeで構築済。しかし、nginxの設定が足りていないので設定を修正する必要がある。
前提
nginxはド素人。PHP(Laravel)の環境を作る必要があり、使ったことが無いnginxで動かしてみようという軽いノリで始めたメモ。
(dockerで)nginxに関するメモ、(dockerで)php-fpmに関するメモ、(dockerで)mysqlに関するメモ、(dockerで作った環境に)Laravelプロジェクトを設置したメモ。
./docker/nginx.conf
nginx.conf(一部)... location /projLaravel/public { try_files $uri $uri/ /projLaravel/public/index.php?$query_string; } ...通常は /projLaravel/public をドキュメントルートにするんだろうけど今回は敢えてしない。重要なのは、 Laravelのプロジェクトでは全てのアクセスを index.php(フロントコントローラ)に集めなければならない と言うこと。 Apacheの場合は.htaccessで行っている。
この設定についてはLaravelのインストール手順を見ると載っている。
- 投稿日:2019-02-11T00:44:54+09:00
GraphQL+Lighthouse(+Laravel)でAPI開発2(開発編1 - 非Eloquentモデルの実装)
前回に引き続き、LaravelにおけるGraphQL+Lighthouseの実装方法を紹介していきます。
ぜひ、GraphQL+Lighthouse(+Laravel)でAPI開発1(インストール方法・設定編)もご覧ください。方針
前回のエントリでも記述しておりますが、今回は既存のシステムに途中からGraphQLでAPIを実装することになりました。また、Laravelのアプリケーション実装自体も旧システムの方針を引き継ぎ実装されたため、ORMの恩恵をあまり受けられないDBの設計となってしまっています。
そのため、今回はEloquentを用いない方針でGraphQLのサーバーサイド側を実装していくことにします。
またGraphQLにはQuery
、Mutation
と2種類のメソッドがありますが、今回はQuery
の実装メインの紹介です。実装
1. スキーマの定義
前回のエントリで
routes/graphql/schema.graphql
にスキーマ定義ファイル(以下、型ファイル)の雛形ができていると思います。こちらにクエリの型定義を記述していきましょう。
まずは、今回関係のない型定義を削除してしまいます。
routes/graphql/schema.graphql#"A datetime string with format 'Y-m-d H:i:s', e.g. '2018-01-01 13:00:00'." scalar DateTime @scalar(class: "Nuwave\\Lighthouse\\Schema\\Types\\Scalars\\DateTime") #"A date string with format 'Y-m-d', e.g. '2011-05-23'." scalar Date @scalar(class: "Nuwave\\Lighthouse\\Schema\\Types\\Scalars\\Date") # ここから下削除 # ...上の2つの
scalar
はlighthouse
から提供されている型なので、残しても削除してしまっても問題ありません。次に
Query
の型を定義しましょう。routes/graphql/schema.graphqltype { top: Top }
Query
はtop
でTop
型の値を返します。
Top
型を次に定義します。
小さなアプリケーションの場合1つの型ファイルに全ての型を記述していっても良いですが、開発が進んでいくと型ファイル内がとんでもないことになりそうなので、別の型ファイルtop.graphql
を用意しましょう。今回適当にいくつか型を用意します。routes/graphql/top.graphqltype Top { # nullを許容しない user: User! purchaseHistory: PurchaseHistory # Shopが複数入った配列型 favoriteShops: [Shop] } type User { id: Int! name: String! point: Int! } type PurchaseHistory { timestamp: Date histories: [History] } type Date { year: Int month: Int day: Int hour: Int minute: Int second: Int } type History { shop: Shop purchasedDate: Date purchasedAmount: Int } type Shop { shopId: Int! shopName: String! }こちらの
Top
型をroutes/graphql/schema.graphql
で読み込むために次の一文を追加します。routes/graphql/schema.graphql#import ./top.graphql type { top: Top }
#
とimport
にスペースを空けてしまうと上手く動作しないので気をつけてください。以上でスキーマの定義は終了です。
2. Queryの雛形生成
まずは下記のコマンドを実行して、
Query
の雛形を作成しましょう。php artisan lighthouse:query Top # docker環境の場合 (dc=docker-compose, hoge=${service_name}) dc exec hoge php artisan lighthouse:query Topすると、次のようなファイルが生成されます。
app/Http/GraphQL/Queries/Top.php<?php declare(strict_types = 1); namespace App\Http\GraphQL\Queries; class Top { /** * Return a value for the field. * * @param null $rootValue Usually contains the result returned from the parent field. In this case, it is always `null`. * @param array $args The arguments that were passed into the field. * @param GraphQLContext|null $context Arbitrary data that is shared between all fields of a single query. * @param ResolveInfo $resolveInfo Information about the query itself, such as the execution state, the field name, path to the field from the root, and more. * * @return mixed */ public function resolve($rootValue, array $args, GraphQLContext $context = null, ResolveInfo $resolveInfo): array { } }こちらは通常の
Controller
にあたるものという認識で良いと思います。名前空間
Http
配下に新規にGraphQL
というディレクトリがデフォルトで生成されますが、config/lighthouse.php/* |-------------------------------------------------------------------------- | Namespaces |-------------------------------------------------------------------------- | | These are the default namespaces where Lighthouse looks for classes | that extend functionality of the schema. | */ 'namespaces' => [ 'models' => 'App\\Models', 'queries' => 'App\\Http\\GraphQL\\Queries', 'mutations' => 'App\\Http\\GraphQL\\Mutations', 'interfaces' => 'App\\Http\\GraphQL\\Interfaces', 'unions' => 'App\\Http\\GraphQL\\Unions', 'scalars' => 'App\\Http\\GraphQL\\Scalars', ],上記の設定ファイル内で名前空間を指定すると任意の場所にファイルが生成されます。
Custom Query/Resolver
また、今回は
top
というクエリに対し対応する名前のクラスを雛形として生成しましたが、
php artisan lighthouse query:CustomQuery
として任意のクラス名で雛形を生成することも可能です。
その際はschema.graphql
内で対象のクエリに@field
ディレクティブを用いてresolver
を指定する必要があります。schema.graphqltype { top: Top @field(resolver: "App\\GraphQL\\Queries\\CustomQuery@resolverMethodName") # config/lighthouse.phpで指定した名前空間と同じ場合、省略記法が使える # @field("CustomQuery@resolverMethodName") }3. Queryの実装
続いて、実際にリクエストをハンドルしレスポンスを返す部分を実装していきましょう。
php artisan lighthouse query:Top
を実行するとLaravelのController部分にあたる部分のQueryクラスが生成されます。GraphQLのエンドポイントにリクエストを投げるとresolve
というメソッドが実行されます。引数にはリクエストのコンテキスト情報などが入っています。3.1 ダミーデータの準備
まずは、定義した型に対応する連想配列を返り値として準備しましょう。
Top.php<?php declare(strict_types = 1); namespace App\Http\GraphQL\Queries; class Top { /** * Return a value for the field. * * @param null $rootValue Usually contains the result returned from the parent field. In this case, it is always `null`. * @param array $args The arguments that were passed into the field. * @param GraphQLContext|null $context Arbitrary data that is shared between all fields of a single query. * @param ResolveInfo $resolveInfo Information about the query itself, such as the execution state, the field name, path to the field from the root, and more. * * @return mixed */ public function resolve($rootValue, array $args, GraphQLContext $context = null, ResolveInfo $resolveInfo) { return [ 'user' => [ 'id' => 10, 'name' => 'GraphQL Taro', 'point' => 3000, ], 'purchaseHistory' => [ 'timestamp' => [ 'year' => 2019, 'month' => 2, 'day' => 10, 'hour' => 22, 'minute' => 10, 'second' => 0, ], 'histories' => [ [ 'shop' => [ 'shopId' => 100, 'shopName' => 'shop1', ], 'purchasedDate' => [ 'year' => 2019, 'month' => 1, 'day' => 1, 'hour' => 10, 'minute' => 0, 'second' => 0, ], 'purchasedAmount' => 1000, ], ], ], 'favoriteShops' => [ ['shopId' => 100, 'shopName' => 'shop1'], ['shopId' => 200, 'shopName' => 'shop2'], ], ]; } }基本的にはこのようなレスポンスを返すようなものであれば、どのようなクラス設計でも問題ありません。
Repositoryパターンを用いてデータベースやその他のストレージからデータを取得するなど、LaravelではEloquentなしでも自由な設計が可能です。DDD設計からドメイン毎にServiceクラスなどを用意しHistory
、Shop
などのモデルクラスを返すこともできるでしょう。最終的なレスポンスの形だけ決めてしまえば、GraphQLのサーバーサイド開発はほとんど完了します。
3.2 カラム毎の処理
GraphQLの特徴であるクエリ毎に必要なデータを返す機能を実装する場合、全てのロジックプロセスを実行してからデータをフィルタリングするよりも、必要なプロセスのみを実行する方が好まれるでしょう。今回は下記のような
columnResolver
メソッドを用意し、クエリに含まれるカラムに対応したロジックを実行するための実装を行いました。Top.php<?php declare(strict_types = 1); namespace App\Http\GraphQL\Queries; use Throwable; class Top { public const USER = 'user'; public const PURCHASE_HISTORY = 'purchaseHistory'; public const FAVORITE_SHOPS = 'favoriteShops'; public const COLUMNS = [ self::USER, self::PURCHASE_HISTORY, self::FAVORITE_SHOPS, ]; /** @var string[] */ protected $columns = []; /** @var mixed[] */ protected $response = []; /** * Return a value for the field. * * @param null $rootValue Usually contains the result returned from the parent field. In this case, it is always `null`. * @param array $args The arguments that were passed into the field. * @param GraphQLContext|null $context Arbitrary data that is shared between all fields of a single query. * @param ResolveInfo $resolveInfo Information about the query itself, such as the execution state, the field name, path to the field from the root, and more. * * @return mixed */ public function resolve($rootValue, array $args, GraphQLContext $context = null, ResolveInfo $resolveInfo) { $this->columns = $resolveInfo->getFieldSelection(); foreach (self::COLUMNS as $column) { $this->resolveColumn($column); } return $this->response; } public function resolveColumn(string $name): void { $resolver = $this->getResolverName($name); // リクエストに含まれるカラムのみを処理するためのフィルタ if (!isset($this->columns[$name])) { return; } try { $this->response[$name] = $resolver(); } catch (Throwable $throwable) { // Log残すなどの処理を含めても良い $this->response[$name] = null; } } public function getUser(): array { return [ 'id' => 10, 'name' => 'GraphQL Taro', 'point' => 3000, ]; } public function getPurchaseHistory(): array { // 省略 return []; } public function getFavoriteShops(): array { // 省略 return []; } private function getResolverName(string $name): string { return 'get' . ucwords($name); } }以上でQueryの実装は終了です。まだまだこれから、GraphQLがより浸透し様々な実装方法が提案されるでしょう。私の方でも引き続き試行錯誤していこうと思います。
次回へ向けて
次回は今回実装したものに関して自作のエラーハンドリングを追加する場合について書いていこうと思います。