20191011のlaravelに関する記事は5件です。

最低限の労力でLaravelのタスクスケジュール ( Task Scheduling ) + AWS ECS

最低限の労力でLaravelのタスクスケジュール ( Task Scheduling ) + AWS ECS

※ ある程度LaravelのCommandやECSを利用している人向けの記事です
※ イメージにはphp:7.3-fpm-alpineベースで、他にミドルウェアを色々入れたものを利用
※ Laravelは6.0を利用

EC2 + cron + Laravelで動いていた定期処理実行サーバを、ECSに移すことになりました。
以前から、WebはECS + Laravel (+ Nginx)で動かしてました
まあ、インフラ管理方法が2種類あるのは負担でしかないですよね。。。

それで、方法を考えると、ECSの"タスクのスケジューリング"だろうとは思ったのですが

  • たくさんECSのスケジュールタスクを作るのは面倒...
  • CloudFormationを書くには、このプロジェクトは遅すぎた。cloudformationの運用も面倒...

そんなわけで、Laravelのタスクスケジュールと、ECSの”タスクのスケジューリング”を組み合わせてみました。
両方似たような名前で頭がオカシクなりそうです。

注意事項

これから紹介する方法は、以下を許容できる処理に限定した方が良いと思います

  • スケジュールが多少いい加減で良い
    • イメージの内容によるが、コンテナ(タスク)が立ち上がるまでに数十秒かかる
  • 時間に対応したスケジュールが一個ずつ実行されるため、全ての処理が完了するまでに時間がかかる
    • ちなみにrunInBackground()を利用したら処理完了前にコンテナが終了すると思います(戒め)
  • 時間がかかる処理では、同じコマンドが並列実行されるリスクがある
    • つまり、コンテナが多重起動している状態もあるということ
    • 多重でScheduleを実行させないための実装がLaravelにあり
      • onOneServer(), withoutOverlapping()
      • onOneServer()の方が良さそう(小並感)
    • コンテナ(ECSタスク)の多重起動をECS側で禁止する方法もあるが、大事なタイミングでコマンドが実行されない危険性があるので、おすすめしない

重要な処理は、個別にECSタスク定義とECSスケジュールタスクを作った方が良いかと思います。
(同じタスク定義を利用しても設定でCMDを上書きできます。しかし、タスクの設定でタスク定義のリビジョンを上げるたびに再度指定することになります。おすすめしません。)

全体像

Laravelのタスクスケジュールで、定期実行の設定を記述

以下を参考に、淡々と記述すればOK。
https://readouble.com/laravel/6.0/ja/scheduling.html

EC2なんかでcronですでに設定されていれば、cron()使えば楽にスケジュールを指定可能。

ECSのスケジュールタスクを設定

  • ECSでLaravelが動くDockerイメージが作成できていれば、それを利用してバッチ処理用のタスク定義を作成
    • タスク定義では、コンテナ設定でCMDを php artisan schedule:run に変更
      • すでにLaravelがPHP-FPMとかで動くイメージを利用する場合、起動時の動作(CMD)を上書きできる状態に変更
        • ENTRYPOINTが指定されていた場合、ENTRYPOINTは実行時に上書きできないし、CMDはENTRYPOINTの引数扱いとなり、実行時の挙動を上書きできない
        • ベースがphp-fpmのイメージなら、DockerfileでENTRYPOINTを [] に上書き(無効化)して、CMDを["docker-entry-point","php-fpm"]とかに変えればいいと思います

ECSで"タスクのスケジューリング"を設定

上記のタスクを毎分起動するように設定。

(おまけ) エラー通知

例外発生時には、通知はもちろんスタックトレースとかも確認できるようにしたい。
LaravelのScheduleには、before, after, onFailuereといったフックがあるんですが、出力を参照できないっぽいから、こいつらは大した事はできないです。これはすごい悩んだ。

いやまあ、杞憂でしたけどorz。

エラー時には、普通にHandlerに引っかかります。
つまり、app/Exceptions/Handler.php内の処理。
既にSlack通知なりが設定されてれば、ECSスケジュールタスクでの例外発生時にも、同様にSlack通知されます。
定期処理だけ特別な処理をしたいなら、タスク定義に定期処理フラグ的な環境変数でも適当に設定して、その環境変数の値をもってHandlerの処理を分岐させれば良いと思われます。

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

Laravelの実行環境をDockerで構築する

概要

docker-composeを使ってLaravelの実行環境を構築します。
作成するコンテナはPHP-FPM、Nginx、MySQLの3つです。

環境

Docker:19.03 ホストOS:Ubuntu 18.04(Vagrant)
Laravel:6.1.0 PHP-FPM:7.3 PHP:7.3.10  Nginx:1.17.4 MySQL:8.0
*Dockerおよびdocker-composeはホストOS(Vagrant上のUbuntu)上にインストールされているものとします。

ディレクトリ構成

docker
|- docker-compose.yml
|- laravel
|- mysql 
|   |- Dockerfile
|   |- my.cnf
|   ∟ sql
|     ∟ init.sql
|- nginx
|   |- Dockerfile 
|   ∟  default.conf
∟  php
   |- Dockerfile 
   ∟  local.ini

構築手順

1.Laravelとパッケージのインストール

初めに、ホストOS上にLaravelとパッケージをインストールします。コンテナにマウントする形でLaravelのファイルのコピーを行います。

・Laravelのインストール

最上位のdockerディレクトリ内で以下のコマンドを実行します。gitがない場合、先にインストールしてください。

git clone https://github.com/laravel/laravel.git laravel

dockerディレクトリにlaravelというディレクトリが作成され、Git HubからLaravelのプロジェクトがクローンされます。

・パッケージのインストール

作成されたlaravelディレクトリに移動して、コンテナからcomposer installを実行します。

cd laravel
docker run --rm --interactive --tty --volume $PWD:/app composer install

このコマンドは、Composerイメージからコンテナを一時的に作成し、Laravelに必要なパッケージをインストールします。

Laravelディレクトリをコンテナ内にマウントすることで、Composerコンテナがcomposer.jsonファイルを読み込んで利用できるようになっています。コンテナは実行後に消滅しますが、ディレクトリをマウントしているためホストOS上にインストールしたパッケージが残ります。公式のComposerイメージで紹介されている使用法です。

公式Composerイメージ

*ComposerイメージがホストOS上にない場合、イメージがdocker hubよりpullされます。

*注意:Composer実行中に以下のエラーが発生する場合があります。
proc_open(): fork failed errors
これはホストのメモリ不足によるものです。VMのメモリが1024MBだと発生するのでメモリを増やしましょう。

2.docker-compose.yml作成

続いて、以下のようなdocker-compose.ymlを作成します。

docker-compose.yml
version: '3'

services:

  php:
    build: ./php
    image: app_php:7.3-fpm
    container_name: php
    restart: unless-stopped
    working_dir: /var/www
    volumes:
      - ./php/local.ini:/usr/local/etc/php/conf.d/local.ini
      - ./laravel:/var/www
    networks:
      - app-network

  nginx:
    build: ./nginx
    image: app_nginx:1.17.4
    container_name: nginx
    restart: unless-stopped
    ports:
      - 80:80
      - 443:443
    volumes:
      - ./laravel:/var/www
    networks:
      - app-network
    depends_on:
      - php

  mysql:
    build: ./mysql
    image: app_mysql:8.0
    container_name: mysql
    restart: always    
    ports:
      - 3306:3306    
    environment:
      MYSQL_DATABASE: laravel
      MYSQL_ROOT_PASSWORD: pass
      MYSQL_USER: user
      MYSQL_PASSWORD: pass
      TZ: 'Asia/Tokyo'
    command: --default-authentication-plugin=mysql_native_password
    volumes:
      - ./mysql/sql:/docker-entrypoint-initdb.d
      - mysql-volume:/var/lib/mysql/
    networks:
      - app-network

networks:
  app-network:
    driver: bridge

volumes:
  mysql-volume:
    driver: local

・PHPコンテナ

・image
通常、imageはコンテナで使用するイメージを指定しますが、「build」を使ってDockerfileからコンテナを起動する場合、docker-composeで作成するイメージにイメージ名とタグ名を付けることができます。ここでは「app_php」というイメージ名、「7.3-fpm」というタグ名を付与しています。

・volumes
./php/local.ini:/usr/local/etc/php/conf.d/local.ini
ホスト上のphpディレクトリ内のlocal.iniを、コンテナ内の「/usr/local/etc/php/conf.d/local.ini」にコピーします。こうすることで、コンテナ内のデフォルトのphp.iniを上書きすることができます。php.iniファイル自体は上書きされませんが、phpinfo()で情報を出力すると上書きされていることを確認できます。local.iniは後に作成します。

./laravel:/var/www
ホスト上でインストールしたlaravelディレクトリを、コンテナの「/var/www」とマウントします。こうすることで、ドキュメントルートを「/var/www/public」とすることができます。
開発環境であれば問題ありませんが、データをホストOSにマウントするのは推奨されていないため、本番で使う時はDockerボリュームを作成してマウントすることを検討しましょう。

*注意
セキュリティを向上させるため、ディレクトリをリードオンリーでマウントすることが推奨されていますが、Laravelではファイルシステムをリードオンリーにするとエラーが発生します。

./laravel:/var/www:ro(リードオンリー) → エラーが発生します。

・Nginxコンテナ

・volumes
./laravel:/var/www
Nginxコンテナでもlaravelディレクトリをマウントします。こうすることで、Nginxが受け付けたリクエストをLaravelで返すことができます。

・MySQLコンテナ

・command
--default-authentication-plugin=mysql_native_password
上記のコマンドでMySQLの認証方式を変更しています。MySQLはバージョン8.0から認証方式を変更しており、「caching_sha2_password」がデフォルトとなっています。PHPのMySQL接続ライブラリが未対応のため、認証方式を「mysql_native_password」に戻します。

・volumes
./mysql/sql:/docker-entrypoint-initdb.d
コンテナ内の「docker-entrypoint-initdb.d」以下にSQLを配置することで、初期設定などを行うことができます。

mysql-volume:/var/lib/mysql/
MySQLのデータをDockerのボリュームにマウントして永続化します。Dockerのボリュームはコンテナを削除しても残ります。そのため、コンテナに変更を加える場合、volumeを削除しなければenvironment等の設定を変更しても反映されないので気を付けましょう。

・environment
任意のユーザーとパスワードを設定してください。

*注意
Dockerにおける永続化データの保存には、データ専用コンテナを作成する方法がありますが、docker-composeのバージョン3からvolumes_fromオプションが削除されているため使用することができません。

3.Dockerfileの作成

次に、各コンテナを生成するDockerfileを作成します。

・PHPコンテナのDockerfile

phpディレクトリ内に以下のようなDockerfileを作成します。

Dockerfile
FROM php:7.3-fpm

# Set working directory
WORKDIR /var/www

# Install dependencies
RUN apt-get update && apt-get install -y \
    build-essential \
    default-mysql-client \
    libpng-dev \
    libjpeg62-turbo-dev \
    libfreetype6-dev \
    libmcrypt-dev \
    libmemcached-dev \
    libzip-dev \
    locales \
    zip \
    jpegoptim optipng pngquant gifsicle \
    vim \
    unzip \
    git \
    curl

# Clear cache
RUN apt-get clean && rm -rf /var/lib/apt/lists/*

# Install extensions
RUN docker-php-ext-install pdo_mysql exif bcmath && \
    docker-php-ext-configure zip --with-libzip && \
    docker-php-ext-install zip && \
    docker-php-ext-configure gd \
    --with-gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ --with-png-dir=/usr/include/ && \
    docker-php-ext-install gd

# Install composer
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

# Add user for laravel application
RUN groupadd -g 1000 php && \
    useradd -u 1000 -ms /bin/bash -g php laravel

# Change current user to laravel
USER laravel

# Expose port 9000 and start php-fpm server
EXPOSE 9000
CMD ["php-fpm"]

・ベースイメージ
公式イメージである「php:7.3-fpm」をベースイメージとしています。php:7.3-fpmのベースイメージは「debian:buster-slim」となっています。LaravelのプログラミングはPHPコンテナに入って行いますが、debianの上で行っていることを頭に入れておきましょう。

・ライブラリ、エクステンションのインストール
PHPおよびLaravelの実行に必要なライブラリ、エクステンションをインストールしています。

default-mysql-client
debian:buster-slimはmariadbをデフォルトとしており、mariadb-clientが使用されます。そのため、mysql-clientはインストールできません。その代わり、mysqlを利用するために「default-mysql-client」というものが用意されているので、そちらをインストールしています。

bcmath
Laravelを実行するためのサーバー要件としていくつかのエクステンションが必要ですが、多くはPHP本体に含まれています。ただし、bcmathが含まれていないため、別途「docker-php-ext-install」でインストールしています。

Laravelのサーバー要件

・ユーザーの作成
rootユーザーでDockerを操作するのはセキュリティ上好ましくないため、laravelというユーザーを作成しています。

・NginxコンテナのDockerfile

nginxディレクトリ内に以下のDokcerfileを作成します。

Dockerfile
FROM nginx:1.17.4
COPY default.conf /etc/nginx/conf.d/default.conf

イメージはdocker-compose.ymlに指定することもできますが、docker-composeで作成するイメージにイメージ名を付与することができるため、docker-composeのbuildからイメージを作成するようにしています。また、後に作成するdefault.confをコンテナ内にコピーしています。

・MySQLコンテナのDockerfile

mysqlディレクトリ内に以下のDokcerfileを作成します。

Dockerfile
FROM mysql:8.0
COPY my.cnf /etc/mysql/conf.d/my.cnf
RUN chmod 644 /etc/mysql/conf.d/my.cnf
CMD ["mysqld"]
EXPOSE 3306

コンテナ内の「/etc/mysql/conf.d/my.cnf」に設定ファイルをコピーすることで、デフォルトのmy.cnfを上書きすることができます。
Windowsを使用している場合、VirtualBox経由でマウントしているディレクトリ/ファイルはすべてパーミッションが777 になります。MySQLは777の.cnfファイルを読み込みません。そのため、ファイルのパーミッションを変更しています。

4.設定ファイルの作成

次に、各コンテナで使用する設定ファイルを作成します。

・PHPの設定ファイル

phpディレクトリ内にlocal.iniという設定ファイルを作成します。

local.ini
disable_functions = pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,
memory_limit = 256M
post_max_size = 100M
upload_max_filesize = 100M
date.timezone = "Asia/Tokyo"
mysqlnd.collect_memory_statistics = Off
mbstring.language = Japanese
mbstring.internal_encoding = UTF-8
mbstring.http_input = pass
mbstring.http_output = pass
mbstring.encoding_translation = Off
mbstring.detect_order = auto

デフォルトのphp.iniが上書きされるので、変更が必要な項目のみ記載します。設定はLaravelの公式の開発環境であるHomesteadやLaradockを参考にしています。Homesteadではセキュリティの関係上、pcntl系の関数を使用しないようにしているようです。「memory_limit」や「post_max_size」などは、必要に応じて適宜変更してください。

・Nginxの設定ファイル

nginxディレクトリ内にdefault.confという設定ファイルを作成します。

default.conf
server {
    listen 80;
    root /var/www/public;

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

    index index.html index.htm 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 php:9000;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }

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

この設定はLaravelの公式ドキュメントの設定です。公式の設定では「fastcgi_pass unix:/var/run/php/php7.2-fpm.sock;」となっているところを「fastcgi_pass php:9000;」と変更しています。コンテナ間で通信が行えるようにするため、php7.2-fpm.sock(Unixドメインソケット)を使うのではなく、PHPコンテナの9000番のポートを指定(php:9000)してTCP通信を行います。

Laravel公式ドキュメントのNginx設定

・MySQLの設定ファイル

mysqlディレクトリ内にmy.confという設定ファイルを作成します。

my.conf
[mysql]
default-character-set=utf8mb4

[mysqld]
character-set-server=utf8mb4
skip-character-set-client-handshake

[mysqldump]
default-character-set=utf8mb4

[client]
default-character-set=utf8mb4

こちらもデフォルトのmy.cnfを上書きできるため、変更する項目のみ記載します。日本語を使えるようにする設定です。

また、mysql/sqlディレクトリ内に以下のinit.sqlというファイルを作成します。

init.sql
GRANT ALL PRIVILEGES ON *.* to user@"%";
FLUSH PRIVILEGES ;

こちらはコンテナが起動する際に実行されるコマンドです。作成したユーザーに権限を与えます。

5.コンテナの起動とMySQLの接続確認

・コンテナの起動

docker-compose.yml、Dockerfile、設定ファイルを作成した後、コンテナを起動します。
コンテナを起動する前に、Laravelの設定ファイルを「.env」として作成します。その後、docker-compose.ymlファイルのあるdockerディレクトリに移動します。

cp .env.example .env
cd ..

docker-composeでコンテナを起動します。以下のコマンドを実行すると、各コンテナのイメージも作成されます。途中で「debconf: delaying package configuration, since apt-utils is not installed」という警告が出ますが、単なる警告なので問題ありません。すべての処理が完了するまで数分かかります。

docker-compose up -d

処理が完了したら、以下のコマンドで確認します。

docker-compose ps

以下のようにコンテナが立ち上がっていたら成功です。

Name               Command               State                    Ports
-----------------------------------------------------------------------------------------
mysql   docker-entrypoint.sh --def ...   Up      0.0.0.0:3306->3306/tcp, 33060/tcp
nginx   nginx -g daemon off;             Up      0.0.0.0:443->443/tcp, 0.0.0.0:80->80/tcp
php     docker-php-entrypoint php-fpm    Up      9000/tcp

次に、Laravelのアプリケーションキーを設定します。PHPコンテナ内でphp artisanコマンドを実行します。

docker-compose exec php php artisan key:generate

Laravelの設定ファイル.envの「APP_KEY」にキーが設定されます。次のコマンドで設定をキャッシュに反映させます。

docker-compose exec php php artisan config:cache

ホストOSのIPアドレス(Vagrantfileに設定したIP)にブラウザからアクセスして以下の画面が表示されたら正常に動作しています。

laravel.png

・MySQLの接続確認

最初に、Laravelのデータベースの設定を行います。.envファイルを次のように設定します。

DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=user
DB_PASSWORD=pass

DB_HOSTにはコンテナ名であるmysqlと記述します。続いて、config/database.phpファイルを設定します。「'strict' => false」は、「mysql_native_password」の認証方式を利用するための設定です。

'host' => env('DB_HOST', 'mysql'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'user'),
'password' => env('DB_PASSWORD', 'pass'),
'strict' => false,

以下のコマンドで設定をキャッシュに反映させます。

docker-compose exec php php artisan config:cache

PHPコンテナに入り、接続の確認をしていきます。

docker exec -it php /bin/bash

コンテナに入ったら、以下のコマンドでデータベースのマイグレーションを実行します。

php artisan migrate

続いてtinkerを起動します。

php artisan tinker

次のコマンドを実行して、マイグレーションを行ったデータが返ってきたらMySQLとの接続が成功しています。

\DB::table('migrations')->get();

「Ctrl + C」を押してtinkerを終了し、ロールバックを行います。

php artisan migrate:rollback

なお、PHPコンテナにログインすると、Dokcerfileで定義したユーザーとしてログインします。rootユーザーではないため、コマンドの実行が制限されています。rootとしてログインしたい場合は、以下のようにuオプションに0を指定します。

docker exec -it -u 0 php /bin/bash

以上でDockerによるLaravelの実行環境の構築は終了です。

参考にした記事

今回の環境構築は次の記事を参考にさせて頂きました。

How To Set Up Laravel, Nginx, and MySQL with Docker Compose

Dockerを使ってLaravel開発環境構築

Dockerは非常に奥が深く、習得するのに時間がかかるツールと思いますが、Dockerfileやdocker-compose.ymlを自分で書くことで理解を深めていくことができます。

この記事が参考になれば幸いです。

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

CircleCIでのテスト実行時間を短縮する:コンパイル結果のキャッシュ

CircleCIでのテスト実行時間を短縮する:テストの並列化 を行いましたが
まだ速くなるはず。

実行結果を眺めるとnpm run devでかなりの時間がかかっている。
pug, css, vueあたりのassets配下が更新されていなければキャッシュできるのではと思い対応してみる。

キャッシュに対応したCircleCI設定

.circleci/config.yml
version: 2
references:
  php-c-image: &php-c-image
    image: sencorp/docker-php-c:v2.0.0
  postgres-image: &postgres-image
    image: circleci/postgres:10.1
    environment:
      POSTGRES_DB: hoge
      POSTGRES_USER: fuga
      POSTGRES_PASSWORD: fuga
jobs:
  build:
    docker:
      - *php-c-image
    working_directory: ~/repo
    steps:
      - checkout
      - restore_cache:
          keys:
              - composer-v2-{{ checksum "laravel/composer.lock" }}
      - run:
          name: composer install
          working_directory: ~/repo/laravel
          command: composer install -n --prefer-dist
      - save_cache:
          key: composer-v2-{{ checksum "laravel/composer.lock" }}
          paths:
            - laravel/vendor
      - restore_cache:
          keys:
              - npm-v2-{{ checksum "laravel/package-lock.json" }}
      - run:
          name: if there is no node_modules, npm ci
          working_directory: ~/repo/laravel
          command: |
            if [ ! -d node_modules ]; then
              npm ci
            fi
      - save_cache:
          key: npm-v2-{{ checksum "laravel/package-lock.json" }}
          paths:
            - laravel/node_modules
  test:
    docker:
      - *php-c-image
      - *postgres-image
    environment:
      APP_ENV: testing
      DB_HOST: localhost
      DB_PORT: 5432
      PAGER: cat # prevent psql commands using less
    working_directory: ~/repo
    steps:
      - checkout
      - restore_cache:
          keys:
              - composer-v2-{{ checksum "laravel/composer.lock" }}
      - restore_cache:
          keys:
              - npm-v2-{{ checksum "laravel/package-lock.json" }}
      - run:
          name: output assets last commit
          command: git log -1 laravel/resources/assets/ > assets_last_commit
      - restore_cache:
          keys:
              - npm_run-v2-{{ checksum "laravel/package-lock.json" }}-{{ checksum "assets_last_commit" }}
      - run:
          name: if there is no public/mix-manifest.json, npm run dev
          working_directory: ~/repo/laravel
          command: |
            if [ ! -f public/mix-manifest.json ]; then
              npm run dev
            fi
      - save_cache:
          key: npm_run-v2-{{ checksum "laravel/package-lock.json" }}-{{ checksum "assets_last_commit" }}
          paths:
            - laravel/resources/views
            - laravel/public/assets
            - laravel/public/mix-manifest.json
      - run:
          name: npm run dev
          working_directory: ~/repo/laravel
          command: npm run dev
      - run:
          name: dump-autoload
          working_directory: ~/repo/laravel
          command: composer dump-autoload
      - run:
          name: db migrate
          working_directory: ~/repo/laravel
          command: php artisan migrate
      - run:
          name: seed data
          working_directory: ~/repo/laravel
          command: php artisan db:seed
      - run:
          name: run test
          working_directory: ~/repo/laravel
          command: ./vendor/bin/paratest --processes 10
workflows:
  version: 2
  build-syntax-test:
    jobs:
      - build:
      - test:
          requires:
              - build

どうキャッシュするか

npm run devが必要な判定をどう簡単に実現するのかを少し悩みました。
辿り着いたのは resources/assets/ 配下にコンパイルが必要なファイルが入っているので、そこのgit logを利用することに。

あとはpublic/mix-manifest.jsonファイルが存在しているかどうかでキャッシュが復元されているかを判断しています。

結果

キャッシュがヒットしないときにはコンパイルの時間がそのまま必要ですが、
ヒットした際にはrestoreされるだけですので1秒以下に!

これでかなり改善されました。

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

Laravel テスト周りの設定

Laravelを使った開発時のテスト周りの設定の覚え書きです。

環境

・Laravel 5.8.35
・PHP 7.1.3
・PHPUnit 7.5.16

1. テスト用DBの作成

DBのデータを取り扱う処理をテストすることがあると思われますので、テスト用のDBを作成します。
普段の開発で使用しているDBをそのまま使用することもできますが、デメリットが多いのでテストで使用するDBは別に作成するのが無難でしょう。
DBの名前はphpunit.xml(後述)の記述との整合性がとれていればなんでも大丈夫です。

DBを作成したら、必要に応じてmigrationやseedingなどを行います。
このときartisanコマンドを使用すると思いますが、データベースを指定するオプションを忘れないよう注意してください。config/database.phpの'connections'内に追記した項目名(後述)を指定してください。

php artisan migrate --database=[config/database.phpの'connections'内に追記した項目名]
php artisan db:seed --database=[config/database.phpの'connections'内に追記した項目名]

2. config/database.phpにテスト用DBの設定を追加

テスト用DBを作成したら、config/database.phpに設定を追加します。記述場所は、'connections'の配列の中です。今回は開発環境でMySQLを使用していますので、同じくMySQLの設定を追記します。

    'connections' => [

        'sqlite' => [
            'driver' => 'sqlite',
            'database' => env('DB_DATABASE', database_path('database.sqlite')),
            'prefix' => '',
        ],
        //開発用DB
        'mysql' => [
            'driver' => 'mysql',
            'host' => env('DB_HOST', '127.0.0.1'),
            'port' => env('DB_PORT', '3306'),
            'database' => env('DB_DATABASE', 'forge'),
            'username' => env('DB_USERNAME', 'forge'),
            'password' => env('DB_PASSWORD', ''),
            'unix_socket' => env('DB_SOCKET', ''),
            'charset' => 'utf8mb4',
            'collation' => 'utf8mb4_unicode_ci',
            'prefix' => '',
            'strict' => true,
            'engine' => null,
        ],

        'pgsql' => [
            'driver' => 'pgsql',
            'host' => env('DB_HOST', '127.0.0.1'),
            'port' => env('DB_PORT', '5432'),
            'database' => env('DB_DATABASE', 'forge'),
            'username' => env('DB_USERNAME', 'forge'),
            'password' => env('DB_PASSWORD', ''),
            'charset' => 'utf8',
            'prefix' => '',
            'schema' => 'public',
            'sslmode' => 'prefer',
        ],

        'sqlsrv' => [
            'driver' => 'sqlsrv',
            'host' => env('DB_HOST', 'localhost'),
            'port' => env('DB_PORT', '1433'),
            'database' => env('DB_DATABASE', 'forge'),
            'username' => env('DB_USERNAME', 'forge'),
            'password' => env('DB_PASSWORD', ''),
            'charset' => 'utf8',
            'prefix' => '',
        ],

        //テスト用DB
        'mysql_test' => [
            'driver' => 'mysql',
            'host' => env('DB_HOST', '127.0.0.1'),
            'port' => env('DB_PORT', '3306'),
            'database' => 'test_db',
            'username' => env('DB_USERNAME', 'forge'),
            'password' => env('DB_PASSWORD', ''),
            'unix_socket' => env('DB_SOCKET', ''),
            'charset' => 'utf8mb4',
            'collation' => 'utf8mb4_unicode_ci',
            'prefix' => '',
            'strict' => true,
            'engine' => null,
        ],

    ],

上記の'mysql_test'の項目がテスト用DBの設定です。'database'の項目だけテスト用DBの名前に変更して、あとは開発用DBの設定をそのままコピペで基本的には大丈夫みたいです。
今回は設定の項目名を'mysql_test'としましたが、phpunit.xml(後述)の記述との整合性がとれていれば、なんでも大丈夫です。

3. phpunit.xmlの編集

PHPUnitの設定ファイルであるphpunit.xmlを編集します。
タグ内でテスト用のenvやDBなどの設定を記述します。今回は以下のように編集しました。

    <php>
        <server name="APP_ENV" value="testing"/>
        <server name="BCRYPT_ROUNDS" value="4"/>
        <server name="CACHE_DRIVER" value="array"/>
        <server name="MAIL_DRIVER" value="array"/>
        <server name="QUEUE_CONNECTION" value="sync"/>
        <server name="SESSION_DRIVER" value="array"/>
        <server name="DB_DATABASE" value="test_db"/>
        <server name="DB_CONNECTION" value="mysql_test"/>
    </php>

"DB_DATABASE"を"test_db"(テスト用に作成したDBの名前)
"DB_CONNECTION"を"mysql_test"(config/database.phpで追記した項目名)
にしました。

テスト用にenvファイルを作成している場合もここで設定できます。
.env.testing などという名前でenvを作成しておき、

<server name="APP_ENV" value="testing"/>

とすると適用されます。テスト用のenvがない場合はデフォルトのenvファイルが適用されます。

これでテストを実行する準備が整いました。?

トラブルシューティング

phpunit.xmlの設定が反映されない?

・テスト用のDBを作成してphpunit.xmlに記述しているのに、テストを実行するとなぜか開発用のDBが使用されている
・テスト用にenvファイルを作成してphpunit.xmlに記述しているのに適用されていない
などの場合

<server name="DB_DATABASE" value="test_db" force="true"/>
<server name="DB_CONNECTION" value="mysql_test" force="true"/>

force="true"のオプションを試してみてください。

force="true"を書いてもphpunit.xmlの設定が反映されない?

キャッシュクリアのコマンドを実行して再度試してみてください

php artisan config:clear

何をやってもテスト用の設定が反映されない?

ググって原因をひとつずつ潰していくしかないです。
この状態に陥ったことがありますが、テスト用DBを作成し直してイチから設定しなおしました。

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

【Laravel】 Model::firstOrCreate() をバルクインサートで実現する

元ネタ

深夜の思いつきで laravel/ideas から拾ってきて即席で書いてしまった。

スレ主 findOrCreateMany って書いてるけど個人的には bulkFirstOrCreate() のほうが命名近い気がするのでこれで。

やりたいこと

Model::firstOrCreate()1 SELECT + 1 INSERT によって実現されるが,複数レコードに対してやると無駄が多い。そのため,複数レコードを処理するときにも 1 SELECT + 1 INSERT だけで済むようにしたい。

Laravel の Eloquent Model は公式でバルクインサートをサポートしておらず,あくまでサポートしているのは Eloquent Builder と Query Builder のみであるため,タイムスタンプやオートインクリメント値を自分でセットするなど少々工夫が必要。

実装

完全に Model::firstOrCreate() を再現しようとすると困難なので, ユニーク属性は1つ という制約を設けた上で作成する。第1引数にユニークと見なすキーの名前,第2引数に属性群の配列を渡す。

/**
 * Trait BulkFirstOrCreates
 *
 * @mixin \Illuminate\Database\Eloquent\Model
 */
trait BulkFirstOrCreates
{
    /**
     * @param  string                                   $uniqueKeyName
     * @param  array                                    $attributesArray
     * @return \Illuminate\Database\Eloquent\Collection
     */
    public static function bulkFirstOrCreate(string $uniqueKeyName, array $attributesArray)
    {
        $instance = new static();

        // Retrieve actually existing models
        $existingModels = $instance
            ->newQuery()
            ->whereIn($uniqueKeyName, array_column($attributesArray, $uniqueKeyName))
            ->get();

        // Mix timestamp attributes into $attributesArray entries
        if ($instance->usesTimestamps()) {
            $instance->updateTimestamps();
            $attributesArray = collect($attributesArray)
                ->map(function (array $attributes) use ($instance) {
                    return $attributes + $instance->getAttributes();
                })
                ->all();
        }

        // Create new models only from non-existent values
        $instance->newQuery()->insert(
            $nonExistentAttributesArray = collect($attributesArray)
                ->whereNotIn($uniqueKeyName, $existingModels->pluck($uniqueKeyName))
                ->all()
        );

        // Retrieve last insert ID
        $lastInsertId = (int)$instance->getConnection()->getPdo()->lastInsertId();

        // Simulate model hydration without running SELECT query
        $createdModels = $instance
            ->newQuery()
            ->hydrate($nonExistentAttributesArray)
            ->each(function (self $model) use (&$lastInsertId) {
                // Assign auto-increment value
                if ($model->getIncrementing()) {
                    $model->{$model->getKeyName()} = $lastInsertId++;
                    $model->syncOriginal();
                }

                // Fire "eloquent.created" event
                $model->wasRecentlyCreated = true;
                $model->fireModelEvent('created', false);
            });

        // Sort in the $valuesArray order and return as a Collection
        return $instance
            ->newCollection(array_column($attributesArray, null, $uniqueKeyName))
            ->replace($existingModels->keyBy($uniqueKeyName))
            ->replace($createdModels->keyBy($uniqueKeyName))
            ->values();
    }
}

firstOrNew() updateOrCreate() とかも含めて汎用化できそうだったら今後ライブラリ化するかも…
と思ったが, firstOrNew() なんて作っても意味ないし, updateOrCreate() は MySQL 固有文法の ELT FIELD が出てきて作りづらいのでパスかな…

使用例

class PostCode extends Model
{
    use BulkFirstOrCreates;
}
$codes = PostCode::bulkFirstOrCreate('code', [
    ['code' => '2001'],
    ['code' => '2002'],
]);
var_dump($codes[0]->id); // int(1)
var_dump($codes[0]->code); // string(4) "2001"
var_dump($codes[0]->wasRecentlyCreated); // bool(true)
var_dump($codes[1]->id); // int(2)
var_dump($codes[1]->code); // string(4) "2002"
var_dump($codes[1]->wasRecentlyCreated); // bool(true)

$codes = PostCode::bulkFirstOrCreate('code', [
    ['code' => '2000'],
    ['code' => '2001'],
    ['code' => '2002'],
    ['code' => '2003'],
]);
var_dump($codes[0]->id); // int(3)
var_dump($codes[0]->code); // string(4) "2000"
var_dump($codes[0]->wasRecentlyCreated); // bool(true)
var_dump($codes[1]->id); // int(1)
var_dump($codes[1]->code); // string(4) "2001"
var_dump($codes[1]->wasRecentlyCreated); // bool(false)
var_dump($codes[2]->id); // int(2)
var_dump($codes[2]->code); // string(4) "2002"
var_dump($codes[2]->wasRecentlyCreated); // bool(false)
var_dump($codes[3]->id); // int(4)
var_dump($codes[3]->code); // string(4) "2003"
var_dump($codes[3]->wasRecentlyCreated); // bool(true)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む