20190211のlaravelに関する記事は12件です。

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.html

sudo 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"下に設置される
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

bladeの変数確認

bladeで変数の存在確認

controllerからviewに変数を遅れているかの判定(blade)
php:exam.php
isset($data);

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

LaravelとStripeを使って決済機能を持つショッピングサイトを作った

約1年前にWeb開発の業務を担当させて頂きそこからマイペースにLaravelの勉強をしてきました。
勉強してきたことを1つの成果物としてまとめたいなと思い決済機能を持つ簡単なショッピングサイトを作りました。

人によっては1ヶ月以内で出来る様な出来で恐縮ですがベーシック認証をかけた状態でherokuにデプロイをしました。
SSLの設定や画像ファイルの格納先の権限やらでまだ完璧にデプロイ出きた訳では無いですが、社内でのみ公開が出来る様にしたいと考えております。

ここでは主に画面のキャプチャを用いて画面構成やどの様に作り込みをしたかをまとめていきます。
githubにソースコードを上げているのでソースコードの詳細はそちらをご覧頂ければと存じます。
自習やポートフォリオサイトを作ってみたいという方の助けになれれば幸いです。

*決済はStripeを利用したクレジットカード決済となっています。(テストモードです。)
*以前書いた記事と重複している箇所は説明を割愛します。

動作環境について

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
APache2.4

外部サービス

Mailgun
Stripe

参考資料

【簡単・オシャレ】PHPでstripeの決済を導入する手順【決済システム導入】
【初級編】LaravelでStripeを使って単発決済を実装する

TOPページ

現在DBに登録されている商品が表示されています。
キーワード入力による商品の検索も可能です。

FireShot Capture 6 - ECshop - http___shop1.localhost_.png

TOPページでは決済が出来ないようになっています。
管理者は管理者ログインのリンクを、お客様はお客様ログインのリンクを押下して貰うことでログインページに遷移します。
お客様の新規登録の場合は新規登録のリンクを押下することでログインページに遷移します。

ログイン・ユーザー画面

管理者と顧客それぞれのログイン関連の処理については以前書いた記事にまとめています。

adminテーブルを使って認証する方法
customerテーブルを使って認証する方法

パスワード再設定に使うメールはMailgunを使いますが、送信先が限定されているので本番環境ではsmtpを立てた方が良さそうです。

管理者画面

TOPページと同様に登録されている商品の一覧が表示されています。
TOPページと異なる点は各商品ごとに編集、画像アップロード、削除の処理を行えるボタンを置いていることです。

FireShot Capture 9 - ECshop - http___shop1.localhost_admin_home.png

ファイルアップロードについても別の記事でまとめています。

右上のプルダウンを押すとメニュー画面が出てきます。
ここから管理者のホーム画面への遷移、管理者登録、商品登録、ログアウトを行います。

スクリーンショット 2019-02-11 16.27.48.png

新商品登録

商品登録画面では商品名、価格、説明文を入力できます。

FireShot Capture 10 - ECshop - http___shop1.localhost_admin_goods_regist.png

上記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文字以内
    ];
}

入力誤りがあればメッセージが表示されます。

FireShot Capture 11 - ECshop - http___shop1.localhost_admin_goods_regist.png

最終的な入力内容に誤りが無いか確認する為に確認画面を設けています。

FireShot Capture 12 - ECshop - http___shop1.localhost_admin_goods_confirm.png

「登録する」ボタンを押下すると商品の登録が完了します。

FireShot Capture 13 - ECshop - http___shop1.localhost_admin_home.png

商品情報編集

各商品の「編集」ボタンを押下します。
画面構成自体は新規登録時と違いはありません。
商品ごとにDBに登録されている値が予め入力欄に表示されます。

FireShot Capture 15 - ECshop - http___shop1.localhost_admin_goods_edit_115.png

編集し終えたら確認をし、最後にデータの更新をかけます。

FireShot Capture 24 - ECshop - http___shop1.localhost_admin_home.png

商品削除

「削除」ボタンを押すと確認のアラートが表示されます。「OK」を押すと即座に商品の削除が実行されます。

FireShot Capture 18 - ECshop - http___shop1.localhost_admin_home.png

顧客画面

TOPページと同様に登録されている商品の一覧が表示されています。
TOPページと異なる点は商品のカートへの格納と削除を行うボタン、最終的な購入手続きを行うボタンがあることです。

FireShot Capture 19 - ECshop - http___shop1.localhost_customer_home.png

右上のプルダウンを押すとメニュー画面が出てきます。
ここから顧客のHome画面への遷移、ログアウトを行います。

スクリーンショット 2019-02-11 16.58.09.png

カートに入れる

カートに何も商品を入れないまま購入手続きをしようとしても出来ない様になっています。

FireShot Capture 21 - ECshop - http___shop1.localhost_customer_home.png

「カートに入れる」ボタンを押下することで該当の商品をカートに入れることが出来ます。
1回のボタン押下によりカートに入れることが出来る数量は1つとなっています。

FireShot Capture 22 - ECshop - http___shop1.localhost_customer_home.png

カートから外す

「カートから外す」ボタンを押下することで該当の商品をカートから外すことが出来ます。
1回のボタン押下によりカートから外すことが出来る数量は1つとなっています。

FireShot Capture 23 - ECshop - http___shop1.localhost_customer_home.png

購入内手続き

ホーム画面から「購入内手続きへ」ボタンを押すと購入内容の確認画面に遷移します。

確認画面ではカートに入れた商品のデータと購入数、合計決済金額が表示されています。
確認という観点からこの画面内では一切の商品や購入数の変更が出来ない様になっています。

FireShot Capture 26 - ECshop - http___shop1.localhost_customer_cart_payment_confirm.png

「購入する」ボタンを押下するとstripeの決済画面が表示されます。(テストモードです。)
メールアドレス、カード番号、利用期限、セキュリティコードを入力して「*を支払う」と書いてあるボタンを押下することでクレジットカード決済が完了します。

FireShot Capture 27 - ECshop - http___shop1.localhost_customer_cart_payment_confirm.png

決済処理後はホーム画面に遷移し、決済完了のモーダルが表示されます。

FireShot Capture 28 - ECshop - http___shop1.localhost_customer_home.png

決済の実装で躓いた箇所がモーダルを表示する為のボタンの実装でした。

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を利用させて頂きましたが、深いところを理解出来ていないのが正直な話です。

もっと検討すべき仕様などがございますが時間的な都合で出来なかったことが多々あります。
また、本番公開の段階で発生した修正などもあり、実際に作っている中で自分の力不足を痛感することが多々ございました。

今回の反省点を踏まえてもっと品質の良いプロダクトを作れる様に精進したいと思います。

ご覧頂きありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Web開発を初めて1年の成果として決済機能を持つショッピングサイトを作った話

約1年前にWeb開発の業務を担当させて頂きそこからマイペースにLaravelの勉強をしてきました。
勉強してきたことを1つの成果物としてまとめたいなと思い決済機能を持つ簡単なショッピングサイトを作りました。

人によっては1ヶ月以内で出来る様な出来で恐縮ですがベーシック認証をかけた状態でherokuにデプロイをしました。
SSLの設定や画像ファイルの格納先の権限やらでまだ完璧にデプロイ出きた訳では無いですが、社内でのみ公開が出来る様にしたいと考えております。

ここでは主に画面のキャプチャを用いて画面構成やどの様に作り込みをしたかをまとめていきます。
githubにソースコードを上げているのでソースコードの詳細はそちらをご覧頂ければと存じます。
自習やポートフォリオサイトを作ってみたいという方の助けになれれば幸いです。

*決済はStripeを利用したクレジットカード決済となっています。(テストモードです。)
*以前書いた記事と重複している箇所は説明を割愛します。

動作環境について

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
APache2.4

外部サービス

Mailgun
Stripe

参考資料

【簡単・オシャレ】PHPでstripeの決済を導入する手順【決済システム導入】
【初級編】LaravelでStripeを使って単発決済を実装する

TOPページ

現在DBに登録されている商品が表示されています。
キーワード入力による商品の検索も可能です。

FireShot Capture 6 - ECshop - http___shop1.localhost_.png

TOPページでは決済が出来ないようになっています。
管理者は管理者ログインのリンクを、お客様はお客様ログインのリンクを押下して貰うことでログインページに遷移します。
お客様の新規登録の場合は新規登録のリンクを押下することでログインページに遷移します。

ログイン・ユーザー画面

管理者と顧客それぞれのログイン関連の処理については以前書いた記事にまとめています。

adminテーブルを使って認証する方法
customerテーブルを使って認証する方法

パスワード再設定に使うメールはMailgunを使いますが、送信先が限定されているので本番環境ではsmtpを立てた方が良さそうです。

管理者画面

TOPページと同様に登録されている商品の一覧が表示されています。
TOPページと異なる点は各商品ごとに編集、画像アップロード、削除の処理を行えるボタンを置いていることです。

FireShot Capture 9 - ECshop - http___shop1.localhost_admin_home.png

ファイルアップロードについても別の記事でまとめています。

右上のプルダウンを押すとメニュー画面が出てきます。
ここから管理者のホーム画面への遷移、管理者登録、商品登録、ログアウトを行います。

スクリーンショット 2019-02-11 16.27.48.png

新商品登録

商品登録画面では商品名、価格、説明文を入力できます。

FireShot Capture 10 - ECshop - http___shop1.localhost_admin_goods_regist.png

上記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文字以内
    ];
}

入力誤りがあればメッセージが表示されます。

FireShot Capture 11 - ECshop - http___shop1.localhost_admin_goods_regist.png

最終的な入力内容に誤りが無いか確認する為に確認画面を設けています。

FireShot Capture 12 - ECshop - http___shop1.localhost_admin_goods_confirm.png

「登録する」ボタンを押下すると商品の登録が完了します。

FireShot Capture 13 - ECshop - http___shop1.localhost_admin_home.png

商品情報編集

各商品の「編集」ボタンを押下します。
画面構成自体は新規登録時と違いはありません。
商品ごとにDBに登録されている値が予め入力欄に表示されます。

FireShot Capture 15 - ECshop - http___shop1.localhost_admin_goods_edit_115.png

編集し終えたら確認をし、最後にデータの更新をかけます。

FireShot Capture 24 - ECshop - http___shop1.localhost_admin_home.png

商品削除

「削除」ボタンを押すと確認のアラートが表示されます。「OK」を押すと即座に商品の削除が実行されます。

FireShot Capture 18 - ECshop - http___shop1.localhost_admin_home.png

顧客画面

TOPページと同様に登録されている商品の一覧が表示されています。
TOPページと異なる点は商品のカートへの格納と削除を行うボタン、最終的な購入手続きを行うボタンがあることです。

FireShot Capture 19 - ECshop - http___shop1.localhost_customer_home.png

右上のプルダウンを押すとメニュー画面が出てきます。
ここから顧客のHome画面への遷移、ログアウトを行います。

スクリーンショット 2019-02-11 16.58.09.png

カートに入れる

カートに何も商品を入れないまま購入手続きをしようとしても出来ない様になっています。

FireShot Capture 21 - ECshop - http___shop1.localhost_customer_home.png

「カートに入れる」ボタンを押下することで該当の商品をカートに入れることが出来ます。
1回のボタン押下によりカートに入れることが出来る数量は1つとなっています。

FireShot Capture 22 - ECshop - http___shop1.localhost_customer_home.png

カートから外す

「カートから外す」ボタンを押下することで該当の商品をカートから外すことが出来ます。
1回のボタン押下によりカートから外すことが出来る数量は1つとなっています。

FireShot Capture 23 - ECshop - http___shop1.localhost_customer_home.png

購入内手続き

ホーム画面から「購入内手続きへ」ボタンを押すと購入内容の確認画面に遷移します。

確認画面ではカートに入れた商品のデータと購入数、合計決済金額が表示されています。
確認という観点からこの画面内では一切の商品や購入数の変更が出来ない様になっています。

FireShot Capture 26 - ECshop - http___shop1.localhost_customer_cart_payment_confirm.png

「購入する」ボタンを押下するとstripeの決済画面が表示されます。(テストモードです。)
メールアドレス、カード番号、利用期限、セキュリティコードを入力して「*を支払う」と書いてあるボタンを押下することでクレジットカード決済が完了します。

FireShot Capture 27 - ECshop - http___shop1.localhost_customer_cart_payment_confirm.png

決済処理後はホーム画面に遷移し、決済完了のモーダルが表示されます。

FireShot Capture 28 - ECshop - http___shop1.localhost_customer_home.png

決済の実装で躓いた箇所がモーダルを表示する為のボタンの実装でした。

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を利用させて頂きましたが、深いところを理解出来ていないのが正直な話です。

もっと検討すべき仕様などがございますが時間的な都合で出来なかったことが多々あります。
また、本番公開の段階で発生した修正などもあり、実際に作っている中で自分の力不足を痛感することが多々ございました。

今回の反省点を踏まえてもっと品質の良いプロダクトを作れる様に精進したいと思います。

ご覧頂きありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravel tinkerコマンドでメール送信する

Laravelのartisanコマンドのtinkerコマンドでメール送信する方法。
(事前に、Laravelの環境設定 .envファイルに、smtpサーバの設定が必要です。)

プロジェクトディレクトリの中で、次のartisanコマンドを実行すると、tinkerの対話型シェルが立ち上がる。

php artisan tinker

以下記述を実行して、nullが返れば送信完了。

>>> Mail::raw('メール本文', function($message) { $message->to('mail@example.com')->subject('メールタイトル'); });

参考

https://laravel10.wordpress.com/2015/02/22/%E3%83%A1%E3%83%BC%E3%83%AB%E3%81%AE%E7%92%B0%E5%A2%83%E8%A8%AD%E5%AE%9A/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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のバリデーションで指定できる内容をざっくりまとめ直しました。

画面構成について

全然スマートな画面じゃなくて恐縮ですが・・・。
管理画面にある各商品情報の右に「画像」ボタンを配置しています。

スクリーンショット 2019-02-11 12.04.58.png

「画像」ボタンを押下するとファイルアップロードのモーダルが表示されます。

スクリーンショット 2019-02-11 12.05.18.png

アップロードに成功するとTOPページに各商品ごとの画像が表示される様になります。
*「-------」となっている商品はまだ画像のアップロードを行っていないことを示しています。

スクリーンショット 2019-02-11 12.07.58.png

下記では、この上記画像の動作を実現する方法をまとめています。

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とファイルアップロードを同時平行で作っていたのがアダとなった形です。

取り急ぎでファイルアップロードの機能を試したい時に正常に動かない場合、上記対応で動かせることがわかりました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Laravel】実行されたSQLを取得する

実行されたSQLを出力します。
SQLだけでなく、プリペアドステートメントや実行時間も出力されるので便利です。

Laravel 5.0 データベースの基本的な使用法

ログの出力

// ログを有効化
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
  ]
]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.php
public 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.testing
MESSAGE_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());
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Laravel】既存テーブルのマイグレーションファイルを作成する

Laravelで既存のテーブルを使用したい場合は、マイグレーションファイルを自分で用意する必要があります。

それを自動化するパッケージがあったので、ご紹介します。

使用パッケージ

migrations-generatorというパッケージを利用します。

migrations-generator

参考記事です。

Laravelで既存のDBからmigrationsファイルを作成する「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';
}

これでテーブルが使用できるようになりました!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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());

コアファイルを読む良いきっかけになりました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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のインストール手順を見ると載っている。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GraphQL+Lighthouse(+Laravel)でAPI開発2(開発編1 - 非Eloquentモデルの実装)

前回に引き続き、LaravelにおけるGraphQL+Lighthouseの実装方法を紹介していきます。
ぜひ、GraphQL+Lighthouse(+Laravel)でAPI開発1(インストール方法・設定編)もご覧ください。

方針

前回のエントリでも記述しておりますが、今回は既存のシステムに途中からGraphQLでAPIを実装することになりました。また、Laravelのアプリケーション実装自体も旧システムの方針を引き継ぎ実装されたため、ORMの恩恵をあまり受けられないDBの設計となってしまっています。

そのため、今回はEloquentを用いない方針でGraphQLのサーバーサイド側を実装していくことにします。
またGraphQLにはQueryMutationと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つのscalarlighthouseから提供されている型なので、残しても削除してしまっても問題ありません。

次にQueryの型を定義しましょう。

routes/graphql/schema.graphql
type {
    top: Top
}

QuerytopTop型の値を返します。

Top型を次に定義します。
小さなアプリケーションの場合1つの型ファイルに全ての型を記述していっても良いですが、開発が進んでいくと型ファイル内がとんでもないことになりそうなので、別の型ファイルtop.graphqlを用意しましょう。今回適当にいくつか型を用意します。

routes/graphql/top.graphql
type 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.graphql
type {
    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クラスなどを用意しHistoryShopなどのモデルクラスを返すこともできるでしょう。

最終的なレスポンスの形だけ決めてしまえば、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がより浸透し様々な実装方法が提案されるでしょう。私の方でも引き続き試行錯誤していこうと思います。

次回へ向けて

次回は今回実装したものに関して自作のエラーハンドリングを追加する場合について書いていこうと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む