20201206のlaravelに関する記事は15件です。

laravelでゲストログイン機能を作成

今やポートフォリオには必須の機能とされているゲストログイン機能を作成しました

環境
PHP 7.3.11
Laravel Framework 7.29.3

今回はLoginControllerをいじっていきます

LoginController.php
//省略//

public function guestLogin() {
        $name = 'ゲスト';
        $password = 'guestpass';

        if(Auth::attempt(['name' => $name, 'password' => $password])) {
            return redirect('/home');
        }

        return redirect('/');
    }

Auth::attemptは引数に指定したレコードがDB内にあればtrue,そうでなければfalseを返す

今回は予めゲストログイン用のユーザーアカウントを作成しておき名前とパスワードをname,passwordに代入、
Auth::attemptメソッドの引数に指定すればもちろんtrueが帰ってくるので認証が成功し
/homeにリダイレクトされるといった感じです。

web.php
Route::get('/login/guest', 'Auth\LoginController@guestLogin');

/login/guestを踏めばguestLoginメソッドが実行されます

guestLoginComponent.vue
<template>
  <div>
    <a class="nav-link" @click="openModal">ゲストログイン</a>
    <div class="overlay" @click="closeModal" v-if="showContent">
      <div class="dialog" @click="stopEvent">
        <p class="borderbottom pt-2 pb-4">ゲストユーザーとしてこのサイトをお試しになれます</p>
        <p class="text-right pr-2">
          <a @click="closeModal" class="mr-4 black">キャンセル</a>
          <a href="/login/guest" class="ml-auto">ログイン</a>
        </p>
      </div>
    </div>
  </div>
</template>

<script>
   export default {
     data() {
       return {
         showContent: false
       }
     },

     methods:{
      stopEvent(){
        event.stopPropagation()
      },
      openModal(){
        this.showContent = true
      },
      closeModal(){
        this.showContent = false
      }
    }
  }
</script>


今回はゲストログインボタンを押したらダイアログが表示され「ログイン」をクリックしたらメソッドが発火するというデザインにしてみました。

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

【初心者向け】20分でLaravel開発環境を爆速構築するDockerハンズオン

概要

docker(docker-compose)でLEMP環境(PHP/nginx/MySQL)を構築し、Laravelの新規プロジェクト作成、構築した環境を破棄してから環境の再構築までをハンズオン形式で行います。

ご注意

記事を寄稿したタイミングでは、補足の解説は特になくコマンドだけのご紹介だったので、コマンド部分のみコピペすれば20分で終わる内容でした。
しかし、継ぎ足しで解説を追記していった結果、普通に読みながら進めていくと2時間ぐらいのボリュームになってしまってます?

ご了承下さい?‍♂️

更新履歴

  • 2020/09/09 Laravel8(当時はLaravel6が出た頃)がリリースされ、こちらの記事も内容が古くなったので全て書き直しました。

目的

Dockerの細かい概念などはすっ飛ばして、
このハンズオンの通りに進めればDocker×Laravelの環境を構築できます。
習うより慣れろで行ってみましょう!

ハンズオン終了時のGitHub

https://github.com/ucan-lab/docker-laravel-handson

ハンズオンイベント

【ぺちオブ】ゆーきゃんプレゼンツ!Laravel + Docker 環境構築ハンズオン

https://phper-oop.connpass.com/event/137152

あの有名なぺちオブのイベントとしてハンズオン講師をやらせていただきました!
ありがとうございます?

レビュワー

スペシャルサンクス?‍♂️?

前提条件

  • Mac基準ですが、Windowsでも動作することを確認済み
  • コマンドは [環境] $ とprefixを付けてます。
  • 所要時間は事前準備部分を除いて20分を目安としてます。
  • エディタはファイル編集時にお好みのもので開いて作業ください?

構築環境(Dockerイメージ、各種バージョン)

  • php: 7.4-fpm-buster
  • composer: 1.10
  • nginx: 1.18-alpine
  • mysql: 8.0
  • Laravel: 8.x

dockerおすすめ書籍

冒頭で習うより慣れろと言いましたが、習っておくのもそれはそれで良いことなのでオススメの書籍や記事をご紹介します。私はこの辺りの記事や本を読んで勉強したのでオススメしてます。

マンガでわかるdockerシリーズ

@llminatoll さんがDockerについてマンガで分かりやすく説明されてます。
初めての方はこちらのマンガでDockerの概念を学んでから進めると理解しやすいです。

dockerおすすめ記事・サイト

公式系

記事

スライド

事前準備

Windowsユーザー

Git Bashのインストールをお願いします。(Linuxコマンドをそのまま打てるようになります)

Git Bash を起動した際は下記のコマンドを実行ください。

$ exec winpty bash

GitHubアカウントの作成

Gitの初期設定

user.emailuser.name が設定されていればok!
Gitの初期設定を行ってない場合、下記の記事を参考に初期設定をお願いします。

Mac Git 初期設定

最終的に下記のように表示されればokです。

$ git config --list | grep user
user.email=xxx@yyy.com
user.name=zzz

GitHub SSH接続設定

$ ssh -T github.com
Warning: Permanently added 'github.com,52.69.186.44' (RSA) to the list of known hosts.
Hi ucan-lab! You've successfully authenticated, but GitHub does not provide shell access.

successfully authenticated の文字が出ればok!
Warningは気にしなくてok!

GitHub SSH接続設定を行ってない場合、下記の記事を参考に初期設定をお願いします。
もしくはハンズオンをHTTPSに置き換えて進めてください。

Mac GitHub SSH接続設定

docker, docker-compose のインストール

[Mac] Docker for Macをインストール

[Windows] Docker for Windowsをインストール

イントール確認

[mac] $ docker --version
Docker version 19.03.12, build 48a66213fe

[mac] $ docker-compose --version
docker-compose version 1.26.2, build eefe0d31

Docker起動確認

インストールしたDockerを起動してください。
running 状態になっていればokです。

スクリーンショット 2019-09-29 1.39.55.png


ここまでで事前準備終了です。


今回のハンズオンのゴール

  • 3層アーキテクチャのコンテナの構築
    • ウェブサーバー(web)
      • nginxで静的コンテンツ配信サーバを構築
    • アプリケーションサーバー(app)
      • nginxを経由してPHPを動作させるアプリケーションサーバを構築
      • PHPパッケージ管理ツールComposerのインストール
    • データベースサーバー(db)
      • MySQLデータベースサーバーの構築
  • Laravelをインストールしてwelcome画面の表示
  • LaravelとMySQLを連携し、マイグレーションを実行
  • Docker環境の破棄
  • Docker環境をGitHubから再構築

?【初心者向け】20分でLaravel開発環境を爆速構築するDockerハンズオン?

  • 所要時間: 20分目安

作業ディレクトリを作成

[mac] $ mkdir docker-laravel-handson
[mac] $ cd docker-laravel-handson

最終的なディレクトリ構成

.
├── README.md (この名前にするとGitHubで見た時にHTMLに変換して表示してくれる)
├── infra (*1)
│   ├── mysql (*1)
│   │   ├── Dockerfile
│   │   └── my.cnf (*1)
│   ├── nginx (*1)
│   │   └── default.conf (*1)
│   └── php (*1)
│       ├── Dockerfile (この名前にするとファイル名の指定を省略できる)
│       └── php.ini (*1)
├── docker-compose.yml (この名前にするとファイル名の指定を省略できる)
└── backend (*1)
    └── Laravelをインストールするディレクトリ

このディレクトリ構成を目指します。
(*1) 任意の名前に変更してもokです。

リモートリポジトリを作成

https://github.com/new

GitHub Create remote repository
GitHub docker-laravel-handson

リポジトリ名 docker-laravel-handson のGitHubリモートリポジトリを作成する。

リモートリポジトリへ初回コミット&プッシュ

[mac] $ echo "# docker-laravel-handson" >> README.md
[mac] $ git init
[mac] $ git add README.md
[mac] $ git commit -m "first commit"
[mac] $ git branch -M main

※ 2020/10/1 からデフォルトブランチがmasterからmainに変更されました。

リモートリポジトリを登録します。
リモートリポジトリ先は適宜置き換えてください。

[mac] $ git remote add origin git@github.com:ucan-lab/docker-laravel-handson.git

リモートリポジトリ先が正しく設定されていることを確認してください。

[mac] $ git remote -v
origin  git@github.com:ucan-lab/docker-laravel-handson.git (fetch)
origin  git@github.com:ucan-lab/docker-laravel-handson.git (push)

# もしリモートリポジトリ先を間違えた場合は下記のコマンドから変更できます。
[mac] $ git remote set-url origin <リモートリポジトリ>

リモートリポジトリ先の名前を origin と付けられていること。
リモートリポジトリ先のURL等に間違いがないことを確認する。

GitHubへpushします。

[mac] $ git push -u origin main
  • -u オプションは --set-upstream の省略
    • ローカルリポジトリの現在のブランチ(main)をリモートリポジトリ(origin)のmainにpush先を設定
    • 以降は git push だけで git push origin main と同義

GitHubのリモートリポジトリのmainブランチへpushされていることを確認します。

お好みのGUIエディタで開く

ymlファイルをCUIエディタで書いていくのは大変なのでお好みのGUIエディタ(今回はVSCode)で開きます。
code コマンドがインストールされていれば、簡単にプロジェクトをVSCodeで開けます。

[mac] $ code .

ScreenShot 2020-10-15 2.34.39.png

また、 control + shift + @ で統合ターミナルをVSCodeで開くことができます。
ファイルの編集もコマンドの実行もVSCodeだけで行えます。

ScreenShot 2020-10-15 2.36.26.png

アプリケーションサーバ(app)コンテナを作る

PHPアプリケーションサーバコンテナを作成します。
公式のPHP-FPMイメージをベースイメージとしてカスタマイズします。

ディレクトリ構成

.
├── infra
│   └── php
│       ├── Dockerfile
│       └── php.ini # PHPの設定ファイル
├── backend # Laravelをインストールするディレクトリ
└── docker-compose.yml

docker-compose.yml を作成する

touch コマンドで空ファイルを作成してますが、作成後はお好みのエディタで編集ください。
ファイル名のタイプミス防止のため空ファイルだけ作成してます。
当ハンズオンはこの流れで進めていきます。

[mac] $ touch docker-compose.yml

作成した docker-compose.yml を下記の通りに編集します。

docker-compose.yml
version: "3.8"
services:
  app:
    build: ./infra/php
    volumes:
      - ./backend:/work
  • docker-compose.yml ファイルはインデント(半角スペース)が意味を持ちます。注意してコピペしてください。

docker-compose.yml: 設定値の補足

docker-compose.yml
version: "3.8"

Docker Composeファイルのバージョンを指定しています。

https://matsuand.github.io/docs.docker.jp.onthefly/compose/compose-file/compose-versioning/

通常はメジャー番号とマイナー番号を両方指定します。
ちなみにマイナーバージョンを指定しない場合はデフォルト0が使用されます。

docker-compose.yml
# 下記は同じ指定になる
version: "3"
version: "3.0"
docker-compose.yml
services:
  app: # => サービス名は任意
    build: ./infra/php
    volumes:
      - ./backend:/work

サービス名に app (アプリケーションサーバー)の名前を付けて定義しています。
サービス名は任意に決められます。

build: で指定しているのはビルドコンテキストを指定します。
ビルドコンテキストとは、docker buildを実行する際の現在の作業ディレクトリのことをビルドコンテキスト(build context)と呼びます。

Dockerfile が置かれている ./infra/php ディレクトリをビルドコンテキストとして指定します。
Dockerビルドの際は Dockerfile のファイルを探すので、ファイル名の指定は不要です。

volumes: ではホスト側のディレクトリや名前付きボリュームをコンテナ側へマウントしたい時に指定します。
今回はホスト側の ./backend ディレクトリをappサービスのコンテナ内 /work へマウントしてます。

./docker/php/Dockerfile を作成する

  • Composerコマンドのインストール
  • Laravelで必要なPHP拡張機能のインストール
    • bcmath, pdo_mysql が不足しているので追加インストール
[mac] $ mkdir -p infra/php
[mac] $ touch infra/php/Dockerfile

下記のコードを丸ごとコピーして Dockerfile へ貼り付けてください。

infra/php/Dockerfile
FROM php:7.4-fpm-buster
SHELL ["/bin/bash", "-oeux", "pipefail", "-c"]

ENV COMPOSER_ALLOW_SUPERUSER=1 \
  COMPOSER_HOME=/composer

COPY --from=composer:1.10 /usr/bin/composer /usr/bin/composer

RUN apt-get update && \
  apt-get -y install git unzip libzip-dev libicu-dev libonig-dev && \
  apt-get clean && \
  rm -rf /var/lib/apt/lists/* && \
  docker-php-ext-install intl pdo_mysql zip bcmath

COPY ./php.ini /usr/local/etc/php/php.ini

WORKDIR /work

./docker/php/Dockerfile: 設定値の補足

Dockerfileはテキストファイルであり、Dockerイメージを作り上げるために実行する命令をこのファイルに含めることができます。

各種Docker命令を解説します。

FROM php:7.4-fpm-buster

https://matsuand.github.io/docs.docker.jp.onthefly/engine/reference/builder/#from

FROM命令はイメージビルドのためのベースイメージを設定します。
FROM イメージ名:タグ名 で指定します。

php | Docker Hub公式イメージをベースイメージとして利用します。

SHELL ["/bin/bash", "-oeux", "pipefail", "-c"]

https://matsuand.github.io/docs.docker.jp.onthefly/engine/reference/builder/#shell

SHELL命令は何も指定しない場合は SHELL ["/bin/sh", "-c"] がデフォルト値となります(Linuxの場合)

パイプ中のあらゆる段階でエラーがあれば失敗とするため、pipefailオプションを明示的に指定してます。

-o オプションはオプションを設定するためのオプションです。
-e オプションを定義しておくと、そのシェルスクリプト内で何らかのエラーが発生した時点で、それ以降の処理を中断できます。
-u オプションを定義しておくと、未定義の変数に対して読み込み等を行おうとした際にエラーとなります。
-x オプションを定義しておくと、実行したコマンドを全て標準エラー出力に出してくれます。

おまじないと思ってもらえればokです。SHELL命令は必須ではないです。

ENV COMPOSER_ALLOW_SUPERUSER=1 \
  COMPOSER_HOME=/composer

https://matsuand.github.io/docs.docker.jp.onthefly/engine/reference/builder/#env

ENV命令はコンテナ内のサーバー環境変数を設定します。

COPY --from=composer:1.10 /usr/bin/composer /usr/bin/composer

https://matsuand.github.io/docs.docker.jp.onthefly/engine/reference/builder/#copy

RUN apt-get update && \
  apt-get -y install git unzip libzip-dev libicu-dev libonig-dev && \
  apt-get clean && \
  rm -rf /var/lib/apt/lists/* && \
  docker-php-ext-install intl pdo_mysql zip bcmath

https://matsuand.github.io/docs.docker.jp.onthefly/engine/reference/builder/#run

Debian系のパッケージ管理ツールは apt-get, apt とありますが、Dockerfile内で apt を実行するとCLIインターフェース向けではないと警告が表示されるため、apt-getを使用します。

apt-get update インストール可能なパッケージの「一覧」を更新します。
実際のパッケージのインストール、アップグレードなどは行いません。

apt-get -y install xxx Laravelのインストールに必要なパッケージをインストールします。
下記のパッケージをインストールしておけばokだと思います。

apt-get clean && rm -rf /var/lib/apt/lists/* ここはパッケージインストールで使用したキャッシュファイルを削除しています。

phpの公式Dockerイメージには、docker-php-ext-install, docker-php-ext-enable, docker-php-ext-configure のPHP拡張ライブラリを簡単に利用するための便利コマンドが予め用意されています。

docker-php-ext-install intl pdo_mysql zip bcmath PHPの拡張ライブラリをインストールしています。

COPY ./php.ini /usr/local/etc/php/php.ini

https://matsuand.github.io/docs.docker.jp.onthefly/engine/reference/builder/#copy

WORKDIR /work

https://matsuand.github.io/docs.docker.jp.onthefly/engine/reference/builder/#workdir

./docker/php/php.ini を作成する

php.ini はPHPの設定ファイル

  • PHPエラーメッセージの設定
  • PHPエラーログの設定
  • メモリ等の設定(お好みで)
  • タイムゾーン設定
  • 文字コード設定
[mac] $ touch infra/php/php.ini
php.ini
zend.exception_ignore_args = off
expose_php = on
max_execution_time = 30
max_input_vars = 1000
upload_max_filesize = 64M
post_max_size = 128M
memory_limit = 256M
error_reporting = E_ALL
display_errors = on
display_startup_errors = on
log_errors = on
error_log = /dev/stderr
default_charset = UTF-8

[Date]
date.timezone = Asia/Tokyo

[mysqlnd]
mysqlnd.collect_memory_statistics = on

[Assertion]
zend.assertions = 1

[mbstring]
mbstring.language = Japanese

build & up

  • appコンテナの作成
  • PHPのバージョン確認
  • Laravelで必要なPHP拡張機能の確認
[mac] $ docker-compose up -d --build
  • docker-compose コマンドは docker-compose.yml があるディレクトリで実行します。
  • docker-compose updocker-compose.yml に定義したサービスを起動します。
  • -d 「デタッチド」モードでコンテナを起動します。
    • デフォルトは「アタッチド」モードで全てのコンテナログを画面上に表示
    • 「デタッチド」モードではバックグラウンドで動作
  • --build コンテナの開始前にイメージを構築します
    • 特に変更がない場合はキャッシュが使用されます。
[mac] $ docker-compose ps
            Name                          Command              State    Ports  
-------------------------------------------------------------------------------
docker-laravel-handson_app_1   docker-php-entrypoint php-fpm   Up      9000/tcp

docker-laravel-handson_app_1 コンテナの State が Up になっていたら正常に起動している状態です。

  • docker-compose ps コンテナ一覧を表示します。
    • Name: コンテナ名
    • Command: 最後に実行されたコマンド
    • State: 状態(Up)はコンテナが起動している状態
    • Ports: 9000/tcp(コンテナのポート)
    • 9000番のホストポートは公開されていないので、コンテナの外からはアクセスできない

appコンテナ内ミドルウェアのバージョン確認(コンテナに入ってコマンド実行)

作成したappコンテナの中に入ってPHP, Composerのバージョン、インストール済みの拡張機能を確認します。
Laravel 7.xのサーバ要件に必要な拡張機能が入っていることを確認します。

[mac] $ docker-compose exec app bash

# PHPのバージョン確認
[app] # php -v
PHP 7.4.6 (cli) (built: May 15 2020 13:01:21) ( NTS )
Copyright (c) The PHP Group
Zend Engine v3.4.0, Copyright (c) Zend Technologies

# Composerのバージョン確認
[app] # composer -V
Composer version 1.10.10 2020-08-03 11:35:19

# インストール済みの拡張機能の一覧
[app] # php -m
[PHP Modules]
bcmath
Core
ctype
curl
date
dom
fileinfo
filter
ftp
hash
iconv
intl
json
libxml
mbstring
mysqlnd
openssl
pcre
PDO
pdo_mysql
pdo_sqlite
Phar
posix
readline
Reflection
session
SimpleXML
sodium
SPL
sqlite3
standard
tokenizer
xml
xmlreader
xmlwriter
zip
zlib

[Zend Modules]

exit でコンテナの外に出ます。

[app] $ exit

control + d でもコンテナから出られます。

補足説明

appコンテナ内に入ってphpコマンドを実行しています。

  • docker-compose exec 実行中のコンテナ内で、コマンドを実行します。
  • app サービス名(コンテナ名)を指定します。

PHPのバージョン確認(コンテナの外からコマンド実行)

コンテナの外から php コマンドを実行することもできます。

  • docker-compose exec [サービス名] [実行したいコマンド]
[mac] $ docker-compose exec app php -v
PHP 7.4.6 (cli) (built: May 15 2020 13:01:21) ( NTS )
Copyright (c) The PHP Group
Zend Engine v3.4.0, Copyright (c) Zend Technologies

結果にコミット

[mac] $ git status
  (use "git add <file>..." to include in what will be committed)
    docker-compose.yml
    infra/php/Dockerfile
    infra/php/php.ini

[mac] $ git add .
[mac] $ git status
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
    new file:   docker-compose.yml
    new file:   infra/php/Dockerfile
    new file:   infra/php/php.ini

[mac] $ git commit -m "feat create docker app container"
[main f5a637f] feat create docker app container
 3 files changed, 49 insertions(+)
 create mode 100644 docker-compose.yml
 create mode 100644 infra/php/Dockerfile
 create mode 100644 infra/php/php.ini

[mac] $ git log

ウェブサーバー(web)コンテナを作る

nginxウェブサーバーコンテナを作成します。
nginxのベースイメージをそのまま利用します。

ディレクトリ構成

.
├── infra
│   └── nginx
│       └── default.conf # nginxの設定ファイル
├── backend
│  └── public # 動作確認用に作成
│       ├── index.html # HTML動作確認用
│       └── phpinfo.php # PHP動作確認用
└─── docker-compose.yml

docker-compose.yml へ追記する

  • ポート転送の設定(今回は10080ポートにする)
  • タイムゾーンの設定
version: "3.8"
services:
  app:
    build: ./infra/php
    volumes:
      - ./backend:/work

  # 追記
  web:
    image: nginx:1.18-alpine
    ports:
      - 10080:80
    volumes:
      - ./backend:/work
      - ./infra/nginx/default.conf:/etc/nginx/conf.d/default.conf
    working_dir: /work
  • docker-compose.yml ファイルはインデント(半角スペース)が意味を持ちます。注意してコピペしてください。
  • app コンテナの設定と同じインデントレベルで貼り付けます。

docker-compose.yml: 設定値の解説

    image: nginx:1.18-alpine

https://matsuand.github.io/docs.docker.jp.onthefly/compose/compose-file/#image

コンテナを起動させるイメージを指定します。
nginx | Docker Hubを指定してます。
今回は公式のnginxイメージをそのまま利用しています。(Dockerfileは不要)

ちなみにnginxは1.10, 1.12 等の偶数のバージョンが安定バージョンになります。
特に理由がなければ偶数バージョンをご利用ください。

    ports:
      - 10080:80

https://matsuand.github.io/docs.docker.jp.onthefly/compose/compose-file/#ports

nginxへ外(ホスト側)からコンテナ内へアクセスさせるため公開用のポートを設定します。
ホスト側:コンテナ側 と設定します。

    volumes:
      - ./backend:/work
      - ./infra/nginx/default.conf:/etc/nginx/conf.d/default.conf

https://matsuand.github.io/docs.docker.jp.onthefly/compose/compose-file/#volumes

ホスト側にあるディレクトリ、ファイルをコンテナ内へマウントさせています。

docker/nginx/default.conf を作成する

[mac] $ mkdir infra/nginx
[mac] $ touch infra/nginx/default.conf

Laravel公式にnginxの設定例が用意されているので、こちらを流用します。
https://readouble.com/laravel/7.x/ja/deployment.html

default.conf
server {
    listen 80;
    server_name example.com;
    root /work/public;

    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-XSS-Protection "1; mode=block";
    add_header X-Content-Type-Options "nosniff";

    index index.php;

    charset utf-8;

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

    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    error_page 404 /index.php;

    location ~ \.php$ {
        fastcgi_pass app:9000;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }
}

root, fastcgi_pass ドキュメントルート設定を書き換えてます。
nginxの設定を詳しく知りたい方は下記の記事がオススメです。

build & up

[mac] $ docker-compose down
[mac] $ docker-compose up -d --build
  • docker-compose コマンドは docker-compose.yml があるディレクトリで実行します。
[mac] $ docker-compose ps
            Name                          Command              State           Ports        
--------------------------------------------------------------------------------------------
docker-laravel-handson_app_1   docker-php-entrypoint php-fpm   Up      9000/tcp             
docker-laravel-handson_web_1   nginx -g daemon off;            Up      0.0.0.0:10080->80/tcp

docker-laravel-handson_web_1 コンテナの State が Up になっていたら正常に起動している状態です。

また、Ports の項目が、appコンテナは9000/tcp でwebコンテナは 0.0.0.0:10080->80/tcp と表示形式が異なってます。
これはホスト上の10080番ポートをコンテナの80番ポートへ割り当てています。

nginxのバージョン確認

[mac] $ docker-compose exec web nginx -v
nginx version: nginx/1.18.0

webコンテナの確認

  • webコンテナの動作確認
  • HTMLとPHPが表示されるか
[mac] $ mkdir backend/public
[mac] $ echo "Hello World" > backend/public/index.html
[mac] $ echo "<?php phpinfo();" > backend/public/phpinfo.php

http://127.0.0.1:10080/index.html

「Hello World」が表示されることを確認する。

http://127.0.0.1:10080/phpinfo.php

phpinfoの情報が表示されることを確認する。

[mac] $ rm -rf backend/*

確認用に作成したHTML, PHPファイルは不要なので削除します。

結果にコミット

[mac] $ git status
On branch main
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
    modified:   docker-compose.yml
    modified:   infra/php/Dockerfile

Untracked files:
  (use "git add <file>..." to include in what will be committed)
    infra/nginx/default.conf

[mac] $ git add .
[mac] $ git status
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
    modified:   docker-compose.yml
    new file:   infra/nginx/default.conf
    modified:   infra/php/Dockerfile

[mac] $ git commit -m "feat create docker web container"
[main 5e278c1] feat create docker web container
 3 files changed, 29 insertions(+), 1 deletion(-)
 create mode 100644 infra/nginx/default.conf

[mac] $ git log

Laravelをインストールする

  • app コンテナに入り、Laravelをインストール
  • welcomeページが表示されるか
[mac] $ docker-compose exec app bash
[app] $ composer create-project --prefer-dist "laravel/laravel=8.*" .
[app] $ php artisan -V
Laravel Framework 8.0.0

[app] $ exit

Laravel ウェルカム画面の表示

http://127.0.0.1:10080

LaravelのWelcome画面が表示されることを確認する。

結果にコミット

[mac] $ git status
[mac] $ git add .
[mac] $ git status
[mac] $ git commit -m "feat laravel install"
[mac] $ git log

データベース(db)コンテナを作る

MySQLデータベースコンテナを作成します。
MySQLのベースイメージをそのまま利用します。

参考記事

ディレクトリ構成

.
├── infra
│   └── mysql
│       ├── Dockerfile
│       └── my.cnf # MySQLの設定ファイル
└── docker-compose.yml

docker-compose.yml へ追記する

  • データベース名やユーザー名等の接続情報とタイムゾーンの設定は環境変数で渡す
  • トップレベルvolumeを使用してデータの永続化
version: "3.8"
services:
  app:
    build: ./infra/php
    volumes:
      - ./backend:/work

  web:
    image: nginx:1.18-alpine
    ports:
      - 10080:80
    volumes:
      - ./backend:/work
      - ./infra/nginx/default.conf:/etc/nginx/conf.d/default.conf
    working_dir: /work

  # 追記
  db:
    build: ./infra/mysql
    volumes:
      - db-store:/var/lib/mysql

volumes:
  db-store:

./docker/mysql/Dockerfile を作成する

[mac] $ mkdir infra/mysql
[mac] $ touch infra/mysql/Dockerfile

下記のコードを丸ごとコピーして Dockerfile へ貼り付けてください。

infra/mysql/Dockerfile
FROM mysql:8.0

ENV MYSQL_DATABASE=laravel_local \
  MYSQL_USER=phper \
  MYSQL_PASSWORD=secret \
  MYSQL_ROOT_PASSWORD=secret \
  TZ=Asia/Tokyo

COPY ./my.cnf /etc/mysql/conf.d/my.cnf
RUN chmod 644 /etc/mysql/conf.d/my.cnf

コメントでいただいたのですが、Windows環境でボリュームマウントを行うと、ファイルパーミッションが777となるようです。
my.cnf に書き込み権限が付いてるとMySQLの起動時にエラーが発生します。
その対策としてボリュームマウントではなくDockerfileを作成して my.cnf ファイルコピー、読み取り専用に権限変更してます。

docker/mysql/my.cnf を作成する

  • 文字コードの設定
  • タイムゾーンの設定
  • ログ設定
[mac] $ mkdir infra/mysql
[mac] $ touch infra/mysql/my.cnf
my.cnf
[mysqld]
# character set / collation
character_set_server = utf8mb4
collation_server = utf8mb4_0900_ai_ci

# timezone
default-time-zone = SYSTEM
log_timestamps = SYSTEM

# Error Log
log-error = mysql-error.log

# Slow Query Log
slow_query_log = 1
slow_query_log_file = mysql-slow.log
long_query_time = 1.0
log_queries_not_using_indexes = 0

# General Log
general_log = 1
general_log_file = mysql-general.log

[mysql]
default-character-set = utf8mb4

[client]
default-character-set = utf8mb4

build & up

[mac] $ docker-compose down
[mac] $ docker-compose up -d --build
  • docker-compose コマンドは docker-compose.yml があるディレクトリで実行します。
[mac] $ docker-compose ps
            Name                          Command              State           Ports        
--------------------------------------------------------------------------------------------
docker-laravel-handson_app_1   docker-php-entrypoint php-fpm   Up      9000/tcp             
docker-laravel-handson_db_1    docker-entrypoint.sh mysqld     Up      3306/tcp, 33060/tcp  
docker-laravel-handson_web_1   nginx -g daemon off;            Up      0.0.0.0:10080->80/tcp

docker-laravel-handson_db_1 コンテナの State が Up になっていたら正常に起動している状態です。

[mac] $ docker-compose exec db mysql -V
mysql  Ver 8.0.20 for Linux on x86_64 (MySQL Community Server - GPL)

マイグレーション実行(エラーが発生します)

[mac] $ docker-compose exec app bash
[app] $ php artisan migrate

   Illuminate\Database\QueryException 

  SQLSTATE[HY000] [2002] Connection refused (SQL: select * from information_schema.tables where table_schema = laravel and table_name = migrations and table_type = 'BASE TABLE')

  at vendor/laravel/framework/src/Illuminate/Database/Connection.php:671
    667▕         // If an exception occurs when attempting to run a query, we'll format the error
    668▕         // message to include the bindings with SQL, which will make this exception a
    669▕         // lot more helpful to the developer instead of just the database's errors.
    670▕         catch (Exception $e) {
  ➜ 671▕             throw new QueryException(
    672▕                 $query, $this->prepareBindings($bindings), $e
    673▕             );
    674▕         }
    675▕ 

      +37 vendor frames 
  38  artisan:37
      Illuminate\Foundation\Console\Kernel::handle(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))

[app] $ exit

SQLSTATE[HY000] [2002] Connection refused このエラーはよく見るMySQLのエラーです。
MySQLに接続拒否されたエラーなので、この場合は大体MySQLへの接続設定に誤りがあります。

backend/.env のDB接続設定を修正する。

[mac] $ vim backend/.env
DB_CONNECTION=mysql
DB_HOST=db
DB_PORT=3306
DB_DATABASE=laravel_local
DB_USERNAME=phper
DB_PASSWORD=secret

backend/.env.example も同様にDB接続設定を修正しておきます。

[mac] $ vim backend/.env.example
DB_CONNECTION=mysql
DB_HOST=db
DB_PORT=3306
DB_DATABASE=laravel_local
DB_USERNAME=phper
DB_PASSWORD=secret

マイグレーション実行(再実行)

[mac] $ docker-compose exec app bash
[app] $ php artisan migrate

Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated:  2014_10_12_000000_create_users_table (0.07 seconds)
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated:  2014_10_12_100000_create_password_resets_table (0.04 seconds)
Migrating: 2019_08_19_000000_create_failed_jobs_table
Migrated:  2019_08_19_000000_create_failed_jobs_table (0.02 seconds)

試しにデータを作ってみる

[mac] $ docker-compose exec app bash
[app] php artisan tinker
$user = new App\Models\User();
$user->name = 'phper';
$user->email = 'phper@example.com';
$user->password = Hash::make('secret');
$user->save();
[mac] $ docker-compose exec db bash
[db] $ mysql -u $MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE
[mysql] > SELECT * FROM users;
+----+--------------------+-----------------------------+---------------------+--------------------------------------------------------------+----------------+---------------------+---------------------+
| id | name               | email                       | email_verified_at   | password                                                     | remember_token | created_at          | updated_at          |
+----+--------------------+-----------------------------+---------------------+--------------------------------------------------------------+----------------+---------------------+---------------------+
|  1 | phper              | phper@example.com           | NULL                | $2y$10$/cmcAIdF7twOntcazkn4/e61YAM2F99hCrOo.qRUz7cUa/ZydrbVS | NULL           | 2020-08-24 17:09:53 | 2020-08-24 17:09:53 |
+----+--------------------+-----------------------------+---------------------+--------------------------------------------------------------+----------------+---------------------+---------------------+
1 rows in set (0.00 sec)

結果にコミット

[mac] $ git status
[mac] $ git add .
[mac] $ git status
[mac] $ git commit -m "feat create docker db container"
[mac] $ git log

GitHubにpush

[mac] $ git push

リモートリポジトリへpushされていることを確認します。

Docker環境の再構築

Docker環境の破棄

コンテナの停止、ネットワーク・名前付きボリューム・コンテナイメージ、未定義コンテナを削除

[mac] $ docker-compose down --rmi all --volumes --remove-orphans

作業ディレクトリの削除

プロジェクトを削除するので、GUIエディタは閉じておきましょう。

[mac] $ cd ..
[mac] $ rm -rf docker-laravel-handson

Visual Studio Code等のGUIエディタで開いている場合は、一度エディタを終了しましょう。

環境の再構築

GitHubからリポジトリをクローン

** 自身のリポジトリ先に適宜変更してください **

[mac] $ git clone git@github.com:ucan-lab/docker-laravel-handson.git
[mac] $ cd docker-laravel-handson
[mac] $ docker-compose up -d --build

http://127.0.0.1:10080

/work/public/../vendor/autoload.php を開くのに失敗してエラーになっていることを確認します。
git cloneが終わった状態では app コンテナ内に /work/vendor ディレクトリが存在しないためです。

Laravelインストール

app コンテナに入ります。

[mac] $ docker-compose exec app bash

vendor ディレクトリへライブラリ群をインストールします。
composer.lock ファイルを参照します。

[app] $ composer install

composer install 時は .env 環境変数ファイルは作成されないので、 .env.example を元にコピーして作成します。

[app] $ cp .env.example .env

スクリーンショット 2019-09-29 2.10.51.png

.envAPP_KEY= の値がないとこのエラーが発生します。
このコマンドでアプリケーションキーを生成できます。

[app] $ php artisan key:generate

Welcome画面が表示されることを確認します。

[app] $ php artisan migrate

最後にマイグレーションを実行して成功すればハンズオン終了です。
お疲れ様でした?? コンテナを停止して終了してください。

[app] $ exit
[mac] $ docker-compose down

オマケ

MySQLに接続したい

[mac] $ docker-compose exec db bash -c 'mysql -u${MYSQL_USER} -p${MYSQL_PASSWORD} ${MYSQL_DATABASE}'

mysql> show tables;
+---------------------+
| Tables_in_homestead |
+---------------------+
| migrations          |
| password_resets     |
| users               |
+---------------------+
3 rows in set (0.00 sec)

mysql> desc users;
+-------------------+---------------------+------+-----+---------+----------------+
| Field             | Type                | Null | Key | Default | Extra          |
+-------------------+---------------------+------+-----+---------+----------------+
| id                | bigint(20) unsigned | NO   | PRI | NULL    | auto_increment |
| name              | varchar(255)        | NO   |     | NULL    |                |
| email             | varchar(255)        | NO   | UNI | NULL    |                |
| email_verified_at | timestamp           | YES  |     | NULL    |                |
| password          | varchar(255)        | NO   |     | NULL    |                |
| remember_token    | varchar(100)        | YES  |     | NULL    |                |
| created_at        | timestamp           | YES  |     | NULL    |                |
| updated_at        | timestamp           | YES  |     | NULL    |                |
+-------------------+---------------------+------+-----+---------+----------------+
8 rows in set (0.01 sec)

マイグレーションが実行されるとLaravel側で最初から用意されている users, password_resets のテーブルが生成されています。

Laravelのログをコンテナログに表示する

backend/.env を修正する。

LOG_CHANNEL=stderr

backend/routes/web.php

Route::get('/', function () {
    logger('welcome route.');
    return view('welcome');
});

http://127.0.0.1:10080

$ docker-compose logs
# -f でログウォッチ
$ docker-compose logs -f
# サービス名を指定してログを表示
$ docker-compose logs -f app

MySQLクライアントツールで接続したい

docker-compose.ymldb サービスに下記設定を追記して、コンテナを再起動して設定を反映してください。

    ports:
      - 33060:3306

dbコンテナへのポート転送設定がないとホストからアクセスが行えません。
MySQLクライアントツールは「Sequel Ace」がオススメです。

Windowsエラーメモ

Error response from daemon

$ docker-compose up -d --build
docker: Error response from daemon: Get https://registry-1.docker.io/v2/: net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers).

解決策をいくつか

マイグレーションエラーの補足

Host '172.27.0.2' is not allowed to connect to this MySQL server

$ php artisan migrate
   Illuminate\Database\QueryException  : SQLSTATE[HY000] [1130] Host '172.27.0.2' is not allowed to connect to this MySQL server (SQL: select * from information_schema.tables where table_schema = homestead and table_name = migrations and table_type = 'BASE TABLE')

  at /work/vendor/laravel/framework/src/Illuminate/Database/Connection.php:664
    660|         // If an exception occurs when attempting to run a query, we'll format the error
    661|         // message to include the bindings with SQL, which will make this exception a
    662|         // lot more helpful to the developer instead of just the database's errors.
    663|         catch (Exception $e) {
  > 664|             throw new QueryException(
    665|                 $query, $this->prepareBindings($bindings), $e
    666|             );
    667|         }
    668| 

  Exception trace:

  1   PDOException::("SQLSTATE[HY000] [1130] Host '172.27.0.2' is not allowed to connect to this MySQL server")
      /work/vendor/laravel/framework/src/Illuminate/Database/Connectors/Connector.php:70

  2   PDO::__construct("mysql:host=db;port=3306;dbname=homestead", "homestead", "secret", [])
      /work/vendor/laravel/framework/src/Illuminate/Database/Connectors/Connector.php:70

  Please use the argument -v to see more details.

このエラーが発生した場合は my.cnf を作成する前に docker-compose up -d でビルドしてしまった可能性が高いです。

$ docker-compose down --volumes --rmi all
$ docker-compose up -d --build

設定ファイルがない状態でMySQLの初期化が行われたでデータが永続化されてしまってるので一度ボリューム毎削除してビルドし直せばokです。

docker volumes の共有で Permission denied (SELinux) 問題

CentOS 等の場合 volumes でホストと共有したファイル(ディレクトリ)にアクセスできないことがあります。

書き込み権限を与える

$ chmod -R 777 logs
$ chmod -R 777 src/storage/logs

ログディレクトリはホスト側からもコンテナ側からも書き込みするので、書き込み権限を付与しておきましょう。

応用

より実用的な構成のdocker構成の記事を書いてますので、よかったらこちらの記事も読んでもらえると嬉しいです。

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

[Laravel][HTML] 戻るボタンを作って動かすとバリデーションが働いてしまう場合の対処

状況

Laravelに限った話ではないがLaravelを利用しているときに発生したことなので一応。
●こんな感じで書くと、普通のメニュー画面みたいなところでは問題なく動くが、

anime-suko.php
<a href="/menu"><button>戻る</button></a>

これをフォームの中に書くと、今の画面に移動するわけではないのに何も入力しなくても勝手にバリデーションが発生してしまって移動してくれない。

解決方法

buttonタグのtypeをbuttonにする。こうしないと、HTMLの仕様的にこうしないと、type="submit"の扱いになるHTMLの仕様らしい。

anime-suko.php
<a href="/menu"><button type="button">戻る</button></a>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

LaravelでRequestクラスを作成したら、403エラー

概要

Requestクラスを作成し、バリデーション用コード書き、コントローラーに適応させた!!
さて、ブラウザでフォーム入力行いボタンを、ポチー!!
スクリーンショット 2020-11-24 18.40.47.png

「ファーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーwww」
ここまでが、僕のリクエストクラス作成のルーティンです。(毎回やっている。。。)

戒めも込めて記事にします。

結論

falseをtureに書き換える!!これだけ!!

class TestRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true; ←ここがデフォルトではfalseになっている。
    }

これからは絶対、忘れないようにしよう!絶対に!

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

お前らのLike検索は間違っている (QueryBuilder/Eloquent)

煽りタイトルすみません。
Laravelタグをつけてはいますが、特定の言語/フレームワークに限らない、Like検索時のエスケープの注意点についてお話します。
(@wand_ta 指摘ありがとうございます! 修正しました)

本記事の対象読者

  • 安易に以下のようなコードを書いてしまう方
    • $query->where('name', 'like', "%{$keyword}%")
  • addcslashes ? ナニソレ? 旨いの? って方
  • 1つのバックスラッシュを含むデータを検索するためにコードでは8個のバックスラッシュを書かなければならない場合があることに驚きを隠せない方
    • DB::select("SELECT * FROM items WHERE name LIKE '%\\\\\\\\%'")

よくある間違い

商品テーブルに「100%りんご」という名前のデータが登録されているとします。
あなたの実装した部分一致検索APIは「%」を検索キーワードに指定して正しく検索できるでしょうか?
また、「 _ 」や「 \ 」を指定して正しく検索できるでしょうか?

間違った実装

商品テーブルに対し、指定されたキーワードをSQLのLIKEを使って部分一致で検索するAPIを作るものとします。
とても素朴に、'%' . $keyword . '%'のような感じでリクエストパラメタから得たキーワードの両端に%をつけてLIKE検索した実装をしました。

app/Http/Controllers/ItemController.php
class ItemController extends Controller
{
    public function index(Request $request): JsonResponse
    {
        $keyword = $request->input('keyword') ?? '';

        // キーワードで商品名を検索 ***間違った実装***
        $rows = Item::where('name', 'like', '%' . $keyword . '%')->get();

        return response()->json($rows);
    }
}

何がまずいか?

テーブル

まず、こんな商品テーブルがあるとします。
商品名として「%」や「_」や「\」といった文字が含まれています。

itemsテーブル
mysql> SELECT * FROM items;
+----+---------------+
| id | name          |
+----+---------------+
|  1 | 100%りんご      |
|  2 | abc_コーラ      |
|  3 | CC\1000       |
+----+---------------+
3 rows in set (0.00 sec)

curlで確認 (意図した挙動をしない)

先程のLaravel APIにキーワードとして「100%」を指定して検索してみましょう。
「100%りんご」がヒットすることを期待しますが、
実際には「CC\1000」という商品もヒットしてしまいました。
※curlコマンドでは、キーワード「100%」をURLエンコードした?keyword=100%25というクエリ文字列が指定されています。

# 「100%」を含む商品を探したいが。。。
% curl -s 'http://localhost/api/items?keyword=100%25' | jq .
[
  {
    "id": 1,
    "name": "100%りんご"
  },
  {
    "id": 3,
    "name": "CC\\1000"  # ※「CC\1000」のエスケープ文字列表現
  }
]

また、今度はキーワード「\」を含む商品を探したいとき、「CC\1000」がヒットすることを期待しますが、実際には何もヒットしません。
※キーワード「\」をURLエンコードした?keyword=%5cというクエリ文字列が指定されています。

# 「\」を含む商品を探したいが。。。
% curl -s 'http://localhost/api/items?keyword=%5c' | jq .
[]

解説

前置き

  • 本記事は主にLIKE演算子のワイルドカードのエスケープについて説明します
  • 照合順序、Locale等については言及しません
    • 大文字小文字、バイナリ比較、ロケールに応じた照合、など
  • いわゆる「全文検索」機能、形態素解析等については言及しません
  • いわゆるSQL組み立てにおけるエスケープ、SQLインジェクションといったセキュリティについては直接言及しません。
  • 本記事は以下の環境で検証しています。
    • Laravel 7.4
    • MySQL 8.0
    • PostgreSQL 11.5
  • 他のバージョン、他のDB(Oracle, SQL Serverなど)、他の言語/フレームワークでも基本的な考え方は同じですが、具体的な差異については言及しません。

SQLで検索するとき

1. 単純なLIKE検索

単純に「100」という文字列を検索したい場合は、ワイルドカード%で挟んで検索パターン文字列を作り、LIKE演算子で比較します。

単純なLIKE検索
-- 「100」を含むものを探す
mysql> SELECT * FROM items WHERE name LIKE '%100%';
結果
+----+---------------+
| id | name          |
+----+---------------+
|  1 | 100%りんご      |
|  3 | CC\1000       |
+----+---------------+
2 rows in set (0.00 sec)

2. ワイルドカード文字そのものを検索したいとき (要エスケープ)

文字としての%を検索したいときは\でエスケープして、\%としてパターン文字列を指定します。
部分一致で探したいので、前後を%で挟むと %\%% というパターン文字列になります。
このパターン文字列をLIKE演算子の右辺で指定するときに、シングルクォートで囲んであげましょう。
この際、クォート文字列のリテラル表現のために、さらに\をエスケープして\\とする必要があります。
結果として、クォート文字列表現としては'%\\%%'となります。

エスケープしてLIKE検索
-- 「%」を含むものを探す: 間違った例
mysql> SELECT * FROM items WHERE name LIKE '%%%';

-- 「%」を含むものを探す: 正しい例 (\でエスケープする)
mysql> SELECT * FROM items WHERE name LIKE '%\\%%';
結果
+----+---------------+
| id | name          |
+----+---------------+
|  1 | 100%りんご      |
+----+---------------+
1 row in set (0.00 sec)

同様に、文字としての「_」や「\」を探したいときもエスケープが必要です。
※「\」が増殖しているところに注目

-- 「_」を含むものを探す: 正しい例 (\でエスケープする)
mysql> SELECT * FROM items WHERE name LIKE '%\\_%';

-- 「\」を含むものを探す: 正しい例 (\でエスケープする)
mysql> SELECT * FROM items WHERE name LIKE '%\\\\%';

LIKE検索パターンのエスケープまとめ

  • LIKE検索パターン文字列中では、%_ワイルドカード と呼ばれる特殊な役割を持ち、これらの文字列そのものを示したい場合はエスケープする必要があります
  • エスケープ文字は LIKE演算子のESCAPE句で指定可能ですが、多くのDBでは省略した場合はデフォルトは\です。
  • また、\文字そのものを示したい場合も同様にエスケープする必要があります
文字 検索パターン内での役割 エスケープ後文字列
% (パーセント) 任意の長さの文字列(空文字列を含む) \%
_ (アンダースコア) 任意の1文字 \_
\ (バックスラッシュ) エスケープ文字 \\

MS SQL Server のLIKE検索パターンのエスケープ

Microsoft SQL Serverは少し特殊で、文字クラス[]を利用してメタ文字そのものを表現します。

文字 検索パターン内での役割 MS SQL Serverでのエスケープ後文字列
% (パーセント) 任意の長さの文字列(空文字列を含む) [%]
_ (アンダースコア) 任意の1文字 [_]
[ 文字クラスの開始 [[]

SQLのシングルクォート リテラル表現でのメタ文字のエスケープまとめ

SQL中で文字列をリテラル表現する場合はシングルクォートで囲む必要があります。
例: 文字列abc => クォート表現'abc'

この際に シングルクォートそのものなどのメタ文字を含む場合はエスケープ文字として\でエスケープする必要があります。
また、エスケープ文字\を含む場合も同様にエスケープします。
例: 文字列' => クォート文字列表現'\'
例: 文字列\ => クォート文字列表現'\\'

文字 SQL文での役割 エスケープしたクォート文字列表現
' (シングルクォート) 文字列の開始/終了 '\''
\ (バックスラッシュ) エスケープ文字 '\\'

正しいLike検索の実装

エスケープを意識して検索APIの実装を見直し

API実装

LIKE検索パターンのためにワイルドカード文字、及びエスケープ文字をそれぞれエスケープしてあげます。
phpの addcslashes 関数で指定文字をバックスラッシュでエスケープできます。
(THX @wand_ta)

app/Http/Controllers/ItemController.php
class ItemController extends Controller
{
    public function index(Request $request): JsonResponse
    {
        $keyword = $request->input('keyword') ?? '';

        // キーワードのメタ文字をエスケープして商品名を検索
        $pat = '%' . addcslashes($keyword, '%_\\') . '%';
        $rows = Item::where('name', 'LIKE', $pat)->get();

        return response()->json($rows);
    }
}

※↑キーワードが空だったらエラーにする、結果を返さない、(Like条件で絞り込みせずに)全件を返す、 など実際には仕様に応じて実装すると思いますが、単純化のためここでは簡易な実装にしています。

なお、MS SQL Server の場合は以下のようにする必要があるかもしれません。(未検証)
また、preg_replace()の第1引数は正規表現を指定しますので、ここでもさらにエスケープしています。
もちろん、シングルクォート内の「\」をエスケープしています。

SQLServerの場合
        $pat = '%' . preg_replace('/([\\[_%])/', '[$1]', $keyword) . '%';

curlで確認

こんどは意図した検索ができるようになりましたね!

# 「100%」を含む商品を探して「100%りんご」がヒットする
% curl -s 'http://localhost/api/items?keyword=100%25' | jq .
[
  {
    "id": 1,
    "name": "100%りんご"
  }
]

# 「\」を含む商品を探して「CC\1000」がヒットする
% curl -s 'http://localhost/api/items?keyword=%5c' | jq .
[
  {
    "id": 3,
    "name": "CC\\1000"
  }
]

リファクタリング: likeスコープを提供するTrait

コントローラに複雑な処理を書きたくないので、Model側に処理を移しましょう。

Trait実装

以下のようなscopeを提供するトレイトを作成し、

app/Models/Traits/WhereLike.php
namespace App\Models\Traits;

trait WhereLike
{
    // 部分一致検索
    public function scopeWhereLike($query, string $column, string $keyword)
    {
        return $query->where($column, 'like', '%' . addcslashes($keyword, '%_\\') . '%');
    }

    // 前方一致検索
    public function scopeWhereLikeForward($query, string $column, string $keyword)
    {
        return $query->where($column, 'like', addcslashes($keyword, '%_\\') . '%');
    }
}

ModelでTrait利用

商品モデルで先程のトレイトをuseします。

app/Models/Item.php
namespace App\Models;

use App\Models\Traits\WhereLike;
use Illuminate\Database\Eloquent\Model;

class Item extends Model
{
    use WhereLike;
}

tinkerで確認

これで、Itemモデルに部分一致検索whereLike()や前方一致検索whereLikeForward()といったクエリビルダ用メソッドが追加されました。

tinker
# 名前に「%」を含むものを検索
>>> Item::whereLike('name', '%')->get()
=> Illuminate\Database\Eloquent\Collection {#4324
     all: [
       App\Models\Item {#4317
         id: 1,
         name: "100%りんご",
       },
     ],
   }

# 名前に「100」を含むものを検索
>>> Item::whereLike('name', '100')->get()
=> Illuminate\Database\Eloquent\Collection {#4326
     all: [
       App\Models\Item {#4318
         id: 1,
         name: "100%りんご",
       },
       App\Models\Item {#4328
         id: 3,
         name: "CC\1000",
       },
     ],
   }

# 名前を「100」で前方一致検索
>>> Item::whereLikeForward('name', '100')->get()
=> Illuminate\Database\Eloquent\Collection {#4312
     all: [
       App\Models\Item {#4316
         id: 1,
         name: "100%りんご",
       },
     ],
   }

おまけ: プログラム内で生SQLを利用するときのエスケープ

クエリビルダ/ORマッパーを利用せずに、生SQLを利用したい場合が稀にあります。
※もちろんそのような場合でも、バインド値・プレースホルダなどの安全に利用できる仕組みを極力活用すべきです。

  • 再帰クエリ、JSON系の演算子、その他特殊な構文などDB固有の特殊な機能を使いたい場合
  • DB固有のDDLを実行したい場合
  • PostgreSQLの継承テーブルなどの都合で動的にテーブル名を導出したい場合
  • 複雑なサブクエリで、クエリビルダ/ORマッパーでの組み立てがやりづらい場合
  • 開発、検証中の一時的な仮実装

生SQLではLIKE文に限らずセキュリティの観点からエスケープを特に注意ください。

以下は、生SQLでLIKE文を書いた時のエスケープの例です。
※呆れるほど「\」が増殖しているところに注目

tinker
# 名前に「\」を含むものを検索
>>> DB::select("SELECT * FROM items WHERE name LIKE '%\\\\\\\\%'");
=> [
     {#4334
       +"id": 3,
       +"name": "CC\1000",
     },
   ]
  1. 検索対象としての「\」
  2. → LIKE検索パターンにするためにエスケープ → 「\\」 2倍!
  3. → SQLのクォート文字列表現のためのエスケープ → 「\\\\」 4倍!!
  4. → phpのクォート文字列表現のためのエスケープ → 「\\\\\\\\」 8倍!!!!

そうです、見ての通り バックスラッシュは増えるんです。
※ちなみに、Qiitaのこの記事を書くときに Qiita Markdownのためにエスケープするので、私は↑で16個の連続したバックスラッシュを書いています。

※もちろん、Productionコードで↑のような実装をしてはいけませんし、このような実装が必要な場面は考えにくいですが、あえて想像するなら、seederの実装内でシードデータを別テーブルの情報から何かしら探して使うとき、複雑なサブクエリが必要で、SQLターミナルで検証したクエリをそのまま流用して一時的にコードで使う、というケースでしょうか。。。

最後に

いっぱいバックスラッシュを書いた。
それはともかく良い年末を!

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

Vueソーシャルシェアリングのバージョン指定

vue-social-sharingのバージョン指定

ターミナル
npm i vue-social-sharing@2.4.6

@2.4.6の部分を好きなバージョンに指定するだけ!

ver.3だとSNSアイコンが表示されない事が多いのでこちらのバージョンを指定するといけました!

以上!

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

[2020年12月]Herokuにlaravelアプリケーションをデプロイする方法

herokuにLaravelアプリをデプロイする方法について解説します!

前提

①アカウント登録ができていること
②heroku CLIをインストール済であること
公式のガイドを元にherokuのコマンドラインを打てるようにしておきましょう。

2020年12月時点での情報
過去のherokuデプロイ系の記事を見つつ実際にデプロイしたところ、細かい手順で変更があるのを確認しました。
今後も変更があるかもしれないので、うまくいかないことがあったら
公式リファレンスを確認しましょう。

リファレンスページ上部の最終更新履歴(↓画像の赤枠部)が新しくなっていたら、リファレンスの方法に沿ってすすめるのがイイかと!
image.png

ざっくり手順を解説

  1. laravelのプロジェクト作成
  2. gitリポジトリを作成
  3. ProcFile作成
  4. herokuアプリケーションとして登録
  5. herokuへpushする
  6. config(設定)を行う
  7. DB設定をする
  8. マイグレーション実行

既にアプリをgithubにpushして開発している方は、手順3ProcFile作成から参照してください。
では、やっていきましょう!

方法

手順1 laravelプロジェクトの作成

composerコマンドでlaravelのプロジェクトを作成します。
自分の場合はver8のLaravelとlivewireというライブラリを触りたかったのでlivewire-applicationという名前にしました。

composer create-project  laravel/laravel livewire-application --prefer-dist "8.0.*"

手順2 gitリポジトリを作る

herokuにはgithubへpushしたコードを毎日自動デプロイしてくれるなど、便利な機能があります。
逆にいえば、herokuにデプロイするにはgitを使う必要があるということ。

なので、Laravelのプロジェクトを自分のリポジトリにする必要があります。

githubのアカウントページで新しいリポジトリを作成し、pushしましょう。

cd livewire-application/src
git init
git add .
git remote add origin https://github.comユーザー名/リポジトリ名
git commit -m "new livewire project"

これで自分のgithubリポジトリにlaravelプロジェクトが作成されているはず!

手順3 ProcFile作成

Procfileは「あなたのアプリがデプロイされているherokuサーバにアクセスがあった時に、どこを参照するか?」を設定するファイルです。これをLaravel用に設定しましょう。

echo "web: vendor/bin/heroku-php-apache2 public/" > Procfile
git add .
git commit -m "Procfile for Heroku"

上記の通り、Procfileは公開しても問題ないのでgitコミットしてます。

設定について解説すると、

Laravelリファレンス/設定/Publicディレクトリより

Laravelをインストールできたら、Webサーバのドキュメント/Webルートがpublicディレクトリになるように設定してください。

と書かれている通り、Laravelのアプリはpublicディレクトリがアクセスのエンドポイントになります。
heroku側の初期設定がpublicになってないので、設定を変更しているワケですね。

手順4 herokuアプリケーションとして登録

Procfileを作成した場所で、そのままheroku createコマンドを打ちます。

hirog:livewire-application hirog_engineer$ heroku create
Creating app... done, ⬢ shallow-waters-42026
https://Protected-waters-42026.herokuapp.com/ | https://git.heroku.com/shallow-waters-42026.git

ざっくりいうと、これはgitコマンドのgit initと同じです。
「この場所を起点に、これからherokuのアプリを新規登録しますよ。」と宣言しています。

また、ここでheroku側でのアプリ名も決定されます(名前はランダム)。
今回はProtected-waters-42026という名前になりました。

手順5 herokuへpushする

git push heroku masterというコマンドを打ちます。

hirog:livewire-application hirog_engineer$ git push heroku master
Enumerating objects: 123, done.
Counting objects: 100% (123/123), done.
Delta compression using up to 4 threads
Compressing objects: 100% (101/101), done.
Writing objects: 100% (123/123), 68.26 KiB | 1.95 MiB/s, done.
Total 123 (delta 9), reused 0 (delta 0)
remote: Compressing source files... done.
remote: Building source:

このコマンドを打つことで、gitを元にアプリのmasterブランチにあるコードがherokuにpush(送信)されます。

ちなみに、この時点でProcfileを作成し忘れていると、以下のようなメッセージが途中に出てきます。

image.png

「Procfileがないから、アクセスがあった時はrootディレクトリを参照されるようにしたよ」という意味です。
ただ、先ほども書いた通り、Laravelのアプリはpublicをエンドポイントにしなければいけません。

手順を戻ってmasterブランチにcheckoutしてProcfileを作成しましょう。

手順6 config(設定)を行う

herokuにpushする時にも.gitignoreは有効になっているので、ここまでの手順で環境変数.envはheroku上にありません。。

.envに書くような情報はheroku用のコマンドを使って設定する必要があります。
最低限、設定しておく項目は、、、

KEY VALUE 役割
APP_NAME 任意(アプリケーション名) ブラウザタブのタイトルなど、アプリ名の表示に使用
APP_KEY key:generateコマンドで発行 認証情報の暗号化
SESSION_DRIVER database/fileなど(認証方法による) セッション管理の方法
APP_DEBUG true/false デバッグモードON/OFF

※DBの接続情報、外部API連携キー、メールサーバ接続情報なんかも同じ要領で設定します。

SESSION_DRIVERはデフォルト値がfileですが、認証に使っているライブラリによっても変わります。.envに書かれている値をそのまま入れます。

※最初にデプロイする時はAPP_DEBUGをtrueに設定することをオススメします。デプロイ初期にエラーが起こっている時、デバッグモードじゃないと原因の把握がしづらいため。認証周りを含めデプロイがうまくいったところでfalseに変更するといいかと。

config設定の方法はCUI(コマンドライン)とGUI(herokuのアプリページ内)の2種類。書き換え・確認のしやすさからGUIでやるのがオススメです。

CUIでconfig設定する

srcディレクトリ内(Procfileがある場所)からheroku config:setコマンドで設定できます。

// 例
// 暗号化キー
heroku config:set APP_KEY=$(php artisan --no-ansi key:generate --show).
# APP_KEY: base64:.... みたいな表示がされる

// アプリ名
heroku config:set APP_NAME=livewire-project

// アプリのベースURL (git push heroku master で表示されたURLを入力)
heroku config:set APP_URL=https://sutara79-laravel.herokuapp.com

// セッションドライバ
heroku config:set SESSION_DRIVER=database

// デバッグモード
heroku config:set APP_DEBUG=true

// 設定の確認コマンド
heroku config

GUIでconfig設定する

①Dashboardページから、アプリを選択します。
image.png

②アプリページ内のタブ一覧からSettingsを選択、Config Vars欄に、
KEY-VALUEの形式で値を入力するだけ!
image.png

手順7 DB設定をする

DBの設定は、それだけでひと仕事になるので、別記事を用意しました。
長文ですが、初めてデプロイする方でも分かりやすいよう書いたので、
よかったら参照してください。

herokuデプロイ時のDB設定まとめ

手順8 マイグレーション実行

herokuにpushした場合、composer installは自動でしてくれているので、
他の初期設定やマイグレーションをやっていきましょう。

heroku runに続けてartisanコマンドを打ちこみます。

heroku run php artisan migrate

production環境では以下のような表示があるかもですが、気にせずyes
image.png

マイグレーションが終わったら、アプリケーションのURLにアクセス!

エラーなく画面が表示されたらAPP_DEBUGをfalseにするのを忘れずに。
image.png

お疲れさまでした!

その他やっておくと便利な設定・コマンド

個人的な目線でコレしたいな、というものを数点紹介。
今後、自分で実践したら記事にしたいと思います!

httpsを強制する

ログを確認する
heroku logsコマンドで確認(参考資料)。

ファイルをS3へ保存する

注意書き

herokuの記事はたくさんの方が投稿して下さっているのですが、過去の記事に書かれていた設定手順が、今となっては不要になっている、というトコロも数点ありました。

・PHPの国際化用拡張モジュール(intl)を使う
↓のようなエラーが出て実行できなかったため、断念
image.png

BuildpacksにPHPを設定
image.png

↑の通り、git push heroku masterした時にheroku側がpush内容から自動でBuildPackを設定してくれているっぽいです。
GUI(herokuのアプリ管理画面)のSettingsタブにもPHPが設定されているのが確認するだけでOKかと。

あくまで私自身がやってみて不要だった、というだけなので、場合によってはこれらの設定も必要になるかもしれません。

参考資料

公式リファレンス
Laravel5.7: Herokuにデプロイする
【Heroku】LaravelとMySQLでデプロイする
(youtube動画)How to Deploy Laravel Project with Database on Heroku for Free | Step by Step Tutorial by Code Band
Herokuでpg:psqlを実行しようとしたらThe local psql command could not be located
HerokuのPostgreSQLのdbをみる方法

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

【Portfolio】laravelでサイトを構築(設計→実装→デプロイ)

成果物

★こちらクリック! → Go To "tiny-happiness"★
★こちらクリック! → Go To GitHub★

実装環境

インフラ:AWS
サーバー側:PHP、Laravel 5.8
ソート管理:Git
フロント側:Html,CSS,JavaScript,jQuery

機能一覧

■記事一覧表示
■記事詳細表示
■記事投稿/コメントをつける
■ユーザー新規登録
■ユーザーログイン/ログアウト
■DB操作:Create(生成)、Read(読み取り)、Update(更新)、Delete(削除)
DBテーブルのリレーション操作
■権限分け(RBAC|ロールベースアクセス制御)
■Ajax機能(jQuery)
■画像アップロード

機能の詳細

■記事一覧表示
 ・Laravelのページネーションを使い、configの中にページ数を定義することで、一括で修正することは可能 
■記事詳細表示
 ・登録済みのユーザーが記事の中でコメントできる
 ・記事と記事のコメントをすべて表示する
■記事投稿/コメントをつける
 ・ユーザーのログイン状態を確認してから、登録済みのユーザーがコメントできる
 ・画像アップロードできる(下記の【画像アップロード】にご参照)
■ユーザー新規登録
 ・新規登録するとき、提出前に、ユーザー名の使用状況を確認(下記の【Ajax機能(jQuery)】にご参照)
■ユーザーログイン/ログアウト
 ・ログインするとき、ユーザー名とパスワードのバリデーションを検証する
 ・ログアウトしない限り、ログイン情報がSessionに保存し、2時間以内再度ログインする必要がない
■DB操作:Create(生成)、Read(読み取り)、Update(更新)、Delete(削除)
 ・Create(生成)とき、正規表現とバリデーションでデータの有効性を確認
 ・Read(読み取り)、キーワードによりの検索
 ・Update(更新)、登録済みのデータが表示されたうえで更新する
 ・Delete(削除)、論理削除と物理削除を分ける
DBテーブルのリレーション操作
 ・ユーザーのIDから役割を取得(1対多の逆関係 → belongsTo)
 ・役割から権限を取得(多対多 → belongsToMany)
 ・記事からすべてのコメントを取得(1対多 → hasMany)
 ・記事のIDから記事のユーザーを取得(1対多の逆関係 → belongsTo)
 ・コメントのIDからコメントのユーザーを取得(1対多の逆関係 → belongsTo)
■権限分けで管理操作(RBAC|ロールベースアクセス制御)
 ・Middlewareを設定して、ログインしていないユーザーは管理画面へアクセスできない
 ・管理者:すべての権限を持つ
 ・スタッフ:管理者から付与した権限範囲内で操作
 ・一般ユーザー:自分のデータしか操作できない
 ・権限以外の画面が表示しない
■Ajax機能(jQuery)
 ・新規登録するとき、ユーザー名が存在するかどうか
 ・crud操作する時、Ajaxで送信
■画像アップロード
 ・API(Webupload)を使って、画像アップロードする
 ・画像がアップロードしないとき、ディフォルトの画像を使う
■単体テスト

紹介動画(作成中)

完全版:
分割版(一本/1min以内)

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

【サルが書く】Lazy Eager loadingでもキャストしたい

わきゃ!!

GIGアドベントカレンダー9日目になります!
GIGメンバーの投稿はこちら? から見てみてね!
https://qiita.com/organizations/gig-inc

$test = Test::where(['test' => 'test'])->get();

$test->load(['table_name' => function ($query) {
            $query->orderByRaw('CAST(table_name.column_name AS signed) ASC');
}]);

これでいけると思います。

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

PHPの無名関数を使ってみた

無名関数とは?

公式ドキュメントの引用です。

無名関数はクロージャとも呼ばれ、 関数名を指定せずに関数を作成できるようにするものです。 callable パラメータとして使う際に便利ですが、用途はそれにとどまりません。

https://www.php.net/manual/ja/functions.anonymous.php

callableパラメータ

callableパラメータとしての使い方を見てみます。

<?php

// 使用方法1

// クロージャ
$double = function($number) {
    return $number * 2;
};

$numbers = [1, 2, 3, 4];

// 各値を2倍にしていく
$new_numbers = array_map($double, $numbers);

// 結果
[2, 4, 6, 8]


// 使用方法2

$numbers = [1, 2, 3, 4];

$new_numbers = array_map(function($number) {
    return $number * 2;
}, $numbers);

// 結果
[2, 4, 6, 8]

変数の使用

次に、外部から変数を使用する場合の使い方を見てみます。

エラー例

<?php
$numbers = [1, 2, 3, 4];

// 倍率を指定する。
$magnification = 3;

$new_numbers = array_map(function($number) {
    return $number * $magnification;
}, $numbers);

// 結果
PHP Notice:  Undefined variable: magnification in......

そのまま、変数を使用しようとするとエラーとなります。
そこで、useを使用します。

<?php
$numbers = [1, 2, 3, 4];

// 倍率を指定する
$magnification = 3;

$new_numbers = array_map(function($number) use ($magnification) {
    return $number * $magnification;
}, $numbers);

// 結果
[3, 6, 9, 12]

もちろん参照渡しにすると、変数の値を変更できます。

<?php
$numbers = [1, 2, 3, 4];

// 倍率を指定する
$magnification = 3;

$new_numbers = array_map(function($number) use (&$magnification) {
    // loopごとに倍率に1を足す
    $magnification++;

    return $number * $magnification;
}, $numbers);

// 結果
[4, 10, 18, 28]

型指定

型を指定することができます。
指定していないからといってエラーにはならないですが、安全性、保守性の向上を考えると指定しておいたほうが良いです。
例としてLaravelのEloquentsを使用します。

<?php

$persons = App\Person::all();

array_filter($persons, function(Person $person) {
    // IDEによってはgenderに補完が効き、定義元へジャンプできるようになる。
    return $person->gender == '男子';
});

Laravel使用例

最後にLaravelのCollectionでよく使うので紹介します。

collect(['太郎', null, '次郎'])
    ->reject(function ($name) {
        // 名前がない要素を除外
        return empty($name);
    })
    ->map(function ($name) {
        // 末尾に「さん」をつける
        return $name . 'さん';
    })
    ->values();

values()を使うことでキーをリセットしてくれます。

公式ドキュメントのweb内検索でfunction()と検索するとどのような場面でfunction()を使用しているかわかるので参考になります。
https://readouble.com/laravel/5.8/ja/collections.html

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

LaravelでRailsみたいなデバックがしたい。(スクール卒業生向け)

概要

巷にあふれる某スクール卒業生。
彼らがスクールで学ぶのはRuby on Railsなのだが他人との差別化を図る上でLaravelを用いてポートフォリオを作成する方々を散見する。

彼らがスクールで学んだ、デバック方法は「binding.pry」ほぼ一択。

Laraveでよく見かけるデバック方法は,var_dump(); や dd(); でちょっとモヤモヤしてる方向けの記事になります。

結論

「binding.pry」みたいなことはLaravelでも出来る!!

eval(\Psy\sh());

処理を止めたいところに「binding.pry」のように書いて、ターミナルで確認するだけです。

恐らく、この説明でスクール卒業生は使いこなせると思います。
「大金払ってRuby on Railsを学び、実務で使うのはLaravelなんですか?」
という意見もあると思うのですが、個人的には自分が卒業生でlaravelエンジニアというのもあり応援したい気持ちです。

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

laravel リレーション関係

データベーステーブルの関係は大体下記のように分けられます。

・1対1
・1対多
・多対多

1対1

例:userとphoneの関係、基本一人には一つの番号しか持ってないですね。 

userでphoneを検索したいとき、二つの関係の定義:hasOne
phoneでuserを検索したいとき、二つは逆関係の定義:belongsTo

1対多

例:articleとcommentの関係、一つの記事に複数のcommentが存在しますね。 

articleでcommentを検索したいとき、二つの関係の定義:hasMany
commentでarticleを検索したいとき、二つは逆関係の定義:belongsTo

多対多

例:userとroleの関係、userが多くのroleを持ち、rolesも大勢のuserを持っています。 
二つの表の関係を別の表user_roleで結びつけます。

userでroleを検索したいとき、二つの関係の定義:belongsToMany

Models
Class User extends Model
{
 public function roles()
 {
   return this->belongsToMany('App\Role','role_user','user_id','role_id');
 }
}

roleでuserを検索したいとき、二つは逆関係の定義:belongsToMany

Models
Class Role extends Model
{
 public function users()
 {
   return this->belongsToMany('App\User','role_user','role_id','user_id');
 }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravel6 チェックボックスの値を受け取り保存する

目的

  • Laravel6でチェックボックスの実装とtrue falseを受け取りそれに応じてデータベースのカラムに0か1を格納する

実施環境

  • ハードウェア環境
項目 情報
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.8 Homebrewを用いてこちらの方法で導入→Mac HomebrewでPHPをインストールする
Laravel バージョン 6.X commposerを用いてこちらの方法で導入→Mac Laravelの環境構築を行う
MySQLバージョン 8.0.19 for osx10.13 on x86_64 Homwbrewを用いてこちらの方法で導入→Mac HomebrewでMySQLをインストールする

前提条件

  • 実施環境に近い環境が構築されていること。(筆者は直接Macに環境を構築しているがLaravel6の動作環境なら基本本方法で確認可能である。)

前提情報

  • Authを用いた認証機能があるLaravel6アプリがあること。
  • usersテーブルに1つ以上のデータレコードが存在すること。
  • Larvel6でチェックボックスを実装する。
  • そのチェックボックスがチェックされているかいないかによってDBのusersテーブルのcheck_flagカラム」に格納する値を変更する。
  • 「check_flagカラム」はチェックされていないと0、チェックボックスにチェックされていると1が入る。
  • 「check_flagカラム」に1が入っていた時にチェックボックス入力ページのチェックボックスは最初からチェックがついた状態にする。

概要

  1. カラム作成
  2. ルーティング情報の記載
  3. コントローラファイルの作成と記載
  4. ビューファイルの作成と記載
  5. 確認

詳細

  1. カラム作成

    1. アプリ名ディレクトリで下記コマンドを実行してマイグレーションファイルを作成する。

      $ php artisan make:migrate add_check_flag_columns_to_users_table --table=users
      
    2. 作成されたマイグレーションファイルを開き下記のように記載する。下記にはマイグレーションファイルの全内容を記載する。

      アプリ名ディレクトリ/database/migrations/YYYY_MM_DD_HHMMSS_add_check_flag_columns_to_users_table.php
      <?php
      
      use Illuminate\Database\Migrations\Migration;
      use Illuminate\Database\Schema\Blueprint;
      use Illuminate\Support\Facades\Schema;
      
      class AddCheckFlagColumnsToUsersTable extends Migration
      {
          /**
           * Run the migrations.
           *
           * @return void
           */
          public function up()
          {
              Schema::table('users', function (Blueprint $table) {
                  //
                  $table->tinyInteger('check_flag')->default(0)->after('password');
              });
          }
      
          /**
           * Reverse the migrations.
           *
           * @return void
           */
          public function down()
          {
              Schema::table('users', function (Blueprint $table) {
                  //
                  $table->dropColumn('check_flag');
              });
          }
      }
      
  2. ルーティング情報の記載

    1. ルーティングファイルを開き、下記の3行を追記する。

      アプリ名ディレクトリ/routes/web.php
      Route::get('/input', 'TestController@input')->name('input');
      Route::post('/check_register', 'TestController@register')->name('check.register');
      Route::get('/output', 'TestController@output')->name('output');
      
  3. コントローラファイルの作成と記載

    1. アプリ名ディレクトリで下記コマンドを実行してコントローラファイルを作成する。

      $ php artisan make:controller TestController
      
    2. 作成されたファイルに下記の内容を記載する。下記にはコントローラファイルの全内容を記載する。

      アプリ名ディレクトリ/app/Http/Controllers/TestController.php
      <?php
      
      namespace App\Http\Controllers;
      
      use Illuminate\Http\Request;
      use App\User;
      
      class TestController extends Controller
      {
          public function input()
          {
              $user_infos = User::select('*')->get();
              return view('tests.input', ['user_infos' => $user_infos]);
          }
      
          public function register(Request $request)
          {
              $input_data = $request->all();
      
              foreach ($input_data['user_id'] as $key => $value) {
                  $user_info = User::select('*')->find($key);
                  $user_info->check_flag = $value;
                  $user_info->save();
              }
              Log::info($request);
              return redirect(route('output'));
          }
      
          public function output()
          {
              $user_infos = User::select('*')->get();
              return view('tests.output', ['user_infos' => $user_infos]);
          }
      }
      
  4. ビューファイルの作成と記載

    1. アプリ名ディレクトリで下記コマンドを実行してビューファイル格納用のディレクトリを作成する。

      $ mkdir resources/views/tests
      
    2. input.blade.phpファイルとoutput.blade.phpファイルをアプリ名ディレクトリ/resources/views/tests直下に作成する。

    3. 先に作成した2つのビューファイルに下記の内容を記載する。

      • input.blade.php

        input
        <form action="{{ route('check.register') }}" method="post">
            @csrf
            <table border="1">
                <tr>
                    <th>チェックボックス</th>
                    <th>id</th>
                    <th>name</th>
                    <th>email</th>
                </tr>
                @foreach ($user_infos as $user_info)
                <tr>
                    <input type="hidden" name="user_id[{{ $user_info['id'] }}]" value="0">
                    @if ($user_info['check_flag'] === 1)
                    <td><input type="checkbox" checked="checked" name="user_id[]" value="{{ $user_info['id'] }}"></td>
                    @else
                    <td><input type="checkbox" name="user_id[]" value="{{ $user_info['id'] }}"></td>
                    @endif
                    <td>{{ $user_info['id'] }}</td>
                    <td>{{ $user_info['name'] }}</td>
                    <td>{{ $user_info['email'] }}</td>
                </tr>
                @endforeach
            </table>
            <button type="submit">保存</button>
        </form>
        
      • output.blade.php

        output
        <table border="1">
            <tr>
                <th>チェックボックス</th>
                <th>id</th>
                <th>name</th>
                <th>email</th>
            </tr>
            @foreach ($user_infos as $user_info)
            <tr>
                @if ($user_info['check_flag'] === 1)
                <td><input type="checkbox" checked="checked" name="user_id[]" value="{{ $user_info['id'] }}"></td>
                @else
                <td><input type="checkbox" name="user_id[]" value="{{ $user_info['id'] }}"></td>
                @endif
                <td>{{ $user_info['id'] }}</td>
                <td>{{ $user_info['name'] }}</td>
                <td>{{ $user_info['email'] }}</td>
            </tr>
            @endforeach
        </table>
        
  5. 確認

    1. ローカルサーバを起動し下記にアクセスする。
    2. テーブル状にusersテーブルのデータが表示されるので任意のレコードのチェックボックスにチェックをいれて「保存」をクリックする。

      127_0_0_1_8001_input.png

    3. outputの画面に移動する。

    4. 再度下記にアクセスする。

    5. 先程チェックを入れて保存したカラムのチェックボックスはデフォルトでチェックが入った状態になっている。

      127_0_0_1_8001_input-2.png

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

LaravelのBladeテンプレートにCSSやJavascriptを書かない理由はこれかな?

はじめに

この記事は、私個人の意見を文章にしているだけの記事です。
そのため、コードサンプルや、技術の説明のような情報はありません。
もし、コードサンプルや技術説明をお探しであれば、別の方の記事を参考にしてください。

Bladeテンプレートでの問題点

以前、次のような記事を書きました。

LaravelのBladeテンプレートをVue.jsっぽく書いてみた

この手法だと、部品毎で分割しやすく、コードも見やすくなると思っていたんですよね。
しかし、実際に開発していて大きな問題が1つ発生しました。
それは、JavascriptのUnitテストができないことです。

解決方法の検討

次のように解決方法を検討しました。

1. Javascriptを外部のJSファイルに記述する

これは、Vue.jsやReact等の、フロントエンドのフレームワークを利用しない場合、当たり前の記述方法だと思います。
Laravelでは、Bladeテンプレートファイルの中でCSSやJavascriptを使用することは推奨されていません。
しかし、明確に理由まで記載してある情報ソースを見つけることができなかったので、自分は「盲目的にルールを守ること」はしませんでした。
その結果、問題点が発覚したので、反省が必要ですね。
とはいえ、Bladeテンプレート内に書くJavascriptのコードはほとんどがDOM操作です。
なので、外部ファイルにして単体テストをしたところで、どこまで信頼できるものかわかりませんね。
※DOM操作は他のUIに依存するものが発生しやすいので、結局は判定ロジックや、算出ロジックだけのテストになるのかと。。

2. フロントエンドフレームワークを利用する

これは、ある程度大きなプロダクトでは、当たり前の話です。
しかし、技術選定時は、「できるだけ多くの人が参画しやすいようにする」をテーマに、Laravelを選定しました。
というのも、私が参画させていただいている案件は、企業したてのスタートアップ企業様の案件です。
そのため、スペシャリストよりも、フルスタックな人が必要だったんですよね。

経営を考慮しての技術選定だったので、現段階でLaravelを選定したことについては仕方なかったと思っています。
しかし、Unitテストができないというのは、品質を担保する指標が1つなくなること。
なので、後々はフロントエンドのスペシャリストに参画していただき、Vue.jsやReactを採用してUnitテストができる状態も必要なのかなとも思います。
その時は、売り上げが拡大されてきた時ですかね。
※余談ですが、VueやReactを使えばSPAも簡単に実現できますし、付加価値は付けやすいでしょう。

3. Integrationテストの仕組み構築に尽力する

現段階では、これが一番現実的なのではないかと考えています。
ここまでは、Unitテストのことしか考えていませんでした。
しかし、Integrationテストでしっかりテストをする仕組みがあり、かつ、デバッグがしやすいのであれば問題ないかと思っています。
というのも、Integrationテストでバグが発生したらUnitテストからやり直しになることも多々あります。
また、網羅率100%のUnitテスト用コードを目指すと、テストをすることが目的になる可能性もあるんですよ。
そうなると、無駄なことに工数をかけてしまい、ビジネス的にはよろしくないですよね。
なので、今回はこの方向性が一番有効かと思います。

最後に

この記事では、設計による問題の解決手法を検討した内容を記載しました。
今回の場合だと、予算が潤沢にある企業様だとしても「技術選定からやり直し」や、「Unitテストのためだけの設計変更」はするべきではないと思います。
というのも、出戻りばかりしていたら、リリースまでたどり着けないからです。
スタートアップの企業様ですと収益化できない期間が長くなるのは、より苦しいですよね。
そのため、今回は、「出戻りはしない」かつ、「品質は落とさない」のどちらにも配慮して検討してみました。
もし、本当に設計変更やフレームワークの導入をするのであれば、スケールして、予算に余裕が出てきた時で良いと思います。

最後まで読んでいただき、ありがとうございます:relaxed:

私は、参画させていただいている企業様に心より感謝し、プロジェクトの成功に尽力致します。そして、自分のスキルアップに尽力することが、お世話になる企業の関係者様の利益になると信じています。

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

Laravel SubstituteBindingsミドルウェア

Laravel SubstituteBindingsミドルウェアについて調べてみた。
SubstituteBindingsミドルウェアは、Kernel.phpでwebとapiグループのミドルウェアとして指定されている。
Substitute = 代替(代わりに使う)。Binding = 紐づける

Kernel.php
    protected $middlewareGroups = [
        'web' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            // \Illuminate\Session\Middleware\AuthenticateSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            \App\Http\Middleware\VerifyCsrfToken::class,
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],

        'api' => [
            'throttle:api',
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],
    ];

SubstituteBindingsは

ルートモデルバインディング用の機能。
https://laravel.com/docs/6.x/routing#route-model-binding

routeにfunctionを入れてモデルを引数にすると、pathの値を元に自動でfindOrFailして引数として渡してくれる。
{user} pathでUserが取得できない場合は404エラーが返される。

api.php
Route::get('/user/{user}', function (\App\Models\User $user) {
    return $user;
});
curl http://127.0.0.1:8000/api/user/1
{"id":1,"name":"aaa","email":"hoge@co.jp","email_verified_at":null,"api_token":"WzLJB6Djg7HS9vDp8Ss8RqcIKYPYvs2pJBUl8CYMwsnVCHXK3FeIEdLBFq8Q","created_at":"2020-12-04T14:46:11.000000Z","updated_at":"2020-12-04T14:46:11.000000Z"}%     

SubstituteBindingsをミドルウェアから外した状態で再度アクセスするとLaravelコンテナによってUserモデルがインスタンス化された状態で引数に追加される。($userはnullではないが$user->idとかはnullになる)

SubstituteBindings読む

SubstituteBindingsのコンストラクタでRegisterを引数でもらっている。

SubstituteBindings.php
    public function __construct(Registrar $router)
    {
        $this->router = $router;
    }

handle関数では、bindingを行っている。
binding処理はコンストラクタで受け取ったRegisterクラスで行っている。

    public function handle($request, Closure $next)
    {
        $this->router->substituteBindings($route = $request->route());

        $this->router->substituteImplicitBindings($route);

        return $next($request);
    }

Register

Illuminate\Foundation\Application.phpのregisterCoreContainerAliases関数でRegisterのaliasが設定されている。
Registerクラスは\Illuminate\Routing\Router::classの型を使ってDIされる。

Application.php
'router'               => [\Illuminate\Routing\Router::class, \Illuminate\Contracts\Routing\Registrar::class, \Illuminate\Contracts\Routing\BindingRegistrar::class],

substituteBindings

substituteBindingsは明示的なpathとModelの結合の処理に使用される。
substituteBindingsの関数を見ると、routeのparameterを元にパラメータを設定している。
Routerにはpath名と型を紐づけるbinderを管理するbidnersプロパティがあり、そこに該当するbinderが設定されているとperformBinding関数を使ってパラメータを取得し、setParameterでセットしている。
binderはbind関数で追加されるが、これはmodel関数で使用されている。
model関数は Route::model('user', \App\Models\User::class); のように使われる。

Router.php
    public function substituteBindings($route)
    {
        foreach ($route->parameters() as $key => $value) {
            if (isset($this->binders[$key])) {
                $route->setParameter($key, $this->performBinding($key, $value, $route));
            }
        }

        return $route;
    }

↓routeのパラメータはpathを分解した時のpath名をkeyに値をvalueにした配列になっている。

Route::get('hoge/{hogeId}', [\App\Http\Controllers\HogeController::class, 'hoge']);
↓route->parameters()の値
{"hogeId":"11"}

substituteImplicitBindings

substituteImplicitBindingsは暗黙的なpathとモデルの結合に使用される。
Implicit = 暗黙的

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