20200629のlaravelに関する記事は17件です。

How to adopt Sentry to Laravel 6 with user context

Check your Laravel version before get started.

$ php artisan --version

Installation Sentry

$ composer require sentry/sentry-laravel:1.8.0

Adding sentry DSN on your env

.env
SENTRY_LARAVEL_DSN=https://[YOUR_URL]        

Make config file for Sentry

$ touch config/sentry.php
config/sentry.php
<?php
return array(
    'dsn' => env('SENTRY_LARAVEL_DSN'),
    'breadcrumbs.sql_bindings' => true,
    'send_default_pii' => true
);

Modify your Hander file

app/Exceptions/Handler.php
     public function report(Exception $exception)
     {
+        if (app()->bound('sentry') && $this->shouldReport($exception)) {
+            app('sentry')->captureException($exception);
+        }
+
         parent::report($exception);
     }

Adding user context

$ php artisan make:middleware SentryContext
app/Http/Middleware/SentryContext.php
<?php
namespace App\Http\Middleware;

use Closure;
use Sentry\State\Scope;

class SentryContext
{
    /** 
     * Handle an incoming request.
     *
     * @param \Illuminate\Http\Request $request
     * @param \Closure                 $next
     *
     * @return mixed
     */
    public function handle($request, Closure $next)
    {   
        if (auth()->check() && app()->bound('sentry')) {
            \Sentry\configureScope(function (Scope $scope): void {
                $scope->setUser([
                    'id' => auth()->user()->id,
                    // since email is sensitive information, be careful to handle it.
                    //'email' => auth()->user()->email,
                ]); 
            }); 
        }   

        return $next($request);
    } 
}
app/Http/Kernel.php
    protected $routeMiddleware = [ 
        ...
+       'sentry.context' => \App\Http\Middleware\SentryContext::class
    ];  

Adding group on route

route/web.php
Route::middleware(['sentry.context'])->group(function () {
    Route::get('/', 'ToppageController@index')->name('home');
    // or page as you like
}); 

Artboard.png

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

Laradock + phpMyAdmin + Laravelの環境構築

Docker環境のMySQLは、コマンドで確認しないといけないため、初学者にとってとても大変。。
phpMyAdminを使用するとデータベース内がブラウザで確認、変更、削除ができるため重宝します。

phpMyAdminは、MAMPXAMPPだけしか利用できないかと思っていましたがDocker環境でも利用できるようです。
 
 
laradockを導入した基本フォルダ構成は、以下のようになります。

 laravel_projects    #laradockとlaravelプロジェクトをまとめるフォルダ
    ├── laradock      # laradockをインストール
    └── project       # laravelをインストール

 

早速、環境構築していきましょう!

今回は、以下のディレクトリで作成します。

 laravel-docker    
    ├── laradock   
    └── laravelDev

 
ワークディレクトリを作成します。

#まとめるフォルダを作成
$ mkdir laravel-docker

#作成したフォルダに移動
$ cd laravel-docker

#laradockをインストール
$ git clone https://github.com/LaraDock/laradock.git

 
laradockの設定に関わるファイルのひとつである、.envファイルを作成します。
laradockのenv-exampleファイルを.envに変更して作成します。

#laradockフォルダに移動
$ cd laradock

#env-exampleをコピーして 名前を.envへ変更
$ cp env-example .env

 
laradockフォルダをエディタで開いて.envファイルを編集します。

変更前.
APP_CODE_PATH_HOST=../
DATA_PATH_HOST=~/.laradock/data
COMPOSE_PROJECT_NAME=laradock
変更後.
APP_CODE_PATH_HOST=../laravelDev
DATA_PATH_HOST=./laradock/data
COMPOSE_PROJECT_NAME=laravel-docker

 APP_CODE_PATH_HOSTの変更 
 アプリケーションのディレクトリを指定します。 laravelをインストールするディレクトリです。

 DATA_PATH_HOSTの変更 
 デフォルトではデータベースなどがホームディレクトリへ保存されます。
 それをプロジェクトディレクトリ下へ変更します。

 COMPOSE_PROJECT_NAMEの変更 
 デフォルトで$ docker-compose upすると、laradock_***という名称のコンテナが起動します。
 上記工程で、プロジェクトのディレクトリは分けていますが、そのまま$ docker-compose upすると、
 既存プロジェクトのコンテナが(同じ名称のコンテナが)立ち上がってしまうことを避けるために
 COMPOSE_PROJECT_NAME をlaradockからlaravel-dockerへ変更します。
 
 
phpMyAdminを使うために続けてlaradock.envを編集します。

変更前.
### MYSQL #################################################

MYSQL_VERSION=latest
MYSQL_DATABASE=default
MYSQL_USER=default
MYSQL_PASSWORD=secret
MYSQL_PORT=3306
MYSQL_ROOT_PASSWORD=root
MYSQL_ENTRYPOINT_INITDB=./mysql/docker-entrypoint-initdb.d

### REDIS #################################################
変更後.
### MYSQL #################################################

MYSQL_VERSION= 5.7         #編集
MYSQL_DATABASE=laravelDev  #編集
MYSQL_USER=root            #編集
MYSQL_PASSWORD=root      #編集
MYSQL_PORT=3306
MYSQL_ROOT_PASSWORD=root #編集
MYSQL_ENTRYPOINT_INITDB=./mysql/docker-entrypoint-initdb.d

### REDIS #################################################

 

変更前.
### PHP MY ADMIN ##########################################

# Accepted values: mariadb - mysql

PMA_DB_ENGINE=mysql

# Credentials/Port:

PMA_USER=default
PMA_PASSWORD=secret
PMA_ROOT_PASSWORD=secret
PMA_PORT=8080

### MAILDEV ###############################################
変更後.
### PHP MY ADMIN ##########################################

# Accepted values: mariadb - mysql

PMA_DB_ENGINE=mysql

# Credentials/Port:

PMA_USER=root           #編集
PMA_PASSWORD=root       #編集
PMA_ROOT_PASSWORD=root  #編集
PMA_PORT=8080           #8080を使用していたら8081などに編集

### MAILDEV ###############################################

コンテナ作成、起動

docker-composeコマンドでnginx、mysql、workspace、phpmyadminの作成と起動をします。

$ docker-compose up -d nginx mysql workspace phpmyadmin

※エラーが出てもコマンドを2、3回繰り返すと解決することがあります。
 
起動しているか確認します。

#Stateが全部Upになっているか確認
$ docker-compose ps

 

Laravel プロジェクトの作成

作業は、workspace コンテナの中に入って行います。
デフォルトではコンテナの中に入ると root ユーザーになるが、composer を root ユーザーで実行しようとすると警告が表示されてしまうため、--user オプションを laradock で指定してログインする。

#workspace コンテナの中に入ります。
$ docker-compose exec --user=laradock workspace bash

#Laravel プロジェクトを作成
# ※APP_CODE_PATH_HOST=../laravelDevをしているためアプリ名は,ドット . です
/var/www$ composer create-project --prefer-dist laravel/laravel . "6.0"

ブラウザでhttp://localhostにアクセスしてLaravelのトップ画面が表示されれば成功です!

http://localhost:8080phpMyAdminにアクセスできることを確認します。
※PMA_PORTを8081に変更していればhttp://localhost:8081になりますのでご注意ください。

ログインは、.envで設定したユーザ名, パスワードを使います。
サーバ:mysql
ユーザ名:root
パスワード:root
ログインできれば成功です!

Laravel MySQL接続設定

laravelDevをエディタで開き.envを編集します。

変更前.
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=
変更後.
DB_CONNECTION=mysql
DB_HOST=mysql       #編集
DB_PORT=3306
DB_DATABASE=laravelDev #編集
DB_USERNAME=root    
DB_PASSWORD=root     #編集
/var/www$ php artisan migrate
Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated:  2014_10_12_000000_create_users_table
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated:  2014_10_12_100000_create_password_resets_table

phpMyAdminにアクセスして確認してみましょう。
 

ログイン、登録機能のUI追加

/var/www$ composer require laravel/ui
Your requirements could not be resolved to an installable set of packages.
・
・
・
Installation failed, reverting ./composer.json to its original ,content.

エラーが発生しましたので、バージョンをつけてみましょう。

/var/www$ composer require laravel/ui:2.*
Your requirements could not be resolved to an installable set of packages.
・
・
・
Installation failed, reverting ./composer.json to its original ,content.

バージョンを下げてみましょう。

/var/www$ composer require laravel/ui:1.*
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 1 install, 0 updates, 0 removals
  - Installing laravel/ui (v1.2.0): Downloading (100%)  
・
・
・

v1.2.0でインストールできました!

/var/www$ php artisan ui vue --auth
/var/www$ npm install && npm run dev
ERROR in ./resources/sass/app.scss

sassのコンパイルがうまくいかないようです。
npmで管理しているsass-loaderのパッケージのバージョン表示してみましょう。

/var/www$ npm info sass-loader versions

バージョンを下げてみましょう。

#先にアンインストールをします。
/var/www$ npm uninstall --save-dev sass-loader

#先ほど確認したバージョンの中から前よりバージョンが低いsass-loaderをインストール
/var/www$ npm install --save-dev sass-loader@7.1.0

#コンパイル
/var/www$ npm run dev

うまくいきました!

http://localhostにアクセスしてLaravelのトップ画面の右上にlogin registerが表示されれば成功です!

参考になったら幸いです。
 

参考にさせて頂いた記事
Laradockを使ってLaravelを動かすまで
https://qiita.com/ichimura/items/27d4a628ff22e2ba8b35
 
【初心者】Laradockで別のプロジェクト(コンテナ)を立ち上げる場合
https://qiita.com/tomokei5634/items/74c7734701fff8b99904
 

  • このエントリーをはてなブックマークに追加
  • 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勉強 その6 リクエストのライフサイクル

前回記事

Laravel勉強 その5 Laravelのデプロイ

目的

  • 勉強のため、Laravel 7.xの公式ドキュメントを読み解いていく。
  • 今回は、公式ドキュメントの「リクエストのライフサイクル」の章を扱う。

参考

Laravel 7.x リクエストのライフサイクル

イントロダクション

このドキュメントの目的は、どのようにLaravelが動作しているのか、ハイレベルな概念を理解してもらうためです。
フレームワークの概念をより良く知ってもらうことで、すべてに対し「不可解さ」が減り、より自信を持ってアプリケーションを開発してもらえます。
これらの言葉を今すぐ理解できなくても、気落ちしないでください。ただ何が行われているかを理解するための基本だけを理解してください。
ドキュメントの他のセクションで明確にされる内容を理解すれば、知識は育っていくことでしょう。

この辺は確かに気になっていたので楽しみ。

概論

始まり

アプリケーションに対するリクエストは、すべてpublic/index.phpファイルが入り口になります。
Webサーバ(Apache/Nginx)の設定により、すべてのリクエストをこのファイルへ渡しています。
index.phpファイルは、さほどコードを持っていません。フレームワークの残りをロードするための入り口にすぎません。

index.phpファイルは、Composerが生成したオートローダーの定義をロードします。
それから、bootstrap/app.phpスクリプトから、Laravelアプリケーションのインスタンスを取得します。
Laravel自身の最初のアクションは、アプリケーション/サービスコンテナのインスタンスを生成することです。

オートローダーとは、「ファイルを自動で読み込む仕組みのこと」らしい。
composerを利用してライブラリをインストールした際に作成される vendor/autoload.php がオートローダーの本体で、
このファイルだけを一度だけrequireしてあげれば、vendor以下のライブラリをすべてrequireしてくれるとのこと。
逆にこれがなければ、phpファイルの冒頭には大量のrequireが必要になってしまうと。

HTTP/Consoleカーネル

次にアプリケーション起動のきっかけにより、送信されてきたリクエストをHTTPカーネルかコンソールカーネルのどちらかに送ります。
これらのカーネルは、全リクエストフローの中心に位置し動作します。
ここではapp/Http/Kernel.phpにあるHTTPカーネルへ焦点を合わせます。

ちなみにコンソールカーネルは、Laravelで使用するコマンド(php artisan ***)のセットアップをしてくれるらしい。

HTTPカーネルはIlluminate\Foundation\Http\Kernelクラスを拡張しています。
このKernelクラスは、リクエストの実行前に処理されるbootstrappers(起動コード)の配列を定義してます。
これらの起動コードはエラー処理、ログ設定、アプリケーション動作環境の決定、そのほか実際にリクエストが処理される前に行う必要のあるタスクです。

HTTPカーネルはさらに、アプリケーションによりリクエストが処理される前に通す必要のある、HTTPミドルウェアのリストも定義しています。
これらのミドルウェアはHTTPセッションの読み書き、アプリケーションがメンテナンスモードであるかの決定、CSRFトークンの確認などを行います。

HTTPカーネルのhandleメソッドの使い方はとてもシンプルで、Requestを受け取り、Responseをリターンします。
カーネルをアプリケーション全体を表す大きなブラックボックスだと考えてください。HTTPリクエストを流し込み、HTTPレスポンスが返ってきます。

要は、リクエストが処理される前に以下の処理を行うと。
- 起動コード(エラー処理、ログ設定、動作環境設定など)の定義
- ミドルウェアの定義(セッションの読み書き、メンテナンスモード、CSRFトークンの確認)の定義

これらの前処理が終わったらリクエストを受け取り、色々な処理をしてレスポンスを返すと。

サービスプロバイダ

カーネル初期起動時の重要な処理の一つは、アプリケーションのサービスプロバイダをロードすることです。
アプリケーションの全サービスプロバイダは、config/app.phpファイルのproviders配列で設定されています。
最初に全プロバイダのregisterメソッドが呼び出され、その後に登録されている全プロバイダのbootメソッドが呼び出されます。

サービスプロバイダーは、もの(オブジェクト)をサービスコンテナにバインド(配置)し、サービスコンテナからものを解決(取得)する役目で、
サービスコンテナは、アプリケーションのブートストラッププロセスで開始されたすべてのものが配置されるkey => valueの場所とのこと。

サービスプロバイダはデータベース、キュー、バリデーション、ルーティングなど、フレームワークのさまざまなコンポーネントの初期起動処理に責任をもちます。
フレームワークにより提供されている全機能を初期化し、設定するものですから、サービスプロバイダはLaravelの初期処理の過程全体で一番重要な機能です。

なるほど。

リクエストのディスパッチ

アプリケーションの初期処理が済み、全サービスプロバイダが登録されたら、ディスパッチ(実行制御の移行)するためにルーターへRequestが手渡されます。
ルーターはそのリクエストをルートかコントローラにディスパッチし、その時にルートへ指定されているミドルウェアも実行されます。

ディスパッチが何なのかはあまりわからないが、とりあえず準備が終わったら実行に移る、ということだと思う。
調べてみたけれどもあまりわからず…

サービスプロバイダの精査

サービスプロバイダはLaravelアプリケーションの初期起動段階における、まさに鍵となるものです。
アプリケーションインスタンスが作成され、サービスプロバイダが登録され、初期起動を終えたアプリケーションでリクエストが処理されます。とてもシンプルです!

Laravelアプリケーションがどう構築されているか、そしてサービスプロバイダによる初期起動の仕組みをしっかりと把握することは、とても重要です。
皆さんのアプリケーションのためのデフォルトサービスプロバイダは、app/Providersディレクトリに設置されています。

AppServiceProviderはデフォルトで完全に空っぽです。
このプロバイダは皆さんのアプリケーション自身の初期起動や、サービスコンテナの結合を追加するために用意されています。
大きなアプリケーションであれば、まとまった初期起動種別ごとに分け、好きなだけサービスプロバイダを作成できます。

サービスプロバイダーの作成については、以下リンク先がわかりやすかった。
Laravelのサービスプロバイダーの仕組みやメリットとは
Laravel サービスプロバイダーついに理解
サービスプロバイダーについて
試しに作ってみようと思う。

作成前のapp/Providersはこんな感じ。

# ll app/Providers/
合計 20
-rw-rw-r-- 1 root root  578  5月 21 20:33 AuthServiceProvider.php
-rw-rw-r-- 1 root root  403  5月 21 20:33 AppServiceProvider.php
-rw-rw-r-- 1 root root  380  5月 21 20:33 BroadcastServiceProvider.php
-rw-rw-r-- 1 root root  710  5月 21 20:33 EventServiceProvider.php
-rw-rw-r-- 1 root root 1658  5月 21 20:33 RouteServiceProvider.php

リンク先の例に従って、サービスプロバイダーを作成する。
毎回、Dockerのコンテナ内に入って、というのを繰り返すのは面倒なので、コンテナを指定してコマンドを実行するようにする。

# docker exec -it e1394b974a68 php artisan make:provider AnimalServiceProvider /コンテナIDを指定
Provider created successfully.

成功したっぽい。
app/Providersを確認してみる。

# ll app/Providers/
合計 24
-rw-r--r-- 1 root root  374  6月 24 16:58 AnimalServiceProvider.php
-rw-rw-r-- 1 root root  403  5月 21 20:33 AppServiceProvider.php
-rw-rw-r-- 1 root root  578  5月 21 20:33 AuthServiceProvider.php
-rw-rw-r-- 1 root root  380  5月 21 20:33 BroadcastServiceProvider.php
-rw-rw-r-- 1 root root  710  5月 21 20:33 EventServiceProvider.php
-rw-rw-r-- 1 root root 1658  5月 21 20:33 RouteServiceProvider.php

ちゃんと作成されている。
中身はこんな感じ。

app/Providers/AnimalServiceProvider.php
<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class AnimalServiceProvider extends ServiceProvider
{
    /**
     * Register services.
     *
     * @return void
     */
    public function register()
    {
        //
    }

    /**
     * Bootstrap services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }
}

このファイルを、以下の様に編集する。

app/Providers/AnimalServiceProvider.php
<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Animals\Bird;
use App\Animals\Food;

class AnimalServiceProvider extends ServiceProvider
{
    /**
     * Register the Bird class instance to the container.
     * Food class will be auto injected
     */
    public function register() {
        $this->app->bind('bird', function($app){
            return new Bird($app->make(Food::class));
        });
    }
}

ポイントとしては(たぶん)こんなところ?
- use
関連する(必要な)クラスを登録しておく。
この例のサービスプロバイダーは
- アプリケーションがBirdクラスを使用できる様に、サービスコンテナに登録する
- BirdクラスはFoodクラスを必要とする

ため、Birdクラス・Foodクラスを登録する。

  • register
    bindメソッドを使用して、Birdクラスを'bird'というキーでサービスコンテナにバインドする。

  • boot
    デフォルトではbootメソッドがあるが、これはregisterメソッドによる初期処理が完了した後に実行されるもの。
    使用しないため削除する。

完成したサービスプロバイダーを、config/app.php に登録する。

config/app.php
(略)
/*
 * Application Service Providers...
 */
App\Providers\AppServiceProvider::class,
App\Providers\AuthServiceProvider::class,
// App\Providers\BroadcastServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
App\Providers\AnimalServiceProvider::class,        /ここを追加
(略)

Birdクラス、Foodクラスを作成する。

# mkdir app/Animals

# vi app/Animals/Bird.php
# vi app/Animals/Food.php
# cat app/Animals/Bird.php
<?php
namespace App\Animals;
use App\Animals\Food;

class Bird
{
    private $food = null;
    public function __construct(Food $food){
        $this->food = $food;
    }

    public function sound(){
        return "tweet tweet";
    }

    public function getFood(){
        return $this->food;
    }
}

# cat app/Animals/Food.php
<?php
namespace App\Animals;

class Food
{
    public function foodType(){
        return "warms";
    }
}

?>

コントローラーを作成する。

# docker exec -it e1394b974a68 php artisan make:controller AnimalController
Controller created successfully.

成功したっぽい。
app/Providersを確認してみる。

# ll app/Http/Controllers/
合計 8
-rw-r--r-- 1 root root 123  6月 29 15:36 AnimalController.php
-rw-rw-r-- 1 root root 361  5月 21 20:33 Controller.php

作成されている。
中身はこんな感じ。

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

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class AnimalController extends Controller
{
    //
}

このファイルを、以下の様に編集する。

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

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class AnimalController extends Controller
{
    public function index ()
    {
        $bird = app()->make('bird');
        $returnText = "Sound : ". $bird->sound();
        $returnText .= "<br>Food : ". $bird->getFood()->foodType();
        echo $returnText;
    }
}

routes/web.phpの末尾に以下を追加

./routes/web.php
Route::get('/animal', 'AnimalController@index');

これで http://<IP>/animal にリクエストすると、
Sound : tweet tweet
Food : warms
が表示された。

わかったようなわからないような感じだが、とりあえずちゃんと動いたので今回はここまで。

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

laravel+Vueプロダクトのデプロイサーバセットアップ

書いてあること

EC2上にlaravel+vueで開発したプロダクトの実行環境を構築する方法。
OSはAmazonLinux2です。

手順

1.EC2のセットアップ

初期ユーザやrootにパスワードかける。
「yum update -y」でパッケージアップデートする。
日本時間にして日本語に対応させる。

$ timedatectl set-timezone Asia/Tokyo
$ localectl set-locale LANG=ja_JP.UTF-8
$ localectl set-keymap jp106
$ date
Wed Apr 22 13:53:00 JST 2020

ホスト名設定

$ hostnamectl set-hostname host.example.com

ホスト名設定

$ vi /etc/sysconfig/network
NETWORKING=yes
NOZEROCONF=yes
+HOSTNAME=host.example.com

2.証明書取得

httpsで公開したいので。
rootユーザで

$ wget https://dl.eff.org/certbot-auto
$ chmod 700 certbot-auto
EC2用に書き換え
$ vi certbot-auto
elif [ -f /etc/issue ] && grep -iq "Amazon Linux" /etc/issue ; then
  Bootstrap() {
    ExperimentalBootstrap "Amazon Linux" BootstrapRpmCommon
  }
  BOOTSTRAP_VERSION="BootstrapRpmCommon $BOOTSTRAP_RPM_COMMON_VERSION"elif grep -i "Amazon Linux" /etc/issue > /dev/null 2>&1 || \
   grep 'cpe:.*:amazon_linux:2' /etc/os-release > /dev/null 2>&1; then
  Bootstrap() {
    ExperimentalBootstrap "Amazon Linux" BootstrapRpmCommon
  }
  BOOTSTRAP_VERSION="BootstrapRpmCommon $BOOTSTRAP_RPM_COMMON_VERSION"

コマンド移動

$ sudo mv ./certbot-auto /usr/local/bin

証明書取得

$ certbot-auto certonly --standalone -d csfhost.example.com --debug
/etc/letsencrypt/live/host.example.com/ 以下に証明書ができる。

3.nginxのインストール

rootユーザで

$ amazon-linux-extras install nginx1.12 -y

nginxの起動とインスタンス起動時自動起動の設定

$ sudo systemctl start nginx
$ sudo systemctl enable nginx
$ systemctl status nginx

4.phpのインストール

rootユーザで

$ amazon-linux-extras info php7.4
$ sudo amazon-linux-extras install php7.4 -y
$ php-fpm -v
PHP 7.4.x (fpm-fcgi) (built: Aug 14 2018 16:48:43)
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies
$ yum install -y php-mbstring.x86_64
$ yum install -y php-xml.x86_64
$ yum install php-gd.x86_64

5.nginxとphp-fpmの連携設定

[/etc/nginx/nginx.conf]
# For more information on configuration, see:
#   * Official English Documentation: http://nginx.org/en/docs/
#   * Official Russian Documentation: http://nginx.org/ru/docs/

user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;

# Load dynamic modules. See /usr/share/nginx/README.dynamic.
include /usr/share/nginx/modules/*.conf;

events {
  worker_connections 1024;
}

http {
  log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for"';

  access_log  /var/log/nginx/access.log  main;

  sendfile            on;
  tcp_nopush          on;
  tcp_nodelay         on;
  keepalive_timeout   65;
  types_hash_max_size 2048;

  include             /etc/nginx/mime.types;
  default_type        application/octet-stream;

  # Load modular configuration files from the /etc/nginx/conf.d directory.
  # See http://nginx.org/en/docs/ngx_core_module.html#include
  # for more information.
  #include /etc/nginx/conf.d/*.conf;
  index   index.php index.html index.htm;

  server {
    listen       80 default_server;
    listen       [::]:80 default_server;
    server_name  localhost;

    return 301   https://$host$request_uri;
  }

  server {
    listen       443 ssl http2 default_server;
    listen       [::]:443 ssl http2 default_server;
    server_name  localhost;

    ssl_certificate "/etc/letsencrypt/live/host.example.com/fullchain.pem";
    ssl_certificate_key "/etc/letsencrypt/live/host.example.com/privkey.pem";

    ssl_session_cache shared:SSL:1m;
    ssl_session_timeout  10m;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;

    root   /usr/share/nginx/html/xxx/public;
    index  index.php index.html index.htm;

    access_log  /var/log/nginx/xxx-access.log  main;
    error_log   /var/log/nginx/xxx-error.log  warn;

    location / {
      try_files $uri $uri/ /index.php?$query_string;
    }

    error_page 404 /404.html;
    location = /40x.html {
      root   /usr/share/nginx/html;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
      root   /usr/share/nginx/html;
    }

    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass   unix:/run/php-fpm/xxx.sock;
        fastcgi_index  index.php;
        fastcgi_param  SCRIPT_FILENAME  $document_root/index.php;
        fastcgi_param  HOSTNAME host.example.com;

        fastcgi_max_temp_file_size 0;
        fastcgi_buffer_size 4K;
        fastcgi_buffers 64 4k;

        include        fastcgi_params;
    }
  }
}

/etc/php-fpm.d/www.conf

/etc/php-fpm.d/xxx.conf にリネームして以下のように編集

[xxx.conf]
-; Start a new pool named 'www'.
+; Start a new pool named 'xxx'.
; the variable $pool can we used in any directive and will be replaced by the
-; pool name ('www' here)
-[www]
+; pool name ('xxx' here)
+[xxx]

; Per pool prefix
(略)
; Unix user/group of processes
; Note: The user is mandatory. If the group is not set, the default user's group
;       will be used.
; RPM: apache user chosen to provide access to the same directories as httpd
-user = apache
+user = nginx
; RPM: Keep a group allowed to write in log dir.
-group = apache
+group = nginx

; The address on which to accept FastCGI requests.
; Valid syntaxes are:
;   'ip.add.re.ss:port'    - to listen on a TCP socket to a specific IPv4 address on
;                            a specific port;
;   '[ip:6:addr:ess]:port' - to listen on a TCP socket to a specific IPv6 address on
;                            a specific port;
;   'port'                 - to listen on a TCP socket to all addresses
;                            (IPv6 and IPv4-mapped) on a specific port;
;   '/path/to/unix/socket' - to listen on a unix socket.
; Note: This value is mandatory.
-listen = /run/php-fpm/www.sock
+listen = /run/php-fpm/xxx.sock

; Set listen(2) backlog.
; Default Value: 511
;listen.backlog = 511
(略)
; these options, value is a comma separated list of user/group names.
; When set, listen.owner and listen.group are ignored
-listen.acl_users = apache
+listen.acl_users = apache,nginx
;listen.acl_groups =

; List of addresses (IPv4/IPv6) of FastCGI clients which are allowed to connect.
(略)
; The log file for slow requests
; Default Value: not set
; Note: slowlog is mandatory if request_slowlog_timeout is set
-slowlog = /var/log/php-fpm/www-slow.log
+slowlog = /var/log/php-fpm/xxx-slow.log

; The timeout for serving a single request after which a PHP backtrace will be
; dumped to the 'slowlog' file. A value of '0s' means 'off'.
(略)
; Default Value: nothing is defined by default except the values in php.ini and
;                specified at startup with the -d argument
;php_admin_value[sendmail_path] = /usr/sbin/sendmail -t -i -f www@my.domain.com
;php_flag[display_errors] = off
-php_admin_value[error_log] = /var/log/php-fpm/www-error.log
+php_admin_value[error_log] = /var/log/php-fpm/xxx-error.log
php_admin_flag[log_errors] = on
;php_admin_value[memory_limit] = 128M
(略)

php-fpm再起動

$ sudo systemctl restart php-fpm.service                            
nginx起動                         
$ sudo systemctl restart nginx                          

プロジェクトのドキュメントルートにphpinfoのファイルを作成して表示できるか確認

$ echo '<?php phpinfo(); ?>' > /usr/share/nginx/html/xxx/public/phpinfo.php

ブラウザでhttps://host.example.com/phpinfo.phpをたたいて確認

6.DBセットアップ

望むバージョンを入れるためリポジトリファイル新規作成

$ vi /etc/yum.repos.d/MariaDB.repo

以下のように定義

[mariadb]
name = MariaDB
baseurl = http://yum.mariadb.org/10.3/centos7-amd64
gpgkey=https://yum.mariadb.org/RPM-GPG-KEY-MariaDB
gpgcheck=1

インストール

$ yum install MariaDB-server MariaDB-client -y

起動

$ systemctl start mariadb

バージョン確認

$ mysql -V
mysql  Ver 15.1 Distrib 10.3.22-MariaDB, for Linux (x86_64) using readline 5.1

有効化

$ systemctl enable mariadb
$ systemctl is-enabled mariadb

セキュリティ設定 rootのパスワード等を設定します。

$ mysql_secure_installation
Set root password? [Y/n] Y
Remove anonymous users? [Y/n] Y
Disallow root login remotely? [Y/n] n
Remove test database and access to it? [Y/n] n
Reload privilege tables now? [Y/n] n
 ...
Thanks for using MariaDB!

rootのパスワードを設定しておく。
リモート接続できるように。

# mysql -u root -p
Enter password:
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MariaDB connection id is 520
Server version: 10.1.31-MariaDB MariaDB Server

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MariaDB [(none)]> GRANT ALL PRIVILEGES ON *.* TO root@"%" IDENTIFIED BY '[リモート接続パスワード]' WITH GRANT OPTION;
MariaDB [(none)]> FLUSH PRIVILEGES;

セキュリティを高めるためにポートを変えます。一旦停止してから設定に追記します。

$ systemctl stop mariadb
$ vi /etc/my.cnf.d/server.cnf

/etc/my.cnf.d/server.cnfの末尾に追記

port=3406

起動

$ systemctl start mariadb

7.DB構築実行

.envの接続設定のポート番号が合っているか確認する。
マイグレーション実行

$ php artisan migrate

シーダー実行

$ php artisan db:seed

10.ファイルパーミッション変更

以下4つのフォルダのパーミッションを777にする。

/usr/share/nginx/html/csf/storage/logs
/usr/share/nginx/html/csf/storage/framework/cache
/usr/share/nginx/html/csf/storage/framework/sessions
/usr/share/nginx/html/csf/storage/framework/views
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Laravel7でユーザー認証_9】マルチ認証機能を使って管理者を作成する

はじめに

Laravelのユーザー認証機能に、管理者権限を追加する(マルチ認証機能を使う)手順をまとめます。

環境

XAMPP環境でLaravelが使えるように設定してあります。

  • Windows10 Pro 64bit
  • PHP 7.3.18
  • Laravel 7.12.0
  • MariaDB 10.1.32

また、Laravelプロジェクトは以下の手順で作業を進めており、ユーザーは自身で情報を登録・変更・削除ができるようになっています。

実装手順

Adminモデルとadminsテーブルの作成

以下のコマンドで、Adminモデルとマイグレーションファイルを作成します。

php artisan make:model Admin -m

Adminモデルは、Userモデルの内容をコピーして使います。

app/Admin.php
<?php

namespace App;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Admin extends Authenticatable implements MustVerifyEmail
{
    use Notifiable;
    use SoftDeletes;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
            'name', 'email', 'username', 'password',
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
            'password', 'remember_token',
    ];

    /**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [
            'email_verified_at' => 'datetime',
    ];
}

マイグレーションファイルも、現在のusersテーブルの仕様に合わせます。

database/migrations/日付_create_admins_table.php
<?php

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

class CreateAdminsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('admins', function (Blueprint $table) {
            $table->id();
            $table->string('username', 32);
            $table->string('name');
            $table->string('email');
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();
            $table->softDeletes();
            $table->unique(['email', 'deleted_at'], 'admins_email_deleted_at_unique');
            $table->unique(['username', 'deleted_at'], 'admins_username_deleted_at_unique');
        });
    }

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

マイグレーションを実行すると、usersと同じ構造のadminsテーブルが出来上がります。

php artisan migrate

管理者用のガードとプロバイダを追加

認証の設定ファイル config/auth.php に、adminが使うガードとプロバイダを設定します。

config/auth.php
      'guards' => [
          'web' => [
              'driver' => 'session',
              'provider' => 'users',
          ],
+ 
+         'admin' => [
+             'driver' => 'session',
+             'provider' => 'admins',
+         ],

          'api' => [
              'driver' => 'token',
              'provider' => 'users',
              'hash' => false,
          ],
      ],

      /*
      |--------------------------------------------------------------------------
      | User Providers
      |--------------------------------------------------------------------------
      |
      | All authentication drivers have a user provider. This defines how the
      | users are actually retrieved out of your database or other storage
      | mechanisms used by this application to persist your user's data.
      |
      | If you have multiple user tables or models you may configure multiple
      | sources which represent each model / table. These sources may then
      | be assigned to any extra authentication guards you have defined.
      |
      | Supported: "database", "eloquent"
      |
      */

      'providers' => [
          'users' => [
              'driver' => 'eloquent',
              'model' => App\User::class,
          ],
+ 
+         'admins' => [
+             'driver' => 'eloquent',
+             'model' => App\Admin::class,
+         ],

          // 'users' => [
          //     'driver' => 'database',
          //     'table' => 'users',
          // ],
      ],

configファイル変更後は、キャッシュをクリアしておきます。

php artisan config:cache

ADMIN_HOME の定義

管理者用のhomeとして、ADMIN_HOMEを定義します。

app/Providers/RouteServiceProvider.php
      /**
       * The path to the "home" route for your application.
       *
       * @var string
       */
      public const HOME = '/home';
+     public const ADMIN_HOME = '/admin/home';

リダイレクト処理

未ログイン時のリダイレクト

管理者としてログインしていない閲覧者が管理者権限が必要なページにアクセスした場合、ログイン画面にリダイレクトさせる設定をします。

app/Http/Middleware/Authenticate.php
+ use Illuminate\Support\Facades\Route;

  class Authenticate extends Middleware
  {
      /**
       * Get the path the user should be redirected to when they are not authenticated.
       *
       * @param  \Illuminate\Http\Request  $request
       * @return string|null
       */
      protected function redirectTo($request)
      {
          if (! $request->expectsJson()) {
-             return route('login');
+             if (Route::is('admin.*')) {
+                 return route('admin.login');
+             } else {
+                 return route('login');
+             }
          }
      }

管理者ログイン直後のリダイレクト

管理者としてログインした際、ADMIN_HOMEにリダイレクトさせるよう設定します。

app/Http/Middleware/RedirectIfAuthenticated.php
      /**
       * Handle an incoming request.
       *
       * @param  \Illuminate\Http\Request  $request
       * @param  \Closure  $next
       * @param  string|null  $guard
       * @return mixed
       */
      public function handle($request, Closure $next, $guard = null)
      {
          if (Auth::guard($guard)->check()) {
-             return redirect(RouteServiceProvider::HOME);
+             if ($guard === 'user') {
+                 return redirect(RouteServiceProvider::HOME);
+             } elseif ( $guard === 'admin') {
+                 return redirect(RouteServiceProvider::ADMIN_HOME);
+             }
          }

          return $next($request);
      }

コントローラ作成

ユーザー用のコントローラをコピーして、管理者用の HomeControllerLoginController を作成します。

コピー元:app/Http/Controllers/HomeController.php
コピー先:app/Http/Controllers/Admin/HomeController.php

app/Http/Controllers/Admin/HomeController.php
  <?php

- namespace App\Http\Controllers;
+ namespace App\Http\Controllers\Admin;

  use Illuminate\Http\Request;

  class HomeController.php extends Controller
  {
      /**
       * Create a new controller instance.
       *
       * @return void
       */
      public function __construct()
      {
-         $this->middleware('auth');
+         $this->middleware('auth:admin');
      }

      /**
       * Show the application dashboard.
       *
       * @return \Illuminate\Contracts\Support\Renderable
       */
      public function index()
      {
-         return view('home');
+         return view('admin.home');
      }
  }

コピー元:app/Http/Controllers/Auth/LoginController.php
コピー先:app/Http/Controllers/Admin/Auth/LoginController.php

app/Http/Controllers/Admin/Auth/LoginController.php
  <?php

- namespace App\Http\Controllers\Auth;
+ namespace App\Http\Controllers\Admin\Auth;

  use App\Http\Controllers\Controller;
  use App\Providers\RouteServiceProvider;
  use Illuminate\Foundation\Auth\AuthenticatesUsers;

  class LoginController extends Controller
  {
      /*
      |--------------------------------------------------------------------------
      | Login Controller
      |--------------------------------------------------------------------------
      |
      | This controller handles authenticating users for the application and
      | redirecting them to your home screen. The controller uses a trait
      | to conveniently provide its functionality to your applications.
      |
      */

      use AuthenticatesUsers;

      /**
       * Where to redirect users after login.
       *
       * @var string
       */
-     protected $redirectTo = RouteServiceProvider::HOME;
+     protected $redirectTo = RouteServiceProvider::ADMIN_HOME;

      /**
       * Create a new controller instance.
       *
       * @return void
       */
      public function __construct()
      {
-         $this->middleware('guest')->except('logout');
+         $this->middleware('guest:admin')->except('logout');
      }
+
+     protected function guard()
+     {
+         return Auth::guard('admin');
+     }
+
+     public function showLoginForm()
+     {
+         return view('admin.auth.login');
+     }

      public function username()
      {
          return 'username';
      }
  }

ルーティングの設定

  • App\Http\Controllers\Admin 名前空間下のコントローラを読み込ませたいので、namespace メソッドで Admin を指定
  • 管理者用のURIは、admin/ でまとめたいので、prefix メソッドで admin を指定
  • ルート名もまとめてadmin.というプリフィックスをつけたいので、nameメソッドで admin. を指定
routes/web.php
+ Route::namespace('Admin')->prefix('admin')->name('admin.')->group(function() {
+     Route::get('home', 'HomeController@index')->name('home');
+     Route::get('login', 'Auth\LoginController@showLoginForm')->name('login');
+     Route::post('login', 'Auth\LoginController@login');
+     Route::post('logout', 'Auth\LoginController@logout')->name('logout');
+ }

管理者用のviewを作成

管理者用とユーザー用の画面がわかりやすいように、管理者はダークモードにしたいと思います。
viewの構成は、以下のようにしました。

views/
  ├ admin/
  │  ├ auth/
  │  │  └ login.blade.php  #resources/views/auth/login.blade.phpをコピー
  │  └ home.blade.php  #resources/views/home.blade.phpをコピー
  │
  └ layouts/
     └ admin/
       └ app.blade.php  #resources/views/layouts/app.blade.phpをコピー

@guest で判定していた箇所は、 @if (Auth::guard('admin')->check()) で、管理者かどうかのチェックをするように変更しました。

resources/views/layouts/admin/app.blade.php
<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- CSRF Token -->
    <meta name="csrf-token" content="{{ csrf_token() }}">

    <title>{{ config('app.name', 'Laravel') }}</title>

    <!-- Scripts -->
    <script src="{{ asset('js/app.js') }}" defer></script>

    <!-- Fonts -->
    <link rel="dns-prefetch" href="//fonts.gstatic.com">
    <link href="https://fonts.googleapis.com/css?family=Nunito" rel="stylesheet">

    <!-- Styles -->
    <link href="{{ asset('css/app.css') }}" rel="stylesheet">
</head>
<body>
    <div id="app">
        <nav class="navbar navbar-expand-md navbar-dark bg-dark shadow-sm">
            <div class="container">
                <a class="navbar-brand" href="{{ url('/') }}">
                    {{ config('app.name', 'Laravel') }}
                </a>
                <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="{{ __('Toggle navigation') }}">
                    <span class="navbar-toggler-icon"></span>
                </button>

                <div class="collapse navbar-collapse" id="navbarSupportedContent">
                    <!-- Left Side Of Navbar -->
                    <ul class="navbar-nav mr-auto">

                    </ul>

                    <!-- Right Side Of Navbar -->
                    <ul class="navbar-nav ml-auto">
                        <!-- Authentication Links -->
                        @if (Auth::guard('admin')->check())
                            <li class="nav-item dropdown">
                                <a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" v-pre>
                                    {{ Auth::user()->name }} <span class="caret"></span>
                                </a>

                                <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
                                    <a class="dropdown-item" href="{{ route('admin.logout') }}"
                                       onclick="event.preventDefault();
                                                     document.getElementById('logout-form').submit();">
                                        {{ __('Logout') }}
                                    </a>

                                    <form id="logout-form" action="{{ route('admin.logout') }}" method="POST" style="display: none;">
                                        @csrf
                                    </form>

                                </div>
                            </li>
                        @else
                            <li class="nav-item">
                                <a class="nav-link" href="{{ route('admin.login') }}">{{ __('Login') }}</a>
                            </li>
                        @endif
                    </ul>
                </div>
            </div>
        </nav>

        <main class="py-4">
            @yield('content')
        </main>
    </div>
</body>
</html>

▼ヘッダ部分について、作成した layout/admin/app.blade.php を読み込むようにします。

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

@section('content')
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <div class="card">
                <div class="card-header">Dashboard</div>

                <div class="card-body">
                    @if (session('status'))
                        <div class="alert alert-success" role="alert">
                            {{ session('status') }}
                        </div>
                    @endif

                    You are logged in!
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

▼ヘッダ部分について、作成した layout/admin/app.blade.php を読み込むようにして、formのaction先もadminのloginとなるようにルートを指定します。

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

@section('content')
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <div class="card">
                <div class="card-header">{{ __('Admin') }}{{ __('Login') }}</div>

                <div class="card-body">
                    <form method="POST" action="{{ route('admin.login') }}">
                        @csrf

                        <div class="form-group row">
                            <label for="username" class="col-md-4 col-form-label text-md-right">{{ __('UserName') }}</label>

                            <div class="col-md-6">
                                <input id="username" type="username" class="form-control @error('username') is-invalid @enderror" name="username" value="{{ old('username') }}" required autocomplete="username" autofocus>

                                @error('username')
                                    <span class="invalid-feedback" role="alert">
                                        <strong>{{ $message }}</strong>
                                    </span>
                                @enderror
                            </div>
                        </div>

                        <div class="form-group row">
                            <label for="password" class="col-md-4 col-form-label text-md-right">{{ __('Password') }}</label>

                            <div class="col-md-6">
                                <input id="password" type="password" class="form-control @error('password') is-invalid @enderror" name="password" required autocomplete="current-password">

                                @error('password')
                                    <span class="invalid-feedback" role="alert">
                                        <strong>{{ $message }}</strong>
                                    </span>
                                @enderror
                            </div>
                        </div>

                        <div class="form-group row">
                            <div class="col-md-6 offset-md-4">
                                <div class="form-check">
                                    <input class="form-check-input" type="checkbox" name="remember" id="remember" {{ old('remember') ? 'checked' : '' }}>

                                    <label class="form-check-label" for="remember">
                                        {{ __('Remember Me') }}
                                    </label>
                                </div>
                            </div>
                        </div>

                        <div class="form-group row mb-0">
                            <div class="col-md-8 offset-md-4">
                                <button type="submit" class="btn btn-primary">
                                    {{ __('Admin') }}{{ __('Login') }}
                                </button>
                            </div>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

翻訳ファイルに追記

view内で新たにAdminという翻訳文字列を使ったので、翻訳ファイルに追加します。

resources/lang/ja.json
+     "Admin": "管理者"
  }

管理者情報の登録

http://127.0.0.1:8000/admin/login にアクセスすると、管理者ログインフォームが表示されるまではできあがりました。
ですが、管理者の登録画面を作っていないので、このままだとログインも登録もできない状態です。
まずはログインができるかどうかを試したいので、データベースに管理者のデータを直接入れ込んでしまって確認します。

SQLで直接データを入れてしまってもいいのですが、せっかくなのでLaravelのシーダー機能を使ってデータを入れてみます。

シーダーファイルの作成

AdminsTableSeeder というシーダーファイルを作成します。

php artisan make:seeder AdminsTableSeeder

adminsテーブルに入っていてほしいデータを指定します。

項目 カラム名
氏名 name 管理者
ユーザー名 username admin
メールアドレス email admin@example.com
パスワード password secret
database/seeds/AdminsTableSeeder.php
<?php

use Illuminate\Database\Seeder;

class AdminsTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
      DB::table('admins')->delete();

      DB::table('admins')->insert([
             [
                  'name'      => '管理者',
                  'username'  => 'admin',
                  'email'     => 'admin@example.com',
                  'email_verified_at' => date('Y-m-d H:i:s'),

                  'password' => Hash::make('secret'),

                  'created_at' => date('Y-m-d H:i:s'),
                  'updated_at' => date('Y-m-d H:i:s') 
               ]
          ]);
    }
}
database/seeds/DatabaseSeeder.php
      public function run()
      {
-         //
+         $this->call(AdminsTableSeeder::class);
      }

シーダーを実行して、データが入ったかどうか確認します。

php artisan db:seed

動作確認

設定した情報でログインができるかどうか確認します。

▼私はこんなエラーと戦いました。

フォーム表示→ Auth guard [admin] is not defined.
config/app.php を変更した後、php artisan config:cache をしていませんでした。

ログイン→ Class 'App\Http\Controllers\Admin\Auth\Auth' not found

app/Http/Controllers/Admin/Auth/LoginController.php に、use Illuminate\Support\Facades\Auth; が抜けていました。

ログイン→ Class 'App\Http\Controllers\Admin\Controller' not found

app/Http/Controllers/Admin/HomeController.php に、use App\Http\Controllers\Controller; が抜けていました。

未ログイン状態で /admin/home にアクセス→ Class 'App\Http\Middleware\Route' not found

app/Http/Controllers/Middleware/Authenticate.phpuse Illuminate\Support\Facades\Route; が抜けていました。
リダイレクト分岐判定のRouteの前にバックスラッシュを置いてグローバルクラスにすれば( if (\Route::is('admin.*')) { )useを使わなくてもよいようです。

おわりに

管理者のログイン、ホームの表示ができるようになりました。
機能としてはまだ不十分なので、次回は、管理者のパスワード変更やリセットができるように修正したいと思います。

参考サイト

  • このエントリーをはてなブックマークに追加
  • 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+vueプロダクトのローカル開発環境

書いてあること

Laravelでバックエンド、Vueでフロントエンドの構成のプロダクトをWindowsクライアントで開発する際のクライアントのローカル環境の構築方法。

手順

1.「Visual Studio Code(VS Code)」のインストール

公式サイトからダウンロードしてインストール

2.「VS Code」拡張機能のインストール

「VS Code」を起動する。
アクティビティバーの「Extensions(拡張)」アイコンをクリック。
image.png

以下の検索ボックスを選択。
image.png

以下の拡張機能名を入力し、拡張機能をインストールしていく。

拡張機能名 説明
.ejs EJSファイル(HTMLのテンプレートファイル)に対して構文をハイライトしてくれる拡張機能。
Bracket Pair Colorizer 対となるカッコ色付けして見やすくしてくれる拡張機能。
ESLint 構文チェックするために必要な拡張機能。
Japanese Language Pack for Visual Studio Code 日本語化するために必要な拡張機能。
php cs fixer PHPのコード整形するために必要な拡張機能。「PHP CS Fixer」や「PHP-CS-Fixer」など、似たような名前の拡張機能が存在するため、間違えないようにインストールしてください。
PHP Intelephense PHPのコード補完をしてくれる拡張機能。
Vetur 「.Vue」ファイルを扱うために必要な拡張機能。
GitLens Gitで管理しているファイルの変更点をショートカットキー等で開ける拡張機能。
Debugger for Chrome VSCodeでフロントサイドのデバッグするための拡張機能。

インストールを実行するには以下の「Install」ボタンを実行する。
image.png

3.「Node.js」のインストール

公式サイトからダウンロードしてインストール。

4.phpインストール

公式サイトから「php-7.3.17-Win32-VC15-x64.zip」をダウンロードし解凍して任意の場所に配置。
配置したフォルダを環境変数PATHに追加
配置したフォルダに入り”php.ini-development”を”php.ini"でコピー。
php.iniを開き以下編集

[php.ini]
-;extension_dir = "ext"
-;extension=fileinfo
-;extension=mbstring
-;extension=gd2
+extension_dir = "ext"
+extension=fileinfo
+extension=mbstring
+extension=gd2

・・・

+extension=php_openssl
+extension=php_pdo_mysql

5.「Composer(コンポーザー)」のインストール

公式サイトからダウンロードしてインストール。
Developer modeはチェック入れる
phpの場所は手順4の配置フォルダ中のphp.exeを指定
update this php.iniはチェック外してスキップ
proxyは未設定のままスキップ

6.「SourceTree」のインストール

公式サイトから「SourceTreeSetup-2.6.10.exe」をダウンロードしてインストール。
求められるアカウント認証は作るかグーグルアカウントがあればそれでできます。

7.「SourceTree」を使ってソースの取得

以下の「Clone」アイコンをクリックする。
image.png

以下のような画面が表示される。
image.png

以下のとおり設定し、「クローン」ボタンを実行する。

《設定内容》 《設定値》
元のパス/URL https://develop.fan-technology.com/gitlab/InHouseDev/xxx.git
保存先のパス {SourceTreeのワークパス}{リポジトリ名}
名前 リポジトリ名
Local Folder [ルート]
詳細オプション ※デフォルトのまま

「Git Flow」メニューを実行。
image.png

デフォルト設定のまま「OK」ボタンを実行。
image.png

「develop」のブランチが作成されたことを確認する。
image.png

8.ライブラリで利用するモジュールをインストール

「Node.js」で使用している拡張機能の物理ファイル群(「node_modules」の中身)をインストールする作業です。
「laravel」で使用している拡張機能の物理ファイル群(「vendor」の中身)をインストールする作業です。

「コマンドプロンプト」を起動します。
以下のコマンドを入力して実行(Enterキー押下)し、「xxx」のソースがあるフォルダにカレントフォルダを設定します。

cd C:\Work\SourceTree\xxx

以下のコマンドを入力して実行(Enterキー押下)します。

npm install

初回はネットから落としてくるファイルが多いので10分ほどかかる場合があります。
つづいて以下のコマンドを入力して実行(Enterキー押下)します。

composer install

初回はネットから落としてくるファイルが多いので10分ほどかかる場合があります。

9.DBセットアップ

resourceフォルダのmariadb-10.4.10-winx64.msiでデフォルト設定にて。

rootユーザのパスワードは"root"で。

※「HeidiSQL」というソフトが入ってしまうようですが、使わないので無視してください。

10.DBマイグレーション

リポジトリ名のフォルダにコマンドプロンプトで入り以下実行

php artisan migrate 

11.初期データ投入

php artisan db:seed 

※「class not found」などのエラーが出る場合は「composer dump-autoload」のコマンドを実行して再度試してください。

12.実行

コマンドプロンプトを立ち上げてxxxのフォルダに移動

php artsan serve    

コマンドプロンプトを立ち上げてxxxのフォルダに移動

npm run watch

※ビルドが終わると自動でブラウザでサイトを開いてくれます。
http://localhost:3000/

また、browserSyncというホットリロードの仕組みを取り入れていますので、ソース系のファイル変更を検知してリビルドしてブラウザリロードしてくれます。

もしリロードしてほしくない場合は以下のURLでアクセスしてください。
http://localhost:8000/

13.「Postman」のインストール

「Postman」はAPIの動作確認を行う際に有用なツール。
GUIで動くcurl。
公式サイトからダウンロードしてインストール。

  • このエントリーをはてなブックマークに追加
  • 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になっています。

"itemImages/kmWqHDsESJtX1iTrvDbRRSsbIJOlOABHAuKfFY7X.jpeg"

ここまでで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で続きを読む

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で続きを読む

scaffold laravel php エラーがでる件

composer require laralib/l5scaffold --dev

を行うと、could not findなどいろんなエラーがでてくる。さまざまな解決サイトがあり試したが、変わらず。
下記サイトで解決。
https://awesome-linus.com/2019/05/17/laravel-l5scaffold-install-how-to-use/

もともとapp.phpでproviderに入れろと言われていたが、

// 開発環境でのみ必要なプロバイダー
    'dev-providers' => [
        Laralib\L5scaffold\GeneratorsServiceProvider::class,
    ],

という形でいれる。理由は上記サイトを参照。

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

proc_open(): fork failed errors#

タイトルの通りメモリーがないと怒られてしまった。

https://getcomposer.org/doc/articles/troubleshooting.md#proc-open-fork-failed-errors

sudo /bin/dd if=/dev/zero of=/var/swap.1 bs=1M count=1024
sudo /sbin/mkswap /var/swap.1
sudo /sbin/swapon /var/swap.1

で解決

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