20200629のPHPに関する記事は13件です。

WordPressでエリア検索を実装したい

WordPressで、関東地方の歴史建造物を検索できるサイトを作っており、エリア検索を実装したいと思っています。
キャンプ場検索サイト「なっぷ」のように検索窓をクリックすると、関東地方(1都6県)が縦に表示され、
その中で例えば東京都をクリックすると右側に市区町村が縦に表示されるようにしたいです。
スクリーンショット 2020-06-29 22.55.27.png

市区町村はある程度グループ化して、表記するつもりですが、それでも縦に長くなるので、スクロールをつけると良いのではと考えています。参考はJAL公式サイトです。⬇︎
スクリーンショット 2020-06-29 22.57.43.png

(都県)   (市区町村)
東京  →  新宿・文京・豊島    ‖←スクロールバー 
神奈川   渋谷・港・千代田・中央
埼玉    目黒・品川・大田
千葉    世田谷・杉並・中野
茨城    練馬・板橋・北
栃木    江東・台東・荒川
群馬    葛飾・足立・江戸川    (これ以上の市区町村はスクロールすることで表示可能) 
ーーーーーーーーーーーーーーーーー

上記のような実装はカテゴリ検索と呼ぶのでしょうか?
的確な表現がわからず、調べる段階でつまづいております、、
また、実装の手順をおおよそ教えていただけますでしょうか?
お力貸していただけると光栄です。
よろしくお願いいたします。

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

【PHP】Cloud9で$_SESSIONが反映されなかったときの対処法

はじめに

学習記録のアウトプット・自分自身の備忘録として書いた記事です。
今回はPHPのセッションがうまくいかなかったためその概要と解決方法についてメモしていきます。

やりたかったこと

以下のように設定した$_SESSION['$csrfToken']をページ遷移後に反映したい。

$csrfToken = bin2hex(random_bytes(32));
$_SESSION['$csrfToken'] = $csrfToken;

トラブルの症状

ページ遷移後に$_SESSIONが記録されていませんでした。

セッションファイルの出力先を指定する。

(これは必要ではないかもしれませんが念のため。)
Cloud9はそもそもデフォルトではphp5.6のため
random_bytesは使えません。
そのためphp7.2をyumでインストール。

この状態でphpinfo() を実行すると、session.save_pathの右カラムはno valueになっています。
デフォルトのセッションファイルの格納先が特に指定されていなかったため/etc/php.ini

php.ini
; RPM note : session directory must be owned by process owner
; for mod_php, see /etc/httpd/conf.d/php.conf
; for php-fpm, see /etc/php-fpm.d/*conf
; session.save_path = "/tmp"

↓";"を外す。

php.ini
; RPM note : session directory must be owned by process owner
; for mod_php, see /etc/httpd/conf.d/php.conf
; for php-fpm, see /etc/php-fpm.d/*conf
session.save_path = "/tmp"

変更後デバックしてみて以下が作成されていることを確認。
/tmp/sess_*

session_start()の書く位置

<?php
session_start();

<?php直下に書く必要があります。
尚且つ$_SESSIONの直前ではなく、一番最初のphpタグの直下です。。。

参考

https://note.kiriukun.com/entry/20191125-session-not-working-in-php#file

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

【PHP】Cloud9でセッションが反映されなかったときの対処法

はじめに

学習記録のアウトプット・自分自身の備忘録として書いた記事です。
今回はPHPのセッションがうまくいかなかったためその概要と解決方法についてメモしていきます。

やりたかったこと

以下のように設定した$_SESSION['$csrfToken']をページ遷移後に反映したい。

$csrfToken = bin2hex(random_bytes(32));
$_SESSION['$csrfToken'] = $csrfToken;

トラブルの症状

ページ遷移後に$_SESSIONが記録されていませんでした。

セッションファイルの出力先を指定する。

(これは必要ではないかもしれませんが念のため。)
Cloud9はそもそもデフォルトではphp5.6のため
random_bytesは使えません。
そのためphp7.2をyumでインストール。

この状態でphpinfo() を実行すると、session.save_pathの右カラムはno valueになっています。
デフォルトのセッションファイルの格納先が特に指定されていなかったため
/etc/php.ini

php.ini
; RPM note : session directory must be owned by process owner
; for mod_php, see /etc/httpd/conf.d/php.conf
; for php-fpm, see /etc/php-fpm.d/*conf
; session.save_path = "/tmp"

↓";"を外す。

php.ini
; RPM note : session directory must be owned by process owner
; for mod_php, see /etc/httpd/conf.d/php.conf
; for php-fpm, see /etc/php-fpm.d/*conf
session.save_path = "/tmp"

変更後デバックしてみて以下が作成されていることを確認。
/tmp/sess_*

session_start()の書く位置

<?php
session_start();

<?php直下に書く必要があります。
尚且つ$_SESSIONの直前ではなく、一番最初のphpタグの直下です。。。

参考

https://note.kiriukun.com/entry/20191125-session-not-working-in-php#file

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

【PHP】Symfony2, SwiftMailerでUTF-8のbase64encodeした日本語メールを送りたい

要件として

  • クソ長い日本語文字列をメールの差出人名として表示したい

なぜUTF-8の送りたいのか

「メール送信する際にiso-2022-jpで送ればいいじゃない。」
って声が聞こえてきそうですが、iso-2022-jpだと
環境依存文字、絵文字、他の言語(ハングル文字、タイ語)が化ける

日本語かどうか判定するのもめんどくさいと思ったので
ならいっそのことUTF-8統一で全メール送ろう と思った次第です。

実装

環境は以下
Symfony 2
PHP 5
Swiftmailer 5.2.1(だと思う)

ちょいと古いSymfonyを使用してる手前、書き方が最新ではないかもしれません

\Swift::init(function () {
    \Swift_DependencyContainer::getInstance()
        ->register('mime.qpheaderencoder')
        ->asAliasOf('mime.base64headerencoder');
    \Swift_Preferences::getInstance()->setCharset(Constant::UTF8);
});

$transport = new \Swift_SmtpTransport(
    127.0.0.1,
    25
);

$mailer = new \Swift_Mailer($transport);

$message = (new \Swift_Message())
    ->setFrom($fromAddress, $fromName)
    ->setTo($to)
    ->setSubject($subject)
    ->setBody($body, 'text/plain')
;

$mailer->send($message);

ざっくりとこんな感じですかね。

問題

とまぁ、例によって普通に実装したんですが、この状態だと日本語文字が化けることがあるんですね
一定数以上長い日本語文字を件名や、差出人名に入れると化けます。

原因

単純に言うとSwiftmailerのバグかと思われます。
base64エンコード + UTF-8 だとエンコードされた日本語1文字の途中で改行がされることがあり、そのタイミングから文字が化ける
※ 最新のswiftmailerのコード見ても「この処理がバグってるのだろう」という部分はそのままだったのでおそらく最新でも発生する

対応

「ライブラリのバグです。」
「じゃあ、仕方ないね。」
で片付けられないこの世の中。

多少無理してでもやります。

改行しているコードを直すのは厳しそうだったので、改行されないようにします

ゴリ押し解決

まずBase64エンコードしてる部分を改行仕様を弄ります

swiftmailer/lib/classes/Swift/Encoder/Base64Encoder.php
if (0 >= $maxLineLength || 76 < $maxLineLength) {
    $maxLineLength = 76;
}

をこうしてやります

swiftmailer/lib/classes/Swift/Encoder/Base64Encoder.php
if (0 >= $maxLineLength || 998 < $maxLineLength) {
    $maxLineLength = 998;
}

次にメールヘッダの一行の最大文字数を弄ります

swiftmailer/lib/Swift/MIME/Headers/AbstractHeader.php
abstract class Swift_Mime_Headers_AbstractHeader implements Swift_Mime_Header
{
    ...

    /**
     * The maximum length of a line in the header.
     *
     * @var int
     */
    private $_lineLength = 78;

    ...

    protected function encodeWords(Swift_Mime_Header $header, $input, $usedLength = -1)
    {
        ...

        $header->setMaxLineLength(76); // Forcefully override

        ...
    }

    protected function getTokenAsEncodedWord($token, $firstLineOffset = 0)
    {

        ...

        if ($firstLineOffset >= 75) { //Does this logic need to be here?
            $firstLineOffset = 0;
        }

        ...

        $encodedTextLines = explode("\r\n",
            $this->_encoder->encodeString(
                $token, $firstLineOffset, 75 - $encodingWrapperLength, $this->_charset
                )
        );

        ...
    }

    ...
}

をこうしてやります

swiftmailer/lib/Swift/MIME/Headers/AbstractHeader.php
abstract class Swift_Mime_Headers_AbstractHeader implements Swift_Mime_Header
{
    ...

    /**
     * The maximum length of a line in the header.
     *
     * @var int
     */
    private $_lineLength = 998;

    ...

    protected function encodeWords(Swift_Mime_Header $header, $input, $usedLength = -1)
    {
        ...

        $header->setMaxLineLength(998); // Forcefully override

        ...
    }

    protected function getTokenAsEncodedWord($token, $firstLineOffset = 0)
    {

        ...

        if ($firstLineOffset >= 997) { //Does this logic need to be here?
            $firstLineOffset = 0;
        }

        ...

        $encodedTextLines = explode("\r\n",
            $this->_encoder->encodeString(
                $token, $firstLineOffset, 997 - $encodingWrapperLength, $this->_charset
                )
        );

        ...
    }

    ...
}

これすると何が起きる?

簡単に申しますと、メールヘッダが998文字まで入ります('ω')
つまり998文字までなら改行されないので改行バグには当たらないです・・・・・('ω')

???根本解決にはなっていない???

最後に

脳筋ごり押し対応で無理やりバグを避けてみましたが
これは実際、根本解決に至っていない + ライブラリを改変しているので推奨できるやり方ではございません。

また、もっとベストな方法で実装できる知見をお持ちの方がいらっしゃいましたら、ご教授いただけると幸いです。。。
(実際エンコード前段階で区切り文字を挿入しておくみたいなアプローチもあったかなぁ…

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

SQL Laravel 論理削除されたカラムを検索に含めない

# 目的

  • Laravelにて論理削除するための設定を行ったDBのテーブルに対するSQLのselect文にてすでに論理削除されたレコードを出力しないwhere条件をまとめる

実施環境

  • ハードウェア環境
項目 情報
OS macOS Catalina(10.15.5)
ハードウェア MacBook Pro (13-inch, 2020, Four Thunderbolt 3 ports)
プロセッサ 2 GHz クアッドコアIntel Core i5
メモリ 32 GB 3733 MHz LPDDR4
グラフィックス Intel Iris Plus Graphics 1536 MB
  • ソフトウェア環境
項目 情報 備考
PHP バージョン 7.4.3 Homwbrewを用いて導入
Laravel バージョン 7.0.8 commposerを用いてこちらの方法で導入→Mac Laravelの環境構築を行う
MySQLバージョン 8.0.19 for osx10.13 on x86_64 Homwbrewを用いてこちらの方法で導入→Mac HomebrewでMySQLをインストールする

前提条件

読後感

  • SQL文でselect句を使用した際に論理削除されたレコードの情報を表示しないようにwhere句の条件が記載できる。

概要

  1. Laravel側からのデータ削除
  2. where句の条件の追加と実行

詳細

  1. Laravel側からのデータ削除

    1. アプリ名ディレクトリで下記コマンドを実行してtinkerを起動する。

      $ php artisan tinker
      
    2. 下記の公式ドキュメントに従い任意のレコードを論理削除する。

      >>> 変数A = App\論理削除を行うレコードが存在するテーブルのデータ操作を行うモデル名::find(削除するレコードのid);
      >>> 変数A->delete();
      
  2. where句の条件の追加と実行

    1. 下記コマンドを実行してMySQLにターミナルからログインする。(MySQLのrootユーザのパスワードを忘れてしまった方はこちら→Mac ローカル環境の MySQL 8.0 のrootパスワードを忘れた時のリセット方法)

      $ mysql -u root -p
      
    2. 下記を実行してこれから実行するSQL文がどのテーブルに対するものなのかを設定する。

      use Laravelアプリに紐づいたDB;
      
    3. 下記を一旦実行してselect句を使用してusersテーブルの内容を出力する。(論理削除のレコードも出力される。)

      select * from users;
      
    4. 下記を実行して論理削除のレコードを除いたusersテーブルの内容を出力する。(deleted_atカラムがnullとして条件付けしたのはdeleted_atは論理削除された日時を格納するカラムなのでnullなら論理削除していないことになるためである。)

      select * from users
      where deleted_at is null;
      

null判定の落とし穴

  1. nullは=を使って判定できない。
    • where deleted_at = null;などの条件だとnullは判定することができない。間違えやすいので注意する。
  2. nullの判定はis nullもしくはis not nullを使用する。
    • =での判定ができないためnullの判定に関しては条件の記載方法が変わることに注意する。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravel,transaction

皆さん、こんにちは:hand_splayed:
今回はLaravelの勉強していた時に、transactionについてなにも分からないまま使っていたので、調べて見ました!

開発環境

・Mac
・php 7.4.6
・Laravel 7.15.0
・MySQL

transaction

transactionは、複数の処理をまとめて実行したい時に使います。
掲示板アプリがあるとしましょう。掲示板にはユーザーが自由にコメントを書くことが出来ます。その上で、ある特定の投稿を削除するときのことを考えます。この時、投稿自体は削除出来たけど、コメントを削除する段階でエラーが発生したらどうなるでしょうか。もちろん、コメントテーブルと投稿のテーブルはリレーションを張っているわけなので、この場合投稿だけが残りコメントだけ残ってしまいます。このようなことを防ぐためにtransactionを使います。

DB::transaction(function() {
   //処理を記述
});

クロージャーの中で例外が発生した場合は、ロールバックされます。またクロージャー内で返した値が戻り値となります。

transactionの例

QuestionController.php
$question = Question::findOrFail($question_id)
\DB::transaction(function() use ($question){
    $question->answers()->delete();
    $question->delete();
});

Q&Aがあり、questionsテーブルとanswersテーブルにリレーションがはられています。
transactionによって必ずまとめて削除されるか、エラーが出ても片方だけが実行されるということはありません。
指摘がありましたらコメントお願いします:pray:

参考にした記事、サイト

Laravel Tips ? transactionメソッドは値を返す
Laravelでトランザクション処理の方法をサンプル付きで解説

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

Laravel S3で複数枚画像をアップロードする

LaravelでAWS S3へ画像をアップロードする

前提条件

AWS S3でユーザ登録とバケットの作成ができていること。
まだ作成してない方はこちらが参考になります

S3パッケージインストール

パッケージはflysystem-aws-s3-v3を使用します。
以下を実行してインストールしてください。

composer require league/flysystem-aws-s3-v3

ファイルシステム設定

/config/filesystems.phpを以下のように編集します。

  'default' => env('FILESYSTEM_DRIVER', 'local'),

  // 追記
  'cloud' => env('FILESYSTEM_CLOUD', 's3'),

  // 〜 略 〜

  'disks' => [
    'local' => [
      // 〜 略 〜
    ],
    'public' => [
      // 〜 略 〜
    ],

    // 以下を追記
    's3' => [
      'driver' => 's3',
      'key' => env('AWS_ACCESS_KEY_ID'),
      'secret' => env('AWS_SECRET_ACCESS_KEY'),
      'region' => env('AWS_DEFAULT_REGION'),
      'bucket' => env('AWS_BUCKET'),
    ],

環境変数設定

プロジェクト直下にある.envファイルを以下のように編集します。

AWS_ACCESS_KEY_ID='AWSで作成したAccess key ID'
AWS_SECRET_ACCESS_KEY='AWSで作成したSecret access key'
AWS_DEFAULT_REGION=ap-northeast-1
AWS_BUCKET='作成したバケット名'

Laravelの実装

実際にS3に保存していきます。
最低限のコードは次節以降になります。

viewの実装

今回は複数枚アップロードできるようにしていきます。

          <div class="name-filed width">
            <div class="first-name-box">
              <div class="text-label">
                <p class="name">希望画像<span class="red">✳︎必須(5枚まで可)</span></p>
              </div>
              <div>
              {!! Form::file('item_url[]', ['multiple' => 'multiple']) !!}
              </div>
              @if ($errors->has('item_url') || $errors->has('item_url.*') )
                <div class="alert alert-danger">{{ $errors->first('item_url') . $errors->first('item_url.*') }}</div>
              @endif
            </div>
          </div>

point 1

formに'multiple' => 'multiple'をつけることで複数選択が可能になる。(Ctrl キーなどを押しながら写真を選択する)

point 2

Form::file()メソッドを利用します。
今回は複数枚になるので第一引数は配列で取得できるようにします。

'item_url[]'

余談

配列にフォームリクエストでバリデーションする場合は下記のように、 .* というプレースホルダーが使用する。

item_url.*

controllerの実装

formから送られてきた値をS3に保存していきます。
laravelには簡単にS3を扱えるようにするファイルストレージ機能があります。
簡単なのでぜひ目を通してくださいね!

public function store(ItemRequest $request)
    {
        $disk = Storage::disk('s3');
        $images = $request->file('item_url');
        foreach ( $images as $image) {
            $path = $disk->putFile('itemImages', $image, 'public');
            $url[] = $disk->url($path);
        }

        return view('item.top');
    }

流れの解説
$disk変数にlaravelが用意してくれている Storage::disk('s3');を代入。

$imageにはformから送られてきた写真をfile()を使用して取得する。                                
※$imageにはURLではなく、UploadedFile クラスが入ってる。

foreachを使用して画像一枚ずつS3に保存していく。
putfile()メソッドの第一引数はバケット内の保存するフォルダを指定している。第二引数は保存する値。第三引数でpublicにすることで
どのユーザーでもweb上で画像を見ることが可能になる。※指定しないとweb上に画像が表示されない。

$pathの中身は"バケットの保存先のフォルダ/画像"のpathになっています。

ここまででS3への保存は完了になります!

最後の$urlにはurl()を使用してS3からのURLを作成しています。

以上でS3への複数枚画像アップロードは完了になります!

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

Laravel S3で複数の画像をアップロードする

LaravelでAWS S3へ画像をアップロードする

前提条件

AWS S3でユーザ登録とバケットの作成ができていること。
まだ作成してない方はこちらが参考になります

S3パッケージインストール

パッケージはflysystem-aws-s3-v3を使用します。
以下を実行してインストールしてください。

composer require league/flysystem-aws-s3-v3

ファイルシステム設定

/config/filesystems.phpを以下のように編集します。

  'default' => env('FILESYSTEM_DRIVER', 'local'),

  // 追記
  'cloud' => env('FILESYSTEM_CLOUD', 's3'),

  // 〜 略 〜

  'disks' => [
    'local' => [
      // 〜 略 〜
    ],
    'public' => [
      // 〜 略 〜
    ],

    // 以下を追記
    's3' => [
      'driver' => 's3',
      'key' => env('AWS_ACCESS_KEY_ID'),
      'secret' => env('AWS_SECRET_ACCESS_KEY'),
      'region' => env('AWS_DEFAULT_REGION'),
      'bucket' => env('AWS_BUCKET'),
    ],

環境変数設定

プロジェクト直下にある.envファイルを以下のように編集します。

AWS_ACCESS_KEY_ID='AWSで作成したAccess key ID'
AWS_SECRET_ACCESS_KEY='AWSで作成したSecret access key'
AWS_DEFAULT_REGION=ap-northeast-1
AWS_BUCKET='作成したバケット名'

Laravelの実装

実際にS3に保存していきます。
最低限のコードは次節以降になります。

viewの実装

今回は複数枚アップロードできるようにしていきます。

          <div class="name-filed width">
            <div class="first-name-box">
              <div class="text-label">
                <p class="name">希望画像<span class="red">✳︎必須(5枚まで可)</span></p>
              </div>
              <div>
              {!! Form::file('item_url[]', ['multiple' => 'multiple']) !!}
              </div>
              @if ($errors->has('item_url') || $errors->has('item_url.*') )
                <div class="alert alert-danger">{{ $errors->first('item_url') . $errors->first('item_url.*') }}</div>
              @endif
            </div>
          </div>

point 1

formに'multiple' => 'multiple'をつけることで複数選択が可能になる。(Ctrl キーなどを押しながら写真を選択する)

point 2

Form::file()メソッドを利用します。
今回は複数枚になるので第一引数は配列で取得できるようにします。

'item_url[]'

余談

配列にフォームリクエストでバリデーションする場合は下記のように、 .* というプレースホルダーが使用する。

item_url.*

controllerの実装

formから送られてきた値をS3に保存していきます。
laravelには簡単にS3を扱えるようにするファイルストレージ機能があります。
簡単なのでぜひ目を通してくださいね!

public function store(ItemRequest $request)
    {
        $disk = Storage::disk('s3');
        $images = $request->file('item_url');
        foreach ( $images as $image) {
            $path = $disk->putFile('itemImages', $image, 'public');
            $url[] = $disk->url($path);
        }

        return view('item.top');
    }

流れの解説
$disk変数にlaravelが用意してくれている Storage::disk('s3');を代入。

$imageにはformから送られてきた写真をfile()を使用して取得する。                                
※$imageにはURLではなく、UploadedFile クラスが入ってる。

foreachを使用して画像一枚ずつS3に保存していく。
putfile()メソッドの第一引数はバケット内の保存するフォルダを指定している。第二引数は保存する値。第三引数でpublicにすることでどのユーザーでもweb上で画像を見ることが可能になる。※指定しないとweb上に画像が表示されない。
$pathの中身は"バケットの保存先のフォルダ/画像"のpathになっています。
ここまででS3への保存は完了になります!

最後の$urlにはurl()を使用してS3からのURLを作成しています。

以上でS3への複数枚画像アップロードは完了になります!

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

WordPressにショートコードを使ってPHPを埋め込む方法

  • 環境
    • Windows 10 Pro 64bit バージョン1909
    • Local by Flywheel 5.6.1
      • PHP 7.4.1 / MySQL 8.0.16 / Apach / WordPress 5.4.2
      • テーマ : Lightning

ショートコードをつくる基本の方法

  1. functions.phpを開く
  2. PHPで関数を定義して処理を書く
  3. add_shortcode('{ショートコード用文字}', '{関数名}');を書く
  4. WordPressの記事などに[{ショートコード用文字}]を書く
  5. プレビューなんかで見ると処理の結果が出力される
functions.phpに書く
function short_cord_func() {
    return 'ショートコードをつくる基本の方法';
}
add_shortcode('short_cord_string', 'short_cord_func');
記事ショートコードを書く
[short_cord_string]

image.png

外部ファイルを読み込んでショートコードをつくる方法

  1. 外部ファイルにPHPで処理を書く
  2. functions.phpを開く
  3. PHPで関数を定義して外部ファイルを呼び出す処理を書く
  4. あとは「ショートコードをつくる基本の方法」と同じ
テーマ用ディレクトリ配下にディレクトを作って外部ファイルをおいてみた
テーマのディレクトリ
├ functions.php
└ customphp
 └ short-cord.php << 外部ファイル
short-cord.phpという外部ファイルにPHPで処理を書く
// この外部ファイルからの出力がそのまま埋め込まれるイメージ
<p><?php echo '外部ファイルを読み込んでショートコードをつくる方法'; ?><p>
functions.phpにPHPで関数を定義して外部ファイルを呼び出す処理を書く
function short_cord_func() {
    // 出力バッファリングを有効化する
    ob_start();
    // 外部ファイルを読み込む
    // 「.php(拡張子部分)」は不要
    get_template_part('customphp/short-cord');
    // 出力バッファを削除する
    return ob_get_clean();
}
add_shortcode('short_cord_string', 'short_cord_func');

image.png

外部ファイルを読み込むショートコードを複数作る場合

外部ファイルを読み込む専用の関数を作っておくと便利

こんな風に外部ファイルおいてみた
テーマのディレクトリ
├ functions.php
└ customphp
 ├ hoge.php << 外部ファイル
 └ fuga.php << 外部ファイル
functions.php
/**
 * 外部ファイルを読み込む専用関数.
 * @param array 読み込む外部ファイル名(拡張子なし)を値に持つ連想配列(keyはfile).
 **/
function custom_func($args) {
    // $file変数を配列からインポートする
    extract(shortcode_atts(array('file' => 'default'), $args));
    // 出力バッファリングを有効化する
    ob_start();
    // 外部ファイルを読み込む
    get_template_part("$file");
    // 出力バッファを削除する
    return ob_get_clean();
}

/** hoge.phpを使うショートコード用関数. */
function hoge() {
    return custom_func(['file' => 'customphp/hoge']);
}
add_shortcode('hoge', 'hoge');

/** fuga.phpを使うショートコード用関数. */
function fuga() {
    return custom_func(['file' => 'customphp/fuga']);
}
add_shortcode('fuga', 'fuga');

テーマを選定中でちょいちょい変える場合

外部ファイルを複数作っている場合にはどっかのディレクトリにまとめておくとテーマが変わった時にディレクトリ毎持っていけるから便利
だけど、テーマを選定中で使うテーマのディレクトリ毎にコピーしていくのは面倒くさい
そんな時はシンボリックリンクを使うと便利

外部ファイルを入れるディレクトリを作る
{サイトのディレクトリ}/app/public
├ wp-admin
├ wp-includes
├ wp-content
|└ themes
| ├ lightning         << テーマのディレクトリ
| |├ functions.php
| |└ customphp       << シンボリックリンク
| └ twentynineteen   << テーマのディレクトリ
|  ├ functions.php
|  └ customphp       << シンボリックリンク
└ customphp  << 外部ファイル用ディレクトリ
 ├ hoge.php
 └ fuga.php
Windowsのコマンドプロンプトでシンボリックリンクを作る方法
# 1. 管理者権限でコマンドプロンプトを起動する
# 2. シンボリックリンクを作りたいところに移動する
>cd "c:\Users\{ユーザ}\Local Sites\sample\app\public\wp-content\themes\twentynineteen"
# 3. ディレクトリ(/D)のシンボリックリンクをつくる(mklink)
>mklink /D customphp "c:\Users\{ユーザ}\Local Sites\sample\app\public\customphp"
customphp <<===>> c:\Users\{ユーザ}\Local Sites\sample\app\public\customphp のシンボリック リンクが作成されました

ショートコードを作ってみた

WordPressでPHPを埋め込む方法がなんとなくわかったので早速Google検索した結果を表示するショートコードを作ってみた。
WordPressでGoogleのCustom Search APIを使ってみる - ponsuke_tarou’s blog

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

【Laravel】Eloquent の has() や whereHas() が遅い?なら速くしてやるぜ

はじめに

Laravel は「リレーション先が存在するか」という制約条件を has() whereHas() メソッドで表現できます。

取得結果が複数になる HasMany と,取得結果が単一になるHasOne BelongsTo の, 2 通りのパターンを考えましょう。

以下のようなモデル定義があるとします。

class Post extends Model
{
    use SoftDeletes;

    public function comments()
    {
        return $this->hasMany(Post::class);
    }
}
class Comment extends Model
{
    use SoftDeletes;

    public function post()
    {
        return $this->belongsTo(Post::class);
    }
}

HasMany の場合

$posts = Post::query()
    ->has('comments')
    ->limit(5)
    ->get();
select * from `posts`
where exists (
  select * from `comments`
  where `posts`.`id` = `comments`.`post_id`
    and `comments`.`deleted_at` is null
)
and `posts`.`deleted_at` is null
limit 5

posts.id がサブクエリの中で参照されていて,これは 1 行ごとに変化する値であるため, MySQL は 1 行ごとにサブクエリを実行 します。これは 相関サブクエリ と呼ばれ,場合によっては著しいパフォーマンスの低下を引き起こします。

但し,相関サブクエリがいつでも悪者になるというわけではありません。

  • メインクエリのレコード数 ≪ サブクエリのレコード数 のときは,相関サブクエリを素直に使ってもいい
  • メインクエリのレコード数 ≫ サブクエリのレコード数 のときは,相関サブクエリは避けたほうが無難

サブクエリを繰り返し実行したとしても,メインクエリのみの条件で絞り込みが十分に行われていれば,サブクエリの実行回数が削減されるため,問題ないと言えるケースもあるでしょう。以降では,メインクエリのみの条件で十分に絞り込めないケースを見ていきます。

相関サブクエリを JOIN に変換する

まずは誰もが思いつきそうな JOIN を試してみましょう。

$posts = Post::query()
    ->join('comments', function (JoinClause $join) {
        $join->on('posts.id', '=', 'comments.post_id');
        $join->whereNull('comments.deleted_at');
    })
    ->limit(5)
    ->select('posts.*') // 不要なカラムが含まれないように posts.* のみに絞り込む
    ->get();
select `posts`.* from `posts`
inner join `comments`
        on `posts`.`id` = `comments`.`post_id`
       and `comments`.`deleted_at` is null
where `posts`.`deleted_at` is null
limit 5

シンプルな JOIN クエリになりました!

但し,これは期待したように動作しません。なぜならば, HasMany の相手を JOIN すると 自分自身のレコードも増えてしまう からです。

期待する結果
+--------+-------------+
| Post 1 | Comment 1-1 |
|        | Comment 1-2 |
|        | Comment 1-3 |
|        | Comment 1-4 |
|        | Comment 1-5 |
+--------+-------------+
| Post 2 | Comment 2-1 |
+--------+-------------+
実際の結果
+--------+-------------+
| Post 1 | Comment 1-1 |
+--------+-------------+
| Post 1 | Comment 1-2 |
+--------+-------------+
| Post 1 | Comment 1-3 |
+--------+-------------+
| Post 1 | Comment 1-4 |
+--------+-------------+
| Post 1 | Comment 1-5 |
+--------+-------------+
| Post 2 | Comment 2-1 |
+--------+-------------+

対策としては DISTINCT が使えます。

$posts = Post::query()
    ->join('comments', function (JoinClause $join) {
        $join->on('posts.id', '=', 'comments.post_id');
        $join->whereNull('comments.deleted_at');
    })
    ->limit(5)
    ->select('posts.*') // 不要なカラムが含まれないように posts.* のみに絞り込む
    ->distinct()
    ->get();
select distinct `posts`.* from `posts`
inner join `comments`
        on `posts`.`id` = `comments`.`post_id`
       and `comments`.`deleted_at` is null
where `posts`.`deleted_at` is null
limit 5

一応これで解決はできます。但し, DISTINCT + LIMIT の組み合わせはテンポラリテーブルの生成などによって LIMIT の性能を悪化させてしまう 場合があり,銀の弾丸とは言えません。論理削除など モデルのグローバルスコープの機能も使えない ので,自分でそれらの制約条件を付与する必要がある点も懸念点です。

相関サブクエリを通常のサブクエリに変換する

以下のようにすると,相関サブクエリを通常のサブクエリに変換することができます。 whereIn() は第 2 引数にサブクエリを取ることができます。

$posts = Post::query()
    ->whereIn('posts.id', Comment::query()->select('comments.id'))
    ->limit(5)
    ->get();
select * from `posts`
where `posts`.`id` in (
  select `comments`.`id` from `comments`
   where `comments`.`deleted_at` is null
)
and `posts`.`deleted_at` is null
limit 5

comments テーブルに対するクエリから posts のカラム参照が消えて,相関サブクエリが普通のサブクエリになりました!またこちらの方法では DISTINCT を使わずに済んでいます。グローバルスコープが自動で付与されている点もポイントが高いです。

実際の実行計画としては JOIN とほぼ同等になる可能性が高いですが, DISTINCT を使用していないためテンポラリテーブル問題は解決できるでしょう。 JOIN よりはこちらを優先して使っていきたいところです。

  • MySQL 5.5 の場合はサブクエリの最適化が効かない可能性があるので, JOIN を使ったほうが無難でしょう。
  • MySQL 5.6~5.7 でも UPDATE DELETE に対しては最適化が効かないので,その際も JOIN を使いましょう。
  • MySQL 8.x の場合はサブクエリ統一で問題なさそうです。

HasOne BelongsTo の場合

相関サブクエリを JOIN に変換する

HasMany は芳しくない結果になってしまいましたが, 取得結果が単一になれば何の問題もありません。躊躇なく JOIN しちゃって OK です。

$comments = Comment::query()
    ->join('posts', function (JoinClause $join) {
        $join->on('comments.post_id', '=', 'posts.id');
        $join->whereNull('posts.deleted_at');
    })
    ->limit(5)
    ->select('comments.*') // 不要なカラムが含まれないように posts.* のみに絞り込む
    ->get();
select `comments`.* from `comments`
inner join `posts`
        on `comments`.`post_id` = `posts`.`id`
       and `posts`.`deleted_at` is null
where `comments`.`deleted_at` is null
limit 5

相関サブクエリを通常のサブクエリに変換する

こちらの方法でもいけます!サブクエリは万能でいいなぁ〜

$posts = Post::query()
    ->whereIn('posts.id', Comment::query()->select('comments.id'))
    ->limit(5)
    ->get();
select * from `comments`
where `comments`.`post_id` in (
  select `posts`.`id` from `posts`
   where `posts`.`deleted_at` is null
)
and `comments`.`deleted_at` is null
limit 5

ライブラリを作りました

こんなもん手作業で書いてたら保守性が終わるので, Laravel 標準の has() whereHas() に謙遜無い感じで使えるライブラリを 2 つ実装しました!

できることに差はありますが,基本的にはサブクエリ版のほうが上位互換だと思ってください。どっちもインストールしておくのもありです。

$posts = Post::query()
    ->hasByNonDependentSubquery('comments')
    ->limit(5)
    ->get();
$posts = Comment::query()
    ->hasByJoin('posts')
    ->limit(5)
    ->get();

これで最高の Eloquent ライフを送りましょう!!!

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

【Laravel】 Eloquent の has() や whereHas() が遅い?なら速くしてやるぜ

はじめに

Laravel は「リレーション先が存在するか」という制約条件を has() whereHas() メソッドで表現できます。

取得結果が複数になる HasMany と,取得結果が単一になるHasOne BelongsTo の, 2 通りのパターンを考えましょう。

以下のようなモデル定義があるとします。

class Post extends Model
{
    use SoftDeletes;

    public function comments()
    {
        return $this->hasMany(Comment::class);
    }
}
class Comment extends Model
{
    use SoftDeletes;

    public function post()
    {
        return $this->belongsTo(Post::class);
    }
}

HasMany の場合

$posts = Post::query()
    ->has('comments')
    ->limit(5)
    ->get();
select * from `posts`
where exists (
  select * from `comments`
  where `posts`.`id` = `comments`.`post_id`
    and `comments`.`deleted_at` is null
)
and `posts`.`deleted_at` is null
limit 5

posts.id がサブクエリの中で参照されていて,これは 1 行ごとに変化する値であるため, MySQL は 1 行ごとにサブクエリを実行 します。これは 相関サブクエリ と呼ばれ,場合によっては著しいパフォーマンスの低下を引き起こします。

但し,相関サブクエリがいつでも悪者になるというわけではありません。

  • メインクエリのみで十分に絞り込めている場合,相関サブクエリを素直に使ってもいい
  • メインクエリのみで絞り込みが不足している場合,相関サブクエリは避けたほうが無難

サブクエリを繰り返し実行したとしても,メインクエリのみの条件で絞り込みが十分に行われていれば,サブクエリの実行回数が削減されるため,問題ないと言えるケースもあるでしょう。以降では,メインクエリのみの条件で十分に絞り込めないケースを見ていきます。

相関サブクエリを JOIN に変換する

まずは誰もが思いつきそうな JOIN を試してみましょう。

$posts = Post::query()
    ->join('comments', function (JoinClause $join) {
        $join->on('posts.id', '=', 'comments.post_id');
        $join->whereNull('comments.deleted_at');
    })
    ->limit(5)
    ->select('posts.*') // 不要なカラムが含まれないように posts.* のみに絞り込む
    ->get();
select `posts`.* from `posts`
inner join `comments`
        on `posts`.`id` = `comments`.`post_id`
       and `comments`.`deleted_at` is null
where `posts`.`deleted_at` is null
limit 5

シンプルな JOIN クエリになりました!

但し,これは期待したように動作しません。なぜならば, HasMany の相手を JOIN すると 自分自身のレコードも増えてしまう からです。

期待する結果
+--------+-------------+
| Post 1 | Comment 1-1 |
|        | Comment 1-2 |
|        | Comment 1-3 |
|        | Comment 1-4 |
|        | Comment 1-5 |
+--------+-------------+
| Post 2 | Comment 2-1 |
+--------+-------------+
実際の結果
+--------+-------------+
| Post 1 | Comment 1-1 |
+--------+-------------+
| Post 1 | Comment 1-2 |
+--------+-------------+
| Post 1 | Comment 1-3 |
+--------+-------------+
| Post 1 | Comment 1-4 |
+--------+-------------+
| Post 1 | Comment 1-5 |
+--------+-------------+
| Post 2 | Comment 2-1 |
+--------+-------------+

対策としては DISTINCT が使えます。

$posts = Post::query()
    ->join('comments', function (JoinClause $join) {
        $join->on('posts.id', '=', 'comments.post_id');
        $join->whereNull('comments.deleted_at');
    })
    ->limit(5)
    ->select('posts.*') // 不要なカラムが含まれないように posts.* のみに絞り込む
    ->distinct()
    ->get();
select distinct `posts`.* from `posts`
inner join `comments`
        on `posts`.`id` = `comments`.`post_id`
       and `comments`.`deleted_at` is null
where `posts`.`deleted_at` is null
limit 5

一応これで解決はできます。但し, DISTINCT + LIMIT の組み合わせはテンポラリテーブルの生成などによって LIMIT の性能を悪化させてしまう 場合があり,銀の弾丸とは言えません。論理削除など モデルのグローバルスコープの機能も使えない ので,自分でそれらの制約条件を付与する必要がある点も懸念点です。

相関サブクエリを通常のサブクエリに変換する

以下のようにすると,相関サブクエリを通常のサブクエリに変換することができます。 whereIn() は第 2 引数にサブクエリを取ることができます。

$posts = Post::query()
    ->whereIn('posts.id', Comment::query()->select('comments.id'))
    ->limit(5)
    ->get();
select * from `posts`
where `posts`.`id` in (
  select `comments`.`id` from `comments`
   where `comments`.`deleted_at` is null
)
and `posts`.`deleted_at` is null
limit 5

comments テーブルに対するクエリから posts のカラム参照が消えて,相関サブクエリが普通のサブクエリになりました!またこちらの方法では DISTINCT を使わずに済んでいます。グローバルスコープが自動で付与されている点もポイントが高いです。

実際の実行計画としては JOIN とほぼ同等になる可能性が高いですが, DISTINCT を使用していないためテンポラリテーブル問題は解決できるでしょう。 JOIN よりはこちらを優先して使っていきたいところです。

  • MySQL 5.5 の場合はサブクエリの最適化が効かない可能性があるので, JOIN を使ったほうが無難でしょう。
  • MySQL 5.6~5.7 でも UPDATE DELETE に対しては最適化が効かないので,その際も JOIN を使いましょう。
  • MySQL 8.x の場合はサブクエリ統一で問題なさそうです。

HasOne BelongsTo の場合

相関サブクエリを JOIN に変換する

HasMany は芳しくない結果になってしまいましたが, 取得結果が単一になれば何の問題もありません。躊躇なく JOIN しちゃって OK です。

$comments = Comment::query()
    ->join('posts', function (JoinClause $join) {
        $join->on('comments.post_id', '=', 'posts.id');
        $join->whereNull('posts.deleted_at');
    })
    ->limit(5)
    ->select('comments.*') // 不要なカラムが含まれないように posts.* のみに絞り込む
    ->get();
select `comments`.* from `comments`
inner join `posts`
        on `comments`.`post_id` = `posts`.`id`
       and `posts`.`deleted_at` is null
where `comments`.`deleted_at` is null
limit 5

相関サブクエリを通常のサブクエリに変換する

こちらの方法でもいけます!サブクエリは万能でいいなぁ〜

$posts = Post::query()
    ->whereIn('posts.id', Comment::query()->select('comments.id'))
    ->limit(5)
    ->get();
select * from `comments`
where `comments`.`post_id` in (
  select `posts`.`id` from `posts`
   where `posts`.`deleted_at` is null
)
and `comments`.`deleted_at` is null
limit 5

ライブラリを作りました

こんなもん手作業で書いてたら保守性が終わるので, Laravel 標準の has() whereHas() に遜色無い感じで使えるライブラリを 2 つ実装しました!

できることに差はありますが,基本的にはサブクエリ版のほうが上位互換だと思ってください。どっちもインストールしておくのもありです。

コメントを持つ投稿を 5件 取得
$posts = Post::query()
    ->hasByNonDependentSubquery('comments')
    ->limit(5)
    ->get();
コメント先の投稿が削除されていないコメントを 5 件取得
$comments = Comment::query()
    ->hasByNonDependentSubquery('posts')
    ->limit(5)
    ->get();
自分のコメントで,かつコメント対象の投稿を作成したユーザの名前が John であるものを 5 件取得
$comments = Auth::user()
    ->comments()
    ->hasByNonDependentSubquery(
        'posts.author',
        null,
        fn (BelongsTo $q) => $q->where('name', 'John')
    )
    ->limit(5)
    ->get();
自分のコメントで,かつコメント対象の投稿を作成したユーザの名前が John であるものを 5 件取得,さらに投稿が論理削除されているものも含める
$comments = Auth::user()
    ->comments()
    ->hasByNonDependentSubquery(
        'posts.author',
        fn (BelongsTo $q) => $q->withTrashed(),
        fn (BelongsTo $q) => $q->where('name', 'John')
    )
    ->limit(5)
    ->get();

これで最高の Eloquent ライフを送りましょう!!!

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

Laravel 7で簡易的なECサイトを作る+Twitterログインを実装する

以下のサイトを参考に、ECサイトを構築します。
エラーが起こった箇所のみ書いていますが、結構長いです。
Laravel6.0(PHP7.3)+MySQL+Laradockで簡易的なECサイトを作る
TwitterAPI アカウント申請〜許可まで【2020年版】
Laravel6.0+SocialiteでTwitterログインを実装する
Laravel 6.5 と Socialite で Twitter OAuth ログインを実現する

※ソースコードはgithubにて公開しているので、宜しけば参考にしてください。

作業環境

OS:Windows 10 HOME Edition(ver.2004)
Laravel:7.15.0
Xampp:7.4.6
Composer:1.10.7
Node.js:13.9.0

環境構築

Laradockではなく、いつもどおりのXamppで行いました。

Laravelのインストール

下記のコマンドを実行すると、Laravelのインストールができます。今回は「ec_app」というアプリ名にしました。

$ cd c:\xampp\htdocs
$ composer create-project laravel/laravel ec_app --prefer-dist

デバックバーのインストール

以下のコマンドを実行して、デバックバーのインストールします。

$ composer require barryvdh/laravel-debugbar

Laravelのタイムゾーンと言語設定

「config/app.php」を編集します。

config/app.php
70行目 'timezone' => 'Asia/Tokyo',
83行目 'locale' => 'ja',

データベースの言語設定

「config/database.php」を編集します。

config/database.php
55行目 'charset' => 'utf8',
56行目 'collation' => 'utf8_unicode_ci',

データベースの設定

xamppのMySqlのAdminボタンを押してphpMyAdminを起動し、今回使用するデータベースを作成します。
202006241817.png
今回はデータベース名は「ec_app_db」としていますが、任意の名前でOKです。だたし、言語は「utf8_general_ci」を選ぶ点に注意してください。
下記を参考に、データベースの設定を「.env」ファイルに追記します。

.env
 9行目 DB_CONNECTION=mysql
10行目 DB_HOST=127.0.0.1
11行目 DB_PORT=3306
12行目 DB_DATABASE=データベース名
13行目 DB_USERNAME=ユーザー名
14行目 DB_PASSWORD=パスワード

Laravelの日本語化

Laravel7を日本語化する方法
上記の記事を参考に、Laravelを日本語化します。

1.以下からZipファイルをダウンロードします。
https://github.com/caouecs/Laravel-lang

2.解凍したLaravel-lang-masterフォルダのjson/ja.jsonを、プロジェクトフォルダの resources/langに移動します。

3.Laravel-lang-master/src内のjaフォルダを、プロジェクト内のresources/langに移動します。最終的に下の画像になればOKです。
202006241828.png

Laravel6.0(PHP7.3)+MySQL+Laradockで簡易的なECサイトを作る①

ここから基本的にエラーが起こった場所のみとコードが違う場合のみ書いていきます。

1、まずは認証機能を作成します。

以下のコマンドを実行します。

$ cd ec_app
$ composer require laravel/ui --dev
$ php artisan ui vue --auth
$ npm install
$ npm run dev

課題

$ php artisan make:migration create_carts_table     

上記コマンドを実行して、create_carts_tableを作成し、中身を以下のように変更します。

database/migrations/2020_06_24_185504_create_carts_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateCartsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('carts', function (Blueprint $table) {
            $table->id();
            $table->integer('stock_id');
            $table->integer('user_id');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('carts');
    }
}

下記のコマンドを実行して完了です。

$  php artisan migrate

Laravel6.0(PHP7.3)+MySQL+Laradockで簡易的なECサイトを作る②

エラーはありませんでした。

課題

$ php artisan make:migration create_mines_table   

上記コマンドを実行して、create_mines_tableを作成し、中身を以下のように変更します。

database/migrations/2020_06_24_185504_create_carts_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateMinesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('mines', function (Blueprint $table) {
            $table->id();
            $table->string('name','50');
            $table->integer('age');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('mines');
    }
}

下記のコマンドを実行します。

$ php artisan migrate
$ php artisan make:seed MineTableSeeder

下記の2つのファイルを変更します。

database/seeds/MineTableSeeder.php
<?php

use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;

class MineTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        DB::table('mines')->truncate(); //2回目実行の際にシーダー情報をクリア
        DB::table('mines')->insert([
            'name' => 'フィルムカメラ',
            'age' => 10,
        ]);
        DB::table('mines')->insert([
            'name' => 'イヤホン',
            'age' => 20,
        ]);
    }
}
database/seeds/MineTableSeeder.php
<?php

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     *
     * @return void
     */
    public function run()
    {
        $this->call(MineTableSeeder::class);
    }
}

最後に下記のコマンドを実行すればOKです。

$ composer dump-autoload
$ php artisan db:seed

Laravel6.0(PHP7.3)+MySQL+Laradockで簡易的なECサイトを作る③

今回は何もエラーは起こりませんでしたので、記事通りにすすめてください。

課題

課題も長くなるので省略しますが、下記のコマンドを実行しました。create_carts_tableは既に作成済みのため、省略しています。

$ php artisan make:model Models/Cart

下記のようにファイルを書き換えます。

routes/web.php
//追記
Route::get('/mycart', 'ShopController@myCart');
resources/views/mycart.blade.php
@extends('layouts.app')

@section('content')
<div class="container-fluid">
  <div class="">
    <div class="mx-auto" style="max-width:1200px">
      <h1 style="color:#555555; text-align:center; font-size:1.2em; padding:24px 0px; font-weight:bold;">商品一覧</h1>
      <div class="">
        <div class="d-flex flex-row flex-wrap">
          商品一覧を出したい<br>

          {{-- 追加 --}}

          @foreach($carts as $cart)
          {{$cart->user_id}} <br>
          {{$cart->stock_id}}<br>
          @endforeach

          {{-- ここまで --}}
        </div>
      </div>
    </div>
  </div>
</div>
@endsection
app/Models/Cart.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Cart extends Model
{
    protected $guarded = [
        'stock_id',
        'user_id'
    ];
}
app/Http/Controllers/ShopController.php
<?php

namespace App\Http\Controllers;

use App\Models\Stock;
use App\Models\Cart; //追加
use Illuminate\Http\Request;

class ShopController extends Controller
{
    public function index()
    {
        $stocks = Stock::Paginate(6);
        return view('shop',compact('stocks'));
    }

    public function myCart() //追加
    {
        $carts = Cart::all();
        return view('mycart',compact('carts'));
    }
}

実践課題

下記のようにファイルを書き換えます。

routes/web.php
// ログイン状態
Route::group(['middleware' => 'auth'], function() {
    Route::get('/mycart', 'ShopController@myCart');
});

Laravel6.0(PHP7.3)+MySQL+Laradockで簡易的なECサイトを作る④

前回の課題の答え合わせをして、相違点があったので修正しました。

4、カートの中身を表示する

ここまで来たときに下記のエラーが出ました。
202006281246.png
エラーの内容を見ると、user_idが取得できていないため、エラーになっています。
まだ、userテーブルにはデータを未挿入なので、とりあえず下記のように「ShopController.php」を変更しました。

app/Http/Controllers/ShopController.php
public function addMyCart(Request $request)
    {
        $user_id = 1; 
        $stock_id=$request->stock_id;
        $cart_add_info=Cart::firstOrCreate(['stock_id' => $stock_id,'user_id' => $user_id]);
        if($cart_add_info->wasRecentlyCreated){
            $message = 'カートに追加しました';
        }
        else{
            $message = 'カートに登録済みです';
        }
        $my_carts = Cart::where('user_id',$user_id)->get();
        return view('mycart',compact('my_carts' , 'message'));
}

すると、以下のようなエラーメッセージに変わりました。
202006281303.png
まだログインしていないので、一旦「mycart.blade.php」ファイルを下記のように変更しました。

resources/views/mycart.blade.php
@extends('layouts.app')

@section('content')
<div class="container-fluid">
  <div class="mx-auto" style="max-width:1200px">
    <h1 class="text-center font-weight-bold" style="color:#555555;  font-size:1.2em; padding:24px 0px;">

      <div class="">
        <p class="text-center">{{ $message }}</p><br>
        <div class="d-flex flex-row flex-wrap">

          @foreach($my_carts as $my_cart)
          <div class="mycart_box">
            <p>ユーザーID:{{$my_cart->user_id}}</p>
            <p>ストックID:{{$my_cart->stock_id}}</p>
            @endforeach

          </div>

          <a href="/">商品一覧へ</a>
        </div>
      </div>
  </div>
</div>
@endsection

これで記事通りに表示されました。

Laravel6.0(PHP7.3)+MySQL+Laradockで簡易的なECサイトを作る⑤

記事通りに進める前に、会員登録してUserを1つ作っておきます。
前回変更したファイルを、下記のように変更します。

app/Http/Controllers/ShopController.php
<?php

namespace App\Http\Controllers;

use App\Models\Stock; //追加
use App\Models\Cart; //追加
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class ShopController extends Controller
{
    public function index() //追加
    {
        $stocks = Stock::Paginate(6); //Eloquantで検索
        return view('shop',compact('stocks')); //追記変更
    }

    public function myCart() //追加
    {
        $my_carts = Cart::all(); //Eloquantで検索
        return view('mycart',compact('my_carts')); //追記変更
    }

    public function addMyCart(Request $request)
    {
        $user_id = Auth::id(); 
        $stock_id=$request->stock_id;
        $cart_add_info=Cart::firstOrCreate(['stock_id' => $stock_id,'user_id' => $user_id]);
        if($cart_add_info->wasRecentlyCreated){
            $message = 'カートに追加しました';
        }
        else{
            $message = 'カートに登録済みです';
        }
        $my_carts = Cart::where('user_id',$user_id)->get();
        return view('mycart',compact('my_carts' , 'message'));
    }
}

resources/views/mycart.blade.php
@extends('layouts.app')

@section('content')
<div class="container-fluid">
  <div class="mx-auto" style="max-width:1200px">
    <h1 class="text-center font-weight-bold" style="color:#555555;  font-size:1.2em; padding:24px 0px;">
    {{ Auth::user()->name }}さんのカートの中身</h1>

      <div class="">
        <p class="text-center">{{ $message ?? '' }}</p><br>
        <div class="d-flex flex-row flex-wrap">

          @foreach($my_carts as $my_cart)
          <div class="mycart_box">
            <p>ユーザーID:{{$my_cart->user_id}}</p>
            <p>ストックID:{{$my_cart->stock_id}}</p>
            @endforeach

          </div>

          <a href="/">商品一覧へ</a>
        </div>
      </div>
  </div>
</div>
@endsection

これで記事のとおりに進めてもエラーは起こりませんでした。

課題

①「web.php」に以下を追記します。

routes/web.php
Route::post('/cartdelete', 'ShopController@deleteCart');//追記

②③以下のようにファイルに追記します。

app/Http/Controllers/ShopController.php
public function deleteCart(Request $request, Cart $cart)
{
        $stock_id=$request->stock_id;
        $user = auth()->user();
        $message = $cart->deleteMyCart($user->id, $stock_id);
        $my_carts = $cart->showCart();
        return view('mycart',compact('my_carts' , 'message'));
}
app/Models/Cart.php
public function deleteMyCart(Int $stock_id, Int $user_id)
{
        $user_id = Auth::id(); 
        $this->where('user_id', $user_id)->where('stock_id', $stock_id)->delete();
        return $message = '商品をカート内から削除しました';
}

なんとなくは動きますが、上手く削除できなかったので、次に進んで答えを確認します。

Laravel6.0(PHP7.3)+MySQL+Laradockで簡易的なECサイトを作る⑥

1、課題の答え合わせ

この通りにすると、上手く動きました。

6、購入完了のメールを送信する

ここで下記のエラーが出ました。
202006281303.png
「ShopController.php」を下記のように書き換えました。

public function checkout(Cart $cart)
{
        $checkout_info = $cart->checkoutCart();
        Mail::to('test@example.com')->send(new Thanks); //追記
        return view('checkout');
}

キャッシュをクリアするために、下記のコマンドを実行し、エラーが解消しました。

$ php artisan config:cache

あとは記事通りに進めていけば、エラーは起こりませんでした。

TwitterAPI アカウント申請〜許可まで【2020年版】

Twitterログインを実装するためには、Twitter Apiを使用する許可が必要となるため、この記事を参考に申請しました。
数日かかるようですが、私の場合はすぐに使えるようになりました。
202006282123.png

Laravel6.0+SocialiteでTwitterログインを実装する

ここからは、個人的に始めてやることなので、記録として残していきます。

1、とりあえず認証機能を追加

構築済みなので飛ばします。

2、Socialiteをcomposerでダウンロード。

以下のコマンドを実行して、composerを利用してsocialite機能に必要なやつファイルを一気に取得します。

$ composer require laravel/socialite

3、とりあえずルーティング

routes/web.phpにTwitterのためのルーティングを書きます。

routes/web.php
Route::get('login/twitter', 'Auth\LoginController@redirectToTwitterProvider');
Route::get('login/twitter/callback', 'Auth\LoginController@handleTwitterProviderCallback');

3、コントローラーに処理を記述する①

LoginController.phpに以下を追記します。

app/http/Controllers/Auth/LoginController.php
public function redirectToTwitterProvider()
{
        return Socialite::driver('twitter')->redirect();
}

4、使う前の下準備

Socialite::を使うためにサービスプロバイダへの登録とエイリアスの作成を行います。
config/app.phpの'providers' => []と'aliases' => []に以下を追記します。

config/app.php
// providers
Laravel\Socialite\SocialiteServiceProvider::class,

//aliases
'Socialite' => Laravel\Socialite\Facades\Socialite::class,

config/services.phpに以下を追記します。

config/services.php
'twitter' => [
       'client_id' => env('TWITTER_CLIENT_ID'),
       'client_secret' => env('TWITTER_CLIENT_SECRET'),
       'redirect' => env('CALLBACK_URL')
],

.envに以下を追記します。

.env
TWITTER_CLIENT_ID=************************* //下を参照
TWITTER_CLIENT_SECRET=************************* //下を参照
CALLBACK_URL=http://localhost/login/twitter/callback //よく見たらルーティングしたやつ

TWITTER_CLIENT_IDとTWITTER_CLIENT_SECRETは、下記のように取得します。

https://developer.twitter.com/en/apps
にアクセスして、「create app」をクリックします。
202006282152.png
英語を入力していき、「create」をクリックします。
202006282211.png
下記のモーダルウィンドウが出てくるので、「create」をクリックします。
202006282211.png
下記のような画面に遷移するのでにpermissionsタブを開き、Additional premissionsにチェックが入っていないとEmail情報が取得できないので許可を出しておきます。
202006282214.png
最後に「Keys and tokens」にタブを切変えると、APIが取得できます。
202006282219.png
「TWITTER_CLIENT_ID」に「API Key」を、「TWITTER_CLIENT_SECRET」に「API secret key」を入力します。

5、コントローラーに処理を記述する②

Auth\LoginControllerにhandleTwitterProviderCallbackを実装します。以下を追記します。

app/http/Controllers/Auth/LoginController.php
public function handleTwitterProviderCallback(){

       try {
           $user = Socialite::with("twitter")->user();
       } 
       catch (\Exception $e) {
           return redirect('/login')->with('oauth_error', 'ログインに失敗しました');
           // エラーならログイン画面へ転送
       }

       $myinfo = User::firstOrCreate(['token' => $user->token ],
                 ['name' => $user->nickname,'email' => $user->getEmail()]);
                 Auth::login($myinfo);
                 return redirect()->to('/'); // homeへ転送

}

database/migration/2014_10_12_000000_create_users_tableのupメソッドを以下のように変更します。

database/migration/2014_10_12_000000_create_users_table.php
public function up()
   {
       Schema::create('users', function (Blueprint $table) {
           $table->bigIncrements('id');
           $table->string('name');
           $table->string('email')->unique();
           $table->timestamp('email_verified_at')->nullable();
           $table->string('password')->nullable(); //->nullabale();追加
           $table->string('token')->nullable(); //追加
           $table->rememberToken();
           $table->timestamps();
       });
}

app/User.phpを以下のように編集します。

protected $fillable = [
        'name', 'email', 'password', 'token',
];

後は以下のコマンドを順に実行します。

$ php artisan migrate:fresh --seed
$ composer dump-autoload
$ npm run dev
$ php artisan config:cache
$ php artisan serve

このままだとパスワードに問題があるとのことで、さらにうまくできずにエラーが起きて修正できなかったので、下記に進みます。

Laravel 6.5 と Socialite で Twitter OAuth ログインを実現する

上記の記事を参考に以下の点を変更しました。

5.Twitter Developersから Twitterアプリを登録します

https://dev.twitter.com/index
上記にアクセスして下記を変更しました。

Callback URL: http://あなたのサーバ名/auth/twitter/callback

同様に.envファイルも修正します。

.env
TWITTER_CALLBACK_URL=http://あなたのサーバ名/auth/twitter/callback

6.laravelのルーティングを設定

routes/web.php に以下を追加します。

routes/web.php
// Auth Twitter
Route::get('auth/twitter', 'Auth\AuthController@TwitterRedirect');
Route::get('auth/twitter/callback', 'Auth\AuthController@TwitterCallback');
Route::get('auth/twitter/logout', 'Auth\AuthController@getLogout');

// Auth Google
Route::get('auth/google', 'Auth\AuthController@GoogleRedirect');
Route::get('auth/google/callback', 'Auth\AuthController@GoogleCallback');
Route::get('auth/google/logout', 'Auth\AuthController@getLogout');

// Auth Facebook
Route::get('auth/facebook', 'Auth\AuthController@FacebookRedirect');
Route::get('auth/facebook/callback', 'Auth\AuthController@FacebookCallback');
Route::get('auth/facebook/logout', 'Auth\AuthController@getLogout');

7コントローラーの設定

app/Http/Controllers/Auth/AuthController.php を以下の内容で新規作成します。

app/Http/Controllers/Auth/AuthController.php
<?php
namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Socialite;

class AuthController extends Controller
{

    public function TwitterRedirect()
    {
        return Socialite::driver('twitter')->redirect();
    }

    public function TwitterCallback()
    {
        // OAuthユーザー情報を取得
        $social_user = Socialite::driver('twitter')->user();
        $user = $this->first_or_create_social_user('twitter', $social_user->id, $social_user->name, $social_user->avatar );

        // Laravel 標準の Auth でログイン
        \Auth::login($user);

        return redirect('/home');
    }

    /**
     * ログインしたソーシャルアカウントがDBにあるかどうか調べます
     *
     * @param   string      $service_name       ( twitter , facebook ... )
     * @param   int         $social_id          ( 123456789 )
     * @param   string      $social_avatar      ( https://....... )
     *
     * @return  \App\User   $user
     *
     */
    protected function first_or_create_social_user( string $service_name,
                                                int $social_id, string $social_name, string $social_avatar )
    {
        $user = null;
        $user = \App\User::where( "{$service_name}_id", '=', $social_id )->first();
        if ( $user === null ){
            $user = new \App\User();
            $user->fill( [
                "{$service_name}_id" => $social_id ,
                'name'               => $social_name ,
                'avatar'             => $social_avatar ,
                'password'           => 'DUMMY_PASSWORD' ,
            ] );
            $user->save();
            return $user;
        }
        else{
            return $user;
        }
    }

}

8.URLを叩いてログインをテストする

login.blade.phpに以下を追記します。

resorces/views/auth/login.blade.php
          <div class="form-group row mt-3">
            <div class="col-md-6 offset-md-4">
              <a href="auth/twitter">
                <button type="button" class="btn btn-primary"><i class="fab fa-twitter"></i> Twitterアカウントでログインする</button>
              </a>
            </div>
          </div>

ではエラーが起きたので、に変更しています。

9.実際のログイン後の実装(ユーザー自動作成)

2014_10_12_000000_create_users_table.phpのupメソッドをデフォルトに戻します。

2014_10_12_000000_create_users_table.php
public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();
        });
    }

以下のコマンドを実行します。

$ composer require doctrine/dbal
$ php artisan make:migration change_users_table_add_oauth_columns  --table=users

change_users_table_add_oauth_columns.php を編集します。

2020_06_28_234825_change_users_table_add_oauth_columns.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class ChangeUsersTableAddOauthColumns extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->datetime('avatar')->nullable()->after('remember_token')->comment('アバター画像');
            $table->unsignedBigInteger('twitter_id')->nullable()->after('remember_token')->comment('Twitter ID');
            $table->unsignedBigInteger('facebook_id')->nullable()->after('remember_token')->comment('Facebook ID');
            $table->unsignedBigInteger('github_id')->nullable()->after('remember_token')->comment('GitHub ID');
            $table->unsignedBigInteger('google_id')->nullable()->after('remember_token')->comment('Google ID');
            $table->unsignedBigInteger('yahoo_id')->nullable()->after('remember_token')->comment('Yahoo ID');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropColumn('avatar');
            $table->dropColumn('twitter_id');
            $table->dropColumn('facebook_id');
            $table->dropColumn('github_id');
            $table->dropColumn('google_id');
            $table->dropColumn('yahoo_id');
        });
    }
}

app\User.phpを編集します。

app\Users.php
    // protected $fillable = [
    // 'name', 'email', 'password', 'token',
    // ];

    // guarded
    protected $guarded = ['id', 'created_at', 'updated_at'];

DatabaseSeeder.php を変更します。

DatabaseSeeder.php
    public function run()
    {
        $this->call(MineTableSeeder::class)
              ->call(StockTableSeeder::class);
    }

後は以下のコマンドを順に実行します。

前項「Laravel6.0+SocialiteでTwitterログインを実装する」の項目を戻す

config/app.phpに追加した項目をコメントアウトします。

config/app.php
'providers'
        //これ追加!!
        //Laravel\Socialite\SocialiteServiceProvider::class,

'aliases'
        // 以下を追記
        //'Socialite' => Laravel\Socialite\Facades\Socialite::class,

LoginControllerも戻します。

Auth\LoginController.php
//    public function redirectToTwitterProvider()
//    {
//        return Socialite::driver('twitter')->redirect();
//    }
//
//    public function handleTwitterProviderCallback(){
//
//        try {
//            $user = Socialite::with("twitter")->user();
//        } 
//        catch (\Exception $e) {
//            return redirect('/login')->with('oauth_error', 'ログインに失敗しました');
//            // エラーならログイン画面へ転送
//        }
//
//        $myinfo = User::firstOrCreate(['token' => $user->token ],
//                  ['name' => $user->nickname,'email' => $user->getEmail()]);
//                  Auth::login($myinfo);
//                  return redirect()->to('/'); // homeへ転送
//    }

まだエラーが起きたので、下記を修正

Twitter関連は絵文字を使うので、使えるように修正します。

config/database.php
'mysql' => [
        'charset' => 'utf8mb4',
                'collation' => 'utf8mb4_general_ci',        
],

データベースはphpMyAdminでDBを選択し、「操作」→「照合順序」でか下記のように「utf8mb4_general_ci」に変更します。
202006290054.png
画像のURLをtimestamp型に受け取るになっていたので修正します。

2020_06_28_234825_change_users_table_add_oauth_columns.php
// 変更前
$table->timestamp('avatar')->nullable()->after('remember_token')->comment('アバター画像');

// 変更後
$table->string('avatar')->nullable()->after('remember_token')->comment('アバター画像');

※上記3つが終わったら、一度「$ php artisan migrate:fresh --seed」しないとエラーになるので注意。

Emailを入力していなかったので追加しました。ついでにリダイレクト先も変更しています。

AuthController.php
<?php
namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Socialite;
use App\Models\User;

class AuthController extends Controller
{

    public function TwitterRedirect()
    {
        return Socialite::driver('twitter')->redirect();
    }

    public function TwitterCallback()
    {
        // OAuthユーザー情報を取得
        $social_user = Socialite::driver('twitter')->user();
        $user = $this->first_or_create_social_user('twitter', $social_user->id, $social_user->name, $social_user->email, $social_user->avatar );

        // Laravel 標準の Auth でログイン
        \Auth::login($user);

        return redirect('/');
    }

    /**
     * ログインしたソーシャルアカウントがDBにあるかどうか調べます
     *
     * @param   string      $service_name       ( twitter , facebook ... )
     * @param   int         $social_id          ( 123456789 )
     * @param   string      $social_avatar      ( https://....... )
     *
     * @return  User   $user
     *
     */
    protected function first_or_create_social_user( string $service_name,
                                                int $social_id, string $social_name, string $social_email, string $social_avatar )
    {
        $user = null;
        $user = User::where( "{$service_name}_id", '=', $social_id )->first();
        if ( $user === null ){
            $user = new User();
            $user->fill( [
                "{$service_name}_id" => $social_id ,
                'name'               => $social_name ,
                'email'               => $social_email ,
                'avatar'             => $social_avatar ,
                'password'           => 'DUMMY_PASSWORD' ,
            ] );
            $user->save();
            return $user;
        }
        else{
            return $user;
        }
    }
}

最後に下記のコマンドを実行します。

$ php artisan migrate:fresh --seed
$ composer dump-autoload
$ npm run dev
$ php artisan config:cache
$ php artisan serve

これでTwitterでログインできるようになりました。
ソースコードはhttps://github.com/neneta0921/ec_app にて公開しているので、宜しけば参考にしてください。
長くなりましたが、以上です。

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

Laravel 7で簡易的なECサイトを作る+Twitter OAuth ログインを実装する

以下のサイトを参考に、ECサイトを構築します。
エラーが起こった箇所のみ書いていますが、結構長いです。
Laravel6.0(PHP7.3)+MySQL+Laradockで簡易的なECサイトを作る
TwitterAPI アカウント申請〜許可まで【2020年版】
Laravel6.0+SocialiteでTwitterログインを実装する
Laravel 6.5 と Socialite で Twitter OAuth ログインを実現する

※ソースコードはgithubにて公開しているので、宜しけば参考にしてください。

作業環境

OS:Windows 10 HOME Edition(ver.2004)
Laravel:7.15.0
Xampp:7.4.6
Composer:1.10.7
Node.js:13.9.0

環境構築

Laradockではなく、いつもどおりのXamppで行いました。

Laravelのインストール

下記のコマンドを実行すると、Laravelのインストールができます。今回は「ec_app」というアプリ名にしました。

$ cd c:\xampp\htdocs
$ composer create-project laravel/laravel ec_app --prefer-dist

デバックバーのインストール

以下のコマンドを実行して、デバックバーのインストールします。

$ composer require barryvdh/laravel-debugbar

Laravelのタイムゾーンと言語設定

「config/app.php」を編集します。

config/app.php
70行目 'timezone' => 'Asia/Tokyo',
83行目 'locale' => 'ja',

データベースの言語設定

「config/database.php」を編集します。

config/database.php
55行目 'charset' => 'utf8',
56行目 'collation' => 'utf8_unicode_ci',

データベースの設定

xamppのMySqlのAdminボタンを押してphpMyAdminを起動し、今回使用するデータベースを作成します。
202006241817.png
今回はデータベース名は「ec_app_db」としていますが、任意の名前でOKです。だたし、言語は「utf8_general_ci」を選ぶ点に注意してください。
下記を参考に、データベースの設定を「.env」ファイルに追記します。

.env
 9行目 DB_CONNECTION=mysql
10行目 DB_HOST=127.0.0.1
11行目 DB_PORT=3306
12行目 DB_DATABASE=データベース名
13行目 DB_USERNAME=ユーザー名
14行目 DB_PASSWORD=パスワード

Laravelの日本語化

Laravel7を日本語化する方法
上記の記事を参考に、Laravelを日本語化します。

1.以下からZipファイルをダウンロードします。
https://github.com/caouecs/Laravel-lang

2.解凍したLaravel-lang-masterフォルダのjson/ja.jsonを、プロジェクトフォルダの resources/langに移動します。

3.Laravel-lang-master/src内のjaフォルダを、プロジェクト内のresources/langに移動します。最終的に下の画像になればOKです。
202006241828.png

Laravel6.0(PHP7.3)+MySQL+Laradockで簡易的なECサイトを作る①

ここから基本的にエラーが起こった場所のみとコードが違う場合のみ書いていきます。

1、まずは認証機能を作成します。

以下のコマンドを実行します。

$ cd ec_app
$ composer require laravel/ui --dev
$ php artisan ui vue --auth
$ npm install
$ npm run dev

課題

$ php artisan make:migration create_carts_table     

上記コマンドを実行して、create_carts_tableを作成し、中身を以下のように変更します。

database/migrations/2020_06_24_185504_create_carts_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateCartsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('carts', function (Blueprint $table) {
            $table->id();
            $table->integer('stock_id');
            $table->integer('user_id');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('carts');
    }
}

下記のコマンドを実行して完了です。

$  php artisan migrate

Laravel6.0(PHP7.3)+MySQL+Laradockで簡易的なECサイトを作る②

エラーはありませんでした。

課題

$ php artisan make:migration create_mines_table   

上記コマンドを実行して、create_mines_tableを作成し、中身を以下のように変更します。

database/migrations/2020_06_24_185504_create_carts_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateMinesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('mines', function (Blueprint $table) {
            $table->id();
            $table->string('name','50');
            $table->integer('age');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('mines');
    }
}

下記のコマンドを実行します。

$ php artisan migrate
$ php artisan make:seed MineTableSeeder

下記の2つのファイルを変更します。

database/seeds/MineTableSeeder.php
<?php

use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;

class MineTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        DB::table('mines')->truncate(); //2回目実行の際にシーダー情報をクリア
        DB::table('mines')->insert([
            'name' => 'フィルムカメラ',
            'age' => 10,
        ]);
        DB::table('mines')->insert([
            'name' => 'イヤホン',
            'age' => 20,
        ]);
    }
}
database/seeds/MineTableSeeder.php
<?php

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     *
     * @return void
     */
    public function run()
    {
        $this->call(MineTableSeeder::class);
    }
}

最後に下記のコマンドを実行すればOKです。

$ composer dump-autoload
$ php artisan db:seed

Laravel6.0(PHP7.3)+MySQL+Laradockで簡易的なECサイトを作る③

今回は何もエラーは起こりませんでしたので、記事通りにすすめてください。

課題

課題も長くなるので省略しますが、下記のコマンドを実行しました。create_carts_tableは既に作成済みのため、省略しています。

$ php artisan make:model Models/Cart

下記のようにファイルを書き換えます。

routes/web.php
//追記
Route::get('/mycart', 'ShopController@myCart');
resources/views/mycart.blade.php
@extends('layouts.app')

@section('content')
<div class="container-fluid">
  <div class="">
    <div class="mx-auto" style="max-width:1200px">
      <h1 style="color:#555555; text-align:center; font-size:1.2em; padding:24px 0px; font-weight:bold;">商品一覧</h1>
      <div class="">
        <div class="d-flex flex-row flex-wrap">
          商品一覧を出したい<br>

          {{-- 追加 --}}

          @foreach($carts as $cart)
          {{$cart->user_id}} <br>
          {{$cart->stock_id}}<br>
          @endforeach

          {{-- ここまで --}}
        </div>
      </div>
    </div>
  </div>
</div>
@endsection
app/Models/Cart.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Cart extends Model
{
    protected $guarded = [
        'stock_id',
        'user_id'
    ];
}
app/Http/Controllers/ShopController.php
<?php

namespace App\Http\Controllers;

use App\Models\Stock;
use App\Models\Cart; //追加
use Illuminate\Http\Request;

class ShopController extends Controller
{
    public function index()
    {
        $stocks = Stock::Paginate(6);
        return view('shop',compact('stocks'));
    }

    public function myCart() //追加
    {
        $carts = Cart::all();
        return view('mycart',compact('carts'));
    }
}

実践課題

下記のようにファイルを書き換えます。

routes/web.php
// ログイン状態
Route::group(['middleware' => 'auth'], function() {
    Route::get('/mycart', 'ShopController@myCart');
});

Laravel6.0(PHP7.3)+MySQL+Laradockで簡易的なECサイトを作る④

前回の課題の答え合わせをして、相違点があったので修正しました。

4、カートの中身を表示する

ここまで来たときに下記のエラーが出ました。
202006281246.png
エラーの内容を見ると、user_idが取得できていないため、エラーになっています。
まだ、userテーブルにはデータを未挿入なので、とりあえず下記のように「ShopController.php」を変更しました。

app/Http/Controllers/ShopController.php
public function addMyCart(Request $request)
    {
        $user_id = 1; 
        $stock_id=$request->stock_id;
        $cart_add_info=Cart::firstOrCreate(['stock_id' => $stock_id,'user_id' => $user_id]);
        if($cart_add_info->wasRecentlyCreated){
            $message = 'カートに追加しました';
        }
        else{
            $message = 'カートに登録済みです';
        }
        $my_carts = Cart::where('user_id',$user_id)->get();
        return view('mycart',compact('my_carts' , 'message'));
}

すると、以下のようなエラーメッセージに変わりました。
202006281303.png
まだログインしていないので、一旦「mycart.blade.php」ファイルを下記のように変更しました。

resources/views/mycart.blade.php
@extends('layouts.app')

@section('content')
<div class="container-fluid">
  <div class="mx-auto" style="max-width:1200px">
    <h1 class="text-center font-weight-bold" style="color:#555555;  font-size:1.2em; padding:24px 0px;">

      <div class="">
        <p class="text-center">{{ $message }}</p><br>
        <div class="d-flex flex-row flex-wrap">

          @foreach($my_carts as $my_cart)
          <div class="mycart_box">
            <p>ユーザーID:{{$my_cart->user_id}}</p>
            <p>ストックID:{{$my_cart->stock_id}}</p>
            @endforeach

          </div>

          <a href="/">商品一覧へ</a>
        </div>
      </div>
  </div>
</div>
@endsection

これで記事通りに表示されました。

Laravel6.0(PHP7.3)+MySQL+Laradockで簡易的なECサイトを作る⑤

記事通りに進める前に、会員登録してUserを1つ作っておきます。
前回変更したファイルを、下記のように変更します。

app/Http/Controllers/ShopController.php
<?php

namespace App\Http\Controllers;

use App\Models\Stock; //追加
use App\Models\Cart; //追加
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class ShopController extends Controller
{
    public function index() //追加
    {
        $stocks = Stock::Paginate(6); //Eloquantで検索
        return view('shop',compact('stocks')); //追記変更
    }

    public function myCart() //追加
    {
        $my_carts = Cart::all(); //Eloquantで検索
        return view('mycart',compact('my_carts')); //追記変更
    }

    public function addMyCart(Request $request)
    {
        $user_id = Auth::id(); 
        $stock_id=$request->stock_id;
        $cart_add_info=Cart::firstOrCreate(['stock_id' => $stock_id,'user_id' => $user_id]);
        if($cart_add_info->wasRecentlyCreated){
            $message = 'カートに追加しました';
        }
        else{
            $message = 'カートに登録済みです';
        }
        $my_carts = Cart::where('user_id',$user_id)->get();
        return view('mycart',compact('my_carts' , 'message'));
    }
}

resources/views/mycart.blade.php
@extends('layouts.app')

@section('content')
<div class="container-fluid">
  <div class="mx-auto" style="max-width:1200px">
    <h1 class="text-center font-weight-bold" style="color:#555555;  font-size:1.2em; padding:24px 0px;">
    {{ Auth::user()->name }}さんのカートの中身</h1>

      <div class="">
        <p class="text-center">{{ $message ?? '' }}</p><br>
        <div class="d-flex flex-row flex-wrap">

          @foreach($my_carts as $my_cart)
          <div class="mycart_box">
            <p>ユーザーID:{{$my_cart->user_id}}</p>
            <p>ストックID:{{$my_cart->stock_id}}</p>
            @endforeach

          </div>

          <a href="/">商品一覧へ</a>
        </div>
      </div>
  </div>
</div>
@endsection

これで記事のとおりに進めてもエラーは起こりませんでした。

課題

①「web.php」に以下を追記します。

routes/web.php
Route::post('/cartdelete', 'ShopController@deleteCart');//追記

②③以下のようにファイルに追記します。

app/Http/Controllers/ShopController.php
public function deleteCart(Request $request, Cart $cart)
{
        $stock_id=$request->stock_id;
        $user = auth()->user();
        $message = $cart->deleteMyCart($user->id, $stock_id);
        $my_carts = $cart->showCart();
        return view('mycart',compact('my_carts' , 'message'));
}
app/Models/Cart.php
public function deleteMyCart(Int $stock_id, Int $user_id)
{
        $user_id = Auth::id(); 
        $this->where('user_id', $user_id)->where('stock_id', $stock_id)->delete();
        return $message = '商品をカート内から削除しました';
}

なんとなくは動きますが、上手く削除できなかったので、次に進んで答えを確認します。

Laravel6.0(PHP7.3)+MySQL+Laradockで簡易的なECサイトを作る⑥

1、課題の答え合わせ

この通りにすると、上手く動きました。

6、購入完了のメールを送信する

ここで下記のエラーが出ました。
202006281303.png
「ShopController.php」を下記のように書き換えました。

public function checkout(Cart $cart)
{
        $checkout_info = $cart->checkoutCart();
        Mail::to('test@example.com')->send(new Thanks); //追記
        return view('checkout');
}

キャッシュをクリアするために、下記のコマンドを実行し、エラーが解消しました。

$ php artisan config:cache

あとは記事通りに進めていけば、エラーは起こりませんでした。

TwitterAPI アカウント申請〜許可まで【2020年版】

Twitterログインを実装するためには、Twitter Apiを使用する許可が必要となるため、この記事を参考に申請しました。
数日かかるようですが、私の場合はすぐに使えるようになりました。
202006282123.png

Laravel6.0+SocialiteでTwitterログインを実装する

ここからは、個人的に始めてやることなので、記録として残していきます。

1、とりあえず認証機能を追加

構築済みなので飛ばします。

2、Socialiteをcomposerでダウンロード。

以下のコマンドを実行して、composerを利用してsocialite機能に必要なやつファイルを一気に取得します。

$ composer require laravel/socialite

3、とりあえずルーティング

routes/web.phpにTwitterのためのルーティングを書きます。

routes/web.php
Route::get('login/twitter', 'Auth\LoginController@redirectToTwitterProvider');
Route::get('login/twitter/callback', 'Auth\LoginController@handleTwitterProviderCallback');

3、コントローラーに処理を記述する①

LoginController.phpに以下を追記します。

app/http/Controllers/Auth/LoginController.php
public function redirectToTwitterProvider()
{
        return Socialite::driver('twitter')->redirect();
}

4、使う前の下準備

Socialite::を使うためにサービスプロバイダへの登録とエイリアスの作成を行います。
config/app.phpの'providers' => []と'aliases' => []に以下を追記します。

config/app.php
// providers
Laravel\Socialite\SocialiteServiceProvider::class,

//aliases
'Socialite' => Laravel\Socialite\Facades\Socialite::class,

config/services.phpに以下を追記します。

config/services.php
'twitter' => [
       'client_id' => env('TWITTER_CLIENT_ID'),
       'client_secret' => env('TWITTER_CLIENT_SECRET'),
       'redirect' => env('CALLBACK_URL')
],

.envに以下を追記します。

.env
TWITTER_CLIENT_ID=************************* //下を参照
TWITTER_CLIENT_SECRET=************************* //下を参照
CALLBACK_URL=http://localhost/login/twitter/callback //よく見たらルーティングしたやつ

TWITTER_CLIENT_IDとTWITTER_CLIENT_SECRETは、下記のように取得します。

https://developer.twitter.com/en/apps
にアクセスして、「create app」をクリックします。
202006282152.png
英語を入力していき、「create」をクリックします。
202006282211.png
下記のモーダルウィンドウが出てくるので、「create」をクリックします。
202006282211.png
下記のような画面に遷移するのでにpermissionsタブを開き、Additional premissionsにチェックが入っていないとEmail情報が取得できないので許可を出しておきます。
202006282214.png
最後に「Keys and tokens」にタブを切変えると、APIが取得できます。
202006282219.png
「TWITTER_CLIENT_ID」に「API Key」を、「TWITTER_CLIENT_SECRET」に「API secret key」を入力します。

5、コントローラーに処理を記述する②

Auth\LoginControllerにhandleTwitterProviderCallbackを実装します。以下を追記します。

app/http/Controllers/Auth/LoginController.php
public function handleTwitterProviderCallback(){

       try {
           $user = Socialite::with("twitter")->user();
       } 
       catch (\Exception $e) {
           return redirect('/login')->with('oauth_error', 'ログインに失敗しました');
           // エラーならログイン画面へ転送
       }

       $myinfo = User::firstOrCreate(['token' => $user->token ],
                 ['name' => $user->nickname,'email' => $user->getEmail()]);
                 Auth::login($myinfo);
                 return redirect()->to('/'); // homeへ転送

}

database/migration/2014_10_12_000000_create_users_tableのupメソッドを以下のように変更します。

database/migration/2014_10_12_000000_create_users_table.php
public function up()
   {
       Schema::create('users', function (Blueprint $table) {
           $table->bigIncrements('id');
           $table->string('name');
           $table->string('email')->unique();
           $table->timestamp('email_verified_at')->nullable();
           $table->string('password')->nullable(); //->nullabale();追加
           $table->string('token')->nullable(); //追加
           $table->rememberToken();
           $table->timestamps();
       });
}

app/User.phpを以下のように編集します。

protected $fillable = [
        'name', 'email', 'password', 'token',
];

後は以下のコマンドを順に実行します。

$ php artisan migrate:fresh --seed
$ composer dump-autoload
$ npm run dev
$ php artisan config:cache
$ php artisan serve

このままだとパスワードに問題があるとのことで、さらにうまくできずにエラーが起きて修正できなかったので、下記に進みます。

Laravel 6.5 と Socialite で Twitter OAuth ログインを実現する

上記の記事を参考に以下の点を変更しました。

5.Twitter Developersから Twitterアプリを登録します

https://dev.twitter.com/index
上記にアクセスして下記を変更しました。

Callback URL: http://あなたのサーバ名/auth/twitter/callback

同様に.envファイルも修正します。

.env
TWITTER_CALLBACK_URL=http://あなたのサーバ名/auth/twitter/callback

6.laravelのルーティングを設定

routes/web.php に以下を追加します。

routes/web.php
// Auth Twitter
Route::get('auth/twitter', 'Auth\AuthController@TwitterRedirect');
Route::get('auth/twitter/callback', 'Auth\AuthController@TwitterCallback');
Route::get('auth/twitter/logout', 'Auth\AuthController@getLogout');

// Auth Google
Route::get('auth/google', 'Auth\AuthController@GoogleRedirect');
Route::get('auth/google/callback', 'Auth\AuthController@GoogleCallback');
Route::get('auth/google/logout', 'Auth\AuthController@getLogout');

// Auth Facebook
Route::get('auth/facebook', 'Auth\AuthController@FacebookRedirect');
Route::get('auth/facebook/callback', 'Auth\AuthController@FacebookCallback');
Route::get('auth/facebook/logout', 'Auth\AuthController@getLogout');

7コントローラーの設定

app/Http/Controllers/Auth/AuthController.php を以下の内容で新規作成します。

app/Http/Controllers/Auth/AuthController.php
<?php
namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Socialite;

class AuthController extends Controller
{

    public function TwitterRedirect()
    {
        return Socialite::driver('twitter')->redirect();
    }

    public function TwitterCallback()
    {
        // OAuthユーザー情報を取得
        $social_user = Socialite::driver('twitter')->user();
        $user = $this->first_or_create_social_user('twitter', $social_user->id, $social_user->name, $social_user->avatar );

        // Laravel 標準の Auth でログイン
        \Auth::login($user);

        return redirect('/home');
    }

    /**
     * ログインしたソーシャルアカウントがDBにあるかどうか調べます
     *
     * @param   string      $service_name       ( twitter , facebook ... )
     * @param   int         $social_id          ( 123456789 )
     * @param   string      $social_avatar      ( https://....... )
     *
     * @return  \App\User   $user
     *
     */
    protected function first_or_create_social_user( string $service_name,
                                                int $social_id, string $social_name, string $social_avatar )
    {
        $user = null;
        $user = \App\User::where( "{$service_name}_id", '=', $social_id )->first();
        if ( $user === null ){
            $user = new \App\User();
            $user->fill( [
                "{$service_name}_id" => $social_id ,
                'name'               => $social_name ,
                'avatar'             => $social_avatar ,
                'password'           => 'DUMMY_PASSWORD' ,
            ] );
            $user->save();
            return $user;
        }
        else{
            return $user;
        }
    }

}

8.URLを叩いてログインをテストする

login.blade.phpに以下を追記します。

resorces/views/auth/login.blade.php
          <div class="form-group row mt-3">
            <div class="col-md-6 offset-md-4">
              <a href="auth/twitter">
                <button type="button" class="btn btn-primary"><i class="fab fa-twitter"></i> Twitterアカウントでログインする</button>
              </a>
            </div>
          </div>

ではエラーが起きたので、に変更しています。

9.実際のログイン後の実装(ユーザー自動作成)

2014_10_12_000000_create_users_table.phpのupメソッドをデフォルトに戻します。

2014_10_12_000000_create_users_table.php
public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();
        });
    }

以下のコマンドを実行します。

$ composer require doctrine/dbal
$ php artisan make:migration change_users_table_add_oauth_columns  --table=users

change_users_table_add_oauth_columns.php を編集します。

2020_06_28_234825_change_users_table_add_oauth_columns.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class ChangeUsersTableAddOauthColumns extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->datetime('avatar')->nullable()->after('remember_token')->comment('アバター画像');
            $table->unsignedBigInteger('twitter_id')->nullable()->after('remember_token')->comment('Twitter ID');
            $table->unsignedBigInteger('facebook_id')->nullable()->after('remember_token')->comment('Facebook ID');
            $table->unsignedBigInteger('github_id')->nullable()->after('remember_token')->comment('GitHub ID');
            $table->unsignedBigInteger('google_id')->nullable()->after('remember_token')->comment('Google ID');
            $table->unsignedBigInteger('yahoo_id')->nullable()->after('remember_token')->comment('Yahoo ID');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropColumn('avatar');
            $table->dropColumn('twitter_id');
            $table->dropColumn('facebook_id');
            $table->dropColumn('github_id');
            $table->dropColumn('google_id');
            $table->dropColumn('yahoo_id');
        });
    }
}

app\User.phpを編集します。

app\Users.php
    // protected $fillable = [
    // 'name', 'email', 'password', 'token',
    // ];

    // guarded
    protected $guarded = ['id', 'created_at', 'updated_at'];

DatabaseSeeder.php を変更します。

DatabaseSeeder.php
    public function run()
    {
        $this->call(MineTableSeeder::class)
              ->call(StockTableSeeder::class);
    }

後は以下のコマンドを順に実行します。

前項「Laravel6.0+SocialiteでTwitterログインを実装する」の項目を戻す

config/app.phpに追加した項目をコメントアウトします。

config/app.php
'providers'
        //これ追加!!
        //Laravel\Socialite\SocialiteServiceProvider::class,

'aliases'
        // 以下を追記
        //'Socialite' => Laravel\Socialite\Facades\Socialite::class,

LoginControllerも戻します。

Auth\LoginController.php
//    public function redirectToTwitterProvider()
//    {
//        return Socialite::driver('twitter')->redirect();
//    }
//
//    public function handleTwitterProviderCallback(){
//
//        try {
//            $user = Socialite::with("twitter")->user();
//        } 
//        catch (\Exception $e) {
//            return redirect('/login')->with('oauth_error', 'ログインに失敗しました');
//            // エラーならログイン画面へ転送
//        }
//
//        $myinfo = User::firstOrCreate(['token' => $user->token ],
//                  ['name' => $user->nickname,'email' => $user->getEmail()]);
//                  Auth::login($myinfo);
//                  return redirect()->to('/'); // homeへ転送
//    }

まだエラーが起きたので、下記を修正

Twitter関連は絵文字を使うので、使えるように修正します。

config/database.php
'mysql' => [
        'charset' => 'utf8mb4',
                'collation' => 'utf8mb4_general_ci',        
],

データベースはphpMyAdminでDBを選択し、「操作」→「照合順序」でか下記のように「utf8mb4_general_ci」に変更します。
202006290054.png
画像のURLをtimestamp型に受け取るになっていたので修正します。

2020_06_28_234825_change_users_table_add_oauth_columns.php
// 変更前
$table->timestamp('avatar')->nullable()->after('remember_token')->comment('アバター画像');

// 変更後
$table->string('avatar')->nullable()->after('remember_token')->comment('アバター画像');

※上記3つが終わったら、一度「$ php artisan migrate:fresh --seed」しないとエラーになるので注意。

Emailを入力していなかったので追加しました。ついでにリダイレクト先も変更しています。

AuthController.php
<?php
namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Socialite;
use App\Models\User;

class AuthController extends Controller
{

    public function TwitterRedirect()
    {
        return Socialite::driver('twitter')->redirect();
    }

    public function TwitterCallback()
    {
        // OAuthユーザー情報を取得
        $social_user = Socialite::driver('twitter')->user();
        $user = $this->first_or_create_social_user('twitter', $social_user->id, $social_user->name, $social_user->email, $social_user->avatar );

        // Laravel 標準の Auth でログイン
        \Auth::login($user);

        return redirect('/');
    }

    /**
     * ログインしたソーシャルアカウントがDBにあるかどうか調べます
     *
     * @param   string      $service_name       ( twitter , facebook ... )
     * @param   int         $social_id          ( 123456789 )
     * @param   string      $social_avatar      ( https://....... )
     *
     * @return  User   $user
     *
     */
    protected function first_or_create_social_user( string $service_name,
                                                int $social_id, string $social_name, string $social_email, string $social_avatar )
    {
        $user = null;
        $user = User::where( "{$service_name}_id", '=', $social_id )->first();
        if ( $user === null ){
            $user = new User();
            $user->fill( [
                "{$service_name}_id" => $social_id ,
                'name'               => $social_name ,
                'email'               => $social_email ,
                'avatar'             => $social_avatar ,
                'password'           => 'DUMMY_PASSWORD' ,
            ] );
            $user->save();
            return $user;
        }
        else{
            return $user;
        }
    }
}

最後に下記のコマンドを実行します。

$ php artisan migrate:fresh --seed
$ composer dump-autoload
$ npm run dev
$ php artisan config:cache
$ php artisan serve

これでTwitterでログインできるようになりました。
ソースコードはhttps://github.com/neneta0921/ec_app にて公開しているので、宜しけば参考にしてください。
長くなりましたが、以上です。

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