- 投稿日:2019-10-11T17:59:49+09:00
最低限の労力で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.htmlEC2なんかで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の処理を分岐させれば良いと思われます。
- 投稿日:2019-10-11T17:52:14+09:00
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 laraveldockerディレクトリに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イメージがホストOS上にない場合、イメージがdocker hubよりpullされます。
*注意:Composer実行中に以下のエラーが発生する場合があります。
proc_open(): fork failed errors
これはホストのメモリ不足によるものです。VMのメモリが1024MBだと発生するのでメモリを増やしましょう。2.docker-compose.yml作成
続いて、以下のようなdocker-compose.ymlを作成します。
docker-compose.ymlversion: '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を作成します。
DockerfileFROM 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」でインストールしています。・ユーザーの作成
rootユーザーでDockerを操作するのはセキュリティ上好ましくないため、laravelというユーザーを作成しています。・NginxコンテナのDockerfile
nginxディレクトリ内に以下のDokcerfileを作成します。
DockerfileFROM 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を作成します。
DockerfileFROM 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.inidisable_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.confserver { 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通信を行います。
・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.sqlGRANT 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:generateLaravelの設定ファイル.envの「APP_KEY」にキーが設定されます。次のコマンドで設定をキャッシュに反映させます。
docker-compose exec php php artisan config:cacheホストOSのIPアドレス(Vagrantfileに設定したIP)にブラウザからアクセスして以下の画面が表示されたら正常に動作しています。
・MySQLの接続確認
最初に、Laravelのデータベースの設定を行います。.envファイルを次のように設定します。
DB_CONNECTION=mysql DB_HOST=mysql DB_PORT=3306 DB_DATABASE=laravel DB_USERNAME=user DB_PASSWORD=passDB_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:cachePHPコンテナに入り、接続の確認をしていきます。
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は非常に奥が深く、習得するのに時間がかかるツールと思いますが、Dockerfileやdocker-compose.ymlを自分で書くことで理解を深めていくことができます。
この記事が参考になれば幸いです。
- 投稿日:2019-10-11T17:50:35+09:00
CircleCIでのテスト実行時間を短縮する:コンパイル結果のキャッシュ
CircleCIでのテスト実行時間を短縮する:テストの並列化 を行いましたが
まだ速くなるはず。実行結果を眺めると
npm run dev
でかなりの時間がかかっている。
pug, css, vueあたりのassets
配下が更新されていなければキャッシュできるのではと思い対応してみる。キャッシュに対応したCircleCI設定
.circleci/config.ymlversion: 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秒以下に!これでかなり改善されました。
- 投稿日:2019-10-11T16:55:08+09:00
Laravel テスト周りの設定
Laravelを使った開発時のテスト周りの設定の覚え書きです。
環境
・Laravel 5.8.35
・PHP 7.1.3
・PHPUnit 7.5.161. テスト用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を作成し直してイチから設定しなおしました。
- 投稿日:2019-10-11T05:44:30+09:00
【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)