[{"content":"TestMu AI（旧LambdaTest）は、多様なブラウザや実機OS環境をクラウド上で提供するテストプラットフォームです。Playwrightで作成したテストをTestMu AIのクラウドブラウザで実行することで、手元のマシンを使わずにChrome・Firefox・Safari・Edgeなど複数のブラウザでテストを走らせることができます。今回は、すでに構築済みのローカルプロジェクトに対して、クラウドブラウザからテストを実行することを目指します。\n通常のPlaywrightテストもすでに構築済みの状態からTestMu AIの導入を進めますので、まだPlaywrightの環境を構築していない方はこちらもご参照ください。\nPlaywrightでE2Eテストを自動化（１）セットアップ PlaywrightでE2Eテストを自動化 （２）ログイン・ログアウト\n本記事のゴール：公開サイトからローカル環境のテストまで 「ローカルのプロジェクトに対してクラウドブラウザからテスト実行」を実現するにあたり、この記事では２段階で環境構築を進めてゆきます。まずは公開されているURL（デモサイト）をテスト対象に設定を進め、TestMu AIとPlaywrightとの接続を確認します。\nそこまでできたら次に、ローカルの開発サーバーをテスト対象に変更します。その際、クラウドブラウザからローカルサーバーへ接続するために、TestMu AIが提供しているTunnelという機能を使います。\nなお、サービス名は現在TestMu AIに変更されていますが、今回使用するnpmパッケージ（@lambdatest/node-tunnel）やAPIのエンドポイント（cdp.lambdatest.com）など関連するコードは引き続きLambdaTestの名称が使われています。そのため本記事内でも以降はLambdaTestの表記で統一します。\nLambdaTestの接続情報を取得 クラウドでの自動テストには、LambdaTestのWeb Automationという機能を使う必要があります。無料会員でも100分まではこの機能を使うことができますので、お試しにはぴったりです。\nこちらから会員登録し、こちらにアクセスするとAutomationに必要な接続情報が取得できます。\n取得した接続情報を.env.playwrightに追記します。\n.env.playwright LT_USERNAME=ユーザー名 LT_ACCESS_KEY=アクセスキー デモサイトへアクセスするテスト 必要な情報が揃ったので、次はPlaywrightとLambdaTestの接続設定を進めます。\n冒頭でお伝えしたように、まずは公開されているデモサイトをテスト対象とします。テストもシンプルにデモサイトにアクセスするだけの以下のような内容で、PlaywrightからLambdaTestのクラウドブラウザに接続し、動作を確認します。\ne2e/lambdatest-demo.spec.ts import { test, expect } from \u0026#39;@playwright/test\u0026#39;; test(\u0026#39;トップページが表示される\u0026#39;, async ({ page }) =\u0026gt; { await page.goto(\u0026#39;/\u0026#39;); await expect(page).toHaveTitle(/Your Store/i); }); 公開ページにアクセスするための設定ファイル 次にconfigファイルを作成します。既存のplaywright.config.tsはローカルでテストを実行する際の設定ファイルとして変更せず、新たにplaywright.lambdatest.config.tsを作成して、こちらをLambdaTest用の設定ファイルとします。\nまずはコードの全体をお見せします。\nplaywright.lambdatest.config.ts import { defineConfig, devices } from \u0026#39;@playwright/test\u0026#39;; import dotenv from \u0026#39;dotenv\u0026#39;; import { fileURLToPath } from \u0026#39;url\u0026#39;; import path from \u0026#39;path\u0026#39;; const __dirname = path.dirname(fileURLToPath(import.meta.url)); dotenv.config({ path: path.resolve(__dirname, \u0026#39;.env\u0026#39;) }); dotenv.config({ path: path.resolve(__dirname, \u0026#39;.env.playwright\u0026#39;), override: true }); const LT_USERNAME = process.env.LT_USERNAME ?? \u0026#39;\u0026#39;; const LT_ACCESS_KEY = process.env.LT_ACCESS_KEY ?? \u0026#39;\u0026#39;; // ---- デバイスマッピング ---- // DEVICE 変数1つで「LTブラウザ名・デフォルトOS・Playwrightデバイス」が連動して決まる。 type DeviceConfig = { ltBrowser: string; browserVersion: string; platform: string; device: string; }; const DEVICE_MAP: Record\u0026lt;string, DeviceConfig\u0026gt; = { chrome: { ltBrowser: \u0026#39;Chrome\u0026#39;, browserVersion: \u0026#39;latest\u0026#39;, platform: \u0026#39;Windows 11\u0026#39;, device: \u0026#39;Desktop Chrome\u0026#39; }, firefox: { ltBrowser: \u0026#39;pw-firefox\u0026#39;, browserVersion: \u0026#39;latest\u0026#39;, platform: \u0026#39;Windows 11\u0026#39;, device: \u0026#39;Desktop Firefox\u0026#39; }, edge: { ltBrowser: \u0026#39;Microsoft Edge\u0026#39;, browserVersion: \u0026#39;latest\u0026#39;, platform: \u0026#39;Windows 11\u0026#39;, device: \u0026#39;Desktop Edge\u0026#39; }, safari: { ltBrowser: \u0026#39;pw-webkit\u0026#39;, browserVersion: \u0026#39;latest\u0026#39;, platform: \u0026#39;MacOS Tahoe\u0026#39;, device: \u0026#39;Desktop Safari\u0026#39; }, ios: { ltBrowser: \u0026#39;pw-webkit\u0026#39;, browserVersion: \u0026#39;latest\u0026#39;, platform: \u0026#39;MacOS Tahoe\u0026#39;, device: \u0026#39;iPhone 15\u0026#39; }, }; const deviceKey = process.env.DEVICE ?? \u0026#39;chrome\u0026#39;; if (!(deviceKey in DEVICE_MAP)) { console.error(`エラー: DEVICE=\u0026#34;${deviceKey}\u0026#34; は未定義です。有効な値: ${Object.keys(DEVICE_MAP).join(\u0026#39;, \u0026#39;)}`); process.exit(1); } const { ltBrowser, browserVersion, platform: defaultPlatform, device: deviceName } = DEVICE_MAP[deviceKey]; const ltPlatform = process.env.LT_PLATFORM ?? defaultPlatform; const selectedDevice = devices[deviceName]; // ---- LambdaTest CDP エンドポイント ---- function ltEndpoint(testName: string) { return `wss://cdp.lambdatest.com/playwright?capabilities=${encodeURIComponent( JSON.stringify({ browserName: ltBrowser, browserVersion, \u0026#39;LT:Options\u0026#39;: { user: LT_USERNAME, accessKey: LT_ACCESS_KEY, build: \u0026#39;Playwright Demo\u0026#39;, name: `${testName} [${deviceKey} / ${ltPlatform}]`, platform: ltPlatform, tunnel: false, network: true, console: true, video: true, }, }) )}`; } // ---- Playwright 設定 ---- export default defineConfig({ testDir: \u0026#39;./e2e\u0026#39;, fullyParallel: false, workers: 1, timeout: 60_000, reporter: [[\u0026#39;html\u0026#39;], [\u0026#39;list\u0026#39;]], use: { baseURL: \u0026#39;https://ecommerce-playground.lambdatest.io\u0026#39;, trace: \u0026#39;on-first-retry\u0026#39;, screenshot: \u0026#39;only-on-failure\u0026#39;, }, projects: [ { name: `${deviceKey}`, testMatch: \u0026#39;**/lambdatest-demo.spec.ts\u0026#39;, use: { ...selectedDevice, connectOptions: { wsEndpoint: ltEndpoint(deviceKey) }, }, }, ], }); 大きく分けて ①環境変数の読み込み、②デバイスマッピング、③LambdaTestへの接続設定、④Playwrightの実行設定、の4つのブロックで構成されており、ポイントは以下になります。\n②デバイスマッピング DEVICE_MAPでは、ブラウザとOSの組み合わせを定義しています。\nテスト実行時にDEVICE=safariのように環境変数を渡すと、マップのsafari: { ltBrowser: \u0026lsquo;pw-webkit\u0026rsquo;, browserVersion: \u0026lsquo;26.0\u0026rsquo;, platform: \u0026lsquo;MacOS Tahoe\u0026rsquo;, device: \u0026lsquo;Desktop Safari\u0026rsquo; }に合致してltBrowser（LambdaTestに渡すブラウザ名）・platform（OS）・deviceの3つが同時に確定します。\nこの設定のおかげで、「Windows上のSafari」という存在しない組み合わせを指定することを防げます。\nOSとブラウザの組み合わせについては少しややこしいので、公式ドキュメントやジェネレーターを使って確認することをおすすめします。\n公式ドキュメント ジェネレーター\n③LambdaTestへの接続設定 ltEndpoint()では、使用するブラウザ・バージョン・OS・認証情報など渡し、LambdaTestのクラウドブラウザに接続するためのURLを生成します。このURLを後述のconnectOptionsに渡すことで、PlaywrightがLambdaTestのクラウドブラウザに接続できるようになります。\n④Playwrightの実行設定 Playwrightの設定はローカルで動かす時と似ていますが、以下の違いがあります。\n・・・・・ // ---- Playwright 設定 ---- export default defineConfig({ ・・・・・ use: { baseURL: \u0026#39;https://ecommerce-playground.lambdatest.io\u0026#39;, ・・・・・ projects: [ { name: `${deviceKey}`, testMatch: \u0026#39;**/lambdatest-demo.spec.ts\u0026#39;, use: { ...selectedDevice, connectOptions: { wsEndpoint: ltEndpoint(deviceKey) }, }, }, baseURLには、第１段階なのでデモサイトを対象としています。\nそしてprojectsのconnectOptionsの箇所は、「ローカルのブラウザを起動する代わりにリモートのブラウザに接続する」ための設定です。ここで渡しているのが③で生成したクラウド接続用のURLになっています。\nデモサイトへのテスト実行 ここでいったん動作確認のため、以下のコマンドでテストを実行してみます。環境変数にブラウザ名を渡していないので、デフォルトのchromeで実行されるはずです。\n$ npx playwright test --config=playwright.lambdatest.config.ts テスト実行中、ブラウザでLambdaTestのAutomationダッシュボードへアクセスするとリアルタイムでテストが実行されてデモサイトにアクセスしている様子が確認できます。ちゃんとchromeで実行されていますね。\n無事に動作確認ができたので、次はテスト対象をデモサイトからローカルサーバーに切り替えます。\nTunnelバイナリ管理のライブラリ ローカルサーバー（http://127.0.0.1:8001）をテストするにあたり、クラウドブラウザからlocalhostへは接続できないためTunnel（トンネル）という機能を使う必要があります。\n今回は公式が提供しているNode.jsライブラリを使ってトンネル管理を行います。トンネルを動かすためのバイナリファイルを自動でダウンロードし、そのバイナリをNode.jsのコードから起動・停止できるようにしてくれるパッケージです。\n以下のコマンドでインストールします。\n$ npm install -D @lambdatest/node-tunnel また、このライブラリを使用すると.lambdatestというディレクトリに自動的にバイナリファイルがダウンロードされるため、.gitignoreへ追記が必要です。実行時にはログファイルも出るため、こちらも併せて記載します。\n.gitignore ・・・・・ *.log .lambdatest/ Tunnel実行ファイル 先ほどインストールしたライブラリを使ったトンネルの起動・停止をPlaywrightに組み込むため、以下の実行ファイルを作成します。トンネル起動用・終了用の２ファイルあります。\nトンネル起動用\ne2e/support/lambdatest-tunnel.ts import tunnel from \u0026#39;@lambdatest/node-tunnel\u0026#39;; import dotenv from \u0026#39;dotenv\u0026#39;; import { fileURLToPath } from \u0026#39;url\u0026#39;; import path from \u0026#39;path\u0026#39;; const __dirname = path.dirname(fileURLToPath(import.meta.url)); dotenv.config({ path: path.resolve(__dirname, \u0026#39;..\u0026#39;, \u0026#39;..\u0026#39;, \u0026#39;.env\u0026#39;) }); dotenv.config({ path: path.resolve(__dirname, \u0026#39;..\u0026#39;, \u0026#39;..\u0026#39;, \u0026#39;.env.playwright\u0026#39;), override: true }); const LT_USERNAME = process.env.LT_USERNAME ?? \u0026#39;\u0026#39;; const LT_ACCESS_KEY = process.env.LT_ACCESS_KEY ?? \u0026#39;\u0026#39;; export default async function setup() { if (!LT_USERNAME || !LT_ACCESS_KEY) { throw new Error(\u0026#39;LT_USERNAME と LT_ACCESS_KEY を .env.playwright に設定してください。\u0026#39;); } console.log(\u0026#39;[LambdaTest] トンネルを起動中...\u0026#39;); const t = new tunnel(); await new Promise\u0026lt;void\u0026gt;((resolve, reject) =\u0026gt; { t.start( { user: LT_USERNAME, key: LT_ACCESS_KEY }, (err: Error | null) =\u0026gt; { if (err) reject(err); else { console.log(\u0026#39;[LambdaTest] トンネル起動完了\u0026#39;); resolve(); } } ); }); (globalThis as any).__lt_tunnel__ = t; } トンネル停止用\ne2e/support/lambdatest-tunnel-teardown.ts export default async function teardown() { const t = (globalThis as any).__lt_tunnel__; if (t) { console.log(\u0026#39;[LambdaTest] トンネルを停止中...\u0026#39;); await new Promise\u0026lt;void\u0026gt;((resolve) =\u0026gt; t.stop(resolve)); console.log(\u0026#39;[LambdaTest] トンネル停止完了\u0026#39;); } } このファイルを後述のplaywright.lambdatest.config.tsのglobalSetupとglobalTeardownに指定することで、テスト実行のたびに手動でトンネルを起動する手間がなくなります。\nplaywright.lambdatest.config.tsを更新 playwright.lambdatest.config.tsを、トンネルを使ったバージョンに更新します。以下、更新箇所を抜粋しています。\n・・・・・ // ---- LambdaTest CDP エンドポイント ---- function ltEndpoint(testName: string) { return `wss://cdp.lambdatest.com/playwright?capabilities=${encodeURIComponent( JSON.stringify({ ・・・・・ \u0026#39;LT:Options\u0026#39;: { ・・・・・ tunnel: true, // トンネルをtrueに ・・・・・ }, }) )}`; // ---- Playwright 設定 ---- export default defineConfig({ ・・・・・ globalSetup: \u0026#39;./e2e/support/lambdatest-tunnel.ts\u0026#39;, // トンネル起動 globalTeardown: \u0026#39;./e2e/support/lambdatest-tunnel-teardown.ts\u0026#39;, // トンネル停止 use: { baseURL: \u0026#39;http://127.0.0.1:8001\u0026#39;, // デモサイトのURLからローカルへ変更 ・・・・・ }, projects: [ // DB リセット＆シード（ローカル実行） { name: \u0026#39;db-setup\u0026#39;, testMatch: \u0026#39;**/db.setup.ts\u0026#39; }, // 実行したいテスト { name: `${deviceKey} 未ログイン`, testMatch: \u0026#39;**/auth.spec.ts\u0026#39;, use: { ...selectedDevice, connectOptions: { wsEndpoint: ltEndpoint(`${deviceKey} 未ログイン`) }, }, dependencies: [\u0026#39;db-setup\u0026#39;], }, ], webServer: { command: \u0026#39;php artisan serve --port=8001\u0026#39;, url: \u0026#39;http://127.0.0.1:8001\u0026#39;, reuseExistingServer: true, stdout: \u0026#39;ignore\u0026#39;, stderr: \u0026#39;pipe\u0026#39;, }, }); 重要な変更箇所は以下の４点です。\nトンネル設定：ltEndpoint()でtunnel: trueとし、トンネルを使う旨を設定します。 globalSetup / globalTeardown：先ほど作成したトンネル実行ファイルを指定します。これによりテスト実行前にトンネルが自動で起動し、終了後に停止します。 baseURL：デモサイトのURLからローカルサーバーのURLに変更します。 projects：テストで実行したいprojectを並べる点は通常のPlaywrightの記述と同じですが、DB初期化はローカルで行い、テスト本体のみクラウドブラウザで実行する構成となっています。 この棲み分けはconnectOptionsの指定あり・なしによって決まり、connectOptionsが指定されているprojectはLambdaTestのクラウドブラウザで実行され、指定されていないprojectはローカルのブラウザで実行されます。 テスト実行 トンネルの設定も完了したので、改めてテストを実行してみます。\n$ npx playwright test --config=playwright.lambdatest.config.ts ◇ injected env (44) from .env // tip: ◈ encrypted .env [www.dotenvx.com] ◇ injected env (5) from .env.playwright // tip: ◈ encrypted .env [www.dotenvx.com] ◇ injected env (0) from .env // tip: ⌘ enable debugging { debug: true } ◇ injected env (5) from .env.playwright // tip: ⌘ suppress logs { quiet: true } [LambdaTest] トンネルを起動中... Current version of Node Tunnel: 4.0.11 Verifying credentials Auth succeeded Checking for updates Binary already at latest version Starting tunnel Tunnel successfully initiated. You can start testing now [LambdaTest] トンネル起動完了 Running 4 tests using 1 worker ◇ injected env (0) from .env // tip: ⌘ enable debugging { debug: true } ◇ injected env (5) from .env.playwright // tip: ◈ encrypted .env [www.dotenvx.com] 1 [db-setup] › e2e/support/db.setup.ts:5:1 › reset database ・・・・・中略・・・・・ ✓ 2 [chrome 未ログイン] › e2e/auth.spec.ts:4:3 › 認証機能 › ログインページが表示される (5.6s) [LambdaTest] トンネルを停止中... [LambdaTest] トンネル停止完了 4 passed (1.3m) テストの前後でトンネルが問題なく起動・終了しているようです。クラウドのダッシュボードでも実行中の動作が確認でき、トンネル経由でローカルサーバーへのテストが成功しました！\nsafari/ios対応 ここまでの設定でchrome・edgeの実行は問題なかったのですが、safari・iosで以下のようなエラーが発生しました。\nError: page.goto: Could not connect to the server. ログを確認すると、トンネル接続自体は成功しているものの、LambdaTest側のWebKit（Safari）ブラウザがローカルのbaseURLに接続できずに失敗していることがわかりました。\nこれはWebKit特有の仕様が原因だそうです。LambdaTestの公式ドキュメントでもlocalhostの代わりにlocalhost.lambdatest.comを使うよう案内されており、baseURLを以下のように変更することで解決しました。\nplaywright.lambdatest.config.ts // Before use: { baseURL: \u0026#39;http://127.0.0.1:8001\u0026#39;, ・・・・・ }, // After use: { baseURL: \u0026#39;http://localhost.lambdatest.com:8001\u0026#39;, ・・・・・ }, 公式ドキュメント\nこの修正で、safariやiosでもトンネルを使ったクラウドテストが実行できるようになりました。ですがsafariやiosは接続が少し重く、同じクラウド実行でもchromeなどに比べて1.5倍〜時間がかかる感じがします。\n","date":"2026-04-24T00:21:04+09:00","permalink":"https://www.larajapan.com/2026/04/24/%E3%83%AD%E3%83%BC%E3%82%AB%E3%83%AB%E9%96%8B%E7%99%BA%E7%92%B0%E5%A2%83%E3%81%AEplaywright%E3%83%86%E3%82%B9%E3%83%88%E3%82%92testmu-ai%EF%BC%88lambdatest%EF%BC%89%E3%81%A7%E5%AE%9F%E8%A1%8C/","title":"ローカル開発環境のPlaywrightテストをTestMu AI（LambdaTest）で実行"},{"content":"deployerの話の続きです。今回はユーザー定義のタスク（レシピとも言う）の話です。タスクはdeployerを使って対象のホストに入って実行できるコマンドです。ちなみに前回のdep run dateのrunはdeployerが定義したタスクです。\nLaravelアプリをダウンさせるタスクを定義 このブログの読者はもちろんLaravelユーザーなので、そのアプリのダウンのコマンドをdeployerのタスクの例として定義します。\ndeploy.php namespace Deployer; import(\u0026#39;hosts.yml\u0026#39;); desc(\u0026#39;メンテナンスモードにする\u0026#39;); task(\u0026#39;app:down\u0026#39;, function () { cd(\u0026#39;{{deploy_path}}\u0026#39;); $secret = run(\u0026#39;echo $RANDOM | md5sum | head -c 10\u0026#39;); // ランダムなキーを生成 $result = run(\u0026#34;php artisan down --secret=$secret\u0026#34;); writeln($result); }); 上において、cd(\u0026rsquo;{{deploy_path}}\u0026rsquo;)は、hosts.ymlで定義したホストにおいてLaravelのアプリが存在するパス名です。 つまり、まずそこへcdしてからphp artisan downを実行ということです。\n前回定義した、hosts.ymlは以下です。\nhosts.yml hosts: production: hostname: www.larajapan.com remote_user: deployer deploy_path: ~/example staging: hostname: staging.larajapan.com remote_user: deployer deploy_path: ~/example さきのタスクを実行すると、\n$ dep app:down Select hosts: (comma separated) [0] staging [1] production \u0026gt; 0 task app:down [staging] INFO Application is now in maintenance mode. INFO You may bypass maintenance mode via [http://staging.larajapan.com/040538d125]. ここで大事なのは、上のphp artisan down \u0026ndash;secret=の実行は、そこで与えられたシークレットキーを知るユーザーのみダウンしていない状態でLaravelのアプリへのアクセスを許します。 今回のようにランダムな覚えにくいシークレットキーを生成すると、それをいちいちタイプするのは面倒ですよね。しかし、Laravelのdownコマンドは実行後にそのリンクを親切に出力してくれます。そのために、先のタスクの定義において、writeln($result)として画面に出力するのが重要となります。\nタスクを強化 app:downのタスクは先に見せたように、ローカルから対象のマシンにsshを手動で実行することなく簡単にリモートで操作できてしまいます。その便利さは時には過ち実行も起きやすいということです。実行する前に確認のプロンプトも追加しましょう。 deployerでは、確認のプロンプトはaskConfirmation()の関数が使えます。\n... desc(\u0026#39;メンテナンスモードにする\u0026#39;); task(\u0026#39;app:down\u0026#39;, function () { // 以下が追加した部分です。 $host = currentHost()-\u0026gt;getAlias(); if (! askConfirmation(\u0026#34;\u0026lt;comment\u0026gt;$host\u0026lt;/comment\u0026gt;のホストをメンテナンスモードにしますか？\u0026#34;, false)) { writeln(\u0026#39;\u0026lt;error\u0026gt;実行はキャンセルされました.\u0026lt;/error\u0026gt;\u0026#39;); exit(1); } cd(\u0026#39;{{deploy_path}}\u0026#39;); $secret = run(\u0026#39;echo $RANDOM | md5sum | head -c 10\u0026#39;); // ランダムなキーを生成 $result = run(\u0026#34;php artisan down --secret=$secret\u0026#34;); writeln($result); }); 実行してみましょう。\nSelect hosts: (comma separated) [0] staging [1] production \u0026gt; 0 task app:down [staging] stagingのホストをメンテナンスモードにしますか？ [y/N] n [staging] 実行はキャンセルされました. ERROR: Task app:down failed! 上では、小文字のnをタイプすることで実行をキャンセルできます。もちろん大文字のNでも同じです。 ちなみに、[y/N]とNが大文字表示されているのは、デフォルトがNoということで、単にnやNをタイプせずにリターンキーを押すことでもキャンセルとなります。\n確認のプロンプトを入れることで過ちの防止となりましたが、確認の上で実行としたときに、実行後に本当にアプリがダウンとなったか確かめたいですね。それは、php artisan aboutに表示されるMaintenance Modeの行を見ることで確認できます。\ndesc(\u0026#39;メンテナンスモードにする\u0026#39;); task(\u0026#39;app:down\u0026#39;, function () { $host = currentHost()-\u0026gt;getAlias(); if (! askConfirmation(\u0026#34;\u0026lt;comment\u0026gt;$host\u0026lt;/comment\u0026gt;のホストをメンテナンスモードにしますか？\u0026#34;, false)) { writeln(\u0026#39;\u0026lt;error\u0026gt;実行はキャンセルされました.\u0026lt;/error\u0026gt;\u0026#39;); exit(1); } cd(\u0026#39;{{deploy_path}}\u0026#39;); $secret = run(\u0026#39;echo $RANDOM | md5sum | head -c 10\u0026#39;); $result = run(\u0026#34;php artisan down --secret=$secret\u0026#34;); writeln($result); //　以下でダウンされていか確認 $result = run(\u0026#39;php artisan about | grep -i maintenance\u0026#39;); writeln($result); }); 実行してみましょう。\nSelect hosts: (comma separated) [0] staging [1] production \u0026gt; 0 task app:down [staging] stagingのホストをメンテナンスモードにしますか？ [y/N] y [staging] INFO Application is now in maintenance mode. INFO You may bypass maintenance mode via [http://localhost/3d895d8b57]. [staging] Maintenance Mode ................................................... ENABLED ENABLEDはメンテナンスモードとなって落ちているということで、実行は成功です。OFFと表示されていればダウンとならなかったということで異常が起こったということです。\n","date":"2026-04-15T11:07:41+09:00","permalink":"https://www.larajapan.com/2026/04/15/deployer%E3%81%A7%E8%A4%87%E6%95%B0%E3%82%B5%E3%82%A4%E3%83%88%E3%82%92%E7%AE%A1%E7%90%86%EF%BC%88%EF%BC%92%EF%BC%89%E3%82%BF%E3%82%B9%E3%82%AF%E3%82%92%E5%AE%9A%E7%BE%A9/","title":"deployerで複数サイトを管理（２）タスクを定義"},{"content":"同じLaravelのプログラムを複数のサイトにインストールしていると、いちいちそれらのサイトにsshしてプログラムの変更のインストール（デプロイ）が面倒になってきます。こんなときに、とっても助けとなるのがDeployer。今回はそのツールの紹介です。\nDeployerのインストール まず、Laravelのプロジェクトにdeployerをインストール。 $ composer require deployer/deployer --dev インストール後に、vendor/bin/depが作成されます。 このコマンドは頻繁に使用されるので、bashの初期設定のファイルに~/.bashrcに以下のようにエイリアスを入れて、\n.bashrc ... alias dep=\u0026#39;vendor/bin/dep\u0026#39; ... 以下を実行します。\n. ~/.bashrc これで、コマンドラインでdepだけで実行できます。\nDeployerの初期化 Deployerは、先のdepのコマンドを使用してタスクを実行するために、プロジェクトのルートディレクトリにdeployer.phpのファイルが必要です。 そのファイルの作成には以下を実行します。\n$ dep init 以下のように選択肢が表示され順に答えていきます。最後のHosts以外はデフォルト（括弧の中に表示されている）を選択しています。\nSelect recipe language [php]: [0] php [1] yaml \u0026gt; Select project template [common]: [0 ] cakephp [1 ] codeigniter [2 ] codeigniter4 [3 ] common [4 ] composer [5 ] contao [6 ] craftcms [7 ] drupal7 [8 ] drupal8 [9 ] flow_framework [10] fuelphp [11] joomla [12] laravel [13] magento [14] magento2 [15] pimcore [16] prestashop [17] provision [18] shopware [19] silverstripe [20] spiral [21] statamic [22] sulu [23] symfony [24] typo3 [25] wordpress [26] yii [27] zend_framework \u0026gt; Repository [git@github.com:larajapan/example.git]: \u0026gt; Project name [example]: \u0026gt; Hosts (comma separated) []: \u0026gt; www.larajapan.com,staging.larajapan.com Successfully created deploy.php 作成されたdeployer.phpは、\ndeployer.php namespace Deployer; require \u0026#39;recipe/common.php\u0026#39;; // Config set(\u0026#39;repository\u0026#39;, \u0026#39;git@github.com:larajapan/example.git\u0026#39;); add(\u0026#39;shared_files\u0026#39;, []); add(\u0026#39;shared_dirs\u0026#39;, []); add(\u0026#39;writable_dirs\u0026#39;, []); // Hosts host(\u0026#39;www.larajapan.com\u0026#39;) -\u0026gt;set(\u0026#39;remote_user\u0026#39;, \u0026#39;deployer\u0026#39;) -\u0026gt;set(\u0026#39;deploy_path\u0026#39;, \u0026#39;~/example\u0026#39;); host(\u0026#39;staging.larajapan.com\u0026#39;) -\u0026gt;set(\u0026#39;remote_user\u0026#39;, \u0026#39;deployer\u0026#39;) -\u0026gt;set(\u0026#39;deploy_path\u0026#39;, \u0026#39;~/example\u0026#39;); // Hooks after(\u0026#39;deploy:failed\u0026#39;, \u0026#39;deploy:unlock\u0026#39;); 上での設定は、 ・ depでsshでdeployerのユーザーとしてアクセス可能なホストが２つある。www.larajapan.comとstaging.larajapan.com ・ どちらのホストもLaravelのアプリは、~/exampleのディレクトリにある です。\ndeployer.phpは、もちろん手動で編集可能なので上の設定ではいくらでも変更が可能です。\n複数のサイトにいっぺんにアクセス 準備ができたところ、早速以下を実行してみてください。\n$ dep run date ホストの選択肢が表示されます。１番目つまり0を選択するとそのホストへsshしてdateコマンドを実行します。\nSelect hosts: (comma separated) [0] www.larajapan.com [1] staging.larajapan.com \u0026gt; 0 [www.larajapan.com] Thu Mar 26 08:07:54 JST 2026 選択が面倒なら、コマンドラインでもホストを指定できます。\n$ dep run date www.larajapan.com さらに、長いホスト名のタイプが面倒なら、deployer.phpを編集して、ホストのエイリアスを指定できます。\n// Hosts host(\u0026#39;production\u0026#39;) -\u0026gt;setHostname(\u0026#39;www.larajapan.com\u0026#39;) -\u0026gt;set(\u0026#39;remote_user\u0026#39;, \u0026#39;deployer\u0026#39;) -\u0026gt;set(\u0026#39;deploy_path\u0026#39;, \u0026#39;~/example\u0026#39;); host(\u0026#39;staging\u0026#39;) -\u0026gt;setHostname(\u0026#39;staging.larajapan.com\u0026#39;) -\u0026gt;set(\u0026#39;remote_user\u0026#39;, \u0026#39;deployer\u0026#39;) -\u0026gt;set(\u0026#39;deploy_path\u0026#39;, \u0026#39;~/example\u0026#39;); とすれば、\n$ dep run date production と実行できます。\n最後に、定義されている全部のサイトで実行したいなら、allを渡します。\n$ dep run date all [production] Thu Mar 26 08:12:02 JST 2026 [live] Thu Mar 26 08:12:04 JST 2026 便利ですね。\nホスト情報をYAMLファイルにする ホストが２つくらいなら、deploy.php内でのホストの定義で十分ですが、これが10個もなると読みづらくなります。 そのような場合は、ホストの定義をYAMLのファイルに抽出して分けることが可能です。\nhosts.yml hosts: production: hostname: www.larajapan.com remote_user: deployer deploy_path: ~/example staging: hostname: staging.larajapan.com remote_user: deployer deploy_path: ~/example 定義があった元の部分では、以下のようにimportに置き換えます。\ndeploy.php // Hosts import(\u0026#39;hosts.yml\u0026#39;); 整理されてすっきりしました。\n","date":"2026-03-28T07:19:49+09:00","permalink":"https://www.larajapan.com/2026/03/28/deployer%E3%81%A7%E8%A4%87%E6%95%B0%E3%82%B5%E3%82%A4%E3%83%88%E3%82%92%E7%AE%A1%E7%90%86/","title":"deployerで複数サイトを管理（１）"},{"content":"ローカルの開発環境では、一般的に「開発用DB」と「テスト用DB」を分けて運用します。テストのリセット処理（migrate:freshなど）によって開発中のデータまで消えてしまったり、予期せず書き換わってしまうことがあるからです。Playwrightでも同様に、同じ環境でテストするならDBを分けておくのがベストです。今回は、開発DBとテストDBの切り替え手順について解説します。\nプロジェクトの構成 今回の記事で扱うPlaywrightは、Laravelプロジェクトの中にe2eディレクトリとして存在しており、以下のような構成になっています。\nlaravel-project/ ├── app/ ├── \u0026hellip; └── e2e/（Playwright）\nそして今回はDBだけではなく、それぞれの環境が干渉しないようサーバのポートも以下のように使い分けることにします。\nローカルの開発環境：8000 Playwrightの実行環境：8001\nではここから設定を進めていきます。 順を追って進めるためconfigファイルが複数回に分けて登場しますが、最後にconfigの全体もご紹介します。\n.env.playwright まずは.env.playwrightを作成します。以下のように最小限に内容のみ記載します。\n.env.playwright APP_ENV=playwright DB_DATABASE=database/playwright.sqlite APP_ENVは今回の設定上必須ではありませんが、Laravelの動作中localと認識されないように環境をplaywrightと明示しておきます。そしてplaywright用のDBとして、今回はsqliteを使用します。\ngitignoreに追加することもお忘れなく。\n.gitignore ・・・・・ .env.playwright .env.playwrightをconfigファイルに追加 次に、作成した.env.playwrightをplaywright.config.tsに追加します。（TypeScriptの記述となっています）\nplaywright.config.ts import { defineConfig, devices } from \u0026#39;@playwright/test\u0026#39;; import dotenv from \u0026#39;dotenv\u0026#39;; import { fileURLToPath } from \u0026#39;url\u0026#39;; import path from \u0026#39;path\u0026#39;; const __dirname = path.dirname(fileURLToPath(import.meta.url)); // .env を読み込んだあと、.env.playwright で差分を上書き dotenv.config({ path: path.resolve(__dirname, \u0026#39;.env\u0026#39;) }); dotenv.config({ path: path.resolve(__dirname, \u0026#39;.env.playwright\u0026#39;), override: true }); ・・・・・ dotenv.config()を使用して.envと.env.playwrightの両方を読み込んでいます。その際.env → .env.playwrightの順で読み込み、後者で上書きする形としています。そうすることで.env.playwrightの記述は最低限の変更箇所のみで済みます。\n環境にdotenvがインストールされていない場合はテスト実行時にコケてしまうので、以下のコマンドでインストールしておいてくださいね。\n$ npm install dotenv Playwright用のSeeder 次にSeederを作成します。Laravelプロジェクトならすでに存在しているであろうDatabaseSeederはユニットテストで使用するため、Playwright専用のSeederを作成することにします。\ndatabase/seeders/PlaywrightSeeder.php namespace Database\\Seeders; use App\\Models\\User; use Illuminate\\Database\\Seeder; class PlaywrightSeeder extends Seeder { public function run(): void { $user = User::factory()-\u0026gt;create([ \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;Test User\u0026#39;, \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;test@example.com\u0026#39;, ]); $this-\u0026gt;callWith(FavoriteFoodSeeder::class, [\u0026#39;user\u0026#39; =\u0026gt; $user]); $this-\u0026gt;callWith(MealSeeder::class, [\u0026#39;user\u0026#39; =\u0026gt; $user]); } } ユーザーと、ユーザーに紐づく食事データなどの基本情報を作成するシンプルな内容となっています。\nSeeder実行ファイルの作成と設定 次に、作成したSeederを実行するためのセットアップファイルを作成します。ファイル名はなんでも良いですが、今回はdb.setup.tsとしました。\ne2e/db.setup.ts import { test as setup } from \u0026#39;@playwright/test\u0026#39;; import { execSync } from \u0026#39;child_process\u0026#39;; import { existsSync, writeFileSync } from \u0026#39;fs\u0026#39;; setup(\u0026#39;reset database\u0026#39;, async () =\u0026gt; { // SQLiteファイルが存在しない場合は作成する if (!existsSync(\u0026#39;database/playwright.sqlite\u0026#39;)) { writeFileSync(\u0026#39;database/playwright.sqlite\u0026#39;, \u0026#39;\u0026#39;); } execSync(\u0026#39;php artisan migrate:fresh --seeder=PlaywrightSeeder\u0026#39;, { stdio: \u0026#39;inherit\u0026#39; }); }); setup()を使用して、その中に実行内容を定義します。第１引数に指定している文字列はテスト実行後のHTMLレポートに表示されるので、何をしているのかわかるような内容にしたほうが良いです。\nそしてexecSync()を使ってartisanコマンドを実行します。先ほどconfigファイルで設定した.env.playwrightの情報を元に実行されますので、マイグレーションはテスト用DBであるdatabase/playwright.sqliteに対して行われます。\nまた{ stdio: \u0026lsquo;inherit\u0026rsquo; }を指定することで、マイグレーション実行時の進捗状況や、成功・エラーメッセージがターミナルに表示されるようになります。\nDBセットアップファイルをconfigファイルで読み込む 再びplaywright.config.tsに戻ります。先ほど作成したdb.setup.tsをconfigファイルのprojectsに追加します。\n少しわかりにくいので、追加前の該当部分を先にお見せします。もともとは以下のように「setup（認証情報のセットアップ）」と、「chromium（ブラウザテスト）」の２つのプロジェクトがありました。\ndb-setup追加前のplaywright.config.ts ・・・・・ projects: [ // Setup project for authentication { name: \u0026#39;setup\u0026#39;, testMatch: \u0026#39;**/auth.setup.ts\u0026#39; }, { name: \u0026#39;chromium\u0026#39;, use: { ...devices[\u0026#39;Desktop Chrome\u0026#39;], // Use prepared auth state. storageState: \u0026#39;playwright/.auth/user.json\u0026#39;, }, dependencies: [\u0026#39;setup\u0026#39;], }, ], ・・・・・ ここにDBのセットアッププロジェクトを追加すると、以下のようになります。\ndb-setup追加後のplaywright.config.ts ・・・・・ projects: [ // DB reset and seeding { name: \u0026#39;db-setup\u0026#39;, testMatch: \u0026#39;**/db.setup.ts\u0026#39; }, // ここを追加 // Setup project for authentication { name: \u0026#39;setup\u0026#39;, testMatch: \u0026#39;**/auth.setup.ts\u0026#39;, dependencies: [\u0026#39;db-setup\u0026#39;] }, // ここを追加 { name: \u0026#39;chromium\u0026#39;, // ここは変更なし use: { ...devices[\u0026#39;Desktop Chrome\u0026#39;], // Use prepared auth state. storageState: \u0026#39;playwright/.auth/user.json\u0026#39;, }, dependencies: [\u0026#39;setup\u0026#39;], }, ], ・・・・・ 追加したのは{ name: \u0026lsquo;db-setup\u0026rsquo;, testMatch: \u0026lsquo;**/db.setup.ts\u0026rsquo; },の行と、setupプロジェクトへの依存関係dependencies: [\u0026lsquo;db-setup\u0026rsquo;] の部分です。\n元からあったchromiumプロジェクトにも依存関係dependencies: [\u0026lsquo;setup\u0026rsquo;]がありますので、今回の変更により、テスト開始時の実行順序は以下のようになります。\ndb-setup：まずDBをリセットしてデータを投入する setup：DBの準備が整ったあとで、ログイン認証を行い状態を保存する chromium：全ての準備（DBと認証）が終わったあとで、実際のテストを開始する webServerの設定 最後は、テスト用サーバを起動するための設定です。\nPlaywrightには、テスト開始時に自動でローカルサーバを立ち上げてくれるwebServerという機能があります。これを利用して、開発用の8000番とは異なるテスト専用ポートの8001番でアプリを起動するようconfigファイルを編集します。\nplaywright.config.ts ・・・・・ export default defineConfig({ use: { /* Base URL to use in actions like `await page.goto(\u0026#39;/\u0026#39;)`. */ baseURL: \u0026#39;http://127.0.0.1:8001\u0026#39;, ・・・・・ }, webServer: { command: \u0026#39;php artisan serve --port=8001\u0026#39;, url: \u0026#39;http://127.0.0.1:8001\u0026#39;, reuseExistingServer: !process.env.CI, stdout: \u0026#39;ignore\u0026#39;, stderr: \u0026#39;pipe\u0026#39;, }, }); useセクションのbaseURLを、ポート8000から8001に変更。そしてwebServerセクションを新規に追加しました。webServerでは以下のような項目を設定しています。\ncommand：テスト開始前に実行するシェルコマンドを指定 url：サーバーの起動完了を判定するためのURL（このURLにアクセス可能になるまでテストを待機） reuseExistingServer：すでにサーバーが起動している場合の挙動を制御（!process.env.CIとすることで、ローカルなどCI以外の環境の場合、既存サーバーがあれば再利用、無ければ起動する、という動作になる） stdout：通常ログの出力設定（ignoreを指定すると、正常時のログは表示しない） stderr：エラーログの出力設定（pipeを指定すると、起動失敗やエラー時のみターミナルに表示） このようにwebServerを設定しておくことで、テスト用サーバーを手動でオン・オフにする必要がなく、またテスト中に意図しない開発用DBに接続してしまう危険を回避できます。\nこれでテスト環境とのDB分離・リセットの設定が完了しました！\n最終的なplaywright.config.ts ここまでの設定を反映した、最終的なplaywright.config.tsは以下のようになります。\nplaywright.config.ts import { defineConfig, devices } from \u0026#39;@playwright/test\u0026#39;; import dotenv from \u0026#39;dotenv\u0026#39;; import { fileURLToPath } from \u0026#39;url\u0026#39;; import path from \u0026#39;path\u0026#39;; const __dirname = path.dirname(fileURLToPath(import.meta.url)); // .env を読み込んだあと、.env.playwright で差分を上書き dotenv.config({ path: path.resolve(__dirname, \u0026#39;.env\u0026#39;) }); dotenv.config({ path: path.resolve(__dirname, \u0026#39;.env.playwright\u0026#39;), override: true }); /** * Playwright E2E Test Configuration * @see https://playwright.dev/docs/test-configuration */ export default defineConfig({ testDir: \u0026#39;./e2e\u0026#39;, /* Run tests in files in parallel */ fullyParallel: false, /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, /* Retry on CI only */ retries: process.env.CI ? 2 : 0, /* Opt out of parallel tests on CI. */ // workers: process.env.CI ? 1 : undefined, workers: 1, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: \u0026#39;html\u0026#39;, /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto(\u0026#39;/\u0026#39;)`. */ baseURL: \u0026#39;http://127.0.0.1:8001\u0026#39;, /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: \u0026#39;on-first-retry\u0026#39;, /* Screenshot on failure */ screenshot: \u0026#39;only-on-failure\u0026#39;, /* Video on failure */ video: \u0026#39;retain-on-failure\u0026#39;, }, /* Configure projects for major browsers */ projects: [ // DB reset and seeding { name: \u0026#39;db-setup\u0026#39;, testMatch: \u0026#39;**/db.setup.ts\u0026#39; }, // Setup project for authentication { name: \u0026#39;setup\u0026#39;, testMatch: \u0026#39;**/auth.setup.ts\u0026#39;, dependencies: [\u0026#39;db-setup\u0026#39;] }, // Authenticated tests { name: \u0026#39;chromium\u0026#39;, use: { ...devices[\u0026#39;Desktop Chrome\u0026#39;], // Use prepared auth state. storageState: \u0026#39;playwright/.auth/user.json\u0026#39;, }, dependencies: [\u0026#39;setup\u0026#39;], }, ], /* Run your local dev server before starting the tests */ webServer: { command: \u0026#39;php artisan serve --port=8001\u0026#39;, url: \u0026#39;http://127.0.0.1:8001\u0026#39;, reuseExistingServer: !process.env.CI, stdout: \u0026#39;ignore\u0026#39;, stderr: \u0026#39;pipe\u0026#39;, }, }); ","date":"2026-03-24T11:54:39+09:00","permalink":"https://www.larajapan.com/2026/03/24/playwright%E3%81%A7e2e%E3%83%86%E3%82%B9%E3%83%88%E3%82%92%E8%87%AA%E5%8B%95%E5%8C%96%EF%BC%88%EF%BC%96%EF%BC%89%E9%96%8B%E7%99%BAdb%E3%81%A8%E3%83%86%E3%82%B9%E3%83%88db%E3%81%AE%E5%88%86%E9%9B%A2/","title":"PlaywrightでE2Eテストを自動化（６）開発DBとテストDBの分離と初期化"},{"content":"Playwrightのページオブジェクトモデル（POM）とは、画面やボタンなどの部品をひとまとめにして管理する仕組みのことです。POMを使うことでテストコードがシンプルになり、またUI変更時にも修正がPOMファイルのみで済むといったメリットがあります。\nそしてPOMは、ページ単位だけでなくコンポーネント単位でも導入可能です。この記事では、テストで繰り返し登場する入力項目をPOM化して、再利用できるようにしてゆきます。\nPOM化対象の要素 以下は食事管理アプリの一部の画面です。「食べたものを入力する画面」と「食材をDBに登録する画面」があり、赤枠内にあるカロリー、タンパク質などの入力項目複数が共通しています。実際には編集画面なども存在するため、この入力欄の使用頻度は高いです。\nテストコードでも当然これらの入力欄が繰り返し登場するため、POM化するメリットは大きいです。\nPOM導入前のテストコード 以下は、POM導入前のテストコードです。「食べたものを登録する画面」と「食材をDBに登録する画面」それぞれで登録動作が正常に行えることをテストしています。\nimport { test, expect } from \u0026#39;@playwright/test\u0026#39;; test.describe(\u0026#39;POM使用前のテスト\u0026#39;, () =\u0026gt; { test(\u0026#39;食べたものを登録\u0026#39;, async ({ page }) =\u0026gt; { await page.goto(\u0026#39;/meals/create\u0026#39;); // 今日の日付を設定 const today = new Date().toISOString().split(\u0026#39;T\u0026#39;)[0]; await page.getByLabel(\u0026#39;日付\u0026#39;).fill(today); await page.getByRole(\u0026#39;radio\u0026#39;, { name: \u0026#39;朝食\u0026#39; }).check(); await page.getByLabel(\u0026#39;料理名\u0026#39;).fill(\u0026#39;テスト朝食\u0026#39;); await page.getByLabel(\u0026#39;カロリー (kcal)\u0026#39;).fill(\u0026#39;500\u0026#39;); await page.getByLabel(\u0026#39;タンパク質 (g)\u0026#39;).fill(\u0026#39;20\u0026#39;); await page.getByLabel(\u0026#39;脂質 (g)\u0026#39;).fill(\u0026#39;15\u0026#39;); await page.getByLabel(\u0026#39;炭水化物 (g)\u0026#39;).fill(\u0026#39;60\u0026#39;); await page.getByLabel(\u0026#39;糖質 (g)\u0026#39;).fill(\u0026#39;55\u0026#39;); await page.getByLabel(\u0026#39;メモ\u0026#39;).fill(\u0026#39;テストメモ\u0026#39;); await page.getByRole(\u0026#39;button\u0026#39;, { name: \u0026#39;保存して戻る\u0026#39; }).click(); // ダッシュボードにリダイレクトされることを確認 await expect(page).toHaveURL(\u0026#39;/\u0026#39;); // 成功メッセージが表示されることを確認 await expect(page.getByText(\u0026#39;食事を記録しました\u0026#39;)).toBeVisible(); }); test(\u0026#39;食材をDBに登録\u0026#39;, async ({ page }) =\u0026gt; { await page.goto(\u0026#39;/food-compositions/create\u0026#39;); await page.getByLabel(\u0026#39;食材名\u0026#39;).fill(\u0026#39;じゃがいも\u0026#39;); await page.getByLabel(\u0026#39;カロリー (kcal)\u0026#39;).fill(\u0026#39;150\u0026#39;); await page.getByLabel(\u0026#39;タンパク質 (g)\u0026#39;).fill(\u0026#39;35\u0026#39;); await page.getByLabel(\u0026#39;脂質 (g)\u0026#39;).fill(\u0026#39;10\u0026#39;); await page.getByLabel(\u0026#39;炭水化物 (g)\u0026#39;).fill(\u0026#39;75\u0026#39;); await page.getByLabel(\u0026#39;糖質 (g)\u0026#39;).fill(\u0026#39;55\u0026#39;); await page.getByRole(\u0026#39;button\u0026#39;, { name: \u0026#39;登録する\u0026#39; }).click(); // ダッシュボードにリダイレクトされることを確認 await expect(page).toHaveURL(\u0026#39;/food-compositions\u0026#39;); // 成功メッセージが表示されることを確認 await expect(page.getByText(\u0026#39;食材を登録しました。\u0026#39;)).toBeVisible(); }); }); 前述の通りそれぞれのテストに同じ入力欄が存在しているため、getByLabel()による要素取得がずらずらと記述されています。\n対象要素をPOM化する 早速ですが、以下が作成したPOMコンポーネントになります。POMファイルの置き場所に特にルールはありません。今回は、componentsというディレクトリを作成し、その中にファイルを配置しました。\ne2e/components/NutritionInputs.ts import { Page, Locator } from \u0026#39;@playwright/test\u0026#39;; /** * 栄養素入力フィールドの共通コンポーネント */ export class NutritionInputs { // すべてのロケーターをプロパティとして定義 readonly nameInput: Locator; readonly caloriesInput: Locator; readonly proteinInput: Locator; readonly fatInput: Locator; readonly carbohydratesInput: Locator; readonly sugarInput: Locator; readonly saltInput: Locator; constructor(private page: Page) { this.nameInput = page.getByLabel(/(料理|食材)名/); this.caloriesInput = page.getByLabel(\u0026#39;カロリー (kcal)\u0026#39;); this.proteinInput = page.getByLabel(\u0026#39;タンパク質 (g)\u0026#39;); this.fatInput = page.getByLabel(\u0026#39;脂質 (g)\u0026#39;); this.carbohydratesInput = page.getByLabel(\u0026#39;炭水化物 (g)\u0026#39;); this.sugarInput = page.getByLabel(\u0026#39;糖質 (g)\u0026#39;); this.saltInput = page.getByLabel(\u0026#39;塩分 (g)\u0026#39;); } /** * すべての栄養素を一括入力 */ async fillAll(nutrition: { name: string; calories: string; protein?: string; fat?: string; carbohydrates?: string; sugar?: string; salt?: string; }) { // 各ロケーターを再利用して入力 await this.nameInput.fill(nutrition.name); await this.caloriesInput.fill(nutrition.calories); if (nutrition.protein) await this.proteinInput.fill(nutrition.protein); if (nutrition.fat) await this.fatInput.fill(nutrition.fat); if (nutrition.carbohydrates) await this.carbohydratesInput.fill(nutrition.carbohydrates); if (nutrition.sugar) await this.sugarInput.fill(nutrition.sugar); if (nutrition.salt) await this.saltInput.fill(nutrition.salt); } } constructor()とfillAll()の２つの要素で構成されています。\nまずconstructor()では、このコンポーネントで操作対象とする全ての入力項目をプロパティとして定義しています。page.getByLabel(/(料理|食材)名/);だけは正規表現で要素を指定していますが、これは画面によってラベル名が「料理名」と「食材名」のように異なるからです。\nそしてfillAll()、こちらはメソッド名の通り全ての項目に入力を実行します。その際、先ほどのconstructor()で定義したthis.nameInput、this.caloriesInputなどのプロパティを使って操作を行います。\nまた、必須項目以外はif (nutrition.protein) ・・・として、データがある場合にのみ入力するようにしています。\n今回は入力欄のみを扱っていますが、ボタンやリスト要素などももちろんPOM化できます。メソッドも自由に定義ですますので、詳しくは公式ドキュメントをご参照ください。\n作成したPOMをテストコードで使う このように作成したコンポーネントPOMをテストコード側でどのように使うかというと、以下のようにします。\nimport { NutritionInputs } from \u0026#39;./components/NutritionInputs\u0026#39;; test(\u0026#39;食べたものを登録\u0026#39;, async ({ page }) =\u0026gt; { // コンポーネントPOMを直接作成 const nutrition = new NutritionInputs(page); // コンポーネントのメソッドを使用して入力 await nutrition.fillAll({ name: \u0026#39;じゃがいも\u0026#39;, calories: \u0026#39;200\u0026#39;, protein: \u0026#39;10\u0026#39;, fat: \u0026#39;5\u0026#39;, carbohydrates: \u0026#39;30\u0026#39;, sugar: \u0026#39;15\u0026#39;, salt: \u0026#39;1.0\u0026#39;, }); ・・・・・ まずはNutritionInputsをインスタンス化します。その際引数にpageオブジェクトを渡してください。そして、インスタンスを使ってfillAll()を呼び出します。\ngetByLabel()がずらっと並んでいた部分が、必要な情報を渡すだけという形にスッキリしました。もしも入力欄のラベル名が変更になっても修正はPOMファイル１箇所で済みます。\nFixtureでPOMを定義する これでPOMが使えるようになりましたが、テスト内にnewの記述が入ってしまうことが少し気になります。テストとは直接関係がないのでできればテストの外でnewしたい。\nこういう場合、Fixtureが便利です。Fixtureとは、テストで使う共通の準備処理をまとめて定義できる機能のことです。\n以下は作成したfixtureです。ドキュメントの記述にならって以下のようにしました。\ne2e/fixtures.ts import { test as base } from \u0026#39;@playwright/test\u0026#39;; import { NutritionInputs } from \u0026#39;./components/NutritionInputs\u0026#39;; type MyFixtures = { nutritionInputs: NutritionInputs; }; export const test = base.extend\u0026lt;MyFixtures\u0026gt;({ nutritionInputs: async ({ page }, use) =\u0026gt; { const nutritionInputs = new NutritionInputs(page); await use(nutritionInputs); }, }); export { expect } from \u0026#39;@playwright/test\u0026#39;; まず、このファイルで定義するPOMを宣言します。今回はnutritionInputsという名前でNutritionInputsを使えるようにしています。\nそのあとbase.extend()でPlaywrightのtest()を拡張しています。POMのインスタンスを作成し、await use(nutritionInputs)によって、テスト側でnutritionInputsを使えるように渡す、という流れになっています。\nこれにより、テスト側では以下のようにtest()の引数にnutritionInputsを受け取るだけでPOMを使えるようになります。\nimport { test, expect } from \u0026#39;./fixtures\u0026#39;; test(\u0026#39;食べたものを登録\u0026#39;, async ({ page, nutritionInputs }) =\u0026gt; { await nutritionInputs.fillAll({ name: \u0026#39;テスト朝食\u0026#39;, calories: \u0026#39;500\u0026#39;, protein: \u0026#39;20\u0026#39;, fat: \u0026#39;15\u0026#39;, carbohydrates: \u0026#39;60\u0026#39;, sugar: \u0026#39;55\u0026#39;, salt: \u0026#39;2.0\u0026#39;, }); ・・・・・ POM導入後のテストコード 作成したPOM・Fixtureを使用した、最終的なテストコードは以下になります。\nimport { test, expect } from \u0026#39;./fixtures\u0026#39;; test.describe(\u0026#39;NutritionInputsを使用\u0026#39;, () =\u0026gt; { test(\u0026#39;食べたものを登録\u0026#39;, async ({ page, nutritionInputs }) =\u0026gt; { await page.goto(\u0026#39;/meals/create\u0026#39;); // 今日の日付を設定 const today = new Date().toISOString().split(\u0026#39;T\u0026#39;)[0]; await page.getByLabel(\u0026#39;日付\u0026#39;).fill(today); await page.getByRole(\u0026#39;radio\u0026#39;, { name: \u0026#39;朝食\u0026#39; }).check(); //POMのメソッドで栄養素を一括入力 await nutritionInputs.fillAll({ name: \u0026#39;テスト朝食\u0026#39;, calories: \u0026#39;500\u0026#39;, protein: \u0026#39;20\u0026#39;, fat: \u0026#39;15\u0026#39;, carbohydrates: \u0026#39;60\u0026#39;, sugar: \u0026#39;55\u0026#39;, salt: \u0026#39;2.0\u0026#39;, }); await page.getByLabel(\u0026#39;メモ\u0026#39;).fill(\u0026#39;テストメモ\u0026#39;); await page.getByRole(\u0026#39;button\u0026#39;, { name: \u0026#39;保存して戻る\u0026#39; }).click(); // 保存後のリダイレクト・成功メッセージ確認 await expect(page).toHaveURL(\u0026#39;/\u0026#39;); await expect(page.getByText(\u0026#39;食事を記録しました\u0026#39;)).toBeVisible(); }); test(\u0026#39;食材DBを登録\u0026#39;, async ({ page, nutritionInputs }) =\u0026gt; { await page.goto(\u0026#39;/food-compositions/create\u0026#39;); // POMのメソッドを使用して入力 await nutritionInputs.fillAll({ name: \u0026#39;じゃがいも\u0026#39;, calories: \u0026#39;200\u0026#39;, protein: \u0026#39;10\u0026#39;, fat: \u0026#39;5\u0026#39;, carbohydrates: \u0026#39;30\u0026#39;, sugar: \u0026#39;15\u0026#39;, salt: \u0026#39;1.0\u0026#39;, }); // ページ固有のボタン（Submitなど）は直接pageオブジェクトで操作 await page.getByRole(\u0026#39;button\u0026#39;, { name: \u0026#39;登録する\u0026#39; }).click(); // 保存後のリダイレクト・成功メッセージ確認 await expect(page).toHaveURL(\u0026#39;/food-compositions\u0026#39;); await expect(page.getByText(\u0026#39;食材を登録しました。\u0026#39;)).toBeVisible(); }); }); このように小さな単位からでもPOMを導入できるので、ぜひ使ってみてください！\n","date":"2026-02-18T07:06:37+09:00","permalink":"https://www.larajapan.com/2026/02/18/playwright%E3%81%A7%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%E5%8D%98%E4%BD%8D%E3%81%AEpom%E3%82%92%E5%B0%8E%E5%85%A5%E3%81%97%E3%80%81%E3%83%86%E3%82%B9%E3%83%88%E3%81%A7/","title":"Playwrightでコンポーネント単位のPOMを導入し、テストで使い回す"},{"content":"ウェブから時間がかかる作業、例えばアップロードした画像の整形などを行うときは、ジョブを作成してキュー使って非同期で実行するのが通常。しかし、Laravel 12.xからジョブをキューを使わずに即バックグラウンドで処理することが可能となりました。今回はその紹介です。\n準備 まず使用するジョブを、Backgroundと命名して作成します。\napp/Jobs/Background.php namespace App\\Jobs; use Illuminate\\Support\\Facades\\Log; use Illuminate\\Foundation\\Queue\\Queueable; use Illuminate\\Contracts\\Queue\\ShouldQueue; class Background implements ShouldQueue { use Queueable; /** * Create a new job instance. */ public function __construct() { Log::info(\u0026#34;バックグラウンドのジョブを作成\u0026#34;); } /** * Execute the job. */ public function handle(): void { Log::info(\u0026#39;バックグラウンドのジョブの処理の開始\u0026#39;); sleep(10); Log::info(\u0026#39;バックグラウンドのジョブの処理の終了\u0026#39;); } } このジョブの仕事は見ての通り、10秒間スリープすることですが、実際には代わりに画像の整形などの作業がプログラムされます。\nそれから、以下config/queue.phpにおいて以下の接続が入っていることを確認してください。\nconfig/queue.php ... \u0026#39;connections\u0026#39; =\u0026gt; [ ... \u0026#39;deferred\u0026#39; =\u0026gt; [ \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;deferred\u0026#39;, ], \u0026#39;background\u0026#39; =\u0026gt; [ \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;background\u0026#39;, ], \u0026#39;failover\u0026#39; =\u0026gt; [ \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;failover\u0026#39;, \u0026#39;connections\u0026#39; =\u0026gt; [ \u0026#39;database\u0026#39;, \u0026#39;deferred\u0026#39;, ], ], ], ... ウェブと同じプロセスで同期実行（deferred） 同期実行と書くと、実行が完了するまで待たされること（上のジョブでは10秒のスリープ）を想像しますが、そうではなくウェブのレスポンスをユーザーに即座に返してからジョブが実行されます。つまり、画面で10秒待たされることはありません。 通常のジョブの発信は.envで、QUEUE_CONNECTION=sqsとかで指定したキューに以下のように発信します。\nBackground::dispatch(); しかし、deferredはdefaultのキューの接続を使わないので、onConnection()でdeferredを指定発信します。\nBackground::dispatch()-\u0026gt;onConnection(\u0026#39;deferred\u0026#39;); コントローラではこんな感じでコールします。\napp/Controllers/BackgroundController.php ... use App\\Jobs\\Background; ... final class BackgroundController extends Controller { public function create(): View { ... return view(\u0026#39;create\u0026#39;); } public function store(Request $request): RedirectResponse { Background::dispatch()-\u0026gt;onConnection(\u0026#39;deferred\u0026#39;); // ジョブを発信 return redirect(route(\u0026#39;background.create\u0026#39;)); } } ウェブと違うプロセスで同期実行（background） backgroundを使用したジョブの発信は、引数がbackgroundとなるだけです。\nBackground::dispatch()-\u0026gt;onConnection(\u0026#39;background\u0026#39;); deferredとの違いは、ジョブの処理はジョブを発信したウェブとはまったく違うプロセスで実行されます。ジョブを発信後に、Unixで馴染みなpsコマンドでプロセスをモニターしていると、以下のようにartisanのコマンド実行プロセスが新たに出てきます。\nwww-data 14961 340 4 19:49 ? 00:00:00 /usr/bin/php artisan invoke-serialized-closure あたかも、ウェブからコマンドラインでコマンドを実行しているようです。\ndeferred vs background さて、キューを使わないでバックグラウンドでジョブを処理できる方法を２つ紹介しましたが、どう使い分けたらいいのでしょうか。\n２つの決定的な違いは、deferredではプログラムの実行時間がphpのmax_execution_timeの設定値に限られることです。 プログラムの実行の40秒かかるとしてmax_execution_time=30としたら、実行開始してから30秒後に「Maximum execution time of 30 seconds exceeded」とようなエラーとなり処理が途中停止となってしまいます。\n一方backgroundはあたかもコマンドライン(CLI)でコマンドを実行するように、そのような時間制限はありません。\n必然的に、deferredは短めの作業の実行となり、backgroundは長めの作業に適するということになります。\nこの違いを実際にテストするには、先のBackgroundジョブを以下のように書き換えてdeferedとbackgroundの両方で発信してみてください。 sleep()がここで使われない理由は、スリープをいくら長くしてもはmax_execution_timeで指定する時間制限にカウントされないからです。\napp/Jobs/Background.php ... class Background implements ShouldQueue { use Queueable; ... public function handle(): void { Log::info(\u0026#39;バックグラウンドのジョブ処理の開始\u0026#39;); $startTime = time(); $lastLogTime = $startTime; while (true) { if (time() - $lastLogTime \u0026gt;= 10) { $secondsPassed = time() - $startTime; Log::info(\u0026#34;実行中... ({$secondsPassed} 秒経過)\u0026#34;); $lastLogTime = time(); } if (time() - $startTime \u0026gt; 40) { break; } } Log::info(\u0026#39;バックグラウンドのジョブ処理の終了\u0026#39;); } } ","date":"2026-02-08T03:51:55+09:00","permalink":"https://www.larajapan.com/2026/02/08/%E3%82%A6%E3%82%A7%E3%83%96%E3%81%8B%E3%82%89%E3%83%90%E3%83%83%E3%82%AF%E3%82%B0%E3%83%A9%E3%82%A6%E3%83%B3%E3%83%89%E3%81%A7%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%A0%E3%82%92%E5%AE%9F%E8%A1%8C/","title":"ウェブからバックグラウンドでプログラムを実行"},{"content":"Laravelでは、カスタムに作成したミドルウェアをいろいろな場所で装着することができとてもフレキシブルです。前回ではグローバルやルートで装着しました。今回はLaravel 11.xバージョンから追加されたコントローラでのmiddlewareメソッドで装着してみます。\nカスタムに作成したミドルウェア 前回から結構時間が経過しているので、まずそこで作成したミドルウェアのコードを持ち出します。\napp/Http/Middleware/HideForbiddenWords.php namespace App\\Http\\Middleware; use Closure; use Illuminate\\Support\\Str; use Illuminate\\Http\\Request; class HideForbiddenWords { public static $forbiddenWords = [\u0026#39;禁止１\u0026#39;, \u0026#39;禁止２\u0026#39;, \u0026#39;禁止３\u0026#39;]; public function handle(Request $request, Closure $next): mixed { // 禁止用語を***に変換 $all = $request-\u0026gt;collect()-\u0026gt;map(function ($value, $key) { return is_string($value) ? Str::of($value)-\u0026gt;replace(static::$forbiddenWords, \u0026#39;***\u0026#39;)-\u0026gt;toString() : $value; })-\u0026gt;all(); // もとのリクエストを置き換える $request-\u0026gt;merge($all);　return $next($request); } } このミドルウェアは、$forbiddenWords配列で設定した３つの禁止用語が入力にあれば、***に変換します。\nグローバルでの装着 こちらも前回からのコピーですが、今回のようなローカルなミドルウェアには向いていないです。\nbootstrap/app.php use Illuminate\\Foundation\\Application; use Illuminate\\Foundation\\Configuration\\Exceptions; use Illuminate\\Foundation\\Configuration\\Middleware; return Application::configure(basePath: dirname(__DIR__)) -\u0026gt;withRouting( web: __DIR__.\u0026#39;/../routes/web.php\u0026#39;, commands: __DIR__.\u0026#39;/../routes/console.php\u0026#39;, health: \u0026#39;/up\u0026#39;, ) -\u0026gt;withMiddleware(function (Middleware $middleware) { $middleware-\u0026gt;append( \\App\\Http\\Middleware\\HideForbiddenWords::class, ); }) -\u0026gt;withExceptions(function (Exceptions $exceptions) { // })-\u0026gt;create(); ルートで装着 以下のようにルートの定義で装着するのが現実的です。特定のコントローラの特定のルートに装着できます。 use App\\Http\\Middleware\\HideForbiddenWords; Route::patch(\u0026#39;/profile\u0026#39;, [ProfileController::class, \u0026#39;update\u0026#39;]) -\u0026gt;name(\u0026#39;profile.update\u0026#39;) -\u0026gt;middleware(HideForbiddenWords::class); コントローラで装着 こちら、Laravelの11.xバージョン以降では、以下のようにmiddlewareのメソッド内でも指定できます。\napp/Http/Controllers/ProfileController.php namespace App\\Http\\Controllers; use App\\Http\\Requests\\ProfileUpdateRequest; use Illuminate\\Http\\RedirectResponse; use Illuminate\\Http\\Request; use Illuminate\\Routing\\Controllers\\HasMiddleware; use Illuminate\\Support\\Facades\\Auth; use Illuminate\\Support\\Facades\\Redirect; use Illuminate\\View\\View; class ProfileController extends Controller implements HasMiddleware { public static function middleware(): array { return [\\App\\Http\\Middleware\\HideForbiddenWords::class]; } /** * Display the user\u0026#39;s profile form. */ public function edit(Request $request): View { return view(\u0026#39;profile.edit\u0026#39;, [ \u0026#39;user\u0026#39; =\u0026gt; $request-\u0026gt;user(), ]); } /** * Update the user\u0026#39;s profile information. */ public function update(ProfileUpdateRequest $request): RedirectResponse { $request-\u0026gt;user()-\u0026gt;fill($request-\u0026gt;validated()); if ($request-\u0026gt;user()-\u0026gt;isDirty(\u0026#39;email\u0026#39;)) { $request-\u0026gt;user()-\u0026gt;email_verified_at = null; } $request-\u0026gt;user()-\u0026gt;save(); return Redirect::route(\u0026#39;profile.edit\u0026#39;)-\u0026gt;with(\u0026#39;status\u0026#39;, \u0026#39;profile-updated\u0026#39;); } ... 注意点として、\nuse Illuminate\\Routing\\Controllers\\HasMiddleware;の宣言が必要なこと。 middlewareのメソッドは、staticであること。ゆえにその中では、$this-\u003enameのようなインスタンス変数への使用は不可です。 を忘れずに。\nちなみに、Laravel 10.xバージョン以前では、HasMiddlewareはまだ登場していないので、以下のようにコンストラクターでコールしていました。\napp/Http/Controllers/ProfileController.php ... class ProfileController extends Controller { public function __construct() { $this-\u0026gt;middleware(\\App\\Http\\Middleware\\HideForbiddenWords::class); } ... } ","date":"2026-01-25T06:43:46+09:00","permalink":"https://www.larajapan.com/2026/01/25/%E3%83%9F%E3%83%89%E3%83%AB%E3%82%A6%E3%82%A7%E3%82%A2%E3%82%92%E3%82%82%E3%81%A3%E3%81%A8%E7%9F%A5%E3%82%8D%E3%81%86-%EF%BC%88%EF%BC%94%EF%BC%89%E3%82%B3%E3%83%B3%E3%83%88%E3%83%AD%E3%83%BC%E3%83%A9/","title":"ミドルウェアをもっと知ろう （４）コントローラに装着"},{"content":"以前にご紹介したPlaywrightテスト作成の記事では、一連のログイン動作を共通処理として関数化し、使いまわせるようにしました。この方法ではコード上はすっきりしますが、ログイン処理がテストごとに実行される点は同じです。\nそこで今回は、PlaywrightのAuthenticationを使って「認証状態」自体を使いまわせるようにしてゆきます。毎回ログイン処理を行う必要がなくなるので、テスト時間の短縮が期待できます。\n関連記事：PlaywrightでE2Eテストを自動化 （２）ログイン・ログアウト\nAuthenticationとは Authenticationは、認証済みのブラウザ状態をファイルに保存し、各テストの実行時に読み込ませることで「ログイン済み」の状態からテストを開始できる仕組みです。認証を使用する流れは、簡単に書くと以下のようになっています。\nセットアップ用ファイル（auth.setup.ts）を作成し、playwright.config.tsに定義する テスト開始前に１のセットアップが実行されると、ログイン処理と共にuser.jsonにログイン状態が保存される テストのstorageStateオプションにuser.jsonを指定することで、ブラウザが認証済みの状態で起動する 注意点は、「サーバー側のセッションデータ」は共有されないということです。PlaywrightのAuthenticationは「ブラウザのクッキー」を保存・再利用する仕組みのため、ログアウトのようなサーバー側のセッションを削除する操作を行うと、同じクッキーを使っている他のテストでも認証が切れてしまいます。\nこの問題を避けるため、ログアウトのテストでは他のテストとアカウントを分けることが推奨されています。\nAuthenticationの設定方法 ではAuthenticationを設定します。今回は、任意のテストファイルやテストグループに対してのみ認証を適用する形にしたいので、それを目標に進めてゆきます。\nplaywright/.auth ディレクトリの作成 認証状態を保存するuser.jsonファイル自体は、セットアップ実行時に自動で作成されます。ですがファイルを保存するディレクトリ（Playwrightではplaywright/.auth推奨）は作成されないため、無い場合は以下のコマンドで事前に作成します。\nまた、機密情報を持つファイルをリポジトリに含めないよう、.gitignoreにディレクトリを追加します。\n$ mkdir -p playwright/.auth $ echo $\u0026#39;\\nplaywright/.auth\u0026#39; \u0026gt;\u0026gt; .gitignore セットアップ用ファイルを作成 次に、セットアップファイルを作成します。auth.setup.tsという名前のファイルを作成し、以下のように記載します。\ne2e/auth.setup.ts import { test as setup } from \u0026#39;@playwright/test\u0026#39;; const authFile = \u0026#39;playwright/.auth/user.json\u0026#39;; setup(\u0026#39;認証情報を保存\u0026#39;, async ({ page }) =\u0026gt; { await page.goto(\u0026#39;/login\u0026#39;); await page.getByLabel(\u0026#39;メールアドレス\u0026#39;).fill(process.env.TEST_EMAIL!); await page.getByLabel(\u0026#39;パスワード\u0026#39;).fill(process.env.TEST_PASSWORD!); await page.getByRole(\u0026#39;button\u0026#39;, { name: \u0026#39;ログイン\u0026#39; }).click(); // ログイン成功を確認 await page.waitForURL(\u0026#39;/user/diary\u0026#39;); // 認証状態を保存 await page.context().storageState({ path: authFile }); }); ログイン動作自体は通常のPlaywrightのブラウザ操作と同じ書き方です。重要なのは、コードの最後のpage.context().storageState({ path: authFile });の箇所です。user.jsonのパスを渡し、認証状態を保存しています。\nPlaywrightのドキュメントによると、ログイン動作後すぐに認証状態を保存するのではなく、上のコードのawait page.waitForURL(\u0026rsquo;/user/diary\u0026rsquo;);のように、確実にログインしたことを確認するコードを挟んだ後で保存することがおすすめのようです。\nconfigファイルに定義 playwright.config.tsに、作成したセットアップファイルを定義します。\nplaywright.config.ts ・・・・・ projects: [ { name: \u0026#39;setup\u0026#39;, testMatch: /.*auth\\.setup\\.ts/, }, { name: \u0026#39;chromium\u0026#39;, dependencies: [\u0026#39;setup\u0026#39;], // 先にsetupを実行 use: { ...devices[\u0026#39;Desktop Chrome\u0026#39;] }, }, ・・・・・ name: \u0026lsquo;setup\u0026rsquo;でセットアップ用のプロジェクトを定義し、dependencies: [\u0026lsquo;setup\u0026rsquo;]では、chromiumのテスト実行前にsetupを実行することを定義しています。dependencies: [\u0026lsquo;setup\u0026rsquo;]はブラウザごとに記述する必要があるので、firefoxやwebkitなども使用する場合はそちらにも追記してくださいね。\nテストファイルでuseする 最後に、認証ファイルを任意のテストで読み込みます。以下のようにtest.use()を使い、認証ファイルへのパスを記載します。\ne2e/authenticated.spec.ts import { test, expect, Page } from \u0026#39;@playwright/test\u0026#39;; test.describe(\u0026#39;ログイン済みユーザーを使ったテスト\u0026#39;, () =\u0026gt; { test.use({ storageState: \u0026#39;playwright/.auth/user.json\u0026#39; }); //認証ファイルのパスを指定 test(\u0026#39;ダッシュボードの表示\u0026#39;, async ({ page }: { page: Page }) =\u0026gt; { // すでにログイン済みの状態からテストを開始できる await page.goto(\u0026#39;/user/diary\u0026#39;); ・・・・・ }); }); test.use()の記述位置ですが、個々のtest()の中に書くとエラーとなりますので、上の例のようにtest()の外に配置してください。そうすると、test.describe()のグループ全体、またはそのテストファイル全体に認証状態が適用されます。\nテストの実行時 先ほどのauth.setup.tsでは、メールアドレス・パスワードなどログインに使う情報を、環境変数（process.env）として取得していました。そのためテスト実行時には、ターミナルで以下のコマンドを実行して環境変数をセットしてください。\n$ export TEST_EMAIL=test@example.com; export TEST_PASSWORD=password $ npx playwright test exportコマンドはMacやLinux環境を想定しています。Windowsをお使いの場合は、ご利用のシェルに合わせて適宜コマンドを読み替えて実行ください。\n全体を認証済みとし、特定のテストだけ認証状態をリセットする場合 先ほどの例では任意のテストでのみ認証状態を使用しましたが、テスト全体はログイン済みの状態とし、一部のテストでのみ未ログイン状態としたい場合もあるかと思います。\nそのような場合は、以下のようにplaywright.config.tsで全体に認証情報を設定します。適用したいブラウザ（ここではchromium）プロジェクトのuseオプションに、storageStateとしてuser.jsonファイルを追加します。\nplaywright.config.ts ・・・・・ projects: [ { name: \u0026#39;setup\u0026#39;, testMatch: /.*auth\\.setup\\.ts/, }, { name: \u0026#39;chromium\u0026#39;, dependencies: [\u0026#39;setup\u0026#39;], use: { ...devices[\u0026#39;Desktop Chrome\u0026#39;], // 認証データを使用 storageState: \u0026#39;playwright/.auth/user.json\u0026#39;, // ここを追加 }, }, ・・・・・ そして認証を使いたくないテストファイルでは、以下のようにtest.use()でstorageStateの状態をクリアします。\nimport { test, expect, Page } from \u0026#39;@playwright/test\u0026#39;; test.describe(\u0026#39;未ログインユーザーのテスト\u0026#39;, () =\u0026gt; { test.use({ storageState: { cookies: [], origins: [] } }); test(\u0026#39;ログイン成功\u0026#39;, async ({ page }: { page: Page }) =\u0026gt; { ・・・・・ }); }); テスト時間が変わるのか確認 では、Authenticationを使うことでテスト実行時間に影響があるか確認してみます。対象のテストにはログイン処理を実行する箇所が８箇所あり、これらをAuthenticationを使う形に修正しました。比較するにはテスト数が少ないですが、これでやってみます。\nまずはAuthentication導入前、ログイン処理が８箇所あるテストを実行してみます。\n$ npx playwright test Running 11 tests using 4 workers ・・・・・ 11 passed (8.6s) ８.６秒でテストが完了しました。\n次に、Authenticationによる認証状態を使用したコードでテストを実行します。\n$ npx playwright test Running 11 tests using 4 workers ・・・・・ 11 passed (5.9s) 今度は５.９秒でテストが完了しました。念のためテストをそれぞれ複数回実行しましたが、後者のテストのほうが常に２秒以上短い時間でテストが完了する結果となりました。\nたった８箇所に認証状態を適用しただけで２〜３秒テスト時間が短縮できたので、テスト数が多ければ恩恵は大きいと思います。なによりログイン部分を省略してすっきり書けますので、Playwrightを使用している方はぜひ試してみてください。\n","date":"2026-01-19T03:11:09+09:00","permalink":"https://www.larajapan.com/2026/01/19/playwright%E3%81%A7e2e%E3%83%86%E3%82%B9%E3%83%88%E3%82%92%E8%87%AA%E5%8B%95%E5%8C%96-%EF%BC%88%EF%BC%95%EF%BC%89authentication%E3%81%A7%E3%83%AD%E3%82%B0%E3%82%A4%E3%83%B3%E5%87%A6%E7%90%86%E3%82%92/","title":"PlaywrightでE2Eテストを自動化 （５）Authenticationでログイン処理を簡略化"},{"content":"開発に関わるタスクとして手動で実行するコマンドは日に日に増える一方です。Laravelのシンプルなプロジェクトでも、コード作成後にはpint（コード自動整形ツール）、PHPStan（静的解析ツール）、そしてpest（テスト）を必ず実行します。また、ブラウザで動作チェックするために\nローカル環境を立ち上げるコマンドたち、さらに、gitのようなバージョン管理のためのコマンド実行もあります。問題はこれらのコマンドすべてを覚えるのは大変なのと、いくつかのコマンドは１つ１つタイプして実行するより、まとめて実行して簡素化もしたいです。つまり、タスクを整理するツールは必須です。今回はこれらのツールの紹介です。\n開発に必要なタスクの種類 まず、私の開発環境をもとにして、どのようなタスクがあるかグループして整理してみます。 コード作成後に実行するタスク コード自動整形ツール：pint、rector 静的解析ツール：phpstan テストツール：phpunit、pest バージョンコントロール：git commit, push, branch, mergeなど ローカル開発環境を立ち上げるタスク コンテナ―の立ち上げ：sail up ウェブサーバー：php artisan server CSSやスクリプトのビルド・ホットリロード：npm run dev これらを実行して、ブラウザーでの動作確認が可能となります。 インストール作業のためのタスク バージョンコントロール：git pull ライブラリのインストール：　composer install データーベース変更：php artisan migrate キャッシュの作成：php artisan config:cache, event:cache, route:cache 他にも、ログのチェックのために単に、less storage/logs/laravel.logの実行とかもありますね。\nさて、これらを整理するツールを紹介です。\ncomposer composerは基本的にはphpのプロジェクトで使用するパッケージを管理するコマンドですが、新規のLaravelプロジェクトをインストールすると、以下のように、composer.json内に\"scripts\"のセクションがあり、いくつかのタスクがグループされて実行を可能としています。 composer.json { ... \u0026#34;scripts\u0026#34;: { \u0026#34;post-autoload-dump\u0026#34;: [ \u0026#34;Illuminate\\\\Foundation\\\\ComposerScripts::postAutoloadDump\u0026#34;, \u0026#34;@php artisan package:discover --ansi\u0026#34; ], \u0026#34;post-update-cmd\u0026#34;: [ \u0026#34;@php artisan vendor:publish --tag=laravel-assets --ansi --force\u0026#34; ], \u0026#34;post-root-package-install\u0026#34;: [ \u0026#34;@php -r \\\u0026#34;file_exists(\u0026#39;.env\u0026#39;) || copy(\u0026#39;.env.example\u0026#39;, \u0026#39;.env\u0026#39;);\\\u0026#34;\u0026#34; ], \u0026#34;post-create-project-cmd\u0026#34;: [ \u0026#34;@php artisan key:generate --ansi\u0026#34;, \u0026#34;@php -r \\\u0026#34;file_exists(\u0026#39;database/database.sqlite\u0026#39;) || touch(\u0026#39;database/database.sqlite\u0026#39;);\\\u0026#34;\u0026#34;, \u0026#34;@php artisan migrate --graceful --ansi\u0026#34; ], \u0026#34;dev\u0026#34;: [ \u0026#34;Composer\\\\Config::disableProcessTimeout\u0026#34;, \u0026#34;npx concurrently -c \\\u0026#34;#93c5fd,#c4b5fd,#fb7185,#fdba74\\\u0026#34; \\\u0026#34;php artisan serve\\\u0026#34; \\\u0026#34;php artisan queue:listen --tries=1\\\u0026#34; \\\u0026#34;php artisan pail --timeout=0\\\u0026#34; \\\u0026#34;npm run dev\\\u0026#34; --names=server,queue,logs,vite\u0026#34; ], \u0026#34;test\u0026#34;: [ \u0026#34;@php artisan config:clear --ansi\u0026#34;, \u0026#34;@php artisan test\u0026#34; ] }, ... 例えば、\n$ composer dev を実行すれば、以下のコマンドを、\nウェブサーバーの起動：php artisan server キューを継続的にモニター・処理：php artisan queue ログをモニター：php aritsan pail CSSやスクリプトのビルド・ホットリロード：npm run dev 皆いっぺんに並行実行します。以下は実行時のコマンド画面です。\nつまり、composer.jsonのファイルを編集して、自由に必要なタスクのコマンドをグループ化して実行が可能というわけです。\nnpm npmは、composerと違ってphpではなくjs関連のパッケージの管理ツールですが、composerと同様に、package.jsonに\u0026quot;script\u0026quot;のセクションがあり、先のようなグループタスクを以下のように設定が可能です。\n{ \u0026#34;private\u0026#34;: true, \u0026#34;type\u0026#34;: \u0026#34;module\u0026#34;, \u0026#34;scripts\u0026#34;: { \u0026#34;build\u0026#34;: \u0026#34;vite build\u0026#34;, \u0026#34;dev\u0026#34;: \u0026#34;vite\u0026#34;, \u0026#34;start\u0026#34;: \u0026#34;concurrently -c \\\u0026#34;#93c5fd,#c4b5fd,#fb7185,#fdba74\\\u0026#34; \\\u0026#34;php artisan serve\\\u0026#34; \\\u0026#34;php artisan queue:listen --tries=1\\\u0026#34; \\\u0026#34;php artisan pail --timeout=0\\\u0026#34; \\\u0026#34;npm run dev\\\u0026#34; --names=server,queue,logs,vite\u0026#34; }, ... 実行は、以下のコマンドとなります。\n$ npm run start make C言語のプログラマー（昔は誰しもそうであった）ならとても馴染みのあるmakeコマンドは、タスクコマンドの整理にも使用できます。\nmakeの典型的な使用は、まずMakefileを作成して、そこでターゲットとソースコードの依存を指定し、それを作成するためのコマンドを記述します。\n例えば、以下では、\nMakefile foo: foo.c cc foo.c -o foo １行目は、ターゲットは実行可能なファイル、fooであり、その生成のためにC言語のソースコード、foo.cに依存することを示します。 もし、foo.cがfooより古いあるいは存在しないなら、２行目以降に指定されたコマンドを実行します。そこでは、ccはＣのコンパイラーが記述されていて、-oで生成されるバイナリーのファイル名を指定されています。 この２行目では必ず、行先頭にタブ文字を含むことが必要です。空白文字ではだめです。結構センシティブなので注意を。\nこの実行は、\n$ make となります。\nさて、このMakefileでは、コンパイルでなくとも他のタスクの指定にも使うことが可能です。以下では、先のcomposer.jsonやpackage.jsonのように、ブラウザチェックのために必要なコマンドの起動を指定します。\nMakefile .PHONY: dev dev: npx concurrently -c \u0026#34;#93c5fd,#c4b5fd,#fb7185,#fdba74\u0026#34; \\ \u0026#34;php artisan serve\u0026#34; \\ \u0026#34;php artisan queue:listen --tries=1\u0026#34; \\ \u0026#34;php artisan pail --timeout=0\u0026#34; \\ \u0026#34;npm run dev\u0026#34; \\ --names=server,queue,logs,vite 最初の.PHONY: devは、devが先のCソースコードのコンパイルの例とは異なりターゲットがファイルではないこと意味します。 また、dev:の行は、ターゲット名のみであり、その依存元はありません。そして、続く行は実際に実行されるコマンドです。これはcomposerやnpmと同様です。 しかし、バックスラッシュを使用して改行できたり、コマンドすべてを引用符で囲む必要もないので読みやすくできます。\n私がmakeが好きなのは、開発のタスクの実行コマンドを、パッケージ管理が使用目的のcomposer.jsonやpackage.jsonから明確に分けることができること、そしてbashなどのシェルスクリプトでよく使われる変数、条件式、ループを取り入れることも可能です。、さらにLaravelで使用する.envも読み込むことも可能なことです。\n例えば、.envと.env.exampleを比べて使う変数名に漏れがないか調べるには、Makefileを以下のように設定できます。\nMakefile include .env ENV_EXAMPLE=`grep \u0026#39;=\u0026#39; .env.example | awk -F\u0026#39;=\u0026#39; \u0026#39;{ print $$1}\u0026#39; | sort -u` ENV_ACTUAL=`grep -v \u0026#39;^\\#\u0026#39; .env | grep \u0026#39;=\u0026#39; | awk -F\u0026#39;=\u0026#39; \u0026#39;{ print $$1}\u0026#39; | sort -u` env: for i in $(ENV_ACTUAL); do \\ if ! grep -q \u0026#34;^$$i=\u0026#34; .env.example; then echo \u0026#34;not found $$i in .env.example\u0026#34;; fi; \\ done; \\ for i in $(ENV_EXAMPLE); do \\ if ! grep -q \u0026#34;^$$i=\u0026#34; .env; then echo \u0026#34;not found $$i in .env\u0026#34;; fi; \\ done ","date":"2025-12-17T07:48:04+09:00","permalink":"https://www.larajapan.com/2025/12/17/%E9%96%8B%E7%99%BA%E3%81%AE%E3%82%BF%E3%82%B9%E3%82%AF%E3%82%92%E6%95%B4%E7%90%86%E3%81%99%E3%82%8B%E3%81%9F%E3%82%81%E3%81%AE%E3%83%84%E3%83%BC%E3%83%AB/","title":"開発のタスクを整理するためのツール"},{"content":"Pest4からブラウザテスト機能が追加され、簡単なセットアップだけでPlaywrightを使ったE2Eテストが使えるようになりました。今回はLaravelで作成したプロジェクトを使って、セットアップからテストコード作成までの流れを実際に試してみます。\nセットアップ 私の環境ではユニットテストとしてすでにPestを導入済みですが、もしPHPUnit環境の方は移行に関する以下の記事もご参照ください。\nLaravelのPHPUnitテストをpest-plugin-driftでPestへ変換\nブラウザテストはPest v4系で利用できるため、まずPestを更新します。\n$ composer require pestphp/pest --dev --with-all-dependencies v4.1.4に更新されました。次にブラウザテスト用のプラグインをインストールします。\n$ composer require pestphp/pest-plugin-browser --dev そして、Playwright本体とブラウザをインストールします。\n$ npm install playwright@latest $ npx playwright install 次に、スクリーンショットの保存ディレクトリをgitignoreに追記します。ドキュメントに記載の通り、テスト実行時に生成されるスクリーンショットをGit管理から外しておくためです。\n保存先はブラウザテストのディレクトリによって変わります。私の環境ではtests/Browserにテストを配置するため、以下のようになります。\n.gitignore ... tests/Browser/Screenshots ここまでで準備は完了です。動作確認用に、画面遷移・表示確認だけの以下のようなシンプルなテストを作成しました。\ntests/Browser/BrowserTest.php it(\u0026#39;example\u0026#39;, function () { $page = visit(\u0026#39;/\u0026#39;); $page-\u0026gt;assertSee(\u0026#39;Laravel\u0026#39;); }); テストを実行します。テスト用のサーバーが自動で起動するので、php artisan serveは不要です。\n$ ./vendor/bin/pest tests/Browser/BrowserTest.php FAIL Tests\\Browser\\BrowserTest ⨯ it example 0.04s ──────────────────────────────────────────────── FAILED Tests\\Bro… BindingResolutionException Target class [config] does not exist. テストが失敗してしまいました。新しく作成したBrowserディレクトリが、Pestの設定ファイルに追加されていないことが原因のようです。\nPest.phpを以下のように修正しました。Browserを追加しています。\ntests/Pest.php pest()-\u0026gt;extend(Tests\\TestCase::class) ・・・ -\u0026gt;in(\u0026#39;Feature\u0026#39;, \u0026#39;Browser\u0026#39;); もう一度テストを実行してみます。\n$ ./vendor/bin/pest tests/Browser/BrowserTest.php PASS Tests\\Browser\\BrowserTest ✓ it example 1.33s Tests: 1 passed (1 assertions) Duration: 1.99s テストが通りました！これで準備完了です。\n新規作成フロー・テストコード全体 セットアップが整ったので、ここでは実際に作成したブラウザテストのコード全体を先にご紹介します。\nテスト対象は前回も使用した英語日記アプリの新規作成機能です。\n新規作成は３段階の画面遷移になっており、「日本語入力画面 → 英訳を入力・AI翻訳を実行 → 確認して保存」という流れになっているので、テストでは各画面でのテキスト入力や遷移の動作を確認しています。\nまた１日の入力文字数には制限があるので、入力したテキストの文字数によって文字数のカウント表示が正確に更新されているか、も確認しています。\nuse App\\Models\\User; it(\u0026#39;日記新規作成\u0026#39;, function () { User::factory()-\u0026gt;create([ \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;test@example.com\u0026#39;, \u0026#39;password\u0026#39; =\u0026gt; bcrypt(\u0026#39;password\u0026#39;), ]); // ログインページへ移動 $page = visit(\u0026#39;/login\u0026#39;); // メールアドレスとパスワードを入力 $page-\u0026gt;fill(\u0026#39;email\u0026#39;, \u0026#39;test@example.com\u0026#39;) -\u0026gt;fill(\u0026#39;password\u0026#39;, \u0026#39;password\u0026#39;) -\u0026gt;click(\u0026#39;ログイン\u0026#39;) -\u0026gt;assertPathIs(\u0026#39;/user/diary\u0026#39;); // 新規作成ページへ移動 $page-\u0026gt;click(\u0026#39;a[href*=\u0026#34;/diary/create\u0026#34;]\u0026#39;) -\u0026gt;assertPathIs(\u0026#39;/user/diary/create\u0026#39;); // 入力テキストを準備 $japaneseText = \u0026#39;今日は素晴らしい一日でした。\u0026#39;; $englishText = \u0026#39;Today was a wonderful day.\u0026#39;; // 文字数カウンター確認用に、入力前の残り文字数を取得 $initialRemaining = (int)$page-\u0026gt;text(\u0026#39;#char_count\u0026#39;); $inputLength = mb_strlen($japaneseText); $expectedRemaining = $initialRemaining - $inputLength; // 画面１: 日本語で入力 $page-\u0026gt;fill(\u0026#39;native_content\u0026#39;, $japaneseText) // 文字数カウンターの確認（計算した値と一致するか） -\u0026gt;assertSee(\u0026#34;残り文字数: {$expectedRemaining}\u0026#34;) -\u0026gt;click(\u0026#39;次へ！\u0026#39;); // 画面２: 英訳を入力 $page-\u0026gt;assertSee(\u0026#39;英語で書いてみよう！\u0026#39;) -\u0026gt;fill(\u0026#39;user_translation\u0026#39;, $englishText) -\u0026gt;click(\u0026#39;翻訳する\u0026#39;); // 翻訳中の画面表示が消えるまで待機 $page-\u0026gt;page()-\u0026gt;getByText(\u0026#39;翻訳中...\u0026#39;)-\u0026gt;first()-\u0026gt;waitFor([\u0026#39;state\u0026#39; =\u0026gt; \u0026#39;hidden\u0026#39;]); // 画面３への遷移待機 $page-\u0026gt;wait(1); // アサートに必要なデータの取得 $aiTranslation = $page-\u0026gt;value(\u0026#39;translated_content\u0026#39;); expect($aiTranslation)-\u0026gt;not-\u0026gt;toBeEmpty(\u0026#39;AI翻訳結果が空です\u0026#39;); // 画面３: 保存 $page-\u0026gt;click(\u0026#39;保存\u0026#39;) -\u0026gt;assertSee(\u0026#39;日記が作成されました。\u0026#39;) // 詳細画面での表示確認 -\u0026gt;assertSee($aiTranslation) -\u0026gt;assertSee($japaneseText); }); 複数の画面遷移を含むのでコードは少し長めですが、メソッド名はどれもシンプルで分かりやすいので流れは追いやすいかと思います。\nでは続いて、今回のテストで使用したメソッドをまとめます。\n画面遷移・操作系メソッド まず、画面遷移にはvisit()を使用します。pageオブジェクトが返るので、後続のコードでは$pageを元に、画面を操作したりアサートしたりする形になります。\n$page = visit(\u0026#39;/login\u0026#39;); 入力・クリック操作は、fill()・click()を使用します。\n$page-\u0026gt;fill(\u0026#39;email\u0026#39;, \u0026#39;test@example.com\u0026#39;) -\u0026gt;fill(\u0026#39;password\u0026#39;, \u0026#39;password\u0026#39;) -\u0026gt;click(\u0026#39;ログイン\u0026#39;) 上記のコードでは、email・passwordというname要素に指定の文字列を入力後、ログインボタンをクリックしています。\nまた指定の時間待機したい時はwait()を使用し、引数に秒数を渡します。\n$page-\u0026gt;wait(1); 要素の値を取得するメソッド 次に、要素の値を取得する時は、text()・value()を使用します。\ntext()は、HTML要素内のテキストを取得します。以下の例では、idにchar_countを持つ要素からテキストを取得しています。\n$initialRemaining = $page-\u0026gt;text(\u0026#39;#char_count\u0026#39;); またvalue()は、input・textarea・selectなどフォーム要素のvalue値を取得します。以下のコードでは、idがtranslated_contentという要素から文字列を取得しています。\n$aiTranslation = $page-\u0026gt;value(\u0026#39;translated_content\u0026#39;); セレクタの指定について ここまでの例で、セレクタの指定方法が複数混在していることに気づかれた方もいるかもしれません。\ntext(\u0026rsquo;#char_count\u0026rsquo;)のようにid明示するケースや、click(\u0026lsquo;a[href*=\u0026quot;/diary/create\u0026quot;]\u0026rsquo;)のようにcssセレクタを直接指定する書き方、click(\u0026lsquo;ログイン\u0026rsquo;)のように文字列だけを渡しているケースなどがあります。\nこれはPestで定義されているGuessLocator機能のおかげです。GuessLocatorは、以下の優先順位で引数の情報から自動で要素を選択してくれます。\n#や.で始まる場合 → CSSセレクタとしてそのまま使用 @で始まる場合 → data-testid/data-test属性として検索 通常の文字列の場合 → id、name属性として検索 上記で無い場合は文字列として要素を検索 こちらの詳細は、vendor/pestphp/pest-plugin-browser/src/Support/GuessLocator.phpに定義されています。\nアサーション系 続いてアサーションです。URLの確認には、以下のようにassertPathIs()を使用します。\n$page-\u0026gt;click(\u0026#39;ログイン\u0026#39;) -\u0026gt;assertPathIs(\u0026#39;/user/diary\u0026#39;); またassertSee()では、ページ内に引数のテキストが表示されているかを確認します。\n$page-\u0026gt;assertSee(\u0026#34;日記が作成されました。\u0026#34;); 使用可能なLaravelのメソッド Pest4のブラウザテストでは、Laravelのテストヘルパーメソッドも使用できます。\n先ほどのテスト内で使用しているfactory()だけでなくactingAs()・assertAuthenticated()なども使えるので、ユーザーログインの手順をスキップしたい場合は以下のように簡略化できて便利です。\n$user = User::factory()-\u0026gt;create([ ・・・ ]); $this-\u0026gt;actingAs($user); $this-\u0026gt;assertAuthenticated(); ・・・ Playwrightのメソッドを使いたい場合 最後に、Pestのメソッドだけでは対応できない場合についてご紹介します。\n今回のテストでは「特定の要素が非表示になるまで待機」という処理が必要でしたが、Pestの標準メソッドにはPlaywrightのwaitFor()に該当する機能がありませんでした。このような場合は、page()メソッドを使ってPlaywrightオブジェクトに直接アクセスできます。\n$page-\u0026gt;page()-\u0026gt;getByText(\u0026#39;翻訳中...\u0026#39;)-\u0026gt;first()-\u0026gt;waitFor([\u0026#39;state\u0026#39; =\u0026gt; \u0026#39;hidden\u0026#39;]); このコードでは、「翻訳中\u0026hellip;」というテキストを持つ要素を取得し、その要素がhiddenになるまで待機させる処理を、Playwrightオブジェクトを経由して実行しています。\nまとめ テストコードの書き方自体は、CypressやPlaywrightを単体で使う場合と比べても大きな違いはないかなと思いました。\nただ今回のPest4はLaravelのテストとしてブラウザテストをプロジェクトに統合できるため、factoryでのテスト用レコードの生成や、認証ヘルパーなどがそのままE2Eテストに使用できます。その点が他と比べてもとても便利だと感じました。\n","date":"2025-12-10T07:22:45+09:00","permalink":"https://www.larajapan.com/2025/12/10/pest4%E3%81%AEe2e%E3%83%86%E3%82%B9%E3%83%88-%E3%82%BB%E3%83%83%E3%83%88%E3%82%A2%E3%83%83%E3%83%97%E3%80%9C%E3%83%86%E3%82%B9%E3%83%88%E3%82%B3%E3%83%BC%E3%83%89%E3%81%AE%E5%AE%9F%E8%A3%85%E4%BE%8B/","title":"Pest4のE2Eテスト - セットアップ〜テストコードの実装例"},{"content":"たいていのプロジェクトで扱うデータベースは１つですが、これが２つあるいは３つのデータベースにアクセスするとなったら、どう対応するのでしょう？というのが今回のテーマです。\nデータベースのコネクション まず、データベースのコネクションに関しての説明です。扱うデータベースが１つとすると、 .env ... DB_CONNECTION=mysql DB_HOST=localhost DB_PORT=3306 DB_DATABASE=db1 DB_USERNAME=root DB_PASSWORD=password ... と設定します。これらの環境変数は、以下のconfig/database.phpにおいてのデータベース接続のための値となります。\nconfig/database.php return [ \u0026#39;default\u0026#39; =\u0026gt; env(\u0026#39;DB_CONNECTION\u0026#39;, \u0026#39;mysql\u0026#39;), \u0026#39;connections\u0026#39; =\u0026gt; [ \u0026#39;mysql\u0026#39; =\u0026gt; [ \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;mysql\u0026#39;, \u0026#39;url\u0026#39; =\u0026gt; env(\u0026#39;DB_URL\u0026#39;), \u0026#39;host\u0026#39; =\u0026gt; env(\u0026#39;DB_HOST\u0026#39;, \u0026#39;127.0.0.1\u0026#39;), \u0026#39;port\u0026#39; =\u0026gt; env(\u0026#39;DB_PORT\u0026#39;, \u0026#39;3306\u0026#39;), \u0026#39;database\u0026#39; =\u0026gt; env(\u0026#39;DB_DATABASE\u0026#39;, \u0026#39;laravel\u0026#39;), \u0026#39;username\u0026#39; =\u0026gt; env(\u0026#39;DB_USERNAME\u0026#39;, \u0026#39;root\u0026#39;), \u0026#39;password\u0026#39; =\u0026gt; env(\u0026#39;DB_PASSWORD\u0026#39;, \u0026#39;\u0026#39;), \u0026#39;unix_socket\u0026#39; =\u0026gt; env(\u0026#39;DB_SOCKET\u0026#39;, \u0026#39;\u0026#39;), \u0026#39;charset\u0026#39; =\u0026gt; env(\u0026#39;DB_CHARSET\u0026#39;, \u0026#39;utf8mb4\u0026#39;), \u0026#39;collation\u0026#39; =\u0026gt; env(\u0026#39;DB_COLLATION\u0026#39;, \u0026#39;utf8mb4_unicode_ci\u0026#39;), \u0026#39;prefix\u0026#39; =\u0026gt; \u0026#39;\u0026#39;, \u0026#39;prefix_indexes\u0026#39; =\u0026gt; true, \u0026#39;strict\u0026#39; =\u0026gt; true, \u0026#39;engine\u0026#39; =\u0026gt; null, \u0026#39;options\u0026#39; =\u0026gt; extension_loaded(\u0026#39;pdo_mysql\u0026#39;) ? array_filter([ PDO::MYSQL_ATTR_SSL_CA =\u0026gt; env(\u0026#39;MYSQL_ATTR_SSL_CA\u0026#39;), ]) : [], ], ... 扱うデータベースが２つのとき デフォルトのデータベースへのコネクションが、mysqlとすると、もう１つ扱うデータベースを mysql2 として、先のconfig/database.phpに追加することができます。\nconfig/database.php return [ \u0026#39;default\u0026#39; =\u0026gt; env(\u0026#39;DB_CONNECTION\u0026#39;, \u0026#39;mysql\u0026#39;), \u0026#39;connections\u0026#39; =\u0026gt; [ // デフォルトのデータベース \u0026#39;mysql\u0026#39; =\u0026gt; [ ... ], // 第２のデータベース \u0026#39;mysql2\u0026#39; =\u0026gt; [ \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;mysql\u0026#39;, \u0026#39;url\u0026#39; =\u0026gt; env(\u0026#39;DB2_URL\u0026#39;), \u0026#39;host\u0026#39; =\u0026gt; env(\u0026#39;DB2_HOST\u0026#39;, \u0026#39;127.0.0.1\u0026#39;), \u0026#39;port\u0026#39; =\u0026gt; env(\u0026#39;DB2_PORT\u0026#39;, \u0026#39;3306\u0026#39;), \u0026#39;database\u0026#39; =\u0026gt; env(\u0026#39;DB2_DATABASE\u0026#39;, \u0026#39;laravel\u0026#39;), \u0026#39;username\u0026#39; =\u0026gt; env(\u0026#39;DB2_USERNAME\u0026#39;, \u0026#39;root\u0026#39;), \u0026#39;password\u0026#39; =\u0026gt; env(\u0026#39;DB2_PASSWORD\u0026#39;, \u0026#39;\u0026#39;), \u0026#39;unix_socket\u0026#39; =\u0026gt; env(\u0026#39;DB2_SOCKET\u0026#39;, \u0026#39;\u0026#39;), \u0026#39;charset\u0026#39; =\u0026gt; env(\u0026#39;DB2_CHARSET\u0026#39;, \u0026#39;utf8mb4\u0026#39;), \u0026#39;collation\u0026#39; =\u0026gt; env(\u0026#39;DB2_COLLATION\u0026#39;, \u0026#39;utf8mb4_unicode_ci\u0026#39;), \u0026#39;prefix\u0026#39; =\u0026gt; \u0026#39;\u0026#39;, \u0026#39;prefix_indexes\u0026#39; =\u0026gt; true, \u0026#39;strict\u0026#39; =\u0026gt; true, \u0026#39;engine\u0026#39; =\u0026gt; null, \u0026#39;options\u0026#39; =\u0026gt; extension_loaded(\u0026#39;pdo_mysql\u0026#39;) ? array_filter([ PDO::MYSQL_ATTR_SSL_CA =\u0026gt; env(\u0026#39;MYSQL_ATTR_SSL_CA\u0026#39;), ]) : [], ], ... mysql2では、環境関数名は皆DB2_のプレフィックスとなっていることに注意を。\n.envでは、こんな感じに設定します。\n.env ... DB_CONNECTION=mysql DB_HOST=localhost DB_PORT=3306 DB_DATABASE=db1 DB_USERNAME=root DB_PASSWORD=password DB2_HOST=localhost DB2_PORT=3307 DB2_DATABASE=db2 DB2_USERNAME=root2 DB2_PASSWORD=password2 ... 第２のデータベースにアクセスするには さて、第２のデータベースにアクセスしてみましょう。\nクエリビルダーでは、以下のようにconnection()の引数に第２のデータベースのコネクション mysql2 を指定するだけです。\nDB::connection(\u0026#39;mysql2\u0026#39;)-\u0026gt;table(\u0026#39;user\u0026#39;)-\u0026gt;get(); Eloquentではどうでしょう。\nまず、第２のデータベースのモデルを作成します。\nnamespace App\\Models; use Illuminate\\Database\\Eloquent\\Model; class RemoteUser extends Model { protected $table = \u0026#39;users\u0026#39;; protected $connection = \u0026#39;mysql2\u0026#39;; // ここでコネクションを指定 } ここでは、デフォルトのデータベースと同じように、第２のデータベースでも users というテーブルが存在すると仮定します。 通常と違って、mysql2のコネクションが指定されていることも注意を。\nこの設定なら、クエリの実行はデフォルトのデータベースのEloquetとまったく変わりません。簡単ですね。\nRemoteUser::all(); Eloquentで動的にコネクションを指定 先のEloquentはモデル内ですでに指定されている固定のコネクションでしたが、前もってわからないときはどうしましょう。 つまりモデルの定義が以下のようなときのことです。\nnamespace App\\Models; use Illuminate\\Database\\Eloquent\\Model; class RemoteUser extends Model { protected $table = \u0026#39;users\u0026#39;; } このようなときは、クエリ実行時において、以下のようにコネクションを指定します。\nRemoteUser::on(\u0026#39;mysql2\u0026#39;)-\u0026gt;get(); 上では、RemoteUser::on(\u0026lsquo;mysql2\u0026rsquo;)は、Illuminate\\Database\\Eloquent\\Builderを返すので、RemoteUser::on(\u0026lsquo;mysql2\u0026rsquo;)::all()とは出来ないことに注意を。\n","date":"2025-11-14T12:04:49+09:00","permalink":"https://www.larajapan.com/2025/11/14/%E7%AC%AC%EF%BC%92%E3%81%AE%E3%83%87%E3%83%BC%E3%82%BF%E3%83%99%E3%83%BC%E3%82%B9%E3%82%92%E6%89%B1%E3%81%86/","title":"第２のデータベースにアクセス"},{"content":"Playwrightから新しくAgents機能がリリースされました。テストプランの作成・テストコードの作成・エラーとなったテストの修正を行なう、３つのエージェント機能を備えています。今回は、Claude CodeとAgentsを使ってLaravelプロジェクトにE2Eテストを追加してみます。\nテスト対象のサイト テスト対象のサイトについて簡単にご紹介します。よくある「英語日記」のような機能で、ログイン後のダッシュボードでは日記一覧の表示・音声再生・詳細画面への遷移ができます。\n新規作成は３段階の画面遷移になっており、「日本語で入力 → 英語で入力 → 最後にAIが文章を翻訳・添削してアドバイスを返す」という流れになっています。\n準備・セットアップ ではさっそくAgentsを使う準備から進めてゆきます。\nPlaywrightのインストールがまだの方は、セットアップの記事をご覧ください。\n私の環境ではすでにPlaywrightがインストール済みですが、Agentsは新しい機能のため、バージョンアップが必要かもしれません。プロジェクトのルートディレクトリで以下のコマンドを実行します。\n$ npm install -D @playwright/test@latest $ npx playwright --version Version 1.56.0 執筆時点で最新の1.56.0に更新されました。\n次に、以下のコマンドを実行します。この記事ではClaude Codeを使用するので、オプションでclaudeを指定します。\n$ npx playwright init-agents --loop=claude Claude Code以外を使用する方はドキュメントからコマンドをご確認ください。\nコマンド実行後、新規に以下の５ファイルが作成されました。\nWriting file: .claude/agents/playwright-test-generator.md Writing file: .claude/agents/playwright-test-healer.md Writing file: .claude/agents/playwright-test-planner.md Writing file: .mcp.json Writing file: e2e/seed.spec.ts .claude/agents/ディレクトリには３種類のエージェントに関するファイルが生成され、それぞれのエージェントは以下のような役割になっています。\nplaywright-test-planner.md ： テスト計画を作成するエージェント playwright-test-generator.md ： テストコードを生成するエージェント playwright-test-healer.md ： テストを修復するエージェント また、テストディレクトリにインストールされたe2e/seed.spec.tsを見てみると、初期状態では以下のように特に内容のないファイルとなっています。\ne2e/seed.spec.ts import { test, expect } from \u0026#39;@playwright/test\u0026#39;; test.describe(\u0026#39;Test group\u0026#39;, () =\u0026gt; { test(\u0026#39;seed\u0026#39;, async ({ page }) =\u0026gt; { // generate code here. }); }); こちらはグローバルセットアップや依存関係、またはエージェントへの指示を記述するファイルで、Playwrightの各エージェントから参照されます。実際の運用時には重要となるファイルですが、今回は初期状態のまま進めます。\nplannerでテストプランを作成 ここからClaude Codeを使ってゆきます。Claude Codeを起動すると、次のようなメッセージが表示されました。\nNew MCP server found in .mcp.json: playwright-test │ │ │ │ MCP servers may execute code or access system resources. All tool calls require approval. Learn more in the MCP documentation │ │ (​https://docs.claude.com/s/claude-code-mcp​). │ │ │ │ ❯ 1. Use this and all future MCP servers in this project │ │ 2. Use this MCP server │ │ 3. Continue without using this MCP server 先ほど作成された.mcp.jsonに記載のMCPサーバー（playwright-test）は、コード実行やシステムリソースへのアクセスを行うため、セキュリティ確認が求められているようです。今回は２番の「このMCPサーバーを使用」を選択しました。\nここからやっとプランの作成に入りますが、その前にテスト対象サイトにアクセスできる状態かを確認します。 今回はローカルにあるLaravelプロジェクトを対象としているため、サーバーを起動させておきます。\n$ php artisan serve プラン作成用エージェントを実行すると、ブラウザが起動してPlaywrightが実際にサイトを操作しながらテストプランを作成します。そのため、あらかじめ起動しておく必要があります。\nClaude Codeへのプロンプトは以下のように記述しました。@agent-playwright-test-plannerのように\u0026quot;@\u0026ldquo;付きで明示的にエージェントを起動してもいいですし、以下のように文章のみで渡しても問題なく認識されました。\n$ playwright-test-plannerエージェントを使って次のテストプランを作成ください。ログイン後のダッシュボード画面での日記一覧表示・新規作成・翻訳機能 すると、以下のようにplaywright-test-plannerが起動しました。\nプランナー起動時のスクリーンショット しばらく待つとプラン作成完了のメッセージが表示されました。メッセージが長いので冒頭のみですが、以下のような形で作成したプランの概要を知らせてくれます。\n⏺ テストプランの作成が完了しました！ 作成されたファイル 1. 詳細テストプラン (e2e/test-plan-dashboard.md) 600行以上の詳細なドキュメントで、以下を含みます： - 70以上のテストシナリオ - 12のテストカテゴリ： - 認証とログイン - 日記一覧表示 - 日記詳細表示 - 日記新規作成（3ステップ） - 翻訳機能 - セキュリティとバリデーション - UI/UX - エッジケースとエラー処理など 2. 要約版 (e2e/test-plan-summary.md) エグゼクティブサマリーで、以下を含みます： - 発見された主要機能 - テストシナリオの優先度 - 技術スタック分析 - 推奨される次のステップ ・・・・・ 実際のテストプランはtest-plan-dashboard.mdに出力されており、テスト概要 → シナリオ → 成功基準 という構成で書かれていて読みやすいです。こちらも一部を抜粋します。\n#### 1.1 ログイン後の正常なページロード **手順:** 1. `/user/diary`に遷移 2. ページが完全にロードされるまで待機 **期待される結果:** - ページがエラーなく正常にロード - URLが`/user/diary` - ナビゲーションバーに「テスト用ユーザー」ボタンが表示 - 左上に紫色の作成ボタンが表示 - 日記一覧セクションが表示 - 上部にページネーションコントロールが表示 #### 1.2 日記カードの表示確認 **手順:** 1. `/user/diary`に遷移 2. 一覧内の日記カードを確認 **期待される結果:** - 最低1枚の日記カードが表示（シードデータから5件） - 各カードに以下が表示: - 日付ボックス上部に月番号 - 日付ボックス下部に日番号 - 翻訳コンテンツのテキスト（英語） - 母語コンテンツのテキスト（日本語） - 音声プレーヤーまたは「音声変換中...」メッセージ - カードがレスポンシブグリッドで配置（デスクトップで3列） - カードが作成日降順で表示（最新が最初） #### 1.3 レスポンシブレイアウトの確認 **手順:** 1. デスクトップビューポート（1920x1080）で`/user/diary`をロード 2. グリッドレイアウトが3列になっていることを確認 3. タブレットビューポート（768x1024）にリサイズ 4. グリッドレイアウトが2列になっていることを確認 5. モバイルビューポート（375x667）にリサイズ 6. グリッドレイアウトが1列になっていることを確認 **期待される結果:** - デスクトップ: 3列グリッドレイアウト（`lg:w-1/3`） - タブレット: 2列グリッドレイアウト（`md:w-1/2`） - モバイル: 1列レイアウト（`w-full`） - すべてのコンテンツが読みやすく適切にフォーマット - 横スクロール不要 ページネーションやエッジケースのテストは以下のような感じです。\n#### 3.1 ページネーションコントロールの表示 **手順:** 1. テストデータベースに50件以上の日記エントリーを用意 2. `/user/diary`に遷移 3. 一覧上部のページネーションコントロールを見つける **期待される結果:** - 50件以上のエントリーがある場合、ページネーションコントロールが表示 - 現在のページ番号がハイライト - 「次へ」「前へ」リンクが表示（該当する場合） - ページ番号がクリック可能 #### 3.2 次ページへの遷移 **手順:** 1. データベースに51件以上の日記エントリーがある状態で`/user/diary`に遷移 2. 「次へ」リンクまたはページ2のリンクをクリック **期待される結果:** - URLがページネーションパラメータを含むように更新（例: `/user/diary?page=2`） - 異なる日記エントリーのセットでページがロード - 表示される日記エントリーがページ1と異なる - ページネーションコントロールが現在のページを反映 - 「前へ」リンクがアクティブに #### 6.1 日記エントリーがない場合の表示 **手順:** 1. テストユーザーのすべての日記エントリーをクリア 2. `/user/diary`に遷移 **期待される結果:** - ページがエラーなくロード - 作成ボタンが表示され機能する - 空の一覧セクションが表示 - 日記カードが表示されない - ページネーションコントロールが表示されない - エラーメッセージが表示されない #### 6.2 空状態から最初の日記作成 **手順:** 1. エントリーがない状態で`/user/diary`に遷移 2. 作成ボタンをクリック 3. 日記作成プロセスを完了 4. `/user/diary`に戻る **期待される結果:** - 空状態から作成ページへのナビゲーションが機能 - 日記作成後、一覧に新しいエントリーが表示 - 日記カードがすべての期待されるフィールドを表示 - 音声生成が自動的に開始 #### 11.1 存在しない日記へのナビゲーション処理 **手順:** 1. `/user/diary/99999`（存在しないID）に遷移 **期待される結果:** - 404エラーページまたは適切なエラーメッセージ - ユーザーが一覧に戻ることが可能 - アプリケーションクラッシュなし - エラーが適切にログ記録 パフォーマンス関連のテストもあります。\n#### 13.1 ページロードパフォーマンス **手順:** 1. 50件の日記エントリーがある状態で`/user/diary`に遷移 2. ブラウザDevToolsでページロード時間を測定 **期待される結果:** - 初期ページロードが2秒以内に完了 - Livewireコンポーネントが迅速に初期化 - 画像と音声参照が非同期でロード - ブロッキングリソースなし #### 13.2 ページネーションパフォーマンス **手順:** 1. 50件以上のエントリーがある複数ページ間をナビゲート 2. ページ遷移時間を測定 **期待される結果:** - ページ遷移が1秒以内に完了 - ページ間のスムーズなナビゲーション - 目に見える遅延やラグなし - フラッシュなしでコンテンツが更新 ユーザー認証関連も。\n#### 14.2 ユーザー固有データ表示確認 **手順:** 1. テストユーザー（test@example.com）としてログイン 2. `/user/diary`に遷移 3. テストユーザーの日記のみが表示されることを確認 **期待される結果:** - 認証済みユーザーに属する日記のみ表示 - 日記数がテストユーザーのエントリー数と一致（シードから5件） - 他のユーザーのデータが表示されない - データ分離が維持 以下は表示関連のテストですが、先ほどのページネーションのテストと少し被っている部分もあります。\n#### 15.1 単一日記エントリーの表示 **手順:** 1. テストユーザーがちょうど1件の日記エントリーを持つように設定 2. `/user/diary`に遷移 **期待される結果:** - 単一の日記カードが正しく表示 - ページネーションコントロール非表示 - 作成ボタンが引き続き機能 - レイアウトが適切な構造を維持 #### 15.2 ちょうど50件の日記エントリー（ページネーション境界） **手順:** 1. テストユーザーがちょうど50件の日記エントリーを持つように設定 2. `/user/diary`に遷移 **期待される結果:** - すべての50件のエントリーがページ1に表示 - ページネーションコントロール非表示 - ページが効率的にロード - すべてのカードが正しくレンダリング #### 15.3 51件の日記エントリー（ページネーショントリガー） **手順:** 1. テストユーザーがちょうど51件の日記エントリーを持つように設定 2. `/user/diary`に遷移 **期待される結果:** - 最初の50件のエントリーがページ1に表示 - ページネーションコントロールが表示 - ページ2のリンクが利用可能 - ページ2をクリックすると51件目のエントリーが表示 #### 15.4 非常に長い日記コンテンツ **手順:** 1. 500文字の母語コンテンツで日記を作成 2. 500文字の翻訳コンテンツで日記を作成 3. `/user/diary`に遷移 **期待される結果:** - コンテンツが300文字で適切に省略 - レイアウト崩れなし - カードが一貫した高さを維持 - 詳細ページで完全なコンテンツが利用可能 #### 15.5 コンテンツ内の特殊文字 **手順:** 1. 日本語特殊文字（々、〜など）で日記を作成 2. 絵文字文字で日記を作成 3. HTML特殊文字（\u0026lt;、\u0026gt;、\u0026amp;など）で日記を作成 4. `/user/diary`に遷移 **期待される結果:** - すべての特殊文字が正しく表示 - HTMLインジェクションやXSS脆弱性なし - 文字が適切にエンコード - レイアウトやレンダリング問題なし 基本的な画面表示からパフォーマンス測定、エッジケースまで網羅的にカバーされています。 ページネーションや表示関連ではもう少しケースをまとめられるような気がするので、作成されたテストプランは人間の確認が現時点では必要そうです。\ngenerator・healerでE2Eテストを作成 テストプランができたので、こちらを元にテストコードを作成します。提案されたテストケース数が多いため、今回は一部のテストのみを指定して以下のように生成を依頼しました。\nこのテストプランをもとに、2.1 日記一覧の表示 のテストを作成してください @agent-playwright-test-generatorとエージェントを明示しなくても、文脈から判断して自動でテスト作成用エージェントを起動してくれました。\nテスト作成時のスクリーンショット テスト作成完了後、テストの動作確認を依頼したところエラーが発生しました。すると自動でplaywright-test-healerを起動して修正してくれます。\nエラー修正時のスクリーンショット エラーの原因は、複数のLink要素が画面上に存在しており、一意に指定できなかったためでした。今回はfirst()を追加してエラーが修正されました。\n// 修正前 await expect(page.getByRole(\u0026#39;main\u0026#39;).getByRole(\u0026#39;link\u0026#39;)).toBeVisible(); // 修正後 await expect(page.getByRole(\u0026#39;main\u0026#39;).getByRole(\u0026#39;link\u0026#39;).first()).toBeVisible(); このように検証対象の要素が一意に特定しにくいHTMLとなっていると、テスト作成に時間がかかったり、セレクタ指定が冗長になるということがあります。\nそのため、HTMLにdata-testid属性を付与したり、aria-labelを付与してgetByRole()で取得しやすくしておくと、よりスムーズにテスト作成ができるようになると思います。\nPlaywrightの推奨ロケーターは複数ありますので、ドキュメントをご参照ください。\n実際に作成されたテストコードと、気になった点 修正も完了し、実際に作成されたテストコードが以下となります。\nバリデーション関連のテスト\ntest(\u0026#39;Empty Form Submission\u0026#39;, async ({ page }) =\u0026gt; { // 1. Login with test@example.com / password await page.goto(\u0026#39;http://127.0.0.1:8000/login\u0026#39;); await page.getByRole(\u0026#39;textbox\u0026#39;, { name: \u0026#39;メールアドレス\u0026#39; }).fill(\u0026#39;test@example.com\u0026#39;); await page.getByRole(\u0026#39;textbox\u0026#39;, { name: \u0026#39;パスワード\u0026#39; }).fill(\u0026#39;password\u0026#39;); await page.getByRole(\u0026#39;button\u0026#39;, { name: \u0026#39;ログイン\u0026#39; }).click(); // 2. Navigate to diary creation page (/user/diary/create) await page.goto(\u0026#39;http://127.0.0.1:8000/user/diary/create\u0026#39;); // 3. Leave the textarea empty (do not enter any text) // (No action needed - textarea is already empty) // 4. Click \u0026#34;次へ！\u0026#34; (Next!) button await page.getByRole(\u0026#39;button\u0026#39;, { name: \u0026#39;次へ！\u0026#39; }).click(); // Verify validation error is displayed await expect(page.getByText(\u0026#39;母語のテキストは必ず指定してください。\u0026#39;)).toBeVisible(); // Verify user remains on Step 1 (did not proceed to Step 2) await expect(page.getByRole(\u0026#39;heading\u0026#39;, { name: \u0026#39;Step 1: まずは日本語で書いてみよう\u0026#39; })).toBeVisible(); }); 日記一覧画面表示のテスト\ntest(\u0026#39;日記一覧の表示と基本機能\u0026#39;, async ({ page }) =\u0026gt; { // ログインページにアクセス await page.goto(\u0026#39;http://127.0.0.1:8000/login\u0026#39;); // メールアドレスを入力 await page.getByRole(\u0026#39;textbox\u0026#39;, { name: \u0026#39;メールアドレス\u0026#39; }).fill(\u0026#39;test@example.com\u0026#39;); // パスワードを入力 await page.getByRole(\u0026#39;textbox\u0026#39;, { name: \u0026#39;パスワード\u0026#39; }).fill(\u0026#39;password\u0026#39;); // ログインボタンをクリック await page.getByRole(\u0026#39;button\u0026#39;, { name: \u0026#39;ログイン\u0026#39; }).click(); // ログイン後、ダッシュボード (/user/diary) にアクセスされることを確認 await expect(page).toHaveURL(\u0026#39;http://127.0.0.1:8000/user/diary\u0026#39;); // ナビゲーションバーにユーザー名ボタンが表示されることを確認 await expect(page.getByRole(\u0026#39;button\u0026#39;, { name: \u0026#39;テスト用ユーザー\u0026#39; })).toBeVisible(); // 新規作成ボタン（ペンアイコン）が表示されることを確認 await expect(page.getByRole(\u0026#39;main\u0026#39;).getByRole(\u0026#39;link\u0026#39;).first()).toBeVisible(); // 日記エントリーの英訳された内容（見出し）が表示されることを確認 await expect(page.getByText(\u0026#39;The weather was really nice today. I took a walk in the park this morning and saw beautiful cherry blossoms. I was happy to feel the arrival of spring.\u0026#39;).first()).toBeVisible(); // 日記エントリーの日本語原文が表示されることを確認 await expect(page.getByText(\u0026#39;今日はとても良い天気でした。朝から公園を散歩して、美しい桜を見ました。春の訪れを感じることができて嬉しかったです。\u0026#39;).first()).toBeVisible(); // 日付（月）が表示されることを確認 await expect(page.getByText(\u0026#39;10\u0026#39;).first()).toBeVisible(); // 日付（日）が表示されることを確認 await expect(page.getByText(\u0026#39;18\u0026#39;).first()).toBeVisible(); // 任意の日記カードをクリック await page.getByRole(\u0026#39;heading\u0026#39;, { name: \u0026#39;The weather was really nice\u0026#39; }).first().click(); // 日記詳細ページに遷移することを確認（URL に /user/diary/{id} が含まれる） await expect(page.url()).toMatch(/\\/user\\/diary\\/\\d+/); // 詳細ページに年が表示されることを確認 await expect(page.getByText(\u0026#39;2025\u0026#39;)).toBeVisible(); // 詳細ページに日付が表示されることを確認 await expect(page.getByText(\u0026#39;10-18\u0026#39;)).toBeVisible(); // 日記一覧に戻るボタンが表示されることを確認 await expect(page.getByRole(\u0026#39;link\u0026#39;, { name: \u0026#39;日記一覧\u0026#39; })).toBeVisible(); // 日記一覧に戻るボタンをクリック await page.getByRole(\u0026#39;link\u0026#39;, { name: \u0026#39;日記一覧\u0026#39; }).click(); // ダッシュボードに戻ることを確認 await expect(page).toHaveURL(\u0026#39;http://127.0.0.1:8000/user/diary\u0026#39;); }); 作成されたテストケースは各要素を細かくアサートしてくれており、コメントも十分なほど付いているのでコードを詳しく読まなくても何をテストしているのかがよくわかります。\nこれらをほぼ自動で作ってくれるという点でとても凄いAgents機能ですが、一方、現時点で気になった点もありましたので以下にまとめます。\n・テストごとにログイン処理のコードが記述されている\nバリデーションテスト・一覧表示確認のテストそれぞれにログイン処理のコードが含まれていました。ログイン後の画面で実施するテストであれば、テストに直接関係しないログイン動作は省略するのが理想的です。\nテストプラン作成〜テストコード作成の流れの中では、Playwright側から「ログイン動作をまとめる」という提案は特にありませんでした。本来、認証関連はseed.spec.tsなどにまとめるべきなので、プラン作成前の段階であらかじめ方針を決めておく必要がありそうです。\n・テストのたびに異なる値を固定値でチェックしている\n日記カードの表示確認テストでは、日記タイトルや内容を固定の文字列で比較して存在確認を行うコードになっていました。しかし、これらの値は実行のたびに変化する可能性があるため、固定値での比較は望ましくありません。\n他のテスト項目でも同様に、固定値でチェックする形になっている箇所が複数あり気になりました。こちらもあらかじめルールを設けておく必要がありそうです。\n・外部APIのモック化\n日記の新規作成時には、AI翻訳のため外部APIとの通信が発生する仕組みです。テスト実行のたびにこのAPI通信が発生し課金も伴うため、できればテスト環境ではモック化を検討して欲しかったです。\nこの点も、プラン作成前に「外部通信をどう扱うか」を明確にしておく必要がありそうです。\nまとめ 今回はClaude Codeへの指示文もシンプルで、ほぼAI任せの状態で作成したことを考えると、事前の準備次第ではもっと精度の高いテストが作成できると思います。\nこのAgents機能はリリースされてから間もないため、今後どんどんアップデートされることを踏まえると、近いうちに人間不在でのE2Eテスト作成〜実行が現実味を帯びてきたなと感じます。\n","date":"2025-10-30T11:35:59+09:00","permalink":"https://www.larajapan.com/2025/10/30/playwright%E3%81%A7e2e%E3%83%86%E3%82%B9%E3%83%88%E3%82%92%E8%87%AA%E5%8B%95%E5%8C%96-%EF%BC%88%EF%BC%94%EF%BC%89agents-x-claude-code/","title":"PlaywrightでE2Eテストを自動化 （４）Agents × Claude Code"},{"content":"アプリで使用しているデータベースの整合性を保つために、トランザクションのデータベース関数があります。Laravelでは、現時点で２通りの方法があります。Laravelのバージョン12.32.0において、DB::afterRollBackが登場して、どちらとも機能的に同等になりました。\nトランザクション トランザクションを簡単に説明すると、例えば以下のように複数のデータベース文を実行するとき、\nDB::table(\u0026#39;points\u0026#39;)-\u0026gt;insert([\u0026#39;member_id\u0026#39; =\u0026gt; 1, \u0026#39;point\u0026#39; =\u0026gt; 1000]); DB::table(\u0026#39;points\u0026#39;)-\u0026gt;insert([\u0026#39;member_id\u0026#39; =\u0026gt; 1, \u0026#39;point\u0026#39; =\u0026gt; 2000]); 最初のデータベース文が成功して、２番目のデータベース文が何等かの理由で失敗したときに、成功したものだけを残すのでなく、両方と何も起こらなかった状態に戻す機能です。\nつまり、この２つデータベース文をあたかも１つのデータベース文のように扱い、両方が成功するならコミットとし、失敗が起こるならロールバックします。上の例では、成功なら3000ポイント追加され失敗したならたとえ１つが成功しても0ポイントの追加となります。\nDB::beginTransaction トランザクションをLaravelで実行するには、以下のようにDB::beginTransactionを使います。\nDB::beginTransaction(); try { DB::table(\u0026#39;points\u0026#39;)-\u0026gt;insert([\u0026#39;member_id\u0026#39; =\u0026gt; 1, \u0026#39;point\u0026#39; =\u0026gt; 1000]); DB::table(\u0026#39;points\u0026#39;)-\u0026gt;insert([\u0026#39;member_id\u0026#39; =\u0026gt; 1, \u0026#39;point\u0026#39; =\u0026gt; 2000]); DB::commit(); echo \u0026#34;ポイント追加成功しました。\u0026#34;; } catch (\\Exception $event) { DB::rollBack(); echo \u0026#34;ポイント追加に失敗しました。\u0026#34;; echo $event-\u0026gt;getMessage(); // エラーを表示 } トランザクションと対象となるデータベース文は、try,catchに入れて、成功ならDB::commitで変更を保存し、エラーがあるなら（例外が発生）、DB::rollbackですべての変更を戻します。DB::endTransactionというものはないのでブロックで囲まれてなく変な感じですが、問題はありません。DB::beginTransactionをtryの中の最初の文としても変わりません。\nDB::transaction 先の例では、いちいちコミットとロールバック、それぞれDB::commitとDB::rollbackを指定する必要がありますが、DB::transactionを使い対象のデータベース文を以下のようにクロージャのなかに入れるなら、結構すっきりします。\nDB::transaction(function () { DB::table(\u0026#39;points\u0026#39;)-\u0026gt;insert([\u0026#39;member_id\u0026#39; =\u0026gt; 1, \u0026#39;point\u0026#39; =\u0026gt; 1000]); DB::table(\u0026#39;points\u0026#39;)-\u0026gt;insert([\u0026#39;member_id\u0026#39; =\u0026gt; 1, \u0026#39;point\u0026#39; =\u0026gt; 2000]); }) 以下のようにデータを変数で渡すことも可能です。\n$data = [ [\u0026#39;member_id\u0026#39; =\u0026gt; 1, \u0026#39;point\u0026#39; =\u0026gt; 1000], [\u0026#39;member_id\u0026#39; =\u0026gt; 1, \u0026#39;point\u0026#39; =\u0026gt; 2000] ]; DB::transaction(function () use($data) { DB::table(\u0026#39;points\u0026#39;)-\u0026gt;insert($data[0]); DB::table(\u0026#39;points\u0026#39;)-\u0026gt;insert($data[1]); }) また、DB::beginTransactionのように、コミットとロールバックの後に必要なプログラム文を追加も以下のように可能です。\n$data = [ [\u0026#39;member_id\u0026#39; =\u0026gt; 1, \u0026#39;point\u0026#39; =\u0026gt; 1000], [\u0026#39;member_id\u0026#39; =\u0026gt; 1, \u0026#39;point\u0026#39; =\u0026gt; 2000] ]; DB::transaction(function () use($data) { DB::afterCommit(function() { echo \u0026#34;ポイント追加成功しました。\u0026#34;; }); DB::afterRollBack(function () { echo \u0026#34;ポイント追加に失敗しました。\u0026#34;; }); DB::table(\u0026#39;points\u0026#39;)-\u0026gt;insert($data[0]); DB::table(\u0026#39;points\u0026#39;)-\u0026gt;insert($data[1]); }) 注意する必要があるのは、対象のデータベース文は、DB::afterCommitとDB::afterRollBackの後に必ず置くことです。 また、DB::afterRollBackは、Laravelのバージョン12.32.0においての登場なので、それより前のバージョンでは使えません。\n最後に、DB::beginTransactionの例のようにエラーをキャッチしたいなら、同様にtry,catchを使います。\n$data = [ [\u0026#39;member_id\u0026#39; =\u0026gt; 1, \u0026#39;point\u0026#39; =\u0026gt; 1000], [\u0026#39;member_id\u0026#39; =\u0026gt; 1, \u0026#39;point\u0026#39; =\u0026gt; 2000] ]; try { DB::transaction(function () use($data) { DB::afterCommit(function() { echo \u0026#34;ポイント追加成功しました。\u0026#34;; }); DB::afterRollBack(function () { echo \u0026#34;ポイント追加に失敗しました。\u0026#34;; }); DB::table(\u0026#39;points\u0026#39;)-\u0026gt;insert($data[0]); DB::table(\u0026#39;points\u0026#39;)-\u0026gt;insert($data[1]); }); } catch (\\Exception $event) { echo $event-\u0026gt;getMessage(); } ","date":"2025-10-23T09:15:17+09:00","permalink":"https://www.larajapan.com/2025/10/23/dbtransaction/","title":"DB::beginTransactionとDB::transaction"},{"content":"Playwright、３記事目の今回はcodegenを使ってテストコードを生成します。codegenは、ブラウザ上での実際の操作を記録し、それをPlaywrightのテストコードに変換してくれる便利な機能です。VSCodeの拡張機能「Playwright Test for VSCode」も存在していますが、今回はシンプルにPlaywrightの機能のみを使用して作成します。\ncodegenでテストを作成する流れ codegenでテストを作成する、大まかな流れは以下のようになっています。\ncodegenでブラウザを操作してテストコードを生成 生成されたコードをVScodeなどのエディタにコピー テストコードの修正 前回の記事でも使用したLaravelプロジェクトを使って、ログイン〜ログアウトをこの流れでテストコードにしてゆきます。\ncodegenで動作を記録 Playwrightはすでにインストール済みの状態から始めます。インストールがまだの方はセットアップの記事をご確認ください。\n以下のコマンドでcodegenを起動します。コマンドの最後に対象のURLを書いておくと、URLへ遷移した状態で始めることができます。この例ではローカルのポート8000を使用していますが、実際の環境に合わせてURLを変更ください。\n$ npx playwright codegen http://127.0.0.1:8000 コマンドを実行すると、ブラウザとPlaywrightインスペクターウィンドウが立ち上がりました。\nこの状態から実際にブラウザを操作してテスト対象の動作を行うことで、テストコードが生成されてゆきます。例えば、メールアドレス入力欄に\u0026quot;test@example.com\u0026quot;と入力してみると、インスペクターの方にもコードが出力されます。\nですが、想像していたより多くコードが生成されたようなので内容を確認してみます。以下が実際のコードです。\nimport { test, expect } from \u0026#39;@playwright/test\u0026#39;; test(\u0026#39;test\u0026#39;, async ({ page }) =\u0026gt; { await page.goto(\u0026#39;http://127.0.0.1:8000/login\u0026#39;); await page.getByRole(\u0026#39;textbox\u0026#39;, { name: \u0026#39;メールアドレス\u0026#39; }).click(); await page.getByRole(\u0026#39;textbox\u0026#39;, { name: \u0026#39;メールアドレス\u0026#39; }).press(\u0026#39;Eisu\u0026#39;); await page.getByRole(\u0026#39;textbox\u0026#39;, { name: \u0026#39;メールアドレス\u0026#39; }).fill(\u0026#39;test@example.com\u0026#39;); }); 「メールアドレスを入力」という動作だけのつもりだったのですが、入力欄をクリックする動作やキーボードの英数キーをクリックした動作までコードに反映されてしまっているようです。インスペクタ上ではコードを削除・修正はできないので、こちらはエディタにコピーしてから修正が必要になります。\n肝心のメールアドレス入力は、最後の行にawait page.getByRole(\u0026rsquo;textbox\u0026rsquo;, { name: \u0026lsquo;メールアドレス\u0026rsquo; }).fill(\u0026rsquo;test@example.com\u0026rsquo;);として、ちゃんと生成されていますね。\ncodegenでアサートを追加 テストジェネレーターで追加できるアサーションは以下の３種類となっています。\n左端の目のアイコン assert visibility：要素がページ上で可視状態であることを確認 中央\"ab\"のアイコンassert text：要素が指定されたテキストコンテンツを含んでいることを確認 右端テキストボックスアイコンassert value：入力要素が特定の値を持っていることを確認 今回のテストでは、assert text・assert visibilityの２つを使用します。まずはassert textで、「ログイン成功後にアカウント名が表示されていること」のアサーションを作成します。\n①\u0026quot;ab\u0026quot;アイコンをクリックし、②確認対象の要素（アカウント名）をクリックします。すると以下のように入力ボックスが表示されるので、テスト用の文字列を変更したい場合はこちらで修正が可能です。問題なければ③のチェックマークをクリック。 assert textでは、以下のようにtoContainText()を使用したアサーションが生成されます。\nawait expect(page.getByRole(\u0026#39;button\u0026#39;)).toContainText(\u0026#39;テスト用ユーザー\u0026#39;); 続いてassert visibilityで、「ログアウト後に\u0026quot;ログインボタン\u0026quot;が表示されていること」のアサーションを作成します。\n①目のアイコンをクリックし、②確認対象の要素（ログインボタン）をクリックします。 assert visibilityでは、以下のようにtoBeVisible()を使用したアサーションが作成されます。\nawait expect(page.getByRole(\u0026#39;button\u0026#39;, { name: \u0026#39;ログイン\u0026#39; })).toBeVisible(); 本当は「遷移後のURLが期待通りであるか」のアサーションも作成したかったのですが、現状テストジェネレーターにはURLを直接アサートするための機能はありませんでした。\n基本的に、ページ上の要素（ボタン、テキスト、入力欄など）に対して「表示されているか」「特定のテキストを含んでいるか」などの検証のみとなっているようなので、URLのアサートを追加したい場合は手動で追記する必要があります。\nテストコードの作成が完了したら テストコードをひととおり作成したら、インスペクタの赤丸ボタンをクリックして記録を停止させます。コピーボタンでコードをコピーしたら、あとはエディタに貼り付けて修正を行います。\n不要なコードを削除 codegenが生成してくれたコードは以下の通りです。\nimport { test, expect } from \u0026#39;@playwright/test\u0026#39;; test(\u0026#39;test\u0026#39;, async ({ page }) =\u0026gt; { await page.goto(\u0026#39;http://127.0.0.1:8000/login\u0026#39;); await page.getByRole(\u0026#39;textbox\u0026#39;, { name: \u0026#39;メールアドレス\u0026#39; }).click(); await page.getByRole(\u0026#39;textbox\u0026#39;, { name: \u0026#39;メールアドレス\u0026#39; }).press(\u0026#39;Eisu\u0026#39;); await page.getByRole(\u0026#39;textbox\u0026#39;, { name: \u0026#39;メールアドレス\u0026#39; }).fill(\u0026#39;test@example.com\u0026#39;); await page.getByRole(\u0026#39;textbox\u0026#39;, { name: \u0026#39;メールアドレス\u0026#39; }).press(\u0026#39;Tab\u0026#39;); await page.getByRole(\u0026#39;textbox\u0026#39;, { name: \u0026#39;パスワード\u0026#39; }).fill(\u0026#39;password\u0026#39;); await page.getByRole(\u0026#39;button\u0026#39;, { name: \u0026#39;ログイン\u0026#39; }).click(); await expect(page.getByRole(\u0026#39;button\u0026#39;)).toContainText(\u0026#39;テスト用ユーザー\u0026#39;); await page.getByRole(\u0026#39;button\u0026#39;, { name: \u0026#39;テスト用ユーザー\u0026#39; }).click(); await page.getByText(\u0026#39;ログアウト\u0026#39;).click(); await expect(page.getByRole(\u0026#39;button\u0026#39;, { name: \u0026#39;ログイン\u0026#39; })).toBeVisible(); }); 先述の通りキーボードを押した時の動作などもコードになってしまっているので、これらの不要な行を削除します。\nimport { test, expect } from \u0026#39;@playwright/test\u0026#39;; test(\u0026#39;test\u0026#39;, async ({ page }) =\u0026gt; { await page.goto(\u0026#39;http://127.0.0.1:8000/login\u0026#39;); //ログイン動作 await page.getByRole(\u0026#39;textbox\u0026#39;, { name: \u0026#39;メールアドレス\u0026#39; }).fill(\u0026#39;test@example.com\u0026#39;); await page.getByRole(\u0026#39;textbox\u0026#39;, { name: \u0026#39;パスワード\u0026#39; }).fill(\u0026#39;password\u0026#39;); await page.getByRole(\u0026#39;button\u0026#39;, { name: \u0026#39;ログイン\u0026#39; }).click(); //ログイン後のアサーション await expect(page.getByRole(\u0026#39;button\u0026#39;)).toContainText(\u0026#39;テスト用ユーザー\u0026#39;); //ログアウト動作 await page.getByRole(\u0026#39;button\u0026#39;, { name: \u0026#39;テスト用ユーザー\u0026#39; }).click(); await page.getByText(\u0026#39;ログアウト\u0026#39;).click(); //ログアウト後のアサーション await expect(page.getByRole(\u0026#39;button\u0026#39;, { name: \u0026#39;ログイン\u0026#39; })).toBeVisible(); }); これでスッキリしました。まだここからBaseURLを使用する形に修正したり、URLアサーションの追加などを適宜行う必要はありますが、叩き台を作成してくれるので使い慣れればテストの作成時間を短縮できそうです。\n自分で書いたコードとcodegenが生成したテストコードの違い 今回codegenが生成してくれたコードを見て、前回私が手動で作成したコードとの違いの中で気になったのはロケータの選定です。\n自作のコードでは、getByLabel()を使用しました。\nawait page.getByLabel(\u0026#39;メールアドレス\u0026#39;).fill(\u0026#39;test@example.com\u0026#39;); 一方codegenは、getByRole()を使用しています。\nawait page.getByRole(\u0026#39;textbox\u0026#39;, { name: \u0026#39;メールアドレス\u0026#39; }).fill(\u0026#39;test@example.com\u0026#39;); getByLabel()は、getByRole()に比べてコードが簡潔に書けます。またラベル要素を指定することで「フォーム要素のテストである」という意図がわかりやすい、といったメリットがあります。\ngetByRole()は、こちらも「\u0026ldquo;メールアドレス\u0026quot;というテキストボックスが対象」という意図がわかりやすいです。getByLabel()との一番の違いは、フォーム要素に限らず様々な要素指定に使える汎用性が高いロケーターである、という点です。\nどちらもPlaywrightの推奨ロケータですが、今後codegenに限らずAIでE2Eテストを作成する機会が増えることになるかと思いますので、手動作成とAIが生成したコードの差異を出さないためにも生成コードに寄せていったほうがいいのかな、と思いました。\n","date":"2025-10-02T07:37:05+09:00","permalink":"https://www.larajapan.com/2025/10/02/playwright%E3%81%A7e2e%E3%83%86%E3%82%B9%E3%83%88%E3%82%92%E8%87%AA%E5%8B%95%E5%8C%96-%EF%BC%88%EF%BC%93%EF%BC%89codegen%E3%81%A7%E3%82%B3%E3%83%BC%E3%83%89%E7%94%9F%E6%88%90/","title":"PlaywrightでE2Eテストを自動化 （３）codegenでコード生成"},{"content":"先月からClaude Codeを使い始めました。コンソールでの対話形式に関わらずその利便さと技術の進化に驚嘆して、さらに最近のLaravel Boostの登場で、LaravelのエコシステムにおいてMCPサーバーの開発対応がすでになされていることにさらに驚嘆。MCPサーバーは、ClaudeのようなAIエージェントに道具（ツール）を提供するインターフェースです。と言っても、具体的な例がないとわかりづらいので、早速MCPサーバーを作成してみることにします。\n通貨換算のMCPサーバー Laravelのマニュアルでは天気や温度を尋ねることができるサンプルでMCPサーバーを説明していますが、無料の為替レートのapiを見つけたので、ここではそれをもとに、Claude Codeにおいて、\n\u0026gt; １ドルを円に換算してください ● 1ドルは148.44円です。 このようなことを可能とさせるMCPサーバーを開発します。\nいつものようにブラウザで対話するアプリを作成するのではなく、MCPサーバーのLaravelアプリを開発して、そのアプリのもとでClaude Codeを立ち上げ上の対話ができるように設定します。\nMCPサーバーの開発 開発を始める前にまず必要なのは、laravel/mcpのパッケージのインストール\n$ composer require laravel/mcp そして、artisan makeでサーバーのクラスを作成します。\n$ php artisan make:mcp-server CurrencyConversionServer そして、次はそこで使用されるツールのクラスを作成。\n$ php artisan make:mcp-tool CurrencyConversionTool まずツールのクラスから見ていきます。\napp/mcp/Servers/CurrencyConversionServer.php namespace App\\Mcp\\Tools; use Laravel\\Mcp\\Request; use Laravel\\Mcp\\Response; use Laravel\\Mcp\\Server\\Tool; use Illuminate\\Support\\Facades\\Log; use Illuminate\\Support\\Facades\\Http; use Illuminate\\JsonSchema\\JsonSchema; class CurrencyConversionTool extends Tool { /** * The tool\u0026#39;s description. */ protected string $description = \u0026#39;このツールは、異なる通貨間の換算を行います。\u0026#39;; /** * Handle the tool request. */ public function handle(Request $request): Response { $validated = $request-\u0026gt;validate([ \u0026#39;from\u0026#39; =\u0026gt; \u0026#39;required|string|in:USD,JPY\u0026#39;, \u0026#39;to\u0026#39; =\u0026gt; \u0026#39;required|string|in:USD,JPY\u0026#39;, \u0026#39;amount\u0026#39; =\u0026gt; \u0026#39;required|numeric|min:0\u0026#39;, ], [ \u0026#39;from.in\u0026#39; =\u0026gt; \u0026#39;サポートされていない通貨です。USDまたはJPYを使用してください。\u0026#39;, \u0026#39;to.in\u0026#39; =\u0026gt; \u0026#39;サポートされていない通貨です。USDまたはJPYを使用してください。\u0026#39;, ]); // TODO: 後に外部のAPIを使用して為替レートを取得して通貨換算を行う return Response::text(\u0026#39;100\u0026#39;);　// 今は固定 } /** * Get the tool\u0026#39;s input schema. * * @return array\u0026lt;string, \\Illuminate\\JsonSchema\\JsonSchema\u0026gt; */ public function schema(JsonSchema $schema): array { return [ \u0026#39;from\u0026#39; =\u0026gt; $schema-\u0026gt;string()-\u0026gt;description(\u0026#39;換算元の通貨, \u0026#34;USD\u0026#34;.\u0026#39;)-\u0026gt;required(), \u0026#39;to\u0026#39; =\u0026gt; $schema-\u0026gt;string()-\u0026gt;description(\u0026#39;換算先の通貨, \u0026#34;JPY\u0026#34;.\u0026#39;)-\u0026gt;required(), \u0026#39;amount\u0026#39; =\u0026gt; $schema-\u0026gt;number()-\u0026gt;description(\u0026#39;換算する通貨の金額.\u0026#39;)-\u0026gt;required() ]; } } 上のクラスでは、handle()とschema()のメソッドがありますが、handle()は、コントローラのように入力を受け取るRequestのクラスがあり、入力バリデーションと同様なvalidate()も実行できます。schema()はAIがこのツールを利用してどのような値を渡す必要があるか理解するためのメソッドです。\nAIエージェントから受け取る入力の値は、schema()で説明しているようにfrom, to, amountと３つあり、それぞれ換算元の通貨、換算先の通貨、換算する通貨の金額からなります。例えば、それぞれ、USD, JPY, 1とかの値を受け取ります。ドル、円とかになっていないことに注意、それは頭が良いAIがドルをUSDに円をJPYとしてツールに渡します。\n後に外部のAPIを利用して為替のレートを取得して換算を行うコードをいれますが、今はいつも100の値を返すようにします。\n上で作成したツールは以下のサーバーのクラスに登録します。\napp/mcp/Servers/CurrencyConversionServer.php namespace App\\Mcp\\Servers; use App\\Mcp\\Tools\\CurrencyConversionTool; use Laravel\\Mcp\\Server; class CurrencyConversionServer extends Server { /** * The MCP server\u0026#39;s name. */ protected string $name = \u0026#39;通貨換算サーバー\u0026#39;; /** * The MCP server\u0026#39;s version. */ protected string $version = \u0026#39;0.0.1\u0026#39;; /** * The MCP server\u0026#39;s instructions for the LLM. */ protected string $instructions = \u0026#39;Instructions describing how to use the server and its features.\u0026#39;; /** * The tools registered with this MCP server. * * @var array\u0026lt;int, class-string\u0026lt;\\Laravel\\Mcp\\Server\\Tool\u0026gt;\u0026gt; */ protected array $tools = [ CurrencyConversionTool::class, ]; /** * The resources registered with this MCP server. * * @var array\u0026lt;int, class-string\u0026lt;\\Laravel\\Mcp\\Server\\Resource\u0026gt;\u0026gt; */ protected array $resources = [ // ]; /** * The prompts registered with this MCP server. * * @var array\u0026lt;int, class-string\u0026lt;\\Laravel\\Mcp\\Server\\Prompt\u0026gt;\u0026gt; */ protected array $prompts = [ // ]; } そしてそれを、今度は上のサーバーとルートに登録します。\nroutes/ai.php use App\\Mcp\\Servers\\CurrencyConversionServer; use Laravel\\Mcp\\Facades\\Mcp; Mcp::local(\u0026#39;currency-conversion\u0026#39;, CurrencyConversionServer::class); 上の設定ではMcp::localを使用して、php artisan mcp:startで立ち上げたサーバーでAIエージェントからのアクセスを可能とします。 他にもMcp::webとしてウェブつまりhttpsでもアクセスが可能です。\n最後に、AIエージェントにおいてMCPサーバーを登録します。\n$ claude mcp add -s project currency-conversion php artisan mcp:start currency-conversion この実行は、以下のファイルを作成します。\n.mcp.json { \u0026#34;mcpServers\u0026#34;: { \u0026#34;currency-conversion\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;stdio\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;php\u0026#34;, \u0026#34;args\u0026#34;: [ \u0026#34;artisan\u0026#34;, \u0026#34;mcp:start\u0026#34;, \u0026#34;currency-conversion\u0026#34; ], \u0026#34;env\u0026#34;: {} } } } 動作確認 さて、これでコンソールでClaude Codeを立ち上げて、動作確認します。 立ち上げると、\nNew MCP server found in .mcp.json: currency-conversion │ MCP servers may execute code or access system resources. All tool calls require approval. Learn more in the MCP │ documentation (​https://docs.claude.com/s/claude-code-mcp​). │ │ ❯ 1. Use this and all future MCP servers in this project │ 2. Use this MCP server │ 3. Continue without using this MCP server と問われます。新規のMCPサーバーを発見したということで、使用するかどうか聞いています。 もちろんなので、そのままEnterで進みます。これで次回からは問われません。\nその後、MCPサーバーが登録されているか確認します。\n\u0026gt; /mcp Manage MCP servers ❯ 1. currency-conversion ✔ connected · Enter to view details MCPサーバーに接続されていますね。\nさて質問してみましょう。\n\u0026gt; １ドルを円に換算してください ● currency-conversion - currency-conversion-tool (MCP)(from: \u0026#34;USD\u0026#34;, to: \u0026#34;JPY\u0026#34;, amount: 1) ⎿ 100 ● 1ドルは100円です。 AIがドルをUSDに円をJPYに変えてツールを使用していることわかります。\nこちらの質問はどうかな。バリデーションで米ドルと円のみの対応に限定しているのでエラーとなるはずです\n\u0026gt; １ドルをユーロに換算してください ● currency-conversion - currency-conversion-tool (MCP)(from: \u0026#34;USD\u0026#34;, to: \u0026#34;EUR\u0026#34;, amount: 1) ⎿ Error: サポートされていない通貨です。USDまたはJPYを使用してください。 ● このMCP通貨変換ツールは、現在USDとJPYの間の換算のみサポートしているようです。USD（ドル）からEUR（ユーロ）への変換はサポ ートされていません。 期待通りエラーとなりましたね。\n為替レートを取得 動作確認したところで、実際に為替を取得して通貨換算を行うコードをいれます。\nnamespace App\\Mcp\\Tools; use Laravel\\Mcp\\Request; use Laravel\\Mcp\\Response; use Laravel\\Mcp\\Server\\Tool; use Illuminate\\Support\\Facades\\Log; use Illuminate\\Support\\Facades\\Http; use Illuminate\\JsonSchema\\JsonSchema; class CurrencyConversionTool extends Tool { /** * The tool\u0026#39;s description. */ protected string $description = \u0026#39;このツールは、異なる通貨間の換算を行います。\u0026#39;; /** * Handle the tool request. */ public function handle(Request $request): Response { $validated = $request-\u0026gt;validate([ \u0026#39;from\u0026#39; =\u0026gt; \u0026#39;required|string|in:USD,JPY\u0026#39;, \u0026#39;to\u0026#39; =\u0026gt; \u0026#39;required|string|in:USD,JPY\u0026#39;, \u0026#39;amount\u0026#39; =\u0026gt; \u0026#39;required|numeric|min:0\u0026#39;, ], [ \u0026#39;from.in\u0026#39; =\u0026gt; \u0026#39;サポートされていない通貨です。USDまたはJPYを使用してください。\u0026#39;, \u0026#39;to.in\u0026#39; =\u0026gt; \u0026#39;サポートされていない通貨です。USDまたはJPYを使用してください。\u0026#39;, ]); try { $response = Http::get(\u0026#39;https://api.frankfurter.dev/v1/latest\u0026#39;, [ \u0026#39;amount\u0026#39; =\u0026gt; $validated[\u0026#39;amount\u0026#39;], \u0026#39;base\u0026#39; =\u0026gt; strtoupper($validated[\u0026#39;from\u0026#39;]), \u0026#39;symbols\u0026#39; =\u0026gt; strtoupper($validated[\u0026#39;to\u0026#39;]) ]); if (!$response-\u0026gt;successful()) { return Response::error(\u0026#39;現在、換算サービスが利用できません。\u0026#39;); } $data = $response-\u0026gt;json(); if (!isset($data[\u0026#39;rates\u0026#39;][strtoupper($validated[\u0026#39;to\u0026#39;])])) { return Response::error(\u0026#39;通貨が見つかりません\u0026#39;); } $converted = $data[\u0026#39;rates\u0026#39;][strtoupper($validated[\u0026#39;to\u0026#39;])]; return Response::text((string) $converted); } catch (\\Exception $e) { return Response::error(\u0026#39;通貨変換に失敗しました\u0026#39;); } } ... AIエージェントから受けとった入力は、https://frankfurter.devのサイトのAPIを利用します。 上のコードは基本的に以下のような実行となり、\nhttps://api.frankfurter.dev/v1/latest?amount=1\u0026amp;base=USD\u0026amp;amp;symbols=JPY レスと返ってくるjsonから必要な値を取り出します。\n{ \u0026#34;amount\u0026#34;:1.0, \u0026#34;base\u0026#34;:\u0026#34;USD\u0026#34;, \u0026#34;date\u0026#34;:\u0026#34;2025-09-24\u0026#34;, \u0026#34;rates\u0026#34;:{\u0026#34;JPY\u0026#34;:148.44} } これでMCPサーバー作成のコーディングは完了です。テストしてみましょう。\n\u0026gt; 1000円をドルに換算してください ● currency-conversion - currency-conversion-tool (MCP)(from: \u0026#34;JPY\u0026#34;, to: \u0026#34;USD\u0026#34;, amount: 1000) ⎿ 6.72 ● 1000円は約6.72ドルです。 ばっちりです。\n最後に 今回のMCPサーバーの開発を始める前に、もしかしたらツールを開発するまでもなくAIの方で勝手に情報を取得してくるのでは、という疑問があり尋ねてみたら、\n\u0026gt; １ドルを円に換算してください ● 為替レートは常に変動しているため、現在の正確な換算レートを確認する必要があります。 ● Web Search(\u0026#34;USD JPY exchange rate today 2025\u0026#34;) ⎿ Did 1 search in 16s ● 現在（2025年9月24日時点）の為替レートによると、1ドル = 約147-148円です。 具体的には： - 147.78円（一部のソースより） - 148.71円（別のソースより） - 146.58円（その他のソースより） 為替レートはリアルタイムで変動しているため、各金融機関や為替サイトで若干異なる数値が表示されますが、現在は1ドル = 約147-148円の範囲で取引されています。 どこかのウェブで検索してきて取得したみたいです。\nもちろん、MCPサーバーを作成する方が、正確でいつも安定な答えを取得できるのは確かです。\n","date":"2025-09-26T08:09:51+09:00","permalink":"https://www.larajapan.com/2025/09/26/mcp%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E3%82%92%E4%BD%9C%E3%82%8D%E3%81%86/","title":"MCPサーバーを作ろう"},{"content":"前回の記事で、Playwrightのインストールと動作確認までを行いました。さっそくテストコードの自動生成機能を使って簡単にテストを作成したいところですが、現時点ではまだ生成されたコードをそのまま使うというよりも、人間が確認・修正する場合も多いと思います。\nそこで今回は、シンプルなログイン・ログアウトのテストを使ってPlaywrightの基本的なテストの書き方をご紹介します。\nテスト対象 Laravelプロジェクトの以下のようなログイン・ログアウト画面がテスト対象です。\nログイン画面はこのようなUIです。 テストでは、メールアドレス・パスワードを入力し、ログインボタンをクリックすると http://127.0.0.1:8000/user/diaryへ遷移することを確認します。\nログアウトは、このようなドロップダウンの要素になっています。 ユーザー名をクリックするとドロップダウンが開きます。テストでは、ドロップダウンの中にある「ログアウト」をクリックし、ログアウトが成功してhttp://127.0.0.1:8000/loginへ遷移する、という一連の動作をテストします。\nログイン・ログアウトのテストコード まず、ログイン・ログアウトのテストコード全体を見てみましょう。これは.tsのTypeScriptであることに注意してください。\ne2e/auth.spec.ts import { test, expect, Page } from \u0026#39;@playwright/test\u0026#39;; test.describe(\u0026#39;認証テスト\u0026#39;, () =\u0026gt; { test(\u0026#39;ユーザーログイン\u0026#39;, async ({ page }: { page: Page }) =\u0026gt; { await page.goto(\u0026#39;/login\u0026#39;); await page.getByLabel(\u0026#39;メールアドレス\u0026#39;).fill(\u0026#39;test@example.com\u0026#39;); await page.getByLabel(\u0026#39;パスワード\u0026#39;).fill(\u0026#39;password\u0026#39;); await page.getByRole(\u0026#39;button\u0026#39;, { name: \u0026#39;ログイン\u0026#39; }).click(); // ログイン後の確認 await expect(page).toHaveURL(\u0026#39;/user/diary\u0026#39;); // ユーザー名が表示されていることを確認 await expect(page.getByRole(\u0026#39;button\u0026#39;, { name: \u0026#39;テスト用ユーザー\u0026#39; })).toBeVisible(); }); test(\u0026#39;ユーザーログアウト\u0026#39;, async ({ page }: { page: Page }) =\u0026gt; { // ログイン処理 await page.goto(\u0026#39;/login\u0026#39;); await page.getByLabel(\u0026#39;メールアドレス\u0026#39;).fill(\u0026#39;test@example.com\u0026#39;); await page.getByLabel(\u0026#39;パスワード\u0026#39;).fill(\u0026#39;password\u0026#39;); await page.getByRole(\u0026#39;button\u0026#39;, { name: \u0026#39;ログイン\u0026#39; }).click(); // ログイン後の確認 await expect(page).toHaveURL(\u0026#39;/user/diary\u0026#39;); // ユーザー名をクリックしてドロップダウンメニューを開く await page.getByRole(\u0026#39;button\u0026#39;, { name: \u0026#39;テスト用ユーザー\u0026#39; }).click(); // ログアウトをクリック await page.getByRole(\u0026#39;button\u0026#39;, { name: \u0026#39;ログアウト\u0026#39; }).click(); // ログアウト後の確認 await expect(page).toHaveURL(\u0026#39;/login\u0026#39;); await expect(page.getByRole(\u0026#39;button\u0026#39;, { name: \u0026#39;ログイン\u0026#39; })).toBeVisible(); }); }); 実行は以下のコマンドで行います。\n$ npx playwright test auth 全体像がわかったところで、上のテストスクリプトにおけるPlaywrightの使い方を解説します。\nPlaywrightの構文 Playwrightの基本的な構文は、describe（テストをまとめるグループのようなもの）とtest（テストの１つ１つ）で構成されています。\nimport { test, expect } from \u0026#39;@playwright/test\u0026#39;; test.describe(\u0026#39;グループ\u0026#39;, () =\u0026gt; { test(\u0026#39;テスト１\u0026#39;, async ({ page }) =\u0026gt; { // ... }); test(\u0026#39;テスト２\u0026#39;, async ({ page }) =\u0026gt; { // ... }); }); describeは必須ではないので、testだけを使っても問題ありません。\nPlaywrightのコマンド 次に、今回のテストで使用したPlaywrightのコマンドをご紹介します。\nまずは画面遷移から。goto()を使用します。\nawait page.goto(\u0026#39;http://127.0.0.1:8000\u0026#39;); コマンドの最初にあるawaitですが、こちらは処理の完了を待つために必要なものです。Playwrightの多くのコマンドは非同期処理なので、awaitを付けることで処理を待ってから安全に次に進むことができます。\nそしてawaitの後ろにあるpageは、ページ操作のためのオブジェクトです。このpageオブジェクトの後にメソッドチェーンでgoto()などの各コマンドを繋ぐ形になっています。\n次はフォーム入力です。以下のように「要素の取得」と「動作コマンド」を繋いで記述できます。\nawait page.getByLabel(\u0026#39;メールアドレス\u0026#39;).fill(\u0026#39;test@example.com\u0026#39;); 要素の取得はgetByLabel()というPlaywrightのコマンドを使い、引数に”メールアドレス”というラベルテキストを指定します。\nE2Eテストの要素指定には、修正が発生しやすいCSSセレクタやXpathはできるだけ避け、ユーザーが実際に目にする要素を優先して指定することが推奨されており、そのための色々なロケータが用意されています。\nそしてfill()に入力したい内容を渡します。\n次は、「ログインボタンをクリック」のようなクリック動作です。\nawait page.getByRole(\u0026#39;button\u0026#39;, { name: \u0026#39;ログイン\u0026#39; }).click(); getByRole()というコマンドを使用してまずは要素を取得しています。要素の役割（この場合はbutton）と、nameプロパティには、ラベルや要素のテキスト（この場合は”ログイン”）を渡します。\nそして、click()を繋いでボタンをクリックします。\nPlaywrightのアサーション 次は検証コマンドです。「現在のURLがhttp://127.0.0.1:8000/loginである」ということを確認したい場合には、以下のように記述します。\nawait expect(page).toHaveURL(\u0026#39;http://127.0.0.1:8000/login\u0026#39;); expect()の中に確認したい対象（この例ではpageオブジェクト）を渡し、toHaveURL()を繋いで期待するURLを指定します。\nまた、ログイン成功後の画面で「”テスト用ユーザー”というテキストを持つボタン要素が画面に表示されている」ということを確認したい場合、以下のように記述します。\nawait expect(page.getByRole(\u0026#39;button\u0026#39;, { name: \u0026#39;テスト用ユーザー\u0026#39; })).toBeVisible(); こちらもexpect()から始まり、getByRole()に対象要素のbuttonと、nameプロパティにユーザー名を渡します。そしてtoBeVisible()を繋いでその要素が表示されているかを確認する、という流れです。\nBaseURLを設定 テストで使用するURLは、playwright.config.tsにBaseURLとして指定しておくと便利です。\nplaywright.config.ts export default defineConfig({ ・・・・・ use: { /* Base URL to use in actions like `await page.goto(\u0026#39;/\u0026#39;)`. */ baseURL: \u0026#39;http://127.0.0.1:8000\u0026#39;, ・・・・・ そうすると、goto()やtoHaveURL()で以下のように相対パスで書くことができます。\nawait page.goto(\u0026#39;/login\u0026#39;); // http://127.0.0.1:8000/login にアクセスする 共通処理を関数へ切り出し ログアウトのテストでは、ユーザーがログインしている状態を作るために、ログイン画面にアクセスしてメールアドレスを入力するなど、一連のログイン動作を記述しています。 今回はログイン・ログアウトの２つのテストだけなので必須ではありませんが、「ログイン状態を作るためのコード」を関数として切り出しておくと後々使いまわせますし、テストコードがすっきりします。\ne2e/auth.spec.ts ・・・・・ test(\u0026#39;ユーザーログアウト\u0026#39;, async ({ page }: { page: Page }) =\u0026gt; { // 関数を呼び出す await loginUser(page, \u0026#39;test@example.com\u0026#39;, \u0026#39;password\u0026#39;); // ここからログアウトのテスト ・・・・・ }); }); // ログイン用関数 async function loginUser(page: Page, email: string, password: string): Promise\u0026lt;void\u0026gt; { await page.goto(\u0026#39;/login\u0026#39;); await page.getByLabel(\u0026#39;メールアドレス\u0026#39;).fill(email); await page.getByLabel(\u0026#39;パスワード\u0026#39;).fill(password); await page.getByRole(\u0026#39;button\u0026#39;, { name: \u0026#39;ログイン\u0026#39; }).click(); await page.waitForURL(\u0026#39;/user/diary\u0026#39;); } loginUser()という関数を作成し、そこにログインのためのコードを移しました。そしてログアウトのテストでloginUser()を呼び出しています。\n今後共通化したいテストが増えてきたら別ファイルに切り出したり、Playwrightが提供している認証情報保持の機能などを使ってもいいと思います。\n次回は、PlaywrightのCodegenを使ってテストコードを作成します。\n","date":"2025-08-31T01:30:21+09:00","permalink":"https://www.larajapan.com/2025/08/31/playwright%E3%81%A7e2e%E3%83%86%E3%82%B9%E3%83%88%E3%82%92%E8%87%AA%E5%8B%95%E5%8C%96-%EF%BC%88%EF%BC%92%EF%BC%89%E3%83%AD%E3%82%B0%E3%82%A4%E3%83%B3%E3%83%BB%E3%83%AD%E3%82%B0%E3%82%A2%E3%82%A6/","title":"PlaywrightでE2Eテストを自動化 （２）ログイン・ログアウト"},{"content":"前回のselect2のドロップダウンはjquery依存であったけれど、似たような機能のTom Selectはバニラjsでjqueryの依存がまったくないパッケージです。早速viteでビルドします。\nTom Selectで選択 Tom Selectは、select2と同様にドロップダウンにおいて選択値の検索が可能です。 しかし、select2と比べるとわかりますが、検索窓のための行が追加されるのではなくドロップダウンの最初の行が検索窓となります。そこでBackキーを押すと行が以下のように空になり検索値をタイプできます。\nブレードは以下のようになります。\n... \u0026lt;div class=\u0026#34;row mb-3\u0026#34;\u0026gt; \u0026lt;label for=\u0026#34;prefecture-multiple\u0026#34; class=\u0026#34;col-md-4 col-form-label text-md-end\u0026#34;\u0026gt;都道府県(複数選択)\u0026lt;/label\u0026gt; \u0026lt;div class=\u0026#34;col-md-6\u0026#34;\u0026gt; \u0026lt;select id=\u0026#34;prefecture-multiple\u0026#34; placeholder=\u0026#34;都道府県を選択してください\u0026#34; class=\u0026#34;form-select @error(\u0026#39;prefecture\u0026#39;) is-invalid @enderror\u0026#34; name=\u0026#34;prefectures[]\u0026#34; required multiple\u0026gt; @foreach($prefectures as $prefecture) \u0026lt;option value=\u0026#34;{{ $prefecture }}\u0026#34; {{ in_array($prefecture, old(\u0026#39;prefectures\u0026#39;, [])) ? \u0026#39;selected\u0026#39; : \u0026#39;\u0026#39; }}\u0026gt;{{ $prefecture }}\u0026lt;/option\u0026gt; @endforeach \u0026lt;/select\u0026gt; @error(\u0026#39;prefectures\u0026#39;) \u0026lt;span class=\u0026#34;invalid-feedback\u0026#34; role=\u0026#34;alert\u0026#34;\u0026gt; \u0026lt;strong\u0026gt;{{ $message }}\u0026lt;/strong\u0026gt; \u0026lt;/span\u0026gt; @enderror \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; ... @section(\u0026#39;scripts\u0026#39;) \u0026lt;script type=\u0026#34;module\u0026#34;\u0026gt; document.addEventListener(\u0026#39;DOMContentLoaded\u0026#39;, function() { new TomSelect(document.getElementById(\u0026#39;prefecture\u0026#39;)); }) \u0026lt;/script\u0026gt; @endsection select2のように検索窓を次の行に出すこともTomSelectに渡すオプションで変えることができます。\nコードは以下となります。\n\u0026lt;script type=\u0026#34;module\u0026#34;\u0026gt; document.addEventListener(\u0026#39;DOMContentLoaded\u0026#39;, function() { new TomSelect(document.getElementById(\u0026#39;prefecture\u0026#39;), { plugins: [\u0026#39;dropdown_input\u0026#39;], // 検索窓の位置を変える }); }) \u0026lt;/script\u0026gt; 他のオプションとしては、 空行の選択を許す、 allowEmptyOption: true 全選択値を含む、 maxOptions: null （これがないと、最初50個しか選択値に含まれないので注意を） などがあります。\nオプションに関してのドキュメントは以下となります。 https://tom-select.js.org/docs/\nTom Selectで複数を選択 もちろん、select2と同様に複数のアイテムの選択も可能です。\nブレードは、以下のようになります。\n... \u0026lt;div class=\u0026#34;row mb-3\u0026#34;\u0026gt; \u0026lt;label for=\u0026#34;prefecture-multiple\u0026#34; class=\u0026#34;col-md-4 col-form-label text-md-end\u0026#34;\u0026gt;都道府県(複数選択)\u0026lt;/label\u0026gt; \u0026lt;div class=\u0026#34;col-md-6\u0026#34;\u0026gt; \u0026lt;select id=\u0026#34;prefecture-multiple\u0026#34; placeholder=\u0026#34;都道府県を選択してください\u0026#34; class=\u0026#34;form-select @error(\u0026#39;prefecture\u0026#39;) is-invalid @enderror\u0026#34; name=\u0026#34;prefectures[]\u0026#34; required multiple\u0026gt; @foreach($prefectures as $prefecture) \u0026lt;option value=\u0026#34;{{ $prefecture }}\u0026#34; {{ in_array($prefecture, old(\u0026#39;prefectures\u0026#39;, [])) ? \u0026#39;selected\u0026#39; : \u0026#39;\u0026#39; }}\u0026gt;{{ $prefecture }}\u0026lt;/option\u0026gt; @endforeach \u0026lt;/select\u0026gt; @error(\u0026#39;prefectures\u0026#39;) \u0026lt;span class=\u0026#34;invalid-feedback\u0026#34; role=\u0026#34;alert\u0026#34;\u0026gt; \u0026lt;strong\u0026gt;{{ $message }}\u0026lt;/strong\u0026gt; \u0026lt;/span\u0026gt; @enderror \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; ... @section(\u0026#39;scripts\u0026#39;) \u0026lt;script type=\u0026#34;module\u0026#34;\u0026gt; document.addEventListener(\u0026#39;DOMContentLoaded\u0026#39;, function() { new TomSelect(document.getElementById(\u0026#39;prefecture-multiple\u0026#39;), { maxOptions: null // 選択値制限なし )); }) \u0026lt;/script\u0026gt; @endsection Tom Selectをviteでビルド viteを使ってTom Selectのビルドを作成してみましょう。 まず、Tom Selectのパッケージを以下でローカル環境にインストールします。\n$ npm install tom-select --save-dev そして、bootstrap.jsにおいてそれらをインポートします。\nresources/js/bootstrap.js import \u0026#39;bootstrap\u0026#39;; import TomSelect from \u0026#39;tom-select\u0026#39;; window.TomSelect = TomSelect; // bootstrap5のcssをインポート import \u0026#39;/node_modules/tom-select/dist/css/tom-select.bootstrap5.min.css\u0026#39;; viteの設定は以下となります。\nvite.config.js import { defineConfig } from \u0026#39;vite\u0026#39;; import laravel from \u0026#39;laravel-vite-plugin\u0026#39;; export default defineConfig({ plugins: [ laravel({ input: [ \u0026#39;resources/sass/app.scss\u0026#39;, \u0026#39;resources/js/app.js\u0026#39;, ], refresh: true, }), ], }); 最後に\n$ npm run build とビルドします。\n","date":"2025-08-20T08:02:46+09:00","permalink":"https://www.larajapan.com/2025/08/20/vite%E3%81%A8%E3%83%90%E3%83%8B%E3%83%A9js-tom-select-%E3%83%89%E3%83%AD%E3%83%83%E3%83%97%E3%83%80%E3%82%A6%E3%83%B3/","title":"Viteとバニラjs – Tom Select ドロップダウン"},{"content":"昔のjQuery時代においてjquery-uiとともに有名となったのはselect2のウィジットです。オプションがたくさんあるドロップダウンでの選択や複数の値を選択を必要とされるときに非常に便利なもので、今でもその優れたUIは貴重です。今回はこれをviteでビルドします。\nselect2で選択 通常のドロップダウンは、以下のようにドロップダウンされた選択リストからクリックして選択です。\nしかし、このオプションがたくさんあると探すのが大変。しかし、select2を使うと選択値の検索が可能となります。検索入力箱が表示され、赤箱の部分のように検索値を入れると、以下のようにフィルターされた結果から選択が可能となります。\n通常のドロップダウンをselect2のドロップダウンにするのはとても簡単です。すでにwebpackなどでjqueryやselect2のライブラリがviteなどを利用してバンドルされていると仮定して、以下のようなブレードを作成します。\n@php $prefectures = [ \u0026#39;北海道\u0026#39;, \u0026#39;青森県\u0026#39;, \u0026#39;岩手県\u0026#39;, \u0026#39;宮城県\u0026#39;, \u0026#39;秋田県\u0026#39;, \u0026#39;山形県\u0026#39;, \u0026#39;福島県\u0026#39;, \u0026#39;茨城県\u0026#39;, \u0026#39;栃木県\u0026#39;, \u0026#39;群馬県\u0026#39;, \u0026#39;埼玉県\u0026#39;, \u0026#39;千葉県\u0026#39;, \u0026#39;東京都\u0026#39;, \u0026#39;神奈川県\u0026#39;, \u0026#39;新潟県\u0026#39;, \u0026#39;富山県\u0026#39;, \u0026#39;石川県\u0026#39;, \u0026#39;福井県\u0026#39;, \u0026#39;山梨県\u0026#39;, \u0026#39;長野県\u0026#39;, \u0026#39;岐阜県\u0026#39;, \u0026#39;静岡県\u0026#39;, \u0026#39;愛知県\u0026#39;, \u0026#39;三重県\u0026#39;, \u0026#39;滋賀県\u0026#39;, \u0026#39;京都府\u0026#39;, \u0026#39;大阪府\u0026#39;, \u0026#39;兵庫県\u0026#39;, \u0026#39;奈良県\u0026#39;, \u0026#39;和歌山県\u0026#39;, \u0026#39;鳥取県\u0026#39;, \u0026#39;島根県\u0026#39;, \u0026#39;岡山県\u0026#39;, \u0026#39;広島県\u0026#39;, \u0026#39;山口県\u0026#39;, \u0026#39;徳島県\u0026#39;, \u0026#39;香川県\u0026#39;, \u0026#39;愛媛県\u0026#39;, \u0026#39;高知県\u0026#39;, \u0026#39;福岡県\u0026#39;, \u0026#39;佐賀県\u0026#39;, \u0026#39;長崎県\u0026#39;, \u0026#39;熊本県\u0026#39;, \u0026#39;大分県\u0026#39;, \u0026#39;宮崎県\u0026#39;, \u0026#39;鹿児島県\u0026#39;, \u0026#39;沖縄県\u0026#39; ]; @endphp @section(\u0026#39;content\u0026#39;) \u0026lt;div class=\u0026#34;container\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;row justify-content-center\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;col-md-8\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;card\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;card-header\u0026#34;\u0026gt;会員登録\u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;card-body\u0026#34;\u0026gt; \u0026lt;form method=\u0026#34;POST\u0026#34; action=\u0026#34;{{ route(\u0026#39;register\u0026#39;) }}\u0026#34;\u0026gt; @csrf ... \u0026lt;div class=\u0026#34;row mb-3\u0026#34;\u0026gt; \u0026lt;label for=\u0026#34;prefecture\u0026#34; class=\u0026#34;col-md-4 col-form-label text-md-end\u0026#34;\u0026gt;都道府県\u0026lt;/label\u0026gt; \u0026lt;div class=\u0026#34;col-md-6\u0026#34;\u0026gt; \u0026lt;select id=\u0026#34;prefecture\u0026#34; class=\u0026#34;form-select @error(\u0026#39;prefecture\u0026#39;) is-invalid @enderror\u0026#34; name=\u0026#34;prefecture\u0026#34; required\u0026gt; @foreach($prefectures as $prefecture) \u0026lt;option value=\u0026#34;{{ $prefecture }}\u0026#34; {{ old(\u0026#39;prefecture\u0026#39;) == $prefecture ? \u0026#39;selected\u0026#39; : \u0026#39;\u0026#39; }}\u0026gt;{{ $prefecture }}\u0026lt;/option\u0026gt; @endforeach \u0026lt;/select\u0026gt; @error(\u0026#39;prefecture\u0026#39;) \u0026lt;span class=\u0026#34;invalid-feedback\u0026#34; role=\u0026#34;alert\u0026#34;\u0026gt; \u0026lt;strong\u0026gt;{{ $message }}\u0026lt;/strong\u0026gt; \u0026lt;/span\u0026gt; @enderror \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; ... @endsection @section(\u0026#39;scripts\u0026#39;) \u0026lt;script type=\u0026#34;module\u0026#34;\u0026gt; $(document).ready(function() { $(\u0026#39;#prefecture\u0026#39;).select2({ theme: \u0026#39;bootstrap-5\u0026#39;, }); }); \u0026lt;/script\u0026gt; @endsection prefectureのDOMのノードにselect2のメソッドを装着するだけです。\nselect2で複数を選択 ドロップダウンで複数の選択が必要なときにselect2を使用すると、まず、何も選択されていないときは通常のドロップダウンのように表示されます。\nそこで「北海道」を選択すると、以下のようにピルの形状で選択値が表示されます。そのピルの先頭にあるXをクリックして削除も可能です。\n次に、検索してフィルターされた結果から検索して選択します。\n選択後は以下のように複数の選択値がピルの形状で並びます。\nこちらのブレードは、以下のようになります。\n... \u0026lt;div class=\u0026#34;row mb-3\u0026#34;\u0026gt; \u0026lt;label for=\u0026#34;prefecture-multiple\u0026#34; class=\u0026#34;col-md-4 col-form-label text-md-end\u0026#34;\u0026gt;都道府県(複数選択)\u0026lt;/label\u0026gt; \u0026lt;div class=\u0026#34;col-md-6\u0026#34;\u0026gt; \u0026lt;select id=\u0026#34;prefecture-multiple\u0026#34; class=\u0026#34;form-select @error(\u0026#39;prefecture\u0026#39;) is-invalid @enderror\u0026#34; name=\u0026#34;prefectures[]\u0026#34; required multiple\u0026gt; @foreach($prefectures as $prefecture) \u0026lt;option value=\u0026#34;{{ $prefecture }}\u0026#34; {{ in_array($prefecture, old(\u0026#39;prefectures\u0026#39;, [])) ? \u0026#39;selected\u0026#39; : \u0026#39;\u0026#39; }}\u0026gt;{{ $prefecture }}\u0026lt;/option\u0026gt; @endforeach \u0026lt;/select\u0026gt; @error(\u0026#39;prefectures\u0026#39;) \u0026lt;span class=\u0026#34;invalid-feedback\u0026#34; role=\u0026#34;alert\u0026#34;\u0026gt; \u0026lt;strong\u0026gt;{{ $message }}\u0026lt;/strong\u0026gt; \u0026lt;/span\u0026gt; @enderror \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; ... @section(\u0026#39;scripts\u0026#39;) \u0026lt;script type=\u0026#34;module\u0026#34;\u0026gt; $(document).ready(function() { $(\u0026#39;#prefecture-multiple\u0026#39;).select2({ theme: \u0026#39;bootstrap-5\u0026#39;, placeholder: \u0026#39;都道府県を選択してください\u0026#39;, }); }); \u0026lt;/script\u0026gt; @endsection DOMのノードへのselect2のメソッドの装着は先の単一の選択とたいして変わりません。しかし、選択開始前に表示するplaceholderがオプションとして追加されています。\nselect2をviteでビルド viteを使ってselect2のビルドを作成してみましょう。 まず、select2とBootstrap 5のパッケージを以下でローカル環境にインストールします。\n$ npm install select2 --save-dev $ npm install select2-bootstrap-5-theme --save-dev そして、bootstrap.jsにおいてそれらをインポートします。\nresources/js/bootstrap.js import \u0026#39;bootstrap\u0026#39;; import JQuery from \u0026#39;jquery\u0026#39;; window.$ = window.jQuery = JQuery; import select2 from \u0026#39;select2\u0026#39;; select2();　//　どうして必要なのかわからないけれど、これは必須！ // select2のcssをインポート import \u0026#34;/node_modules/select2/dist/css/select2.min.css\u0026#34;; // さらに、bootstrap5のcssをインポート import \u0026#34;/node_modules/select2-bootstrap-5-theme/dist/select2-bootstrap-5-theme.css\u0026#34;; viteの設定は以下となります。\nvite.config.js import { defineConfig } from \u0026#39;vite\u0026#39;; import laravel from \u0026#39;laravel-vite-plugin\u0026#39;; export default defineConfig({ plugins: [ laravel({ input: [ \u0026#39;resources/sass/app.scss\u0026#39;, \u0026#39;resources/js/app.js\u0026#39;, ], refresh: true, }), ], }); 最後に\n$ npm run build とビルドします。\n","date":"2025-08-02T06:51:28+09:00","permalink":"https://www.larajapan.com/2025/08/02/vite%E3%81%A8select2-%E3%83%89%E3%83%AD%E3%83%83%E3%83%97%E3%83%80%E3%82%A6%E3%83%B3/","title":"Viteとselect2 ドロップダウン"},{"content":"このところ、AIの進化によりテストツールとしてのPlaywrightが便利だという話を目にする機会が多くなってきました。実際、2025年現在GoogleトレンドでもCypressやSeleniumの検索数を上回るなど、Playwrightへの注目度が高くなっているようです。以前に当ブログでCypressについてご紹介しましたが、Playwrightも気になったので少し触ってみました。\nPlaywrightとは Playwrightとは、Microsoftが開発したE2E（End-to-End）テスト自動化ツールです。Webアプリケーションの動作を実際のブラウザで自動テストでき、Chrome、Firefox、Safari（WebKit）の3つのブラウザエンジンに対応しています。\nユーザーの操作を記録してテストコードを自動生成するcodegen機能や、GitHub Actionsとの連携によるコード変更時やデプロイ時の自動テスト実行も可能で、モダンなWeb開発ワークフローに適したツールとなっています。\nインストール環境 Laravelで構築されたアプリケーションのE2EテストとしてPlaywrightを導入します。Laravelアプリとは異なるディレクトリにPlaywrightをインストールすることも可能ですが、今回はプロジェクトルートにインストールします。\nLaravelのテストディレクトリとしてすでにtests/が存在するため、Playwright用ディレクトリはe2e/として以下の構成となるようインストールしてゆきます。\nlaravel-app/ ├── app/ ├── config/ ├── database/ ├── public/ ├── resources/ ├── routes/ ├── tests/ # PHPUnit用のテスト ├── e2e/ # Playwright用のE2Eテスト ← ここ ├── package.json └── composer.json Playwrightインストール まずはインストールです。最新版のインストールにはnode.jsのバージョン20、22もしくは24が必要です。\n公式の手順にそって、プロジェクトのルートディレクトリで以下のコマンドを実行します。\n$ npm init playwright@latest いくつか質問されますので、プロジェクトに合う形で指定してゆきます。\n..... Do you want to use TypeScript or JavaScript? · TypeScript Where to put your end-to-end tests? · e2e Add a GitHub Actions workflow? (y/N) · false Install Playwright browsers (can be done manually via \u0026#39;npx playwright install\u0026#39;)? (Y/n) › true ..... TypeScriptかJavaScriptか？では、今回はTypescriptを選択しました。また、Playwright用のテストケースを置くディレクトリはe2eに。\nGitHub Actions workflowについては、初回は設定をシンプルに保ちたいのでfalseとしました。最後の「Install Playwright browsers」の質問では、Playwright専用のブラウザ（Chromium、Firefox、WebKit）を自動でダウンロードするかを選択できます。今回はtrueを選択して自動インストールしました。\nインストールが完了すると、新しく以下のファイルが作成されます。\nAnd check out the following files: - ./e2e/example.spec.ts - Example end-to-end test - ./tests-examples/demo-todo-app.spec.ts - Demo Todo App end-to-end tests - ./playwright.config.ts - Playwright Test configuration /e2e/example.spec.tsは、テストの動作を確認するためのシンプルなサンプルテスト用のファイルです。 ./tests-examples/demo-todo-app.spec.tsも同じくサンプルテストですが、Playwrightが提供する実際のTodoアプリケーションを対象とした、より実践的なデモ用のE2Eテストが書かれたファイルとなっているようです。そして設定用のplaywright.config.tsが、ルートディレクトリにインストールされています。\nPlaywright設定ファイル Playwrightの動作確認をする前に、playwright.config.tsの中を少し見てみます。\n/playwright.config.ts ..... export default defineConfig({ testDir: \u0026#39;./e2e\u0026#39;, /* Run tests in files in parallel */ fullyParallel: true, /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, /* Retry on CI only */ retries: process.env.CI ? 2 : 0, /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: \u0026#39;html\u0026#39;, /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto(\u0026#39;/\u0026#39;)`. */ // baseURL: \u0026#39;http://localhost:3000\u0026#39;, /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: \u0026#39;on-first-retry\u0026#39;, }, ..... インストール時に指定した通り、テストファイルの置き場所はtestDir: \u0026lsquo;./e2e\u0026rsquo;と設定されています。今のe2eディレクトリにはexample.spec.tsだけが入っており、新しいテストファイルはここに追加してゆく形ですね。\nまた初期設定ではコメントアウトとなっていますが、テスト内で相対パスを使う際のベースとなるbaseURLも項目として記載されています。\nそれ以外の主な設定として、以下のような項目が挙げられています。\nfullyParallel：テストの並列実行 retries：テスト失敗時のリトライ設定 workers：並列実行のプロセス数 reporter：レポートの生成 サンプルテストを実行 では早速、サンプルテストを実行してみます。e2eディレクトリに存在するすべてのspec.tsファイルを対象にテストを実行する場合、以下のコマンドを使います。\n% npx playwright test ファイルを指定して実行する場合は以下のいずれかで可能です。\n$ npx playwright test example.spec.ts $ npx playwright test example コマンドを実行すると、ターミナルに以下が出力されました。\nRunning 6 tests using 4 workers 6 passed (11.4s) To open last HTML report run: npx playwright show-report ヘッドレスモードでテストが実行されたようで、ブラウザも何も起動せずあっという間にテストが完了しました。ターミナルに出力されたメッセージに従って、以下のコマンドを実行します。\n$ npx playwright show-report localhost:9323でHTMLレポートが表示されました。Chrome、Firefox、Safari（webkit）でテストが実行されていますね。このレポートは、Macの場合 Ctrl+C で閉じることができます。\nテストが問題なく実行されたことはわかりましたが、ブラウザが何も立ち上がらないのは少し心配ですので今度はオプションをつけて実行します。\n$ npx playwright test --headed \u0026ndash;headedをつけて実行すると、一瞬ブラウザが立ち上がりましたがあっという間に閉じてテストが完了しました。サンプルテストなので早いというのはあるかもしれませんが、ブラウザの起動からテストが始まるまでがかなり早いように感じます。\nまた、テストを手動で１つづつ動かしながらブラウザでの動作も確認したいという場合は、\u0026ndash;uiをつけて実行します。\n$ npx playwright test --ui PlaywrightのWebUIが立ち上がり、実行するテストファイル・テスト名などを選択して実行することができます。 テストコードも下部に表示されるので、作成したテストコードの動作を確認しながら動かしたりエラー時のデバッグなどに使うと良さそうですね。\nテスト失敗時 今度は失敗した場合の動作を確認します。example.spec.tsを一部編集して、エラーが起こるようにします。以下のtoHaveTitle(/Playwright/)の箇所で「アクセスしたページのタイトルに\u0026quot;Playwright\u0026quot;という文字列が含まれているか」をアサーションしていますので、\u0026ldquo;Plaaywright\u0026quot;とaを重複するよう書き換えてみます。\n/e2e/example.spec.ts ..... test(\u0026#39;has title\u0026#39;, async ({ page }) =\u0026gt; { await page.goto(\u0026#39;https://playwright.dev/\u0026#39;); // Expect a title \u0026#34;to contain\u0026#34; a substring. await expect(page).toHaveTitle(/Plaaywright/); // aを重複するように編集 }); ..... これでテストを実行してみると、ターミナルへ以下のようなエラーメッセージ出力に加え、レポートが自動で立ち上がりました。\n$ npx playwright test Running 6 tests using 4 workers 1) [chromium] › e2e/example.spec.ts:3:1 › has title ────────────────────────────────────────────── Error: Timed out 5000ms waiting for expect(page).toHaveTitle(expected) Locator: locator(\u0026#39;:root\u0026#39;) Expected pattern: /Plaaywright/ Received string: \u0026#34;Fast and reliable end-to-end testing for modern web apps | Playwright\u0026#34; Call log: - Expect \u0026#34;toHaveTitle\u0026#34; with timeout 5000ms - waiting for locator(\u0026#39;:root\u0026#39;) 9 × locator resolved to \u0026lt;html lang=\u0026#34;en\u0026#34; dir=\u0026#34;ltr\u0026#34; data-theme=\u0026#34;light\u0026#34; data-has-hydrated=\u0026#34;true\u0026#34; data-theme-choice=\u0026#34;system\u0026#34; class=\u0026#34;plugin-pages plugin-id-default\u0026#34; data-rh=\u0026#34;lang,dir,class,data-has-hydrated\u0026#34;\u0026gt;…\u0026lt;/html\u0026gt; - unexpected value \u0026#34;Fast and reliable end-to-end testing for modern web apps | Playwright\u0026#34; 5 | 6 | // Expect a title \u0026#34;to contain\u0026#34; a substring. \u0026gt; 7 | await expect(page).toHaveTitle(/Plaaywright/); | ^ 8 | }); 9 | 10 | test(\u0026#39;get started link\u0026#39;, async ({ page }) =\u0026gt; { at /Users/hiro/MySite/diary/e2e/example.spec.ts:7:22 Error Context: test-results/example-has-title-chromium/error-context.md 2) [firefox] › e2e/example.spec.ts:3:1 › has title ..... 次回は、Playwrightの基本的なテストコードの書き方をご紹介します。\n","date":"2025-07-27T01:52:33+09:00","permalink":"https://www.larajapan.com/2025/07/27/playwright%E3%81%A7e2e%E3%83%86%E3%82%B9%E3%83%88%E3%82%92%E8%87%AA%E5%8B%95%E5%8C%96%EF%BC%88%EF%BC%91%EF%BC%89%E3%82%BB%E3%83%83%E3%83%88%E3%82%A2%E3%83%83%E3%83%97/","title":"PlaywrightでE2Eテストを自動化（１）セットアップ"},{"content":"前回ではviteでのjquery-uiパッケージ対応を説明しましたが、今度はjqueryをまったく使わない日付選択とリスト並び替えのウィジットの対応とします。かと言って、javascript（バニラJS？）でそれらのUIウィジットをスクラッチから作成するわけではありません。jquery-uiに取り替わるjqueryを必要としないパッケージを探します。\nVanilla JS Datepicker 日付の選択のパッケージはいくつかありますが、Vanilla JS Datepickerが良さそうです。jquery-uiのdatepickerの日付選択と同様に以下のようにカレンダーが表示できます。 jquery-uiとは違い、年の部分をクリックすれば以下のように月での選択を容易にしています。年の選択も同様に可能です。\nブレードでは以下のようにDOMのid (#datepicker) にDatePickerを関連付けます。jquery-uiと同様にローカライズや日付のフォーマットの指定も可能です。\n\u0026lt;input type=\u0026#34;tel\u0026#34; id=\u0026#34;datepicker\u0026#34; name=\u0026#34;start\u0026#34; class=\u0026#34;form-control\u0026#34;\u0026gt; ... \u0026lt;script type=\u0026#34;module\u0026#34;\u0026gt; document.addEventListener(\u0026#39;DOMContentLoaded\u0026#39;, function() { const datepicker = document.getElementById(\u0026#39;datepicker\u0026#39;); if (datepicker) { new Datepicker(datepicker, { autohide: true, buttonClass: \u0026#39;btn\u0026#39;, format: \u0026#39;mm/dd/yyyy\u0026#39;, //　日付フォーマット language: \u0026#39;ja\u0026#39;, // 日本語化を指定 }); } }); \u0026lt;/script\u0026gt; このパッケージのドキュメントは以下からアクセスできます。 https://mymth.github.io/vanillajs-datepicker\nSortableJS jquery-uiのsortableの代わりとしては、SortableJSがおすすめです。\nこちらはブレードでは以下のように設定が可能です。\n\u0026lt;div class=\u0026#34;row mb-3\u0026#34;\u0026gt; \u0026lt;label class=\u0026#34;col-md-4 col-form-label text-md-end\u0026#34;\u0026gt;順番の並べ替え\u0026lt;/label\u0026gt; \u0026lt;div class=\u0026#34;col-md-6\u0026#34;\u0026gt; \u0026lt;ol id=\u0026#34;sortable\u0026#34;\u0026gt; \u0026lt;li class=\u0026#34;ui-state-default\u0026#34;\u0026gt;アイテム 1\u0026lt;/li\u0026gt; \u0026lt;li class=\u0026#34;ui-state-default\u0026#34;\u0026gt;アイテム 2\u0026lt;/li\u0026gt; \u0026lt;li class=\u0026#34;ui-state-default\u0026#34;\u0026gt;アイテム 3\u0026lt;/li\u0026gt; \u0026lt;/ol\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; ... \u0026lt;script type=\u0026#34;module\u0026#34;\u0026gt; document.addEventListener(\u0026#39;DOMContentLoaded\u0026#39;, function() { Sortable.create(document.getElementById(\u0026#39;sortable\u0026#39;)); }); \u0026lt;/script\u0026gt; viteで対応 これらのパッケージをviteで対応（コンパイル）するには、 以下で２つパッケージと取り入れます。\n$ npm install vanillajs-datepicker --save-dev $ npm install sortablejs --save-dev 次に、bootstrap.jsでそれらをインポートします。DatePickerでは、Bootstrap 5のためのCSSや日本語化を指定します。 SorableとDatepickerはwindow.をプレフィックスとしてグローバル変数としています。jQueryのインポートがないことに注意を。\nresources/js/bootstrap.js import Datepicker from \u0026#39;vanillajs-datepicker/Datepicker\u0026#39;; import\u0026#39;vanillajs-datepicker/css/datepicker-bs5.min.css\u0026#39;; // Bootstrap 5を取り入れる import ja from \u0026#39;vanillajs-datepicker/locales/ja\u0026#39;; // 日本語を取り入れる Object.assign(Datepicker.locales, ja); window.Datepicker = Datepicker; import Sortable from \u0026#39;sortablejs\u0026#39;; window.Sortable = Sortable; そして最後に、vite.config.jsの設定は以下のようになります。\nvite.config.js import { defineConfig } from \u0026#39;vite\u0026#39;; import laravel from \u0026#39;laravel-vite-plugin\u0026#39;; export default defineConfig({ plugins: [ laravel({ input: [ \u0026#39;resources/sass/app.scss\u0026#39;, \u0026#39;resources/js/app.js\u0026#39;, ], refresh: true, }), ], }); ","date":"2025-06-30T11:26:23+09:00","permalink":"https://www.larajapan.com/2025/06/30/vite%E3%81%A8%E3%83%90%E3%83%8B%E3%83%A9js-%E6%97%A5%E4%BB%98%E9%81%B8%E6%8A%9E%E3%81%A8%E3%83%AA%E3%82%B9%E3%83%88%E4%B8%A6%E3%81%B3%E6%9B%BF%E3%81%88%E3%81%AE%E3%82%A6%E3%82%A3%E3%82%B8/","title":"Viteとバニラjs – 日付選択とリスト並び替えのウィジット"},{"content":"FormRequestのユニットテスト２記事目の今回は、prepareForValidation()を含めてテストをしたい場合はどうするのか？についてご紹介します。前回に続いて、「店舗情報更新画面」のバリデーション・認可処理を行うShopUpdateRequestを例にテストを作成します。\n前回の記事はこちらからどうぞ。\nテスト対象 ShopUpdateRequestにはprepareForValidation()が定義されています。name要素の半角カタカナを全角カタカナに変換する、というものです。 ・・・・・ protected function prepareForValidation(): void { // 店舗名の半角カタカナを全角カタカナに変換 $this-\u0026gt;merge([ \u0026#39;name\u0026#39; =\u0026gt; mb_convert_kana($this-\u0026gt;input(\u0026#39;name\u0026#39;) ?? \u0026#39;\u0026#39;, \u0026#39;KV\u0026#39;), ]); } ・・・・・ prepareForValidation()を含めたテスト prepareForValidation()も含めてテストを行う場合、rules()単体のテストで使ったValidator::make()による検証ではprepareForValidation()は呼ばれないので、少し工夫する必要があります。\nLaravel7の時に当サイトでご紹介したFormRequestのテストの記事で記載の$this-\u0026gt;app-\u0026gt;resolving\u0026hellip;を使用した手法は現在も有効ですので、そちらを使って以下のようなテストを書くことができます。\n#[Test] #[DataProvider(\u0026#39;validationWithKanaConversionDataProvider\u0026#39;)] public function 店舗名の半角カタカナが全角カタカナに変換されること(array $input, string $expected): void { $input = array_merge( $input, [ \u0026#39;description\u0026#39; =\u0026gt; \u0026#39;テスト用の説明文です\u0026#39;, \u0026#39;active_flag\u0026#39; =\u0026gt; \u0026#39;Y\u0026#39; ], ); $adminUser = User::factory()-\u0026gt;create([\u0026#39;role\u0026#39; =\u0026gt; \u0026#39;admin\u0026#39;]); $this-\u0026gt;actingAs($adminUser); // インスタンス作成時に実行する処理を設定 $this-\u0026gt;app-\u0026gt;resolving(ShopUpdateRequest::class, function ($resolved) use ($input) { $resolved-\u0026gt;merge($input); }); $request = app(ShopUpdateRequest::class); $this-\u0026gt;assertEquals($expected, $request-\u0026gt;input(\u0026#39;name\u0026#39;)); } public static function validationWithKanaConversionDataProvider(): array { return [ \u0026#39;半角カタカナ\u0026#39; =\u0026gt; [ [\u0026#39;name\u0026#39; =\u0026gt; \u0026#39;ﾃｽﾄｼｮｯﾌﾟ\u0026#39;], \u0026#39;テストショップ\u0026#39;, ], \u0026#39;漢字混在\u0026#39; =\u0026gt; [ [\u0026#39;name\u0026#39; =\u0026gt; \u0026#39;ﾃｽﾄ店舗\u0026#39;], \u0026#39;テスト店舗\u0026#39;, ], \u0026#39;全角カタカナ\u0026#39; =\u0026gt; [ [\u0026#39;name\u0026#39; =\u0026gt; \u0026#39;テストショップ\u0026#39;], \u0026#39;テストショップ\u0026#39;, ], ]; } app(ShopUpdateRequest::class)を実行した時点でprepareForValidation()だけでなくauthorize()、rules()、messages()の全てが呼ばれます。なのでauthorize()を設定している場合は認証を通すための準備も必要になりますので、ご注意ください。\nFormRequest全体をテストしながら、ルートパラメータも取得したい場合 先ほどのコードでFormRequestの各メソッドを通した全体的なテストが可能となりましたが、ルートパラメータを取得してのテストも同時に行いたい場合は追加の情報が必要となります。前回の記事内、rules()単体のテストの項でご紹介したように、リクエストインスタンスにはルート情報がセットされていないからでしたね。\n先ほどのテストコードにあった$this-\u0026gt;app-\u0026gt;resolving()の中でルート情報をセットできるように編集します。setRouteResolver()を使って、以下のように記述します。\n・・・・・ $this-\u0026gt;app-\u0026gt;resolving(ShopUpdateRequest::class, function ($resolved) use ($input, $shop) { $resolved-\u0026gt;merge($input); $route = Route::getRoutes()-\u0026gt;match($resolved); $route-\u0026gt;setParameter(\u0026#39;shop\u0026#39;, $shop); $resolved-\u0026gt;setRouteResolver(fn() =\u0026gt; $route); }); $request = app(ShopUpdateRequest::class); ・・・・・ ただ、ここまでするのであればFeatureテストで書いたほうがシンプルなテストコードで書けるかもしれませんね。\n包括的なテストとメソッド単体のテストまとめ FormRequest全体を通したテストを行えば、より実際の動作に近い形の検証が可能です。ですがシンプルなFormRequestの時や、バリデーションに焦点を当てたテストを書きたい場合などは、より軽量な方法としてrules()・autorize()単体のテストとするほうが良いと思いますので、FormRequestの内容に応じて使い分けたほうが良さそうです。\n前回と今回の記事でご紹介したテストの書き方を簡単に以下にまとめましたので、FormRequestのテストを作成する際のご参考になれば嬉しいです。\nテスト対象 テスト方法 authorize() $request-\u003eauthorize() を直接呼び出す rules() Validator::make($data, $request-\u003erules(), $request-\u003emessages()) rules()（ルートパラメータ必要な場合） setRouteResolver() でルート情報を設定 prepareForValidation() を含めた検証 $this-\u003eapp-\u003eresolving() + app(FormRequest::class) prepareForValidation() + ルートパラメータ resolving() 内で setRouteResolver() でルート情報を設定 ","date":"2025-06-24T08:48:33+09:00","permalink":"https://www.larajapan.com/2025/06/24/laravel-formrequest%E3%81%AE%E3%83%A6%E3%83%8B%E3%83%83%E3%83%88%E3%83%86%E3%82%B9%E3%83%88%EF%BC%88%EF%BC%92%EF%BC%89/","title":"Laravel FormRequestのユニットテスト（２）"},{"content":"webpackベースのLaravel MixからViteへの移行の第２弾ですが、今回は私のお客さんのプロジェクトでも使用されているjquery-uiをViteで対応の話です。\njquery-ui jQuery UIは、jQuerytライブラリの上に構築されたUIに関わるインタラクション、エフェクトとウィジェットのパッケージです。そのパッケージを使うと開発者は複雑なUIコンポーネントを簡単にプログラムに取り入れることができます。jQueryとともに長年に渡って現在もメンテされています。\nどれほどシンプルに取り入れることが可能か、jQuery UIの日付選択とリスト並び替えのウィジットを見てみます。\nまず、日付の選択のdatepickerのUIはこんな感じです。 入力のテキストのところをクリックするとその下にカレンダーのウィジットが表示されて、日付を手動で入れることなくマウスで選択できます。\nブレードでは以下のように指定のDOMのid (#datepicker) にdatepicker()のメソッドを装着するだけです。言語の指定も、選択された日付のフォーマットも可能です。\n\u0026lt;input type=\u0026#34;text\u0026#34; id=\u0026#34;datepicker\u0026#34; name=\u0026#34;select_date\u0026#34; class=\u0026#34;form-control\u0026#34;\u0026gt; ... \u0026lt;script type=\u0026#34;module\u0026#34;\u0026gt; $(document).ready(function() { $(\u0026#39;#datepicker\u0026#39;).datepicker({ regional: \u0026#34;ja\u0026#34;,　// 日本語化 dateFormat: \u0026#39;mm/dd/yy\u0026#39;　// 日付フォーマット }); }); \u0026lt;/script\u0026gt; もちろん、昨今では、\n\u0026lt;input type=\u0026#34;date\u0026#34;\u0026gt; と入力のタイプをdateで指定するとカレンダーが出てきますが、表示されるカレンダーをブラウザーの言語に関わらず日本語で表示したり選択した日付を2025-06-14のようなフォーマットを変えたりなどは可能であってもjQuery UIのように容易には設定できません。\n以下でjQurey UIのいろいろな機能のデモを見ることができます。 https://jqueryui.com/datepicker\n次は、以下のようなリストやテーブルでの行の並び順を変えるsortable。\nブレードでは、こんな感じの指定です。こちらも指定のDOMのid (#sortable)にsortable()のメソッドを装着するだけで、マウスで指定の行をドラッグしてリストの順番を変えることができます。\n\u0026lt;div class=\u0026#34;row mb-3\u0026#34;\u0026gt; \u0026lt;label class=\u0026#34;col-md-4 col-form-label text-md-end\u0026#34;\u0026gt;順番の並べ替え\u0026lt;/label\u0026gt; \u0026lt;div class=\u0026#34;col-md-6\u0026#34;\u0026gt; \u0026lt;ol id=\u0026#34;sortable\u0026#34;\u0026gt; \u0026lt;li class=\u0026#34;ui-state-default\u0026#34;\u0026gt;アイテム 1\u0026lt;/li\u0026gt; \u0026lt;li class=\u0026#34;ui-state-default\u0026#34;\u0026gt;アイテム 2\u0026lt;/li\u0026gt; \u0026lt;li class=\u0026#34;ui-state-default\u0026#34;\u0026gt;アイテム 3\u0026lt;/li\u0026gt; \u0026lt;/ol\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; ... \u0026lt;script type=\u0026#34;module\u0026#34;\u0026gt; $(document).ready(function() { $(\u0026#39;#sortable\u0026#39;).sortable(); }); \u0026lt;/script\u0026gt; viteで対応 さて、この素晴らしいjquery-uiパッケージをviteで対応するには、\nまず、jqueryとjuquery-uiのnpmのパッケージを以下のコマンドで取り入れます。\n$ npm install jquery jquery-ui --save-dev さらに、以下のパッケージのrollupのinjectのプラグインも必要です。このプラグインの説明から察するに、jquery-uiのように$やjQueryのグローバル変数が設定されていると仮定するパッケージにおいてそれらを利用可能にするものと思われます。それらの指定は後にvite.config.jsで行います。これがないと動かないので必須です。\n$ npm install @rollup/plugin-inject --save-dev つぎに、bootstrap.jsでjqueryとjquery-uiをインポートします。また、カレンダーのローカライズのために、datepicker-ja.js、そしてウィジットのスタイルのために、jquery-ui.min.cssも指定が必要です。\nreousrces/js/bootrap.js import \u0026#39;bootstrap\u0026#39;; import jQuery from \u0026#39;jquery\u0026#39;; window.$ = window.jQuery = jQuery; import \u0026#39;jquery-ui/dist/jquery-ui\u0026#39;; import \u0026#39;jquery-ui/ui/i18n/datepicker-ja.js\u0026#39;; import \u0026#39;jquery-ui/dist/themes/base/jquery-ui.min.css\u0026#39;; そして最後に、vite.config.jsの設定は以下のようになります。先ほどのrolleupのプラグインのinjectの設定が必要なので注意を。\nvite.config.js import { defineConfig } from \u0026#39;vite\u0026#39;; import laravel from \u0026#39;laravel-vite-plugin\u0026#39;; import inject from \u0026#34;@rollup/plugin-inject\u0026#34;; export default defineConfig({ plugins: [ inject({ $: \u0026#39;jquery\u0026#39;, jQuery: \u0026#39;jquery\u0026#39;, }), laravel({ input: [ \u0026#39;resources/sass/app.scss\u0026#39;, \u0026#39;resources/js/app.js\u0026#39;, ], refresh: true, }), ], }); そして、以下でビルドを実行して、ブラウザで見れるようにLaravelのウェブサーバーを立ち上げます。\n$ npm run build \u0026amp; php artisan serve もちろん、npm run devとして開発時のデバッグのためにホットリロードを可能としてもよいです。変更が瞬時にブラウザーに反映されて開発が楽しくなります。\n今回はjquery-uiをviteで対応の話でしたが、次の投稿ではjqueryを使わない日付の選択とリストの並び替えのパッケージを将来する予定です。\n","date":"2025-06-16T23:23:14+09:00","permalink":"https://www.larajapan.com/2025/06/16/vite%E3%81%A8jquery-ui-%E6%97%A5%E4%BB%98%E9%81%B8%E6%8A%9E%E3%81%A8%E3%83%AA%E3%82%B9%E3%83%88%E4%B8%A6%E3%81%B3%E6%9B%BF%E3%81%88%E3%81%AE%E3%82%A6%E3%82%A3%E3%82%B8%E3%83%83%E3%83%88/","title":"Viteとjquery-ui - 日付選択とリスト並び替えのウィジット"},{"content":"Laravel12.xでFormRequestのユニットテストを作成します。今回の記事では基本的なテストから、少しややこしい$this-\u0026gt;route()のようなルートパラメータを取得する必要がある場合の書き方などをご紹介します。\nテスト対象 店舗情報の更新画面にて、入力データのバリデーション・認可処理を行うShopUpdateRequestが今回のテスト対象です。 入力項目は、店舗名（name）・店舗説明（description）・有効フラグ（active_flag）の３つ。rules()では必須チェックや文字数チェックなどの基本的なバリデーションと、nameに関しては他店舗と重複不可のユニーク制約も設定されています。\nまた、認可処理authorize()では管理者のみが実行可能なように設定されています。\nApp/Http/Requests/ShopUpdateRequest.php namespace App\\Http\\Requests; use Illuminate\\Foundation\\Http\\FormRequest; use Illuminate\\Validation\\Rule; class ShopUpdateRequest extends FormRequest { /** * Determine if the user is authorized to make this request. */ public function authorize(): bool { return auth()-\u0026gt;user()-\u0026gt;role === \u0026#39;admin\u0026#39;; } /** * Get the validation rules that apply to the request. * * @return array\u0026lt;string, \\Illuminate\\Contracts\\Validation\\ValidationRule|array\u0026lt;mixed\u0026gt;|string\u0026gt; */ public function rules(): array { $shop = $this-\u0026gt;route(\u0026#39;shop\u0026#39;); return [ \u0026#39;name\u0026#39; =\u0026gt; [ \u0026#39;required\u0026#39;, \u0026#39;string\u0026#39;, \u0026#39;max:20\u0026#39;, Rule::unique(\u0026#39;shops\u0026#39;)-\u0026gt;ignore($shop), ], \u0026#39;description\u0026#39; =\u0026gt; [\u0026#39;required\u0026#39;, \u0026#39;string\u0026#39;, \u0026#39;max:100\u0026#39;], \u0026#39;active_flag\u0026#39; =\u0026gt; [\u0026#39;required\u0026#39;, Rule::in([\u0026#39;Y\u0026#39;, \u0026#39;N\u0026#39;])], ]; } public function messages(): array { return [ \u0026#39;name.required\u0026#39; =\u0026gt; \u0026#39;名前は必須です\u0026#39;, \u0026#39;name.max\u0026#39; =\u0026gt; \u0026#39;名前は20文字以内で入力してください\u0026#39;, \u0026#39;name.unique\u0026#39; =\u0026gt; \u0026#39;この名前は既に使用されています\u0026#39;, \u0026#39;description.required\u0026#39; =\u0026gt; \u0026#39;説明は必須です\u0026#39;, \u0026#39;description.max\u0026#39; =\u0026gt; \u0026#39;説明は100文字以内で入力してください\u0026#39;, \u0026#39;active_flag.required\u0026#39; =\u0026gt; \u0026#39;アクティブフラグは必須です\u0026#39;, \u0026#39;active_flag.in\u0026#39; =\u0026gt; \u0026#39;アクティブフラグは Y または N で入力してください\u0026#39;, ]; } protected function prepareForValidation(): void { // 店舗名の半角カタカナを全角カタカナに変換 $this-\u0026gt;merge([ \u0026#39;name\u0026#39; =\u0026gt; mb_convert_kana($this-\u0026gt;input(\u0026#39;name\u0026#39;) ?? \u0026#39;\u0026#39;, \u0026#39;KV\u0026#39;), ]); } } そしてコントローラー側では、以下のような定義となっています。\nApp/Http/Controllers/ShopController.php ・・・・・ use App\\Http\\Requests\\ShopUpdateRequest; class ShopController extends Controller { ・・・・・ public function update(ShopUpdateRequest $request, Shop $shop): RedirectResponse { $shop-\u0026gt;update($request-\u0026gt;validated()); return redirect()-\u0026gt;route(\u0026#39;shops.show\u0026#39;, $shop) -\u0026gt;with(\u0026#39;success\u0026#39;, \u0026#39;店舗情報を更新しました。\u0026#39;); } ・・・・・ } authorize()のテスト まずはauthorize()のテストから。認証はadminユーザーのみ通ることができる、ということを検証します。 以下のようにRequestインスタンスから直接authorize()を呼び出してテストします。\nTests/Unit/Http/Requests/ShopUpdateRequestTest.php namespace Tests\\Unit\\Http\\Requests; use App\\Http\\Requests\\ShopUpdateRequest; use App\\Models\\User; use Illuminate\\Foundation\\Testing\\RefreshDatabase; use PHPUnit\\Framework\\Attributes\\Test; use Tests\\TestCase; class ShopUpdateRequestTest extends TestCase { use RefreshDatabase; #[Test] public function 管理者でないユーザーの場合は認証が通らない(): void { $normalUser = User::factory()-\u0026gt;create([\u0026#39;role\u0026#39; =\u0026gt; \u0026#39;user\u0026#39;]); $this-\u0026gt;actingAs($normalUser); $request = new ShopUpdateRequest; $this-\u0026gt;assertFalse($request-\u0026gt;authorize()); } #[Test] public function 管理者の場合は認証が通る(): void { $adminUser = User::factory()-\u0026gt;create([\u0026#39;role\u0026#39; =\u0026gt; \u0026#39;admin\u0026#39;]); $this-\u0026gt;actingAs($adminUser); $request = new ShopUpdateRequest; $this-\u0026gt;assertTrue($request-\u0026gt;authorize()); } } rules()のテスト 次に、rules()のテストです。正常系のテストは以下のように書くことができます。 Tests/Unit/Http/Requests/ShopUpdateRequestTest.php ・・・・・ use Illuminate\\Support\\Facades\\Validator; use PHPUnit\\Framework\\Attributes\\DataProvider; ・・・・・ #[Test] #[DataProvider(\u0026#39;validationDataProvider\u0026#39;)] public function 正常形のテスト(array $data): void { $rules = (new ShopUpdateRequest)-\u0026gt;rules(); $validator = Validator::make($data, $rules); $this-\u0026gt;assertTrue($validator-\u0026gt;passes()); } public static function validDataProvider(): array { return [ \u0026#39;文字数制限OK\u0026#39; =\u0026gt; [ [ \u0026#39;name\u0026#39; =\u0026gt; str_repeat(\u0026#39;a\u0026#39;, 20), \u0026#39;description\u0026#39; =\u0026gt; str_repeat(\u0026#39;a\u0026#39;, 100) \u0026#39;active_flag\u0026#39; =\u0026gt; \u0026#39;Y\u0026#39;, ], ], ]; } 現時点で最新のLaravel12でも、テストの書き方は特に変わりません。このままデータプロバイダに他の正常系のパターンを追加していけばOKです。\n次は異常系のテストです。エラーメッセージも同時に検証したいので、Validator::make()の第３引数にエラーメッセージの配列$request-\u0026gt;messages()を渡しています。\nTests/Unit/Http/Requests/ShopUpdateRequestTest.php ・・・・・ #[Test] #[DataProvider(\u0026#39;invalidDataProvider\u0026#39;)] public function 異常系のテスト(array $data, array $messages): void { $request = new ShopUpdateRequest; $validator = Validator::make($data, $request-\u0026gt;rules(), $request-\u0026gt;messages()); $this-\u0026gt;assertTrue($validator-\u0026gt;fails()); $this-\u0026gt;assertSame($messages, $validator-\u0026gt;errors()-\u0026gt;toArray()); } public static function invalidDataProvider(): array { return [ \u0026#39;空値を許容しない\u0026#39; =\u0026gt; [ \u0026#39;data\u0026#39; =\u0026gt; [ \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;\u0026#39;, \u0026#39;description\u0026#39; =\u0026gt; \u0026#39;\u0026#39;, \u0026#39;active_flag\u0026#39; =\u0026gt; \u0026#39;\u0026#39;, ], \u0026#39;messages\u0026#39; =\u0026gt; [ \u0026#39;name\u0026#39; =\u0026gt; [\u0026#39;名前は必須です\u0026#39;], \u0026#39;description\u0026#39; =\u0026gt; [\u0026#39;説明は必須です\u0026#39;], \u0026#39;active_flag\u0026#39; =\u0026gt; [\u0026#39;アクティブフラグは必須です\u0026#39;], ], ], \u0026#39;文字数オーバー\u0026#39; =\u0026gt; [ \u0026#39;data\u0026#39; =\u0026gt; [ \u0026#39;name\u0026#39; =\u0026gt; str_repeat(\u0026#39;a\u0026#39;, 21), \u0026#39;description\u0026#39; =\u0026gt; str_repeat(\u0026#39;a\u0026#39;, 101), \u0026#39;active_flag\u0026#39; =\u0026gt; \u0026#39;Y\u0026#39;, ], \u0026#39;messages\u0026#39; =\u0026gt; [ \u0026#39;name\u0026#39; =\u0026gt; [\u0026#39;名前は20文字以内で入力してください\u0026#39;], \u0026#39;description\u0026#39; =\u0026gt; [\u0026#39;説明は100文字以内で入力してください\u0026#39;], ], ], ]; } このような形で、異常系もデータプロバイダにテストデータを追加していけばOKです。\nですが、nameプロパティで１つ問題があります。nameプロパティに設定されている以下のようなunique()制約は、このテストのままでは検証できません。\n$shop = $this-\u0026gt;route(\u0026#39;shop\u0026#39;); ・・・・・ Rule::unique(\u0026#39;shops\u0026#39;)-\u0026gt;ignore($shop), $this-\u0026gt;route()で取得しているルートパラメーターは、現状のテストではnullとなってしまうからです。\nテスト時は実際のHTTPリクエストとして実行するわけではないため、ルート情報が設定されていないことが原因のようです。テスト時にルートパラメータを取り扱うにはどうしたらいいでしょうか。\nルート情報を含んだリクエストインスタンスを作成する ルートパラメータを含めたバリデーションテストをする場合、リクエストインスタンスにルート情報を設定する必要があります。 ここで、検証対象のルーティングを確認します。\nPUT|PATCH shops/{shop} ............................. shops.update › ShopController@update ルーティング情報がわかったので、さっそくルート情報を持ったFormRequestインスタンスを作成してゆきましょう。まずはcreate()というFormRequestクラスに定義されているメソッドを使用します。\n１. create()でリクエストインスタンスを作成 $request = ShopUpdateRequest::create( route(\u0026#39;shops.update\u0026#39;, $shop), \u0026#39;PUT\u0026#39;, $input ); 第１引数にURL、第２引数にHTTPメソッド、第３引数に入力データを渡します。これで、ルーティング情報に合わせたリクエストインスタンスが作成できます。\n入力情報もセットできているので、$request-\u0026gt;input()での値取得も可能です。\nよく使われる$request-\u0026gt;merge($data)でも入力データをセットできますが、ルーティングがすでに用意されている場合は今回のようにcreate()を使うことで、より実際のHTTPリクエストに近い状況を作成することができます。\n2. FormRequestクラスのsetRouteResolver()でルートパラメータを設定 リクエスト情報はセットできましたが、$this-\u003eroute()を有効にするにはさらにルート情報をリクエストに適用する必要があります。インスタンスの作成に続いて、以下のようにデータを適用します。 $request = ShopUpdateRequest::create( ・・・・・ ); // 実際のルートマッチングを取得し、リクエストに適用 $route = Route::getRoutes()-\u0026gt;match($request); $route-\u0026gt;setParameter(\u0026#39;shop\u0026#39;, $currentShop); $request-\u0026gt;setRouteResolver(fn () =\u0026gt; $route); setRouteResolver()は$requestにルートを紐付けるメソッドです。これでやっと、テストコード内で$this-\u0026gt;route(\u0026lsquo;shop\u0026rsquo;)でデータが取得できるようになりました。\nちゃんとデータが取得できるのかをtinkerでも確認してみます。\n$ php artisan tinker Psy Shell v0.12.8 (PHP 8.2.27 — cli) by Justin Hileman \u0026gt; $shop = App\\Models\\Shop::factory()-\u0026gt;create(); = App\\Models\\Shop {#6265 name: \u0026#34;有限会社 桐山\u0026#34;, ・・・・・ \u0026gt; $request = App\\Http\\Requests\\ShopUpdateRequest::create( route(\u0026#39;shops.update\u0026#39;, $shop), \u0026#39;PUT\u0026#39;, [\u0026#39;name\u0026#39; =\u0026gt; \u0026#39;テスト店舗\u0026#39;, \u0026#39;description\u0026#39; =\u0026gt; \u0026#39;テスト説明\u0026#39;, \u0026#39;active_flag\u0026#39; =\u0026gt; \u0026#39;Y\u0026#39;]); = App\\Http\\Requests\\ShopUpdateRequest {#6306 ・・・・・ } \u0026gt; $route = Route::getRoutes()-\u0026gt;match($request); = Illuminate\\Routing\\Route {#976 ・・・・・ \u0026gt; $route-\u0026gt;setParameter(\u0026#39;shop\u0026#39;, $shop); = null \u0026gt; $request-\u0026gt;setRouteResolver(fn() =\u0026gt; $route); = App\\Http\\Requests\\ShopUpdateRequest {#6306 ・・・・・ \u0026gt; $request-\u0026gt;route(\u0026#39;shop\u0026#39;);　// これで取得できるようになった！ = App\\Models\\Shop {#6265 name: \u0026#34;有限会社 桐山\u0026#34;, ・・・・・ 問題なく取得できています！\nルートパラメータを使ったunique制約のユニットテスト 上記の方法を使った「既存のショップ名と重複不可」の検証テストコードは、以下のようになります。 ・・・・・ use App\\Models\\Shop; use Illuminate\\Support\\Facades\\Route; ・・・・・ #[Test] public function 既存のショップ名と重複不可(): void { // 既存のショップを作成 Shop::factory()-\u0026gt;create([\u0026#39;name\u0026#39; =\u0026gt; \u0026#39;既存のショップ名\u0026#39;]); // 更新対象のショップを作成 $currentShop = Shop::factory()-\u0026gt;create([\u0026#39;name\u0026#39; =\u0026gt; \u0026#39;現在のショップ名\u0026#39;]); // 更新リクエストのデータ $input = [ \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;既存のショップ名\u0026#39;, // 既存のショップ名と重複 \u0026#39;description\u0026#39; =\u0026gt; \u0026#39;テスト用の説明文です\u0026#39;, \u0026#39;active_flag\u0026#39; =\u0026gt; \u0026#39;Y\u0026#39;, ]; // リクエストインスタンスの作成 $request = ShopUpdateRequest::create( route(\u0026#39;shops.update\u0026#39;, $currentShop), \u0026#39;PUT\u0026#39;, $input ); // 実際のルートマッチングを取得し、リクエストに適用 $route = Route::getRoutes()-\u0026gt;match($request); $route-\u0026gt;setParameter(\u0026#39;shop\u0026#39;, $currentShop); $request-\u0026gt;setRouteResolver(fn () =\u0026gt; $route); // バリデーション実行 $validator = Validator::make($request-\u0026gt;all(), $request-\u0026gt;rules(), $request-\u0026gt;messages()); // バリデーションが失敗することをアサート $this-\u0026gt;assertTrue($validator-\u0026gt;fails()); // エラーメッセージに重複エラーが含まれることをアサート $this-\u0026gt;assertTrue($validator-\u0026gt;errors()-\u0026gt;has(\u0026#39;name\u0026#39;)); $this-\u0026gt;assertEquals(\u0026#39;この名前は既に使用されています\u0026#39;, $validator-\u0026gt;errors()-\u0026gt;first(\u0026#39;name\u0026#39;)); } テストデータ準備のため少しコードが長くなりましたが、バリデーションの実行・アサートの部分は先ほどの異常系のテストと同じです。\n次回は引き続き、prepareForValidation()を含めたテストの書き方をご紹介します。\n","date":"2025-06-02T01:29:15+09:00","permalink":"https://www.larajapan.com/2025/06/02/laravel12formrequest%E3%81%AE%E3%83%A6%E3%83%8B%E3%83%83%E3%83%88%E3%83%86%E3%82%B9%E3%83%88/","title":"Laravel FormRequestのユニットテスト（１）"},{"content":"前のことになりますが、Laravelのバージョン8以降よりユーザー画面のアセットのビルドのためにViteが登場しています。しかし私のプロジェクトでは、Boostrapのフレームワークを使用しておりjQueryとその関連のライブラリをヘビーに使用していてなかなか従来のwebpackベースのLaravel Mixからは抜け出すことができません。何度かは試みたものの解決できないエラーが出て挫折を繰り返すのみでした。しかし、最近やっとその移行に光が見えてきました。Viteのビルドのスピードと開発環境は良いです。\n基本的に私が抱えるプロジェクトは、画面のUIはSassベースのBootstrapのフレームワークを使用、そしてそれに関連するjsのライブラリはほとんどがjQueryベースです。Laravelが存在する以前のフレームワークなしのPHPで開発されLaravelに進化した大きなプロジェクトで、そう簡単に流行りのテクノロジーには乗り換えることはできません。今回は、そのようなプロジェクトのためのViteへの移行の話です。\nVite まず、Viteを説明するために、最新のLaravel 12.xのプロジェクトを作成します。\n$ composer create-project laravel/laravel l12x-vite この時点で、UIのアセットの元となるresourcesディレクトリはこんな感じです。\n├── css │ └── app.css ├── js │ ├── app.js │ └── bootstrap.js └── views └── welcome.blade.php 上において、app.cssはtailwindcssのための設定ファイルで、\nresources/css/app.css @import \u0026#39;tailwindcss\u0026#39;; @source \u0026#39;../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php\u0026#39;; @source \u0026#39;../../storage/framework/views/*.php\u0026#39;; @source \u0026#39;../**/*.blade.php\u0026#39;; @source \u0026#39;../**/*.js\u0026#39;; @theme { --font-sans: \u0026#39;Instrument Sans\u0026#39;, ui-sans-serif, system-ui, sans-serif, \u0026#39;Apple Color Emoji\u0026#39;, \u0026#39;Segoe UI Emoji\u0026#39;, \u0026#39;Segoe UI Symbol\u0026#39;, \u0026#39;Noto Color Emoji\u0026#39;; bootstrap.jsでは、ajaxのためのaxiosのパッケージがimportされ、\nresources/js/bootstrap.js import axios from \u0026#39;axios\u0026#39;; window.axios = axios; window.axios.defaults.headers.common[\u0026#39;X-Requested-With\u0026#39;] = \u0026#39;XMLHttpRequest\u0026#39;; さらにapp.jsにおいて、そのbootstrap.jsをimportしています。\nresources/js/app.js import \u0026#39;./bootstrap\u0026#39;; そして以下がViteの設定ファイルvite.config.jsです。resources下のファイルが指定されていることがわかります。\nvite.config.js import { defineConfig } from \u0026#39;vite\u0026#39;; import laravel from \u0026#39;laravel-vite-plugin\u0026#39;; export default defineConfig({ plugins: [ laravel({ input: [ \u0026#39;resources/css/app.css\u0026#39;, \u0026#39;resources/js/app.js\u0026#39;, ], refresh: true, }), ], }); また、上では、laravel-vite-pluginがimportされていますが、これが以前のlaravel-mixのようなもので、Viteの設定をとてもシンプルにし、LaravelにおいてUI開発を簡単にします\nViteのアクションを見るには、まず、package.jsonに含まれるパッケージをダウンロードしてnode_modulesのディレクトリにインストールします。\n$ npm install そして、Viteを使ってアセットのビルドをします。\n$ npm run build 注意：npm run devによるhotの方の説明は違う機会にします。そちらもUI開発のためのViteの優れた機能です。\nこのビルドにより、public/buildの以下のファイルが作成されます。\npublic/build ├── assets │ ├── app-D4mwfHMp.css │ └── app-T1DpEqax.js └── manifest.json $ php artisan serve を実行して、http://127.0.0.1:8000/にアクセスすると、\nこの画面のソースを見ると、ビルドで作成されたファイルが使用されているのがわかります。\n\u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;utf-8\u0026#34;\u0026gt; \u0026lt;meta name=\u0026#34;viewport\u0026#34; content=\u0026#34;width=device-width, initial-scale=1\u0026#34;\u0026gt; \u0026lt;title\u0026gt;Laravel\u0026lt;/title\u0026gt; \u0026lt;!-- Fonts --\u0026gt; \u0026lt;link rel=\u0026#34;preconnect\u0026#34; href=\u0026#34;https://fonts.bunny.net\u0026#34;\u0026gt; \u0026lt;link href=\u0026#34;https://fonts.bunny.net/css?family=instrument-sans:400,500,600\u0026#34; rel=\u0026#34;stylesheet\u0026#34; /\u0026gt; \u0026lt;!-- Styles / Scripts --\u0026gt; \u0026lt;link rel=\u0026#34;preload\u0026#34; as=\u0026#34;style\u0026#34; href=\u0026#34;http://127.0.0.1:8000/build/assets/app-D4mwfHMp.css\u0026#34; /\u0026gt; \u0026lt;link rel=\u0026#34;modulepreload\u0026#34; href=\u0026#34;http://127.0.0.1:8000/build/assets/app-T1DpEqax.js\u0026#34; /\u0026gt; \u0026lt;link rel=\u0026#34;stylesheet\u0026#34; href=\u0026#34;http://127.0.0.1:8000/build/assets/app-D4mwfHMp.css\u0026#34; /\u0026gt; \u0026lt;script type=\u0026#34;module\u0026#34; src=\u0026#34;http://127.0.0.1:8000/build/assets/app-T1DpEqax.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;/head\u0026gt; ... 元のブレードファイルを見ると、@viteのディレクティブが使用されているのがわかります。\nresources/views/welcome.blade.php \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;{{ str_replace(\u0026#39;_\u0026#39;, \u0026#39;-\u0026#39;, app()-\u0026gt;getLocale()) }}\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;utf-8\u0026#34;\u0026gt; \u0026lt;meta name=\u0026#34;viewport\u0026#34; content=\u0026#34;width=device-width, initial-scale=1\u0026#34;\u0026gt; \u0026lt;title\u0026gt;Laravel\u0026lt;/title\u0026gt; \u0026lt;!-- Fonts --\u0026gt; \u0026lt;link rel=\u0026#34;preconnect\u0026#34; href=\u0026#34;https://fonts.bunny.net\u0026#34;\u0026gt; \u0026lt;link href=\u0026#34;https://fonts.bunny.net/css?family=instrument-sans:400,500,600\u0026#34; rel=\u0026#34;stylesheet\u0026#34; /\u0026gt; \u0026lt;!-- Styles / Scripts --\u0026gt; @if (file_exists(public_path(\u0026#39;build/manifest.json\u0026#39;)) || file_exists(public_path(\u0026#39;hot\u0026#39;))) @vite([\u0026#39;resources/css/app.css\u0026#39;, \u0026#39;resources/js/app.js\u0026#39;]) @else \u0026lt;style\u0026gt; ... Viteで作成されたmanifest.jsonには、\npublic/build/manifest.json { \u0026#34;resources/css/app.css\u0026#34;: { \u0026#34;file\u0026#34;: \u0026#34;assets/app-D4mwfHMp.css\u0026#34;, \u0026#34;src\u0026#34;: \u0026#34;resources/css/app.css\u0026#34;, \u0026#34;isEntry\u0026#34;: true }, \u0026#34;resources/js/app.js\u0026#34;: { \u0026#34;file\u0026#34;: \u0026#34;assets/app-T1DpEqax.js\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;app\u0026#34;, \u0026#34;src\u0026#34;: \u0026#34;resources/js/app.js\u0026#34;, \u0026#34;isEntry\u0026#34;: true } } のデータがあるので、これをもとにLaravelが@viteで指定されているアセットを実際のファイルに置き換えて表示します。 このメカニズムは、以前のlaravel-mixでのmix()ヘルパーのようなものです。目的は同じく更新したcssやjsのファイルが同じファイル名でブラウザにキャッシュされないためです。\nBootstrapのSass Laravelのデフォルトのプロジェクトはtailwindcssの使用ですが、Bootstrapで使用されているSassの対応のためには、Viteの設定ファイルを変更する必要あります。\nlaravel/uiがいまだにBootstrapをサポートしているのでそれを利用してみます。 先で作成されたプロジェクトに、laravel/uiをインストールして、bootstrapをUIとして指定します。-authはユーザーの登録やログイン画面の機能も追加します。\n$ composer require laravel/ui $ php artisan ui bootstrap --auth この後に、vite.config.jsを見てみますと、\nvite.config.js import { defineConfig } from \u0026#39;vite\u0026#39;; import laravel from \u0026#39;laravel-vite-plugin\u0026#39;; export default defineConfig({ plugins: [ laravel({ input: [ \u0026#39;resources/sass/app.scss\u0026#39;, \u0026#39;resources/js/app.js\u0026#39;, ], refresh: true, }), ], }); resources/css/app.cssがresources/sass/app.scssとなりBootstrapのsassのファイルをポイントしています。\njsの方では、bootstrap.jsにおいて、bootstrapをimportするように変更されています。 注意：このファイル名とCSSのフレームワークのBootsrapと混乱しないように。ここでのbootstrap.jsという名前は単にLaravelのプロジェクトのブートストラップという意味だけで、必要なJSパッケージをインポートするための目的です。\nresources/js/bootstrap.js import \u0026#39;bootstrap\u0026#39;; import axios from \u0026#39;axios\u0026#39;; window.axios = axios; window.axios.defaults.headers.common[\u0026#39;X-Requested-With\u0026#39;] = \u0026#39;XMLHttpRequest\u0026#39;; package.jsonには、bootstrapとsassのパッケージがlarave/uiのインストールで追加されているので、この時点でそれらをダウンロードします。\n$ npm install そして、以下でビルドを実行して、ブラウザで見れるようにLaravelのウェブサーバーを立ち上げます。\n$ npm run build \u0026amp; php artisan serve ブラウザのURLにおいて、以下を指定します。 注意：http://127.0.0.1:8000/でアクセスすると崩れているページとなります。それは、welcome.blade.phpがtailwindcssのままでbootstrapに変更されていないからです。\nhttp://127.0.0.1:8000/register ブラウザでは以下のような画面になり、まずは成功です。\njQuery 次は、jQueryの対応です。まず、npmでjqueryをインストールします。\n$ npm install --save-dev jquery 次に、bootstrap.jsを編集します。\nresources/js/bootstrap.js import \u0026#39;bootstrap\u0026#39;; import JQuery from \u0026#39;jquery\u0026#39;; window.$ = window.jQuery = JQuery; // グローバルで$やjQueryを使えるようにする ... ユーザー登録のブレードにおいて、jqueryを使用したスクリプトを入れてみます。パスワードが８文字以上が必要なことを警告するメッセージを表示するスクリプトです。\nresources/views/auth/register.blade.php @extends(\u0026#39;layouts.app\u0026#39;) @section(\u0026#39;content\u0026#39;) \u0026lt;div class=\u0026#34;container\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;row justify-content-center\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;col-md-8\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;card\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;card-header\u0026#34;\u0026gt;{{ __(\u0026#39;Register\u0026#39;) }}\u0026lt;/div\u0026gt; ... @endsection @section(\u0026#39;scripts\u0026#39;) \u0026lt;script type=\u0026#34;module\u0026#34;\u0026gt; $(document).ready(function() { const minPasswordLength = 8; // 最小パスワード長 // passwordの項目の後に、パスワードの長さに関する説明を追加 $(\u0026#39;#password\u0026#39;).after(\u0026#39;\u0026lt;small class=\u0026#34;form-text text-muted password-help\u0026#34;\u0026gt;パスワードは最低\u0026#39; + minPasswordLength + \u0026#39;文字必要です\u0026lt;/small\u0026gt;\u0026#39;); // passwordの項目に入力があった場合、パスワードの長さをチェック $(\u0026#39;#password\u0026#39;).on(\u0026#39;input\u0026#39;, function() { const passwordValue = $(this).val(); const passwordHelp = $(\u0026#39;.password-help\u0026#39;); if (passwordValue.length \u0026lt; minPasswordLength) { $(this).removeClass(\u0026#39;is-valid\u0026#39;).addClass(\u0026#39;is-invalid\u0026#39;); passwordHelp.removeClass(\u0026#39;text-muted text-success\u0026#39;).addClass(\u0026#39;text-danger\u0026#39;); } else { $(this).removeClass(\u0026#39;is-invalid\u0026#39;).addClass(\u0026#39;is-valid\u0026#39;); passwordHelp.removeClass(\u0026#39;text-muted text-danger\u0026#39;).addClass(\u0026#39;text-success\u0026#39;); } }); }); \u0026lt;/script\u0026gt; @endsection 上で注意することは、必ず","date":"2025-05-11T01:22:18+09:00","permalink":"https://www.larajapan.com/2025/05/11/vite%E3%81%A8bootstrapsass%E3%81%A8jquery/","title":"ViteとBootstrap(sass)とjQuery"},{"content":"前回に作成した禁止用語をフィルターするミドルウェアに今度はパラメータを渡してみます。例えば、管理者権限を持つユーザーなら禁止用語を隠さずに見れる。\nミドルウェアの変更はいたって簡単で、handle()のメソッドに渡す引数を１つ$role追加します。\napp/Http/Middleware/HideForbiddenWords.php namespace App\\Http\\Middleware; use Closure; use Illuminate\\Support\\Str; use Illuminate\\Http\\Request; class HideForbiddenWords { public static $forbiddenWords = [\u0026#39;禁止１\u0026#39;, \u0026#39;禁止２\u0026#39;, \u0026#39;禁止３\u0026#39;]; // $roleの引数を追加 public function handle(Request $request, Closure $next, string $role): mixed { $all = $request-\u0026gt;collect()-\u0026gt;map(function ($value, $key) use($role) { // 管理権限なら禁止用語は隠さずに if ($role === \u0026#39;admin\u0026#39;) { return $value; } else { return is_string($value) ? Str::of($value)-\u0026gt;replace(static::$forbiddenWords, \u0026#39;***\u0026#39;)-\u0026gt;toString() : $value; } })-\u0026gt;all(); $request-\u0026gt;merge($all); return $next($request); } } $roleがadminなら、禁止用語は隠さずに、それ以外は隠すという条件分岐を設けています。\nさっそく、tinkerで実行してみましょう。\n\u0026gt; $request = app(\u0026#39;request\u0026#39;); \u0026gt; $request-\u0026gt;merge([\u0026#39;sentence\u0026#39; =\u0026gt; \u0026#39;私は「禁止１」と叫んでしまいました。\u0026#39;])-\u0026gt;all(); = [ \u0026#34;sentence\u0026#34; =\u0026gt; \u0026#34;私は「禁止１」と叫んでしまいました。\u0026#34;, ] \u0026gt; use App\\Http\\Middleware\\HideForbiddenWords; \u0026gt; (new HideForbiddenWords)-\u0026gt;handle($request, fn($request) =\u0026gt; $request, \u0026#39;admin\u0026#39;)-\u0026gt;all(); = [ \u0026#34;sentence\u0026#34; =\u0026gt; \u0026#34;私は「禁止１」と叫んでしまいました。\u0026#34;, ] 隠さずに表示されましたね。\n以下のようにroutes/web.phpにおいて、ミドルウェアを装着するならコロンと合わせて指定します。\nuse App\\Http\\Middleware\\HideForbiddenWords; Route::patch(\u0026#39;/profile\u0026#39;, [ProfileController::class, \u0026#39;update\u0026#39;]) -\u0026gt;name(\u0026#39;profile.update\u0026#39;) -\u0026gt;middleware(HideForbiddenWords::class.\u0026#39;:admin\u0026#39;); 複数の権限を指定したいなら、つまり以下のように、adminとeditorの指定としたいなら、以下のようにコンマで区切って指定します。\nRoute::patch(\u0026#39;/profile\u0026#39;, [ProfileController::class, \u0026#39;update\u0026#39;]) -\u0026gt;name(\u0026#39;profile.update\u0026#39;) -\u0026gt;middleware(HideForbiddenWords::class.\u0026#39;:admin,editor\u0026#39;); 対応するミドルウェアのコード方は、以下のように引数をstring \u0026hellip;$roles可変引数を使用するように変えて対応します。\n... class HideForbiddenWords { public static $forbiddenWords = [\u0026#39;禁止１\u0026#39;, \u0026#39;禁止２\u0026#39;, \u0026#39;禁止３\u0026#39;]; // $roleの引数を追加 public function handle(Request $request, Closure $next, string ...$roles): mixed { ... ","date":"2025-04-26T09:57:48+09:00","permalink":"https://www.larajapan.com/2025/04/26/%E3%83%9F%E3%83%89%E3%83%AB%E3%82%A6%E3%82%A7%E3%82%A2%E3%82%92%E3%82%82%E3%81%A3%E3%81%A8%E7%9F%A5%E3%82%8D%E3%81%86%EF%BC%88%EF%BC%93%EF%BC%89%E3%83%91%E3%83%A9%E3%83%A1%E3%83%BC%E3%82%BF%E3%82%92/","title":"ミドルウェアをもっと知ろう（３）パラメータを渡す"},{"content":"前回に紹介したLaravelのグローバルミドルウェアですが、必要に応じてカスタムのミドルウェアを作成することも可能です。今回は、入力文に含まれる禁止用語を自動的に隠す（***で置換する）ミドルウェアを例として作成します。\n禁止用語を隠すミドルウェア まず、artisanで空のミドルウェアを作成して、\n$ php artisan make:middleware HideForbiddenWords そのファイルを以下のように編集します。\napp/Http/Middleware/HideForbiddenWords.php namespace App\\Http\\Middleware; use Closure; use Illuminate\\Support\\Str; use Illuminate\\Http\\Request; class HideForbiddenWords { public static $forbiddenWords = [\u0026#39;禁止１\u0026#39;, \u0026#39;禁止２\u0026#39;, \u0026#39;禁止３\u0026#39;]; public function handle(Request $request, Closure $next): mixed { // 禁止用語を***に変換 $all = $request-\u0026gt;collect()-\u0026gt;map(function ($value, $key) { return is_string($value) ? Str::of($value)-\u0026gt;replace(static::$forbiddenWords, \u0026#39;***\u0026#39;)-\u0026gt;toString() : $value; })-\u0026gt;all(); // もとのリクエストを置き換える $request-\u0026gt;merge($all);　return $next($request); } } $forbiddenWords配列で設定した３つの禁止用語が入力にあれば、***に変換します。\ntinkerで試してみましょう。\n\u0026gt; $request = app(\u0026#39;request\u0026#39;); \u0026gt; $request-\u0026gt;merge([\u0026#39;sentence\u0026#39; =\u0026gt; \u0026#39;私は「禁止１」と叫んでしまいました。\u0026#39;])-\u0026gt;all(); = [ \u0026#34;sentence\u0026#34; =\u0026gt; \u0026#34;私は「禁止１」と叫んでしまいました。\u0026#34;, ] \u0026gt; use App\\Http\\Middleware\\HideForbiddenWords; \u0026gt; (new HideForbiddenWords)-\u0026gt;handle($request, fn($request) =\u0026gt; $request)-\u0026gt;all(); = [ \u0026#34;sentence\u0026#34; =\u0026gt; \u0026#34;私は「***」と叫んでしまいました。\u0026#34;, ] 期待した結果になりました。\nグローバルミドルウェアに追加 作成したミドルウェアをグローバルのミドルウェアとして設定するには、ブートストラップに使用されるapp.phpを編集します。 以下は、Laravel 11.xで作成されたデフォルトのファイルで、ここのwithMiddlewareのクロージャでappendするだけです。\nbootstrap/app.php use Illuminate\\Foundation\\Application; use Illuminate\\Foundation\\Configuration\\Exceptions; use Illuminate\\Foundation\\Configuration\\Middleware; return Application::configure(basePath: dirname(__DIR__)) -\u0026gt;withRouting( web: __DIR__.\u0026#39;/../routes/web.php\u0026#39;, commands: __DIR__.\u0026#39;/../routes/console.php\u0026#39;, health: \u0026#39;/up\u0026#39;, ) -\u0026gt;withMiddleware(function (Middleware $middleware) { $middleware-\u0026gt;append( \\App\\Http\\Middleware\\HideForbiddenWords::class, ); }) -\u0026gt;withExceptions(function (Exceptions $exceptions) { // })-\u0026gt;create(); このミドルウェアは、前回に紹介したデフォルトのグローバルミドルウェア（以下）\n\u0026gt; use Illuminate\\Foundation\\Configuration\\Middleware; \u0026gt; (new Middleware)-\u0026gt;getGlobalMiddleware(); = [ \u0026#34;Illuminate\\Foundation\\Http\\Middleware\\InvokeDeferredCallbacks\u0026#34;, \u0026#34;Illuminate\\Http\\Middleware\\TrustProxies\u0026#34;, \u0026#34;Illuminate\\Http\\Middleware\\HandleCors\u0026#34;, \u0026#34;Illuminate\\Foundation\\Http\\Middleware\\PreventRequestsDuringMaintenance\u0026#34;, \u0026#34;Illuminate\\Http\\Middleware\\ValidatePostSize\u0026#34;, \u0026#34;Illuminate\\Foundation\\Http\\Middleware\\TrimStrings\u0026#34;, \u0026#34;Illuminate\\Foundation\\Http\\Middleware\\ConvertEmptyStringsToNull\u0026#34;, ] の最後のConvertEmptyStringsToNullの次に実行されることになります。\nプロジェクトのコントローラのすべてにおいてこのミドルウェアが適用されるのが大げさなら、以下のようにroutes/web.phpにおいて、特定のルートだけにミドルウェアを装着することも可能です。\nuse App\\Http\\Middleware\\HideForbiddenWords; Route::patch(\u0026#39;/profile\u0026#39;, [ProfileController::class, \u0026#39;update\u0026#39;]) -\u0026gt;name(\u0026#39;profile.update\u0026#39;) -\u0026gt;middleware(HideForbiddenWords::class); ","date":"2025-03-28T09:56:53+09:00","permalink":"https://www.larajapan.com/2025/03/28/%E3%83%9F%E3%83%89%E3%83%AB%E3%82%A6%E3%82%A7%E3%82%A2%E3%82%92%E3%82%82%E3%81%A3%E3%81%A8%E7%9F%A5%E3%82%8D%E3%81%86%EF%BC%88%EF%BC%92%EF%BC%89%E3%83%9F%E3%83%89%E3%83%AB%E3%82%A6%E3%82%A7%E3%82%A2/","title":"ミドルウェアをもっと知ろう（２）ミドルウェアを作成"},{"content":"LaravelのミドルウェアはHttpのGETやPOSTなどのリクエストでアプリに入ってくるリクエストの中身をチェックしたり必要なら変えたりする重要なコードです。このミドルウェアの設定は、L11.x以前ではapp/Http/Kernel.phpというファイルの中身に含まれていてわかりやすかったのですが、L11.x以降はLaravelのライブラリの奥に入ってしまい日の目をみなくなってしまいました。これらを引っ張りだしてLaravelのミドルウェアがどのように使用されているか紹介します。\nグローバルミドルウェア リクエストをグローバルに処理するミドルウェアは、グローバルミドルウェアと呼ばれ、tinkerでの以下の実行でデフォルトの設定を見ることができます。\n\u0026gt; use Illuminate\\Foundation\\Configuration\\Middleware; \u0026gt; (new Middleware)-\u0026gt;getGlobalMiddleware(); = [ \u0026#34;Illuminate\\Foundation\\Http\\Middleware\\InvokeDeferredCallbacks\u0026#34;, \u0026#34;Illuminate\\Http\\Middleware\\TrustProxies\u0026#34;, \u0026#34;Illuminate\\Http\\Middleware\\HandleCors\u0026#34;, \u0026#34;Illuminate\\Foundation\\Http\\Middleware\\PreventRequestsDuringMaintenance\u0026#34;, \u0026#34;Illuminate\\Http\\Middleware\\ValidatePostSize\u0026#34;, \u0026#34;Illuminate\\Foundation\\Http\\Middleware\\TrimStrings\u0026#34;, \u0026#34;Illuminate\\Foundation\\Http\\Middleware\\ConvertEmptyStringsToNull\u0026#34;, ] それぞれのミドルウェアのクラスは独立した役割がありますが、ここでの紹介は比較的理解しやすい最後の２つに絞ります。\nTrimStrings このミドルウェアはHttpのGETやPOSTなどのリクエストのデータに含まれる文字列の前後の空白文字を削除します。 ここでの空白文字はすべていわゆる半角文字の範囲で、\n空白 改行（\\n） 復帰（\\r） タブ（\\t） 垂直タブ（\\v） ヌル文字（\\0） が含まれます。日本語の全角空白文字が含まれないことに注意を。 （修正：2025-03-26）日本語の全角スペースも含まれます。 また、以下のユニコードも削除されます。\n表示不可能な文字（\\x{FEFF}） ゼロ幅スペース（\\x{200B}） 左から右へのマーク（\\x{200E}） 削除されるのは文字列中でなく前後であることに注意を。\n早速、tinkerで実行してみましょう。\nまずは、Requestのインスタンスを作成します。 app()を使うと簡単に作成できます。\n\u0026gt; $request = app(\u0026#39;request\u0026#39;); = Illuminate\\Http\\Request {#207 +attributes: Symfony\\Component\\HttpFoundation\\ParameterBag {#208}, +request: Symfony\\Component\\HttpFoundation\\InputBag {#197}, +query: Symfony\\Component\\HttpFoundation\\InputBag {#209}, +server: Symfony\\Component\\HttpFoundation\\ServerBag {#93}, +files: Symfony\\Component\\HttpFoundation\\FileBag {#196}, +cookies: Symfony\\Component\\HttpFoundation\\InputBag {#204}, +headers: Symfony\\Component\\HttpFoundation\\HeaderBag {#100}, } そして、そのリクエストにユーザーの入力をシミュレートした連想配列をマージします。 もちろん、nameの値の前後にしっかりと空白文字を入れてあります。\n\u0026gt; $request-\u0026gt;merge([\u0026#39;name\u0026#39; =\u0026gt; \u0026#39; 山田太郎 \u0026#39;])-\u0026gt;all(); = [ \u0026#34;name\u0026#34; =\u0026gt; \u0026#34; 山田太郎 \u0026#34;, ] 今度はこのリクエストにTrimStringsのミドルウェアを適用します。\n\u0026gt; use Illuminate\\Foundation\\Http\\Middleware\\TrimStrings; \u0026gt; (new TrimStrings)-\u0026gt;handle($request, fn($request) =\u0026gt; $request)-\u0026gt;all(); = [ \u0026#34;name\u0026#34; =\u0026gt; \u0026#34;山田太郎\u0026#34;, ] 前後の空白文字が削除されましたね。\nhandle()のパラメーターは２つ必要されます。最初はリクエストのインスタンス、次はクロージャとして次のミドルウェアを渡します。 それゆえに最初に掲載したグローバルミドルウェアが順々にリクエストに処理を加えていきます。 ここでは、単に、処理されたリクエストを返すクロージャを与えています。\nConverEmptyStringsToNull ConvertEmptyStringsToNullは、先のTrimStringsの次に適用されるミドルウェアで、今度は空の文字列をnullに変換する処理を行います。\nこのミドルウェアをデモするために、今度は空の値のemailの連想配列をリクエストにマージします。\n\u0026gt; $request-\u0026gt;merge([\u0026#39;email\u0026#39; =\u0026gt; \u0026#39;\u0026#39;])-\u0026gt;all(); = [ \u0026#34;name\u0026#34; =\u0026gt; \u0026#34;山田太郎\u0026#34;, \u0026#34;email\u0026#34; =\u0026gt; \u0026#34;\u0026#34;, ] 先ほどTrimpStringsのミドルウェアを適用された同じリクエストを使用しために、nameの値が残っていることに注意を。\nこのリクエストに対して、ConvertEmptyStringsToNullを適用します。\n\u0026gt; use Illuminate\\Foundation\\Http\\Middleware\\ConvertEmptyStringsToNull; \u0026gt; (new ConvertEmptyStringsToNull)-\u0026gt;handle($request, fn($request) =\u0026gt; $request)-\u0026gt;all(); = [ \u0026#34;name\u0026#34; =\u0026gt; \u0026#34;山田太郎\u0026#34;, \u0026#34;email\u0026#34; =\u0026gt; null, ] 空の文字列がnullに変換されましたね。\nグローバルミドルウェアをすべて適用 最後に、最初に掲載したグローバルミドルウェアをすべてリクエストに適用するコードを紹介します。\nLaravelでは以下の関数で、Pipelineのクラスを使用してリクエストをそれぞれのミドルウェアを順に適用していき最後にルートとでマップしているコントローラなどへ渡してます。\nvendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php /** * Send the given request through the middleware / router. * * @param \\Illuminate\\Http\\Request $request * @return \\Illuminate\\Http\\Response */ protected function sendRequestThroughRouter($request) { $this-\u0026gt;app-\u0026gt;instance(\u0026#39;request\u0026#39;, $request); Facade::clearResolvedInstance(\u0026#39;request\u0026#39;); $this-\u0026gt;bootstrap(); return (new Pipeline($this-\u0026gt;app)) -\u0026gt;send($request) -\u0026gt;through($this-\u0026gt;app-\u0026gt;shouldSkipMiddleware() ? [] : $this-\u0026gt;middleware) -\u0026gt;then($this-\u0026gt;dispatchToRouter()); } 上のコードを元に、tinkerで実行できるようにしてみました。\n\u0026gt; use Illuminate\\Foundation\\Configuration\\Middleware; \u0026gt; $middlewares = (new Middleware)-\u0026gt;getGlobalMiddleware(); \u0026gt; $request = app(\u0026#39;request\u0026#39;)-\u0026gt;merge([\u0026#39;name\u0026#39; =\u0026gt; \u0026#39; 山田太郎 \u0026#39;, \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;\u0026#39;]); \u0026gt; use Illuminate\\Pipeline\\Pipeline; \u0026gt; app(Pipeline::class)-\u0026gt;send($request)-\u0026gt;through($middlewares)-\u0026gt;thenReturn()-\u0026gt;all(); = [ \u0026#34;name\u0026#34; =\u0026gt; \u0026#34;山田太郎\u0026#34;, \u0026#34;email\u0026#34; =\u0026gt; null, ] ","date":"2025-03-16T03:15:23+09:00","permalink":"https://www.larajapan.com/2025/03/16/%E3%83%9F%E3%83%89%E3%83%AB%E3%82%A6%E3%82%A7%E3%82%A2%E3%82%92%E3%82%82%E3%81%A3%E3%81%A8%E7%9F%A5%E3%82%8D%E3%81%86%EF%BC%88%EF%BC%91%EF%BC%89%E3%82%B0%E3%83%AD%E3%83%BC%E3%83%90%E3%83%AB%E3%83%9F/","title":"ミドルウェアをもっと知ろう（１）グローバルミドルウェア"},{"content":"テストを書くとき、テストパターンや入力値の組み合わせが増え、データの管理が煩雑になりがちです。Pestではwith()やdataset()を使って基本的なデータ整理ができますが、後から読んでも理解しやすい・修正しやすいコードを目指して、Combining DatasetsやSharing Datasetsを活用し、テストコードを整理してみます。\nテスト対象 テスト対象は以下のクラスです。calculateShippingCost()は、引数として渡された温度帯と配送地域の値に応じて送料を計算する関数です。\nclass ShippingCalculator { private static array $tempCosts = [ \u0026#39;常温\u0026#39; =\u0026gt; 0, \u0026#39;冷蔵\u0026#39; =\u0026gt; 200, \u0026#39;冷凍\u0026#39; =\u0026gt; 500, ]; private static array $regionCosts = [ \u0026#39;東京\u0026#39; =\u0026gt; 500, \u0026#39;大阪\u0026#39; =\u0026gt; 750, \u0026#39;福岡\u0026#39; =\u0026gt; 1000, ]; /** * 温度帯と配送地域から送料を計算する */ public static function calculateShippingCost(string $temp, string $region): int|string { if (!array_key_exists($temp, self::$tempCosts) || !array_key_exists($region, self::$regionCosts)) { return \u0026#39;計算不可\u0026#39;; } $baseCost = self::$regionCosts[$region]; $tempCost = self::$tempCosts[$temp]; return $baseCost + $tempCost; } } 例えば、温度帯が「冷蔵」の場合は200円、配送地域が「大阪」の場合は750円を取得し、合計送料として950円を返します。\n\u0026gt; ShippingCalculator::calculateShippingCost(\u0026#39;冷蔵\u0026#39;, \u0026#39;大阪\u0026#39;); = 950 また、定義されていない値が渡された場合は「計算不可」が返ります。\n\u0026gt; ShippingCalculator::calculateShippingCost(\u0026#39;でたらめ\u0026#39;, \u0026#39;大阪\u0026#39;); = \u0026#34;計算不可\u0026#34; テストコード 以下はcalculateShippingCost()の計算結果が正しいことを検証するテストコードです。\nテストのパターンとして、温度帯と配送地域をいくつかピックアップしてテストする形でもいいかもしれませんが、今回は全ての組み合わせ（３つの温度帯×３つの配送地域 = ９パターン）をテストします。\nit(\u0026#39;全ての組み合わせパターンで送料を検証\u0026#39;, function (string $temp, int $tempCost, string $region, int $regionCost) { // 期待される合計送料 $expected = $tempCost + $regionCost; // 実際の計算結果 $actual = ShippingCalculator::calculateShippingCost($temp, $region); expect($actual)-\u0026gt;toBe($expected); }) -\u0026gt;with([ [\u0026#39;常温\u0026#39;, 0, \u0026#39;東京\u0026#39;, 500], // １回目 [\u0026#39;常温\u0026#39;, 0, \u0026#39;大阪\u0026#39;, 750], // ２回目 [\u0026#39;常温\u0026#39;, 0, \u0026#39;福岡\u0026#39;, 1000], // ３回目 [\u0026#39;冷蔵\u0026#39;, 200, \u0026#39;東京\u0026#39;, 500], // ４回目 [\u0026#39;冷蔵\u0026#39;, 200, \u0026#39;大阪\u0026#39;, 750], // ５回目 [\u0026#39;冷蔵\u0026#39;, 200, \u0026#39;福岡\u0026#39;, 1000], // ６回目 [\u0026#39;冷凍\u0026#39;, 500, \u0026#39;東京\u0026#39;, 500], // ７回目 [\u0026#39;冷凍\u0026#39;, 500, \u0026#39;大阪\u0026#39;, 750], // ８回目 [\u0026#39;冷凍\u0026#39;, 500, \u0026#39;福岡\u0026#39;, 1000], // ９回目 ]); Pestでは、with()を使用してデータパターンをテストに渡します。with()には複数のテストデータを配列で渡せるため、今回のように多次元配列を使うことで、各配列が1回分のテストデータとして処理されます。\nまずはwith()に９パターンを直接記述しましたが、パターンが多いとテストコードと繋がって少しごちゃごちゃして見えますね。そういう場合はdataset()を使用すると、以下のようにテストとパターンを分離させて書くことができます。\nit(\u0026#39;全ての組み合わせパターンで送料を検証\u0026#39;, function (string $temp, int $tempCost, string $region, int $regionCost) { ・・・テストコードは同じ }) -\u0026gt;with(\u0026#39;shippingCosts\u0026#39;); dataset(\u0026#39;shippingCosts\u0026#39;, [ [\u0026#39;常温\u0026#39;, 0, \u0026#39;東京\u0026#39;, 500], [\u0026#39;常温\u0026#39;, 0, \u0026#39;大阪\u0026#39;, 750], [\u0026#39;常温\u0026#39;, 0, \u0026#39;福岡\u0026#39;, 1000], [\u0026#39;冷蔵\u0026#39;, 200, \u0026#39;東京\u0026#39;, 500], [\u0026#39;冷蔵\u0026#39;, 200, \u0026#39;大阪\u0026#39;, 750], [\u0026#39;冷蔵\u0026#39;, 200, \u0026#39;福岡\u0026#39;, 1000], [\u0026#39;冷凍\u0026#39;, 500, \u0026#39;東京\u0026#39;, 500], [\u0026#39;冷凍\u0026#39;, 500, \u0026#39;大阪\u0026#39;, 750], [\u0026#39;冷凍\u0026#39;, 500, \u0026#39;福岡\u0026#39;, 1000], ]); dataset()は第一引数にデータセット名を、第二引数は先ほどwith()に渡していたデータの配列を指定します。\nそしてwith()の方へはデータセット名を指定するだけで良いのでテストが少しすっきりと、可読性が良くなりました。\nCombining Datasetsの活用 ここまでの書き方でも問題ありませんが、９パターンが並んでいて分かりにくい点や、パターンが増減した際の修正が大変な点が気になります。\nそこで、Combining Datasetsを活用します。全ての組み合わせをテストする時はCombining Datasetsを使うとコードをとても簡潔に書くことができます。\nまずは９パターンあったデータセットを、「温度帯」と「配送地域」のそれぞれのデータセットに分けて作成します。\ndataset(\u0026#39;tempWithCost\u0026#39;, [ [\u0026#39;常温\u0026#39;, 0], [\u0026#39;冷蔵\u0026#39;, 200], [\u0026#39;冷凍\u0026#39;, 500], ]); dataset(\u0026#39;regionWithCost\u0026#39;, [ [\u0026#39;東京\u0026#39;, 500], [\u0026#39;大阪\u0026#39;, 750], [\u0026#39;福岡\u0026#39;, 1000], ]); 「温度帯」の３パターンはtempWithCostに、「配送地域」の３パターンはregionWithCostと定義しました。\nそしてこれをテスト側でどう呼び出すかというと、以下のようにwith()を連結させるだけです。\nit(\u0026#39;全ての組み合わせパターンで送料を検証\u0026#39;, function (string $temp, int $tempCost, string $region, int $regionCost) { ... }) -\u0026gt;with(\u0026#39;tempWithCost\u0026#39;) -\u0026gt;with(\u0026#39;regionWithCost\u0026#39;); ちゃんと９パターン全て網羅されるのか、テストを実行して確認します。\nPASS Tests\\Unit\\ShippingCalculatorTest ✓ it 全ての組み合わせパターンで送料を検証 with (\u0026#39;常温\u0026#39;, 0) / (\u0026#39;東京\u0026#39;, 500) 0.01s ✓ it 全ての組み合わせパターンで送料を検証 with (\u0026#39;常温\u0026#39;, 0) / (\u0026#39;大阪\u0026#39;, 750) 0.01s ✓ it 全ての組み合わせパターンで送料を検証 with (\u0026#39;常温\u0026#39;, 0) / (\u0026#39;福岡\u0026#39;, 1000) 0.01s ✓ it 全ての組み合わせパターンで送料を検証 with (\u0026#39;冷蔵\u0026#39;, 200) / (\u0026#39;東京\u0026#39;, 500) 0.01s ✓ it 全ての組み合わせパターンで送料を検証 with (\u0026#39;冷蔵\u0026#39;, 200) / (\u0026#39;大阪\u0026#39;, 750) 0.01s ✓ it 全ての組み合わせパターンで送料を検証 with (\u0026#39;冷蔵\u0026#39;, 200) / (\u0026#39;福岡\u0026#39;, 1000) 0.01s ✓ it 全ての組み合わせパターンで送料を検証 with (\u0026#39;冷凍\u0026#39;, 500) / (\u0026#39;東京\u0026#39;, 500) 0.01s ✓ it 全ての組み合わせパターンで送料を検証 with (\u0026#39;冷凍\u0026#39;, 500) / (\u0026#39;大阪\u0026#39;, 750) 0.01s ✓ it 全ての組み合わせパターンで送料を検証 with (\u0026#39;冷凍\u0026#39;, 500) / (\u0026#39;福岡\u0026#39;, 1000) 0.01s 問題なさそうです。出力にはテスト名だけでなく、どのパターンの組み合わせかも表示してくれています。ちなみにテスト失敗時は以下のようになり、どのケースで失敗したかも分かりやすいです。\n───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── FAILED Tests\\Unit\\ShippingCalculatorTest \u0026gt; it 全ての組み合わせパターンで送料を検証 with (\u0026#39;冷凍\u0026#39;, 500) / (\u0026#39;九州\u0026#39;, 1000) Failed asserting that \u0026#39;計算不可\u0026#39; is identical to 1500. データセットの再利用 dataset()でデータセットとテストを分離しておくと、他のテストでも再利用することができます。\n例としてもう１つテストを作成します。今度は、定義されていない温度帯の場合は\u0026quot;計算不可\u0026quot;が返るパターンのテストです。\nit(\u0026#39;存在しない温度帯は計算不可を検証\u0026#39;, function (string $temp, string $region) { $actual = ShippingCalculator::calculateShippingCost($temp, $region); expect($actual)-\u0026gt;toBe(\u0026#39;計算不可\u0026#39;); }) -\u0026gt;with([\u0026#39;cool\u0026#39;, \u0026#39;　\u0026#39;, \u0026#39;存在しない温度帯\u0026#39;]) -\u0026gt;with(\u0026#39;regionWithCost\u0026#39;); こちらもCombining Datasetsを使用しており、１つめのwith()には失敗パターンの温度帯データを直接記述しています。\nそして２つ目のwith()には、すでに定義済みのregionWithCost（配送地域のデータセット）を渡しました。\nこのように、テストコード内でパターンを重複することなく複数のテストで共有できます。テストを実行してみましょう。\nPASS Tests\\Unit\\ShippingCalculatorTest ✓ it 存在しない温度帯は計算不可を検証 with (\u0026#39;cool\u0026#39;) / (\u0026#39;東京\u0026#39;, 500) 0.01s ✓ it 存在しない温度帯は計算不可を検証 with (\u0026#39;cool\u0026#39;) / (\u0026#39;大阪\u0026#39;, 750) 0.01s ✓ it 存在しない温度帯は計算不可を検証 with (\u0026#39;cool\u0026#39;) / (\u0026#39;福岡\u0026#39;, 1000) 0.01s ✓ it 存在しない温度帯は計算不可を検証 with (\u0026#39;　\u0026#39;) / (\u0026#39;東京\u0026#39;, 500) 0.01s ✓ it 存在しない温度帯は計算不可を検証 with (\u0026#39;　\u0026#39;) / (\u0026#39;大阪\u0026#39;, 750) 0.01s ✓ it 存在しない温度帯は計算不可を検証 with (\u0026#39;　\u0026#39;) / (\u0026#39;福岡\u0026#39;, 1000) 0.01s ✓ it 存在しない温度帯は計算不可を検証 with (\u0026#39;存在しない温度帯\u0026#39;) / (\u0026#39;東京\u0026#39;, 500) 0.01s ✓ it 存在しない温度帯は計算不可を検証 with (\u0026#39;存在しない温度帯\u0026#39;) / (\u0026#39;大阪\u0026#39;, 750) 0.01s ✓ it 存在しない温度帯は計算不可を検証 with (\u0026#39;存在しない温度帯\u0026#39;) / (\u0026#39;福岡\u0026#39;, 1000) 0.01s こちらも問題なく全ての組み合わせでテストできました！\ndatasetをファイルへ切り出す ここまでは、テストコードとデータセットが１つのファイルに共存している状態でした。Pestではデータセットをtests/Datasetsへ切り出すことで、他のテストファイルからも呼び出せるようになるSharing Datasetsという機能があります。\nデータセットをファイルに切り出す際、Laravel環境では前回ご紹介したプラグインををインストールしていると以下のコマンドで簡単にファイルを作成できます。もちろん手動で作成しても問題ありません。\n$ php artisan pest:dataset shippingCosts ファイル名はshippingCostsとしました。ここに、先ほどのデータセットを移行します。テストファイルに記述していたdataset()をそのままコピーすればOKです。\n/tests/Datasets/ShippingCosts.php dataset(\u0026#39;tempWithCost\u0026#39;, [ [\u0026#39;常温\u0026#39;, 0], [\u0026#39;冷蔵\u0026#39;, 200], [\u0026#39;冷凍\u0026#39;, 500], ]); dataset(\u0026#39;regionWithCost\u0026#39;, [ [\u0026#39;東京\u0026#39;, 500], [\u0026#39;大阪\u0026#39;, 750], [\u0026#39;福岡\u0026#39;, 1000], ]); これで、テストコードとデータセットを完全に分離できました！\n最初のテストコードよりはかなりすっきりしたのではないでしょうか。Pestでテストを作成する際は、ぜひ活用してみてください。\n","date":"2025-03-09T08:39:49+09:00","permalink":"https://www.larajapan.com/2025/03/09/pest%E3%81%AE%E3%80%8Cdataset%E3%80%8D%E6%B4%BB%E7%94%A8%E3%81%A7%E3%80%81%E8%A6%8B%E9%80%9A%E3%81%97%E3%81%8C%E8%89%AF%E3%81%84%E3%83%BB%E4%BF%AE%E6%AD%A3%E3%81%97%E3%82%84%E3%81%99%E3%81%84%E3%83%86/","title":"Pestの「dataset」活用で、見通しが良い・修正しやすいテストコードへ"},{"content":"皆さんは、DGFTのベリトランスの支払決済のPHPのSDKキットを使用したことありますか？ それを利用するには彼らのウェブサイトからファイルをダウンロードして、プロジェクトに置いてそれをパッケージとしてインストールします。今回はその仕組みの説明です。ローカルにパッケージを作成してそれをcomposer requireします。\n自分のパッケージを作成 まず、デモとしてあるヘルパー関数の定義を含むパッケージを作成します。このパッケージには１つのヘルパー関数しかなく、インストールしたら以下のように、全角の英数字文字列を半角に変換する関数、to_hankaku()を使うことができます。\nPsy Shell v0.12.7 (PHP 8.2.15 — cli) by Justin Hileman \u0026gt; to_hankaku(\u0026#39;１２３\u0026#39;); = \u0026#34;123\u0026#34; 便利そうでしょう。\nまず、Laravelのプロジェクトを新規作成して、プロジェクトのディレクトリ直下にpackagesのディレクトリを作成して、以下のような構造で必要なファイルを作成します。\n├── app ... packages └── larajapan └── japanese-utils ├── composer.json └── src └── helpers.php ... 上では、composer.jsonとhelpers.phpのみがファイルで他は皆ディレクトリです。larajapanが開発者のアイデンティティでその下のjapanese-utilsが今回のパッケージ名となります。\nまずそのパッケージのcomposer.jsonの中身を見てみましょう（混乱していけないのは、このcomposer.jsonはjapanese-utilsのパッケージのcomposer.jsonであり、これからそれをインストールしようというLaravelのプロジェクト、つまりルートパッケージのcomposer.jsonではありません）。\npackages/larajapan/japanese-utils/composer.json { \u0026#34;name\u0026#34;: \u0026#34;larajapan/japanese-utils\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;helpers to manipuliate a japanese string\u0026#34;, \u0026#34;version\u0026#34;: \u0026#34;1.0.0\u0026#34;, \u0026#34;require\u0026#34;: { \u0026#34;php\u0026#34;: \u0026#34;\u0026gt;=7.4\u0026#34;, \u0026#34;ext-mbstring\u0026#34;: \u0026#34;*\u0026#34; }, \u0026#34;autoload\u0026#34;: { \u0026#34;files\u0026#34;: [ \u0026#34;src/helpers.php\u0026#34; ] } } ここでは、パッケージ名（name）、説明（description）、バージョン（version）、必須条件（require）、ロードの指示（autoload）が含まれます。to_hanakaku()の関数では、mbstringのPHPのモジュールのmb_convert()を使用し、さらにタイプヒントも使用するので、それを対応しているPHPバージョン7.4以上であることをインストールの前提とします.\n今度は、その関数を定義するhelpers.phpを見てみましょう。\npackages/larajapan/japanese-utils/src/helpers.php if (!function_exists(\u0026#39;to_hankaku\u0026#39;)) { function to_hankaku(string $zenkaku): string { return mb_convert_kana($zenkaku, \u0026#39;as\u0026#39;); // 全角英数字と空白を半角に変換 } } とても簡単な定義ですが、他のパッケージですでに定義されていたら困るので、!function_exists(\u0026rsquo;to_hankaku\u0026rsquo;)の条件を入れています。\nLaravelのプロジェクトにインストール 同じプロジェクトのディレクトリ内にパッケージを作成したものの、これだけではプロジェクトで先ほどのto_hanakaku()は使えません。プロジェクトにインストールすることが必要です。\n今度は作成したパッケージのではなくプロジェクト（ルートパッケージ）のcomposer.jsonの編集が必要です。以下のrepositoriesのブロックが追加した部分です。\ncomposer.json { \u0026#34;$schema\u0026#34;: \u0026#34;https://getcomposer.org/schema.json\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;laravel/laravel\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;project\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;The skeleton application for the Laravel framework.\u0026#34;, \u0026#34;keywords\u0026#34;: [\u0026#34;laravel\u0026#34;, \u0026#34;framework\u0026#34;], \u0026#34;license\u0026#34;: \u0026#34;MIT\u0026#34;, \u0026#34;repositories\u0026#34;: [ { \u0026#34;type\u0026#34;: \u0026#34;path\u0026#34;, \u0026#34;url\u0026#34;: \u0026#34;./packages/larajapan/japanese-utils\u0026#34;, \u0026#34;options\u0026#34;: { \u0026#34;symlink\u0026#34;: true } } ], \u0026#34;require\u0026#34;: { \u0026#34;php\u0026#34;: \u0026#34;^8.2\u0026#34;, \u0026#34;laravel/framework\u0026#34;: \u0026#34;^11.31\u0026#34;, \u0026#34;laravel/tinker\u0026#34;: \u0026#34;^2.9\u0026#34; }, ... } \u0026ldquo;type\u0026rdquo;: \u0026ldquo;path\u0026rdquo;がローカルのパッケージがレポジトリということを指示し、その場所 \u0026ldquo;url\u0026rdquo;にパッケージの相対の場所を指定します。そして、\u0026ldquo;options\u0026rdquo;として、\u0026ldquo;symlink\u0026rdquo;: trueは、プロジェクトにおいて使用されるパッケージを収めるvendorのディレクトリにおいて、作成したパッケージへのsymlinkを張るという指示です。ちなみにfalseならファイルのコピーとなります。\nこれでインストールの準備は完了です。通常のパッケージのインストールのようにcomposre requireを実行しましょう。\n$ composer require larajapan/japanese-utils インストール後には、composer.jsonは更新され、\u0026ldquo;larajapan/japanese-utils\u0026rdquo;: \u0026ldquo;^1.0\u0026rdquo;,のエントリーが追加されているのがわかります。\ncomposer.json { ... \u0026#34;require\u0026#34;: { \u0026#34;php\u0026#34;: \u0026#34;^8.2\u0026#34;, \u0026#34;larajapan/japanese-utils\u0026#34;: \u0026#34;^1.0\u0026#34;, \u0026#34;laravel/framework\u0026#34;: \u0026#34;^11.31\u0026#34;, \u0026#34;laravel/tinker\u0026#34;: \u0026#34;^2.9\u0026#34; }, ... } vendorのディレクトリを見ると、以下のようにパッケージへのsymlinkとなっています。\n$ ls -l vendor/larajapan total 0 lrwxrwxrwx 1 kenji kenji 40 Feb 19 16:42 japanese-utils -\u0026gt; ../../packages/larajapan/japanese-utils/ これで、このLaravelのプロジェクト内で、to_hankaku()のヘルパーをどこでも使えるようになります。\n最後に 自分が作成したパッケージは、Packagistに登録したものでもなく、githubのレポジトリに置いたプライベートのパッケージでもありません。そのようなパッケージが必要となるのは不思議と思われるかもしれません。しかし、以下のようにいくつか必要となるケースがあります。\n将来はPackagistにおいて自分で開発したパッケージを共有したいが、完成度が低いので暫くは自分のプロジェクトだけ使う 今まで使用していたgithubにあるパッケージはもうメンテインされていなくダウンロードして自分のプロジェクトだけで使えるようにする プロジェクトが肥大化したのでパッケージとしてモジュール化して使う あえて自社のサービスのSDKキットをPackagistでパブリックで共有することをせずに、サービスを利用する開発者にローカルにダウンロードしてからcomposerでインストールしてもらう。日本で有名なDGFTのベリトランスの決済のPHPのSDKキットはまさにそれです。 ","date":"2025-02-21T10:58:09+09:00","permalink":"https://www.larajapan.com/2025/02/21/composer%E3%82%92%E3%82%82%E3%81%A3%E3%81%A8%E7%9F%A5%E3%82%8D%E3%81%86%EF%BC%88%EF%BC%94%EF%BC%89%E3%83%AD%E3%83%BC%E3%82%AB%E3%83%AB%E3%81%AE%E3%83%91%E3%83%83%E3%82%B1%E3%83%BC%E3%82%B8%E3%82%92lar/","title":"composerをもっと知ろう（４）ローカルのパッケージをLaravelで使う"},{"content":"PestはPHPUnitの上に構築された、テストをよりシンプルに書くためのフレームワークです。そのPestが公式に推奨しているLaravelのためのプラグインpest-plugin-laravelを使って既存のテストを書き直してみました。Pestが掲げる「読みやすく理解しやすい」テストコードに、少し近づけるでしょうか。\nPest導入、基本の記述方法については以下をご参照ください。 PHPUnitテストをpest-plugin-driftでPestへ変換 Pestでモック・例外のテスト\npest-plugin-laravelをインストール・テストファイル作成 Pestはすでにインストールしてあると仮定して、以下のコマンドでプラグインをインストールします。\n$ composer require pestphp/pest-plugin-laravel --dev このプラグインをインストールしたことで、まず、新しいartisanコマンドpest:testが使えるようになっています。 ファイル名をPestTestと指定して以下のコマンドを実行します。\n$ php artisan pest:test PestTest tests/Feature/PestTest.phpにテストファイルが作成されます。\ntests/Feature/PestTest.php it(\u0026#39;has pest page\u0026#39;, function () { $response = $this-\u0026gt;get(\u0026#39;/pest\u0026#39;); $response-\u0026gt;assertStatus(200); }); デフォルトでは、tests/Featureのディレクトリにファイルが作成されますが、\u0026ndash;unitオプションをつけるとtest/Unitにファイルが作成されます。\nもちろん、Laravel標準のmake:testコマンドで作成しても問題ありません。\n$ php artisan make:test PestTest pest-plugin-laravelを使った記述方法 プラグインインストールでテストの記述も少し簡単になります。Laravelのテストでよく使うテストヘルパーを$this-\u0026gt;を使わず呼び出せるようになります。例えば以下のようなユーザーログアウトのテストの場合\ntest(\u0026#39;users can logout\u0026#39;, function () { $this-\u0026gt;actingAs($this-\u0026gt;user) -\u0026gt;post(\u0026#39;/logout\u0026#39;) -\u0026gt;assertRedirect(\u0026#39;/\u0026#39;); $this-\u0026gt;assertGuest(); }); プラグインの機能を使って書き直すと、こんな風になります。\nuse function Pest\\Laravel\\{actingAs, assertGuest}; // インポートする test(\u0026#39;users can logout\u0026#39;, function () { actingAs($this-\u0026gt;user) -\u0026gt;post(\u0026#39;/logout\u0026#39;) -\u0026gt;assertRedirect(\u0026#39;/\u0026#39;); assertGuest(); }); actingAsやassertGuestを直接呼び出すことができるようになるため、$this-\u0026gt;の記述がなくなってコードが少しすっきりしました。名前空間関数のインポートが必要ですので、useをお忘れなく。\n他にも、よく使われるHTTPリクエストの場合\nプラグイン導入前\n... $response = $this-\u0026gt;get(\u0026#39;/login\u0026#39;); ... $response = $this-\u0026gt;post(\u0026#39;/login\u0026#39;, [ \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;test@example.com\u0026#39;, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;password\u0026#39;, ]); ... 導入後\nuse function Pest\\Laravel\\{get, post}; ... $response = get(\u0026#39;/login\u0026#39;); ... $response = post(\u0026#39;/login\u0026#39;, [ \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;test@example.com\u0026#39;, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;password\u0026#39;, ]); ... データベースのアサーションは\nプラグイン導入前\n... $this-\u0026gt;assertDatabaseHas(User::class, [\u0026#39;email\u0026#39; =\u0026gt; \u0026#39;test@example.com\u0026#39;]); ... 導入後\nuse function Pest\\Laravel\\assertDatabaseHas; ... assertDatabaseHas(User::class, [\u0026#39;email\u0026#39; =\u0026gt; \u0026#39;test@example.com\u0026#39;]); ... 他にも色々なコマンドが対応していますので、詳しくはgithubをご覧ください。\nプラグインの機能を使って、以前にご紹介したパスワードリセットのテストをPest形式に書き換えたものが以下になります。（書き換え前のPHPUnitのテストコードはリンクからご確認ください）\nuse function Pest\\Laravel\\{post, assertGuest, assertAuthenticated}; test(\u0026#39;password can be reset with valid token\u0026#39;, function () { Notification::fake(); $user = User::factory()-\u0026gt;create(); post(\u0026#39;/forgot-password\u0026#39;, [\u0026#39;email\u0026#39; =\u0026gt; $user-\u0026gt;email]); Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) { $response = post(\u0026#39;/reset-password\u0026#39;, [ \u0026#39;token\u0026#39; =\u0026gt; $notification-\u0026gt;token, \u0026#39;email\u0026#39; =\u0026gt; $user-\u0026gt;email, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;new_password\u0026#39;, \u0026#39;password_confirmation\u0026#39; =\u0026gt; \u0026#39;new_password\u0026#39;, ]); $response-\u0026gt;assertRedirect(\u0026#39;login\u0026#39;) -\u0026gt;assertSessionHas(\u0026#39;status\u0026#39;, \u0026#39;パスワードをリセットしました。\u0026#39;); return true; }); assertGuest(); post(\u0026#39;/login\u0026#39;, [ \u0026#39;email\u0026#39; =\u0026gt; $user-\u0026gt;email, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;new_password\u0026#39;, ]); assertAuthenticated(); }); post・assertGuest・assertAuthenticatedの箇所が、$this-\u0026gt;を省略した形に置き換えられています。「すごく変わった！」という訳ではないものの、やはり元のテストよりは細かい部分が見やすくなっています。テストコードが増えてくるとかなり違いがあるのではないでしょうか。\nデータセット もう１つ、このプラグインが提供しているコマンドpest:datasetをご紹介します。\n$ php artisan pest:dataset データセット名 このコマンドはtests/Datasets/にデータセット用のファイルを作成してくれます。データセットの機能自体は元からPestにあるものですが、artisanコマンドを使ってより便利に作成できるようになりました。\n例として、以下のテストからデータセットを別ファイルに切り出してみます。ログインに失敗するテストと、そのデータセットをwith()で渡しています。\ntest(\u0026#39;users cannot authenticate with invalid credentials\u0026#39;, function (string $email, string $password) { post(\u0026#39;/login\u0026#39;, [ \u0026#39;email\u0026#39; =\u0026gt; $email, \u0026#39;password\u0026#39; =\u0026gt; $password, ])-\u0026gt;assertStatus(302); assertGuest(); })-\u0026gt;with([ \u0026#39;間違ったパスワード\u0026#39; =\u0026gt; [\u0026#39;test@example.com\u0026#39;, \u0026#39;wrong-password\u0026#39;], \u0026#39;間違ったメールアドレス\u0026#39; =\u0026gt; [\u0026#39;wrong@example.com\u0026#39;, \u0026#39;password\u0026#39;], ]); まずはデータセットのファイルを作成します。ファイル名はLoginDataとて、以下のコマンドを実行します。\n$ php artisan pest:dataset LoginData これでtests/Datasetsにファイルが作成されましたので、テストからデータセットの部分をこちらに移行します。\ntests/Datasets/LoginData.php dataset(\u0026#39;invalidLoginData\u0026#39;, [ \u0026#39;間違ったパスワード\u0026#39; =\u0026gt; [\u0026#39;test@example.com\u0026#39;, \u0026#39;wrong-password\u0026#39;], \u0026#39;間違ったメールアドレス\u0026#39; =\u0026gt; [\u0026#39;wrong@example.com\u0026#39;, \u0026#39;password\u0026#39;], ]); データセット名はinvalidLoginDataとしました。\n元のテストコードの方は、with()の引数にデータセット名であるinvalidLoginDataを渡してあげればOKです。\ntest(\u0026#39;users cannot authenticate with invalid credentials\u0026#39;, function (string $email, string $password) { ・・・ })-\u0026gt;with(\u0026#39;invalidLoginData\u0026#39;); まとめ このプラグインを使っても劇的にテストコードが短くなる訳ではなく、「少し良くなる」というものなので、すでにテストの書き方が確立されている環境にわざわざインストールする必要はなさそうです。が、Laravelの開発コアメンバーが開発しているプラグインなので、新しいプロジェクトでPestのテストを書いてゆくという場合には導入してみるのも面白そうだなと感じました。\n","date":"2025-02-13T08:36:45+09:00","permalink":"https://www.larajapan.com/2025/02/13/%E3%83%86%E3%82%B9%E3%83%88%E3%81%8C%E3%81%A1%E3%82%87%E3%81%A3%E3%81%A8%E3%82%B7%E3%83%B3%E3%83%97%E3%83%AB%E3%81%AB%E3%81%AA%E3%82%8B-pest-plugin-laravel-%E3%83%97%E3%83%A9%E3%82%B0%E3%82%A4/","title":"テストがちょっとシンプルになる pest-plugin-laravel プラグイン"},{"content":"javascriptを書かずに動的でリアクティブなアプリが作れるlivewireですが、既存のjavascriptやjQueryで行っていた事をlivewireでも実現しようとすると「はて、どうすれば良いだろうか？」と手が止まることがあります。直近でドラッグ＆ドロップで並び替え可能なテーブルをlivewireで実装する機会があり調べたので忘れない内にまとめたいと思います。\nドラッグ＆ドロップで並び替え可能なテーブル 例えば、以下の様なテーブルです。 従来はjquery-uiのsortable()を使用して対応していました。こちらをlivewireで実装するにはどうすれば良いでしょうか？\nlivewire-sortable livewire-sortableというパッケージがあったので試してみます。まずは検証の為のページをlivewireで用意しましょう。livewireはversion 3.xを使用していますのでご注意を。\nweb.php まず、ルーティングに/demoへアクセスした際にdemoブレードを返却するように設定します。 routes/web.php use Illuminate\\Support\\Facades\\Route; Route::get(\u0026#39;/demo\u0026#39;, function () { return view(\u0026#39;demo\u0026#39;); }); demo.blade.php blade側は以下の様に作成しました。デザインの為にbootstrap5をcdnで読み込んでいます。livewireを使用する為、headの閉じタグ直前に@livewireStyles、bodyの閉じタグの直前に@livewireScriptsをセットし、sortable-tableコンポーネントを作成して読み込む事にしました。\nresources/views/demo.blade.php \u0026lt;!doctype html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;utf-8\u0026#34;\u0026gt; \u0026lt;title\u0026gt;Demo\u0026lt;/title\u0026gt; \u0026lt;link href=\u0026#34;https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css\u0026#34; rel=\u0026#34;stylesheet\u0026#34;\u0026gt; @livewireStyles \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;div class=\u0026#34;container my-5\u0026#34;\u0026gt; \u0026lt;h1\u0026gt;Sortable Table!\u0026lt;/h1\u0026gt; \u0026lt;livewire:sortable-table /\u0026gt; \u0026lt;/div\u0026gt; @livewireScripts \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; livewire componentの作成 demo.blade.phpで読み込んでいるComponentを作成しましょう。以下のartisanコマンドを実行して下さい。\n% php artisan make:livewire sortable-table COMPONENT CREATED 🤙 CLASS: app/Livewire/SortableTable.php VIEW: resources/views/livewire/sortable-table.blade.php SortableTable.phpとsortable-table.blade.phpが作成されたはずです。作成された SortableTable.phpを以下のように編集します。プロパティに$usersを追加してmount()で初期化しています。$usersはfirst_name, last_name, display_orderプロパティを持つmodelコレクションです。\napp/Livewire/SortableTable.php namespace App\\Livewire; use App\\Models\\User; use Livewire\\Component; class SortableTable extends Component { public $users; public function mount() { $this-\u0026gt;users = User::all(); } public function render() { return view(\u0026#39;livewire.sortable-table\u0026#39;); } } そして以下がviewファイル。livewireが@foreachのループ内で各要素を識別できるようにwire:keyを指定しています。表示する$userはdisplay_order順にソートしています。\nresources/views/livewire/sortable-table.blade.php \u0026lt;div class=\u0026#34;col-lg-8 px-0\u0026#34;\u0026gt; \u0026lt;table class=\u0026#34;table table-hover table-bordered\u0026#34;\u0026gt; \u0026lt;thead class=\u0026#34;table-dark\u0026#34;\u0026gt; \u0026lt;tr\u0026gt; \u0026lt;th\u0026gt;#\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;First\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;Last\u0026lt;/th\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;/thead\u0026gt; \u0026lt;tbody\u0026gt; @foreach ($users-\u0026gt;sortBy(\u0026#39;display_order\u0026#39;) as $user) \u0026lt;tr wire:key=\u0026#34;user-{{ $user-\u0026gt;id }}\u0026#34;\u0026gt; \u0026lt;th\u0026gt;{{ $user-\u0026gt;display_order }}\u0026lt;/th\u0026gt; \u0026lt;td\u0026gt;{{ $user-\u0026gt;first_name }}\u0026lt;/td\u0026gt; \u0026lt;td\u0026gt;{{ $user-\u0026gt;last_name }}\u0026lt;/td\u0026gt; \u0026lt;/tr\u0026gt; @endforeach \u0026lt;/tbody\u0026gt; \u0026lt;/table\u0026gt; \u0026lt;/div\u0026gt; php artisan serverでビルドインサーバを起動して/demoへアクセスしてみましょう。冒頭で表示したシンプルなテーブルが表示されたかと思います。準備が整ったのでlivewire-sortableを使って並び替え出来るように変更してみましょう。\nlivewire-sortableを追加 今回は簡易的な導入なのでdemo.blade.phpにてCDNで追加します。@livewireScriptsの直後にscriptタグを追加してlivewire-sortableのCDNを読み込みます。livewire-sortableのプラグインより先にlivewireのスクリプトが読み込まれる必要がある為、必ず@livewireScriptsより後にセットして下さい。\nresources/views/demo.blade.php \u0026lt;!doctype html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;utf-8\u0026#34;\u0026gt; \u0026lt;title\u0026gt;Demo\u0026lt;/title\u0026gt; \u0026lt;link href=\u0026#34;https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css\u0026#34; rel=\u0026#34;stylesheet\u0026#34;\u0026gt; @livewireStyles \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;div class=\u0026#34;container my-5\u0026#34;\u0026gt; \u0026lt;h1\u0026gt;Sortable Table!\u0026lt;/h1\u0026gt; \u0026lt;livewire:sortable-table /\u0026gt; \u0026lt;/div\u0026gt; @livewireScripts {{-- ここに追加↓ --}} \u0026lt;script src=\u0026#34;https://cdn.jsdelivr.net/gh/livewire/sortable@v1.x.x/dist/livewire-sortable.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 次にsortable-table.blade.phpを編集して並び替え出来るようにします。Githubには特に詳しい解説はありませんが、wire:sortableを並び替えする要素の親要素に追加、指定する値は並び替えを行った際に実行されるメソッド名です。wire:sortable.itemは並び替える対象の要素に付与します。最後にwire:sortable.handleを並び替え操作（ドラッグ＆ドロップ）を行う要素に追加します。\nresources/views/livewire/sortable-table.blade.php \u0026lt;div class=\u0026#34;col-lg-8 px-0\u0026#34;\u0026gt; \u0026lt;table class=\u0026#34;table table-hover table-bordered\u0026#34;\u0026gt; \u0026lt;thead class=\u0026#34;table-dark\u0026#34;\u0026gt; \u0026lt;tr\u0026gt; \u0026lt;th\u0026gt;#\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;First\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;Last\u0026lt;/th\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;/thead\u0026gt; \u0026lt;tbody wire:sortable=\u0026#34;updateOrder\u0026#34;\u0026gt; @foreach ($users-\u0026gt;sortBy(\u0026#39;display_order\u0026#39;) as $user) \u0026lt;tr wire:sortable.item=\u0026#34;{{ $user-\u0026gt;id }}\u0026#34; wire:key=\u0026#34;{{ $user-\u0026gt;id }}\u0026#34;\u0026gt; \u0026lt;th wire:sortable.handle\u0026gt;{{ $user-\u0026gt;display_order }}\u0026lt;/th\u0026gt; \u0026lt;td\u0026gt;{{ $user-\u0026gt;first_name }}\u0026lt;/td\u0026gt; \u0026lt;td\u0026gt;{{ $user-\u0026gt;last_name }}\u0026lt;/td\u0026gt; \u0026lt;/tr\u0026gt; @endforeach \u0026lt;/tbody\u0026gt; \u0026lt;/table\u0026gt; \u0026lt;/div\u0026gt; ここまで実装できたら試しに画面を更新して並び替え操作をしてみましょう。#の列をドラッグすると列が並び替えできると思います。しかし、ドロップすると以下の様なエラーが発生します。これはwire:sortableに指定したメソッド、updateOrderが未実装な為です。\nではSortableTable.phpにupdateOrder()を実装してみましょう。以下の例では並び替えた後の順番通りにdisplay_orderの値を更新しています。\napp/Livewire/SortableTable.php namespace App\\Livewire; use App\\Models\\User; use Livewire\\Component; class SortableTable extends Component { public $users; public function mount() { $this-\u0026gt;users = User::all(); } public function render() { return view(\u0026#39;livewire.sortable-table\u0026#39;); } public function updateOrder($items) { $items = collect($items)-\u0026gt;pluck(\u0026#39;order\u0026#39;, \u0026#39;value\u0026#39;); foreach ($this-\u0026gt;users as $user) { $newDisplayOrder = (int) $items[$user-\u0026gt;id]; // 順番が変わっているならデータ更新 if ($user-\u0026gt;display_order != $newDisplayOrder) { $user-\u0026gt;update([\u0026#39;display_order\u0026#39; =\u0026gt; $newDisplayOrder]); } } } } updateOrder()の引数に$itemsという連想配列が渡されています。$itemsは連想配列で各要素はorderとvalueという値を持ちます。\norderは並び替え後の順番、valueはwire:sortable.itemにて指定した値、つまり$userのidです。実装したupdateOrder()内では最初に$itemsを扱い易いようにidをキー、新しい順番をvalueとするcollectionに変換しています。\n$items = collect($items)-\u0026gt;pluck(\u0026#39;order\u0026#39;, \u0026#39;value\u0026#39;); そして、$usersプロパティをループで回して各userのdisplay_orderが変更されていないかチェック、変更されているならupdate()でDBの値を更新しています（注意：UserのModelクラスにおいて$fillableにdisplay_orderを含むことを忘れずに！）。ページを更新して並び替えを行ってみて下さい。並び替えると同時に#の番号が振り直され、DBに保存されているuserのdisplay_orderが更新されているかと思います。\nまとめ 如何でしたでしょうか？ドラッグ＆ドロップによる並び替え操作は割とよくあるパターンなのでパッケージが開発されており深く悩む事無く実装できました。javascriptやjQueryなどで対応してきた動的なサイトというのはバリエーションに富んでおり、都合よくlivewireに置き換えられないケースがあるかと思います。そんな時は使えるパッケージが無いか探してみたり、Livewire公式のライブラリであるfluxなどで使えるコンポーネントが無いか探してみると良いかもしれません。","date":"2025-01-27T04:46:16+09:00","permalink":"https://www.larajapan.com/2025/01/27/livewire%E3%81%A7%E4%B8%A6%E3%81%B3%E6%9B%BF%E3%81%88%E5%8F%AF%E8%83%BD%E3%81%AA%E3%83%86%E3%83%BC%E3%83%96%E3%83%AB%E3%82%92%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B/","title":"Livewireで並び替え可能なテーブルを作成する"},{"content":"今回は、composer.jsonでrequireやrequire-devにリストされるパッケージのバージョンに関して説明します。今まで気にもとめていなかった ^11.9などの意味を解明します。\ncomposer.json まず、前回の「composerをもっと知ろう（２）composer create-projectでは何が起こっている？」で記載したcomposer.jsonを再度見てみましょう。\n{ \u0026#34;name\u0026#34;: \u0026#34;laravel/laravel\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;project\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;The skeleton application for the Laravel framework.\u0026#34;, \u0026#34;keywords\u0026#34;: [\u0026#34;laravel\u0026#34;, \u0026#34;framework\u0026#34;], \u0026#34;license\u0026#34;: \u0026#34;MIT\u0026#34;, \u0026#34;require\u0026#34;: { \u0026#34;php\u0026#34;: \u0026#34;^8.2\u0026#34;, \u0026#34;laravel/framework\u0026#34;: \u0026#34;^11.9\u0026#34;, \u0026#34;laravel/tinker\u0026#34;: \u0026#34;^2.9\u0026#34; }, \u0026#34;require-dev\u0026#34;: { \u0026#34;fakerphp/faker\u0026#34;: \u0026#34;^1.23\u0026#34;, \u0026#34;laravel/pail\u0026#34;: \u0026#34;^1.1\u0026#34;, \u0026#34;laravel/pint\u0026#34;: \u0026#34;^1.13\u0026#34;, \u0026#34;laravel/sail\u0026#34;: \u0026#34;^1.26\u0026#34;, \u0026#34;mockery/mockery\u0026#34;: \u0026#34;^1.6\u0026#34;, \u0026#34;nunomaduro/collision\u0026#34;: \u0026#34;^8.1\u0026#34;, \u0026#34;phpunit/phpunit\u0026#34;: \u0026#34;^11.0.1\u0026#34; }, ... } \u0026ldquo;laravel/framework\u0026rdquo;: \u0026ldquo;^11.9\u0026rdquo;のような記述があります。laravel/frameworkはパッケージ名で、^11.9はインストールするバージョンを指示していると思われますが、その記事を書いた時点では実際にインストールされたバージョンは11.34.2でした。どうして11.9ではないのでしょう。11.9の前についているキャレット＾はいったい何でしょう。\n^11.9の意味 \u0026ldquo;laravel/framework\u0026rdquo;: \u0026ldquo;^11.9\u0026rdquo;の意味を解明するには、laravel/frameworkのパッケージをローカル環境にインストールして、gitのタグを見る必要があります。\nパッケージをインストールした後に、以下のコマンドを実行して、v11のみのタグを抽出します。\n$ git tag --sort=v:refname | grep v11 コマンドの出力は、以下のようになります。長いので途中\u0026hellip;で割愛と示しています。これを書いている時点では最新はv11.37.0です。\nv11.0.0 v11.0.1 v11.0.2 v11.0.3 v11.0.4 v11.0.5 v11.0.6 v11.0.7 v11.0.8 v11.1.0 v11.1.1 v11.2.0 v11.3.0 v11.3.1 v11.4.0 v11.5.0 v11.6.0 v11.7.0 v11.8.0 v11.9.0 v11.9.1 v11.9.2 ... v11.36.1 v11.37.0 上において、タグの最初の数字がメージャーのバージョン番号で、それ以下はマイナーのバージョン番号です。 例えば、v11.37.0では、11がメジャーで37.0がマイナー。\nLaravelの新規のプロジェクトを作成するときは、composer installが実行されます（この時点では、composer.lockのファイルはまだ存在しないと仮定）。プロジェクトにとってcomposer.jsonで指定されるパッケージはその時点で最新のパッケージのインストールを試みます。しかし必ずしもパッケージの最新とも限りません。例えば、Laravel 10.xをインストールするのに最新の11.xがインストールされては困ります。それゆえに、^11.9のような指定が使われます。\ncomposer.jsonの\u0026ldquo;laravel/framework\u0026rdquo;: \u0026ldquo;^11.9\u0026rdquo;でのキャレット＾は、11.9以上で12.0.0未満のバージョンでその時点で最新のバージョンをインストールしなさい、という指示です。それゆえに、前回の記事を書いた時点では最新のv11.34.2が、この記事を書いている現時点ではv11.37.0がインストールされます。\nこの理解にとてもいいツールがありますので紹介します。\nhttps://semver.madewithlove.com/?package=laravel%2Fframework\u0026constraint=%5E11.9\n上のリンクをクリックすると以下のような画面となります。\n先ほどのlaravel/frameworkのパッケージで^11.9を指定したときにインストールされるバージョンのリストが赤箱でハイライト表示されます。そこで、^11.0とか^11.37とか入力してみてどれがハイライトされるか試してみてください。他のパッケージも指定してみてください。\nさて、ここで不思議に思うはどうしてインストールすべきバージョンが最新の１つ（ここでは、v11.37.0）だけでなくいくつもハイライトされているのかです。v11.9.0からv11.37.0までのすべてのバージョンがハイライトされていますね。\nこれは共にインストールする他のパッケージとの依存があるかもしれないからです。もしかして、他のパッケージでは、laravel/frameworkへの依存があり、そのパッケージのcomposer.jsonにおいて、 \u0026ldquo;laravel/framework\u0026rdquo;: \u0026ldquo;11.18.1\u0026rdquo;と１つのバージョン（バージョンの前にキャレットがない）のみを指定しているかもしれません。その場合はプロジェクトでインストールされるlaravel/frameworkのバージョンは11.18.1のみと制限されます。プロジェクトがインストールしたいバージョンと依存するパッケージがインストールしたいバージョンをともに満たすためにです。\ncomposer.jsonで記載されるバージョンの形式には、他にも~11.9とか11.9.*とか^6.0|^7.0|^8.0|^9.0|^10.0|^11.0とかあります。以下で詳しい説明があります。 https://getcomposer.org/doc/articles/versions.md\ncomposer.lock composer installを最初に実行した後には、composer.lockのファイルが作成されます。これは、その名の通り、実行時にインストールされたすべてのパッケージのバージョンの情報が含まれています。次回に、違う環境（例えばライブ環境とか）でこのファイルがあれば、まったく同じバージョンでパッケージをインストールしてくれるので、異なる環境での同じバージョンのパッケージがインストールされることを保証してくれます。\n","date":"2025-01-06T05:58:08+09:00","permalink":"https://www.larajapan.com/2025/01/06/composer%E3%82%92%E3%82%82%E3%81%A3%E3%81%A8%E7%9F%A5%E3%82%8D%E3%81%86%EF%BC%88%EF%BC%93%EF%BC%89%E3%83%90%E3%83%BC%E3%82%B8%E3%83%A7%E3%83%B3%E3%81%AB%E9%96%A2%E3%81%97%E3%81%A6/","title":"composerをもっと知ろう（３）バージョンに関して"},{"content":"PHPUnit11でテストを実行すると、実行後の出力に「PHPUnit Deprecations」というメッセージが表示されるようになっていました。@testなどの慣れ親しんだアノテーションはPHPUnit12からサポート外となるようですので、新しいアトリビュートへ変更します。\nPHPUnit12で削除される@アノテーション OK, but there were issues! Tests: 28, Assertions: 64, PHPUnit Deprecations: 1. こちらがテスト時に表示されたメッセージです。「PHPUnit Deprecations: 1」だけだと分かりにくいので、詳細内容も出力されるようにphpunit.xmlを少し修正します。\nphpunit.xml ... \u0026lt;phpunit xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:noNamespaceSchemaLocation=\u0026#34;vendor/phpunit/phpunit/phpunit.xsd\u0026#34; bootstrap=\u0026#34;vendor/autoload.php\u0026#34; colors=\u0026#34;true\u0026#34; displayDetailsOnPhpunitDeprecations=\u0026#34;true\u0026#34; //これを追加 \u0026gt; ... phpunitタグにdisplayDetailsOnPhpunitDeprecations=\u0026ldquo;true\u0026rdquo;を追加しました。これで再度テストを実行すると、以下のようなメッセージが出力されました。\nThere was 1 PHPUnit test runner deprecation: 1) Metadata found in doc-comment for method Tests\\Unit\\Models\\UserModelTest::is_admin_check(). Metadata in doc-comments is deprecated and will no longer be supported in PHPUnit 12. Update your test code to use attributes instead. メッセージによると、ドキュメントコメント内のアノテーションは非推奨とのこと。PHPUnit12ではサポート対象外となるため、テストのアノテーションは「アトリビュート」に書き換える必要があるようです。\nアノテーション → アトリビュートの書き換え 今まで使っていたアノテーションの記述は、アトリビュートでは以下のように書き換えます。\n@test → #[Test] @dataProvider → #[DataProvider('データプロバイダ名')] @covers → #[CoversMethod('カバー対象のクラス', 'カバー対象メソッド')] 他のアトリビュートの記述はドキュメントで紹介されていますのでご参照ください。\n基本的にはこれらを書き換えればいいだけなのですが、@coversに関しては記述の位置が大きく変更となるためご注意ください。では具体的にどう変更になったか?というのを、以下のテストコードを使ってご紹介します。\n変更前のテスト（アノテーション使用） こちらがアノテーションを使用したユニットテストです。ドキュメントコメント内に@test、@covers、@dataProviderを使用しています。\nnamespace Tests\\Unit\\Models; use App\\Models\\User; use Illuminate\\Foundation\\Testing\\RefreshDatabase; use Tests\\TestCase; class UserModelTest extends TestCase { use RefreshDatabase; /** * @test * @covers App\\Models\\User::isAdmin * @dataProvider roleDataProvider */ public function is_admin_check(string $role, bool $expected): void { $user = User::factory()-\u0026gt;create([\u0026#39;role\u0026#39; =\u0026gt; $role]); $this-\u0026gt;assertEquals($expected, $user-\u0026gt;isAdmin()); } public static function roleDataProvider(): array { return [ \u0026#39;role = admin\u0026#39; =\u0026gt; [ \u0026#39;role\u0026#39; =\u0026gt; \u0026#39;admin\u0026#39;, \u0026#39;expected\u0026#39; =\u0026gt; true ], \u0026#39;role = user\u0026#39; =\u0026gt; [ \u0026#39;role\u0026#39; =\u0026gt; \u0026#39;user\u0026#39;, \u0026#39;expected\u0026#39; =\u0026gt; false ], \u0026#39;role = role\u0026#39; =\u0026gt; [ \u0026#39;role\u0026#39; =\u0026gt; \u0026#39;editor\u0026#39;, \u0026#39;expected\u0026#39; =\u0026gt; false ], ]; } } 変更後のテスト（アトリビュート使用） 以下が、アトリビュートに書き換えた後のテストになります。\nnamespace Tests\\Unit\\Models; use App\\Models\\User; use Illuminate\\Foundation\\Testing\\RefreshDatabase; use PHPUnit\\Framework\\Attributes\\CoversMethod; use PHPUnit\\Framework\\Attributes\\DataProvider; use PHPUnit\\Framework\\Attributes\\Test; use Tests\\TestCase; #[CoversMethod(User::class, \u0026#39;isAdmin\u0026#39;)] class UserModelTest extends TestCase { use RefreshDatabase; #[Test] #[DataProvider(\u0026#39;roleDataProvider\u0026#39;)] public function test_is_admin_check(string $role, bool $expected): void { $user = User::factory()-\u0026gt;create([\u0026#39;role\u0026#39; =\u0026gt; $role]); $this-\u0026gt;assertEquals($expected, $user-\u0026gt;isAdmin()); } public static function roleDataProvider(): array { return [ \u0026#39;role = admin\u0026#39; =\u0026gt; [ \u0026#39;role\u0026#39; =\u0026gt; \u0026#39;admin\u0026#39;, \u0026#39;expected\u0026#39; =\u0026gt; true ], \u0026#39;role = user\u0026#39; =\u0026gt; [ \u0026#39;role\u0026#39; =\u0026gt; \u0026#39;user\u0026#39;, \u0026#39;expected\u0026#39; =\u0026gt; false ], \u0026#39;role = role\u0026#39; =\u0026gt; [ \u0026#39;role\u0026#39; =\u0026gt; \u0026#39;editor\u0026#39;, \u0026#39;expected\u0026#39; =\u0026gt; false ], ]; } } まず、useで#[Test]などの各属性に対応するクラスをインポートする必要があります。そして、#[Test]・#[DataProvider()]はアノテーションと同様テストケースの直前に配置します。\n一番大きな変更である@coversの位置ですが、アトリビュートではクラス宣言の直前に置く形になります。 ドキュメントにもあるように、#[CoversMethod()]は各テストケースではなくテストクラスに指定する必要があります。\n最初このルールに気が付かず、@coversと同じように各テストケースの前に#[CoversMethod()]を配置したところ以下のようなエラーが発生しテスト失敗となりました。\n$ ./vendor/bin/phpunit An error occurred inside PHPUnit. Message: Attribute \u0026#34;PHPUnit\\Framework\\Attributes\\CoversMethod\u0026#34; cannot target method (allowed targets: class) ちゃんとエラーが出てくれたので、気がつけてよかったです。\n改めてテストを実行 アトリビュートへの書き換えが正しくできているか、再度テストを実行してみます。\n$ ./vendor/bin/phpunit OK (28 tests, 64 assertions) Deprecationsのメッセージが出なくなりました。テスト数やアサーションの数も同じなので、書き換えは問題なくできたようです。\nカバレッジの出力を確認 #[CoversMethod()]の動作確認も兼ねて、カバレッジの出力も見てみます。私の環境ではpcovを使用しています。まだの方はインストールと、php.iniにて以下を設定ください。\nphp.ini extension=/path/to/your/pcov.so pcov.enabled=1 pcov.directory=/path/to/your/project では、カバレッジのオプションをつけてテストを実行します。\n$ ./vendor/bin/phpunit --coverage-html coverage 実行後、プロジェクトルートのcoverageディレクトリにhtmlファイルが生成され、#[CoversMethod]で指定した関数もちゃんとカバレッジが計測されていました。\n","date":"2025-01-06T02:32:20+09:00","permalink":"https://www.larajapan.com/2025/01/06/phpunit-11-%E3%82%A2%E3%83%8E%E3%83%86%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%8B%E3%82%89%E3%82%A2%E3%83%88%E3%83%AA%E3%83%93%E3%83%A5%E3%83%BC%E3%83%88%E3%81%AB%E6%9B%B8%E3%81%8D%E6%8F%9B/","title":"PHPUnit 11 - アノテーションからアトリビュートに書き換える"},{"content":"Laravel 11.xがリリースされてから暫く経過していますが、やっと手持ちのLaravel 10.xのプロジェクトの更新作業に取り掛かっています。今回もLaravel Shiftのサービスを使用していますが、Laravel 10.xからLaravel11.xへの更新をすべて自動で行ってくれるわけではないです。\nしかし、Laravelのサイトで説明されている更新のステップのドキュメンテーションを読むよりは、具体的な更新の変更がヒントとなり役に立ちます。もちろんShiftの変更はそれだけ十分というレベルからは遠く、とりわけ今回の更新では構造的に大きなLaravelの変更があるので、Shiftの変更後は確認とともに手動の編集作業が必須となります。\nここではお客さんのプロジェクトでの更新で私が苦労した部分をいくつか説明しますが、過去の記事にLaravel 11.xのインストールについても説明があるので合わせてお読みください。\nconfigのファイルたちの復元 Laravel Shiftを使用した後に、config/ディレクトリ下のいくつかのファイルが削除あるいは編集されてることに気づきます。\n実際、Laravel 10.xとLaravel 11.xの両方で新規のプロジェクト作成して比較してみると、以下のファイルがLaravel 11.xでは削除されています。フレームワークのスリム化の一環だそうです。\nconfig/broadcasting.php config/cors.php config/hashing.php config/sanctum.php config/view.php 最初は、config/以下のすべてのファイルを削除するという案もあったそうですが、Laravelの作者のTaylor氏は夜寝れないほど悩んで、使用される頻度が少ない上のファイルの削除となったそうです。確かに小さいプロジェクトではそれらは使用することはなさそうです。\nまた、tinkerで上で削除されているconfig/view.phpの設定を見ると、\n\u0026gt; config(\u0026#39;view\u0026#39;) = [ \u0026#34;paths\u0026#34; =\u0026gt; [ \u0026#34;/var/www/repos/laravel-11x/resources/views\u0026#34;, ], \u0026#34;compiled\u0026#34; =\u0026gt; \u0026#34;/var/www/repos/laravel-11x/storage/framework/views\u0026#34;, ] と設定ファイルがないのに裏ではしっかりデフォルトが設定されています。\nさて、私のお客さんのプロジェクトでShiftを使用してLaravel 11.xに更新後では、以下のファイルが削除されていました。\nconfig/broadcasting.php config/cache.php config/cors.php config/hashing.php config/queue.php config/session.php config/view.php 先のLaravel 11.xのデフォルトのプロジェクトと違って、cache.phpやqueue.phpやsession.phpも削除されています。さらに、残ったconfig/app.phpとかでは今まで存在したコメントを含めてほとんどの中身が削除されています。\n基本的に、ShiftはLaravel10.xのデフォルトと同じ中身なら削除、差分があるなら差分だけと残すという方針なのでしょう。先に説明したように、設定ファイルがないからと言ってその設定が存在しなわけではないのです。.envの値もチェックしてLaravelでは以前と同様に実行時には設定値の値をメモリーに設定します。\nしかし、プロジェクトを管理する上で、裏で隠されているのは理解しにくいです。ということで私は手動でShiftで削除や編集されたファイルを復元することに決めました。\n戻す作業は簡単。\n$ php artisan config:publish と実行すると、以下のようなメニューが表示されます。\nそこで戻したいファイルを選択して戻すことができます。\nしかし、すでに存在するファイルを戻したいなら、\n$ php artisan config:publish --force として実行するとすでに存在ファイルが上書きされます。\nベストは、Laravel 11.xの新規プロジェクトを他のディレクトリで作成して、そこでconfig:publishを実行してデフォルトのファイルを書き出し、それとあなたのプロジェクトのLaravel 11.x更新前のファイルと比較しながら変更することをお勧めします。\nそのためには、\n$ php artisan config:publish --all --force を実行するのが良いです。\nせっかくスリムになったのに設定ファイルを戻すのはプログラムのパフォーマンスに影響を与えるのでは？と思うかもしれません。 しかし、本番環境では、以下の実行して最適化のキャッシュファイルを作成すればそのような心配の必要ないです。\n$ php artisan config:cache .envを変更する必要がある config/のファイルを復元して、じみちに以前のファイルと比較して、以前に編集したconfigファイルを戻したりするのですが、そこで気づくのは、Laravel 11.xでは、環境変数の名前が変わっている、設定ファイルの環境変数のデフォルト値が変わっている、そして、以前は.envで設定できなかった環境変数が設定できるようになっていることです。\nお客さんのプロジェクトでは、以下の値の.envへの追加あるいは編集となりました（＋は追加、ーは削除）。大半は以前.envで編集では設定できずにconfig/のファイルを編集していたので、良い変更かもしれません。\n# config/app.phpでの変更により以前.envで設定できない環境変数ができるようになった +APP_TIMEZONE=Asia/Tokyo +APP_LOCALE=ja +APP_FAKER_LOCALE=ja_JP # config/auth.phpでの変更により.envで設定できない環境変数ができるようになった +AUTH_GUARD=admin +AUTH_PASSWORD_BROKER=admin # config/cache.phpでの変更により環境変数名が変わった -CACHE_DRIVER=file +CACHE_STORE=file # config/database.phpでの変更により環境変数のデフォルト値が変わった、また.envで設定できない環境変数ができるようになった +DB_CONNECTION=mysql +DB_CHARSET=utf8mb3 +DB_COLLATION=utf8mb3_general_ci +REDIS_CLIENT=predis # config/logging.phpでの変更により.envで設定できない環境変数ができるようになった +LOG_STACK=single # config/session.phppでの変更により.envで設定できない環境変数ができるようになった +SESSION_EXPIRE_ON_CLOSE=false 削除されたEventServiceProviderの中身の行方 フレームワークのスリム化として、app/Providers/のファイルの統合化があります。その結果削除されたProviderの１つにEventServiceProvider.phpがあります。\n私のお客さんのプロジェクトでは以前は、EventServiceProvider.phpにおいて、以下のようなイベントとリスナーのマップが設定されていました。\napp/Providers/EventServiceProvider.php ... class EventServiceProvider extends ServiceProvider { namespace App\\Providers; use App\\Events\\SanctumUnauthenticated; use App\\Listeners\\CommandFinishedListener; use App\\Listeners\\CommandStartingListener; use App\\Listeners\\LoginListener; use App\\Listeners\\LogoutListener; use Illuminate\\Auth\\Events\\Login; use Illuminate\\Auth\\Events\\Logout; use Illuminate\\Console\\Events\\CommandFinished; use Illuminate\\Console\\Events\\CommandStarting; class EventServiceProvider extends ServiceProvider { /** * The event listener mappings for the application. * * @var array */ protected $listen = [ SanctumUnauthenticated::class =\u0026gt; [ SecureLogging::class, ], Login::class =\u0026gt; [ LoginListener::class, ], Logout::class =\u0026gt; [ LogoutListener::class, ], CommandStarting::class =\u0026gt; [ CommandStartingListener::class, ], CommandFinished::class =\u0026gt; [ CommandFinishedListener::class, ], ];\u0026#34; ... Laravel 11.xの更新でEventServiceProvider.phpが削除された後には、上のマップを以下のように、AppServiceProviderにおいてEvent::listenのクロージャで設定可能となります。しかし、Shiftのサービスではこの自動変換は行われずに、PRでの説明だけとなっておりこの部分はとても頼りにならず、手動の編集が必要となります。\napp/Providers/AppServiceProvider.php namespace App\\Providers; use App\\Events\\SanctumUnauthenticated; use App\\Listeners\\CommandFinishedListener; use App\\Listeners\\CommandStartingListener; use App\\Listeners\\LoginListener; use App\\Listeners\\LogoutListener; use Illuminate\\Support\\ServiceProvider; class AppServiceProvider extends ServiceProvider { /** * Bootstrap any application services. * * @return void */ public function boot() { Event::listen( SanctumUnauthenticated::class, CommandFinishedListener::class, CommandStartingListener::class, LoginListener::class, LogoutListener::class, ); } ... しかし、app/Events/やapp/Listeners/で定義したイベントやリスナーをいちいちリストするのは面倒です。\nもっと簡単なのは、bootstrap/app.phpにおいて、Laravelに見つけてもらうことです。\nbootsrap/app.php ... return Application::configure(basePath: dirname(__DIR__)) -\u0026gt;withProviders() ... -\u0026gt;withEvents(discover: [ __DIR__.\u0026#39;/../app/Events\u0026#39;, __DIR__.\u0026#39;/../app/Listeners\u0026#39;, __DIR__.\u0026#39;/../app/Modules/Pmaster/Listeners\u0026#39;, ]) ... このように、withEvents()で必要なディレクトリーを指定するだけです。将来に追加や削除しても自動でdiscoverされます。\n正しくdiscoverされているかを確認するには、以下のコマンドを実行します。\n$ php artisan event:list そして、本番環境では、以下を実行して先のcacheと同様に必要データをキャッシュします。\n$ php artisan event:cache 最後に Laravel 11.xへの更新作業は、Shiftのサービスとその後の手動確認と編集のハイブリッドになりました。Shiftで変更の対象を学び更新を理解しそれを適用という感じです。もちろんShiftの作業は後者の手動の作業に比べて非常に安い（２９ドル）のですが、一度理解できたら、他のプロジェクトでの同様な更新にShiftの利用は必要ないかもしれません。多分に将来はLaravelの更新作業は、AIですべて完璧に行ってくれるかもしれないですね。現在はまさに過渡期です。\n","date":"2024-12-22T12:30:19+09:00","permalink":"https://www.larajapan.com/2024/12/22/laravel10-x%E3%81%8B%E3%82%89laravel-11-x%E3%81%B8%E3%81%AE%E6%9B%B4%E6%96%B0%E3%81%A7%E6%B3%A8%E6%84%8F%E3%81%99%E3%82%8B%E3%81%93%E3%81%A8/","title":"Laravel10.xからLaravel 11.xへの更新で注意すること"},{"content":"誰しも、composer create-project laravel/laravelのコマンドで新規のLaravelのプロジェクトの開発を開始します。Laravelが凄いのは、この実行後にすぐにプロジェクトを立ち上げて(php artisan serve）ブラウザでサイトを閲覧することが可能となることです。その実行では画面に英語でいろいろと経過が表示されますが、いったい何が起こっているのでしょうか？\nLaravelの新規のプロジェクトの作成 最新のLaravel 11.xを使用してプロジェクトを作成するには、以下のようなコマンドの実行が必要です。 $ composer create-project laravel/laravel laravel-11x 上のコマンドラインにおいて、\ncreate-projectは、composerの新規プロジェクト作成のオプション名です。 laravel/laravellは、プロジェクトのルートとなるパッケージ名です。 指定したパッケージ名をpackagist.orgのサイトでで探します。 https://packagist.org/?query=laravel/laravel マッチするパッケージが存在するgithubのレポからパッケージのダウンロードとなります。 laravel-11xは新規プロジェクト名で、その名前でプロジェクトのディレクトリを作成して、そこに必要なパッケージをダウンロードしてインストールし設定を行います。 ルートパッケージのバージョンの指定も以下のように可能です。\n$ composer create-project laravel/laravel laravel-10x \u0026#34;10.*\u0026#34; 上では、 10.*は、Laravel 10で一番最新のバージョンで新規プロジェクトが作成されます。\nプロジェクト作成の設計図 今回の新規のプロジェクトの作成として指定されるlaravel/laravelは、以下からダウンロードされます。 https://github.com/laravel/laravel そこに含まれる、composer.jsonのファイルに、プロジェクトに必要なパッケージのダウンロードとインストール、そしてプロジェクト立ち上げに必要な.envやDBの作成などが指定されています。 ファイルの中身を見てみましょう。phpのプロジェクトですがフォーマットはjsonとなっています。途中今回の話に関係ないところは\u0026hellip;で省いています。\n{ \u0026#34;name\u0026#34;: \u0026#34;laravel/laravel\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;project\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;The skeleton application for the Laravel framework.\u0026#34;, \u0026#34;keywords\u0026#34;: [\u0026#34;laravel\u0026#34;, \u0026#34;framework\u0026#34;], \u0026#34;license\u0026#34;: \u0026#34;MIT\u0026#34;, \u0026#34;require\u0026#34;: { \u0026#34;php\u0026#34;: \u0026#34;^8.2\u0026#34;, \u0026#34;laravel/framework\u0026#34;: \u0026#34;^11.9\u0026#34;, \u0026#34;laravel/tinker\u0026#34;: \u0026#34;^2.9\u0026#34; }, \u0026#34;require-dev\u0026#34;: { \u0026#34;fakerphp/faker\u0026#34;: \u0026#34;^1.23\u0026#34;, \u0026#34;laravel/pail\u0026#34;: \u0026#34;^1.1\u0026#34;, \u0026#34;laravel/pint\u0026#34;: \u0026#34;^1.13\u0026#34;, \u0026#34;laravel/sail\u0026#34;: \u0026#34;^1.26\u0026#34;, \u0026#34;mockery/mockery\u0026#34;: \u0026#34;^1.6\u0026#34;, \u0026#34;nunomaduro/collision\u0026#34;: \u0026#34;^8.1\u0026#34;, \u0026#34;phpunit/phpunit\u0026#34;: \u0026#34;^11.0.1\u0026#34; }, ... \u0026#34;scripts\u0026#34;: { \u0026#34;post-autoload-dump\u0026#34;: [ \u0026#34;Illuminate\\\\Foundation\\\\ComposerScripts::postAutoloadDump\u0026#34;, \u0026#34;@php artisan package:discover --ansi\u0026#34; ], \u0026#34;post-update-cmd\u0026#34;: [ \u0026#34;@php artisan vendor:publish --tag=laravel-assets --ansi --force\u0026#34; ], \u0026#34;post-root-package-install\u0026#34;: [ \u0026#34;@php -r \\\u0026#34;file_exists(\u0026#39;.env\u0026#39;) || copy(\u0026#39;.env.example\u0026#39;, \u0026#39;.env\u0026#39;);\\\u0026#34;\u0026#34; ], \u0026#34;post-create-project-cmd\u0026#34;: [ \u0026#34;@php artisan key:generate --ansi\u0026#34;, \u0026#34;@php -r \\\u0026#34;file_exists(\u0026#39;database/database.sqlite\u0026#39;) || touch(\u0026#39;database/database.sqlite\u0026#39;);\\\u0026#34;\u0026#34;, \u0026#34;@php artisan migrate --graceful --ansi\u0026#34; ], \u0026#34;dev\u0026#34;: [ \u0026#34;Composer\\\\Config::disableProcessTimeout\u0026#34;, \u0026#34;npx concurrently -c \\\u0026#34;#93c5fd,#c4b5fd,#fb7185,#fdba74\\\u0026#34; \\\u0026#34;php artisan serve\\\u0026#34; \\\u0026#34;php artisan queue:listen --tries=1\\\u0026#34; \\\u0026#34;php artisan pail --timeout=0\\\u0026#34; \\\u0026#34;npm run dev\\\u0026#34; --names=server,queue,logs,vite\u0026#34; ] }, ... } 上において、\u0026ldquo;require\u0026rdquo;は、laravelのルートパッケージがライブの環境で必要なパッケージです。\u0026ldquo;require-dev\u0026rdquo;は、開発環境で必要なパッケージです。たったこれだけですが、前回に説明したようにそれらが依存するパッケージがあるので実際にインストールパッケージは100以上の数になります。 \u0026ldquo;scripts\u0026rdquo;の部分は、特定なイベントごとに実行する指令が記述されています。\ncreate-project実行の作業 composer create-projectを実行すると、以下のような経過表示となります。すべて掲載するととても長いのでパッケージのLockingやDownloadingやInstallingの多くは割愛しています。\n上から順番に説明していきます。\nプロジェクトディレクトリの作成 Creating a \u0026#34;laravel/laravel\u0026#34; project at \u0026#34;./laravel-11x\u0026#34; まず、コマンド実行で指定した新規プロジェクト名のディレクトリlaravel-11xを作成します。\nルートパッケージのインストール Creating a \u0026#34;laravel/laravel\u0026#34; project at \u0026#34;./laravel-11x\u0026#34; Installing laravel/laravel (v11.3.3) - Downloading laravel/laravel (v11.3.3) - Installing laravel/laravel (v11.3.3): Extracting archive Created project in /var/www/repos/laravel-11x laravel/laravelが管理されているgithubのレポからファイルをダウンロードしてlaravel-11xのディレクトリにインストールします。 Downloadingでは、ダウンロードされたファイルは直接larave-11xへのディレクトリには置かれません。将来のインストールを効率化するために、ユーザーのキャッシュのディレクトリにインストールされます。私のubuntuの環境では、ホームディレクトリの.cache/composerにファイルが置かれます。すでに同じバージョンのパッケージがあるなら、Downloadingは行われずに、そのキャッシュのディレクトリから、laravel-11xへのディレクトリにファイルがコピーされます。その作業がInstallingです。\n.envの作成 \u0026gt; @php -r \u0026#34;file_exists(\u0026#39;.env\u0026#39;) || copy(\u0026#39;.env.example\u0026#39;, \u0026#39;.env\u0026#39;);\u0026#34; これは、composer.jsonの\u0026ldquo;scripts\u0026rdquo;セクションにあった、\u0026ldquo;post-root-package-install\u0026rdquo;のスクリプトですね。.env.exampleをコピーして.envを作成です。\ncomposer.lockの作成 Loading composer repositories with package information Updating dependencies Lock file operations: 107 installs, 0 updates, 0 removals - Locking brick/math (0.12.1) - Locking carbonphp/carbon-doctrine-types (3.2.0) ... Writing lock file ここでは、composer.jsonの\u0026ldquo;require\u0026rdquo;と\u0026ldquo;require-dev\u0026rdquo;で指定されたパッケージたちのcomposer.jsonを読み、それらが依存するパッケージのcomposer.jsonも読み、コンフリクトがないようにインストールするすべてのパッケージとそのバージョンを決定します。そして、この時点で初めてcomposer.lockのファイルを作成して決定した情報を書き込みします。それがLockingの作業です。\n\u0026ldquo;require\u0026rdquo;で指定したバージョンのデータと実際にダウンロードされるバージョンの違いに注意してください。 例えば、\u0026ldquo;require\u0026quot;では、\u0026ldquo;laravel/framework\u0026rdquo;: \u0026ldquo;^11.9\u0026rdquo;とありますが、この時点では\n... - Locking laravel/framework (v11.34.2) ... と、v11.34.2のバージョンがインストールされます。\n依存するパッケージのインストール Installing dependencies from lock file (including require-dev) Package operations: 107 installs, 0 updates, 0 removals - Downloading doctrine/inflector (2.0.10) - Downloading doctrine/lexer (3.0.1) ... - Installing doctrine/inflector (2.0.10): Extracting archive - Installing doctrine/lexer (3.0.1): Extracting archive ... 41 package suggestions were added by new dependencies, use `composer suggest` to see details. ここのステップでルートパッケージに依存するすべてのパッケージがcomposer.lockに従ったバージョンでlaravel-11x/vendorにインストールされます。DownloadingやInstallingはルートパッケージのところで説明したものと同様です。\nscriptsの実行 Generating optimized autoload files \u0026gt; Illuminate\\Foundation\\ComposerScripts::postAutoloadDump \u0026gt; @php artisan package:discover --ansi \u0026gt; @php artisan vendor:publish --tag=laravel-assets --ansi --force \u0026gt; @php artisan key:generate --ansi \u0026gt; @php -r \u0026#34;file_exists(\u0026#39;database/database.sqlite\u0026#39;) || touch(\u0026#39;database/database.sqlite\u0026#39;);\u0026#34; \u0026gt; @php artisan migrate --graceful --ansi 残りの作業は、composer.jsonで指定されたイベントにマッチするスクリプトが実行されます。 上から順番に以下のイベントとなります。\n\u0026#34;post-autoload-dump\u0026#34;: [ \u0026#34;Illuminate\\\\Foundation\\\\ComposerScripts::postAutoloadDump\u0026#34;, \u0026#34;@php artisan package:discover --ansi\u0026#34; ], composer autoload-dumpを実行したときにも実行されますが、プロジェクトの初期化で使用される必要なパッケージとそのクラスのリストを含むファイルを作成します。\n\u0026#34;post-update-cmd\u0026#34;: [ \u0026#34;@php artisan vendor:publish --tag=laravel-assets --ansi --force\u0026#34; ], プロジェクトで必要なアセットのファイルをパブリッシュします。上の経過では何もパブリッシュするものがなかったようです。\n\u0026#34;post-create-project-cmd\u0026#34;: [ \u0026#34;@php artisan key:generate --ansi\u0026#34;, \u0026#34;@php -r \\\u0026#34;file_exists(\u0026#39;database/database.sqlite\u0026#39;) || touch(\u0026#39;database/database.sqlite\u0026#39;);\\\u0026#34;\u0026#34;, \u0026#34;@php artisan migrate --graceful --ansi\u0026#34; ], 最後に、ここで.envのAPP_KEYの値を作成し、sqliteのデータベースのファイルを作成して、マイグレーションを実行します。\n上は最新のLaravel 11.xですが、Laravel 10.xでは、\n\u0026#34;post-create-project-cmd\u0026#34;: [ \u0026#34;@php artisan key:generate --ansi\u0026#34; ] となっていて、sqliteのデータベースの作成や設定は行いませんでした。\n","date":"2024-12-08T08:59:55+09:00","permalink":"https://www.larajapan.com/2024/12/08/composer%E3%82%92%E3%82%82%E3%81%A3%E3%81%A8%E7%9F%A5%E3%82%8D%E3%81%86%EF%BC%88%EF%BC%92%EF%BC%89composer-create-project%E3%81%A7%E3%81%AF%E4%BD%95%E3%81%8C%E8%B5%B7%E3%81%93%E3%81%A3%E3%81%A6/","title":"composerをもっと知ろう（２）composer create-projectでは何が起こっている？"},{"content":"前回の記事では、LaravelプロジェクトのPHPUnitテストをプラグインを使ってPestへ自動変換しましたが、今回は自分でPestのテストを作成します。外部APIのモック、基本的なアサーションや例外が投げられたパターンなどのテストです。\nテスト対象 以下が今回のテスト対象であるTestClassです。getTranslate()では引数のテキストをAPIを使って翻訳し、結果をJsonレスポンスで返却。処理に失敗した場合は例外をスローするようになっています。\nラップしているTranslationApiは外部APIなので、今回はこちらはモックとします。\nclass TestClass { public function __construct( private readonly TranslationApi $translator //これをモックする ) {} public function getTranslate(string $content): JsonResponse { try { $translatedContent = $this-\u0026gt;translator-\u0026gt;translate( $content ); return response()-\u0026gt;json([ \u0026#39;status\u0026#39; =\u0026gt; \u0026#39;success\u0026#39;, \u0026#39;message\u0026#39; =\u0026gt; \u0026#39;操作が成功しました。\u0026#39;, \u0026#39;data\u0026#39; =\u0026gt; $translatedContent, ]); } catch (Exception $e) { throw new Exception(\u0026#39;翻訳に失敗しました: \u0026#39;.$e-\u0026gt;getMessage()); } } } Pestの構文 Pestの構文は以下のようになっており、テストをグルーピングするdescribe()メソッドと、１つ１つのテストケースを指すtest()もしくはit()メソッドで構成されています。PHPUnitで書かれているクラス宣言は不要です。\ndescribe(\u0026#39;グループ名\u0026#39;, function () { test(\u0026#39;テスト１\u0026#39;, function () { //テスト内容 }); it(\u0026#39;テスト２\u0026#39;, function () { //テスト内容 }); }); describe()の使用は任意なので、最小構成のtest()、it()のみで問題ありません。\nまたtest()とit()のどちらを使うのかという点ですが、テストの動作自体は同じなのでどちらでも大丈夫です。ただit()を使った場合、実行結果の出力で以下のようにテスト名の前にitがつくようになっています。\n共通処理 ではテストを書いてゆきます。今回は成功・失敗の２パターンテストを書きますので、共通の処理をbeforeEach()を使ってまとめておきます。beforeEach()は各テストケースの実行前に呼び出される、PHPUnitのsetUp()と同じような役割です。\nモック対象であるTranslationApiのモック化と、テスト対象クラスのインスタンス化の２点を以下のように共通処理としました。\nbeforeEach(function () { // モックを作成 $this-\u0026gt;mock = Mockery::mock(TranslationApi::class); // テスト対象のクラスをインスタンス化 $this-\u0026gt;service = new testClass($this-\u0026gt;mock); }); モックにはPHPUnitと同様にMockeryを使用します。ここではmock()メソッドで対象クラスをモック化するのみとして、モックの振る舞いは各テスト内でセットします。\ntestClassクラスのインスタンス化も、PHPUnitと記述は特に変わりません。\nなお$this-\u0026gt;mockや$this-\u0026gt;serviceのプロパティ宣言は、Pestでは不要です。\n成功パターンのテスト 「翻訳が成功して期待するレスポンスが返ってきたケース」のテストコードは以下になります。\ntest(\u0026#39;翻訳 成功\u0026#39;, function () { //準備 $content = \u0026#39;こんにちは\u0026#39;; $expected = \u0026#39;Hello\u0026#39;; $this-\u0026gt;mock -\u0026gt;shouldReceive(\u0026#39;translate\u0026#39;) -\u0026gt;once() -\u0026gt;with($content) -\u0026gt;andReturn($expected); //テスト実行 $response = $this-\u0026gt;testClass-\u0026gt;getTranslate($content); expect($response-\u0026gt;getData(true)) -\u0026gt;toMatchArray([ \u0026#39;status\u0026#39; =\u0026gt; \u0026#39;success\u0026#39;, \u0026#39;message\u0026#39; =\u0026gt; \u0026#39;操作が成功しました。\u0026#39;, \u0026#39;data\u0026#39; =\u0026gt; \u0026#39;Hello\u0026#39;, ]); }); テストコードは大きく前半の準備部分と、後半の実行・アサーションの部分に分かれています。\nまず前半では、モックの振る舞いを定義しています。shouldReceive(\u0026rsquo;translate\u0026rsquo;)は、translateメソッドが呼ばれること、once()は、呼ばれるのは１度だけという回数を、with($content)で渡される引数を、そして最後にandReturn()で、戻り値を指定しています。\n次に後半の、テスト実行と戻り値のアサーションです。\n受け取った$response自体はJsonResponseオブジェクトなので、$response-\u0026gt;getData(true)でテストに必要な値だけを取り出しています。getData()はデフォルトでstdClassオブジェクトを返すので、ここでは引数にtrueを渡すことで配列として戻り値を受け取っています。\nそして検証にはexpect()というPestのアサーションメソッドを使用します。expect()はそれ単体で使用することはなく、通常他のアサーション関数をチェーンします。今回はtoMatchArray()という配列比較の関数を繋ぎました。\ntoMatchArray()は上のテストコードのように全ての項目を比較することもできますし、以下のように、一部の項目だけといった部分一致も検証できるのでとても便利です。\nexpect($response-\u0026gt;getData(true)) -\u0026gt;toMatchArray([ \u0026#39;message\u0026#39; =\u0026gt; \u0026#39;操作が成功しました。\u0026#39;, \u0026#39;data\u0026#39; =\u0026gt; \u0026#39;Hello\u0026#39;, ]); expect($response-\u0026gt;getData(true)) -\u0026gt;toMatchArray([ \u0026#39;data\u0026#39; =\u0026gt; \u0026#39;Hello\u0026#39;, ]); また以下は配列比較でテストが失敗した場合のスクリーンショットですが、差分をこのようにわかりやすく出力してくれます。\n失敗パターンのテスト では次は、「翻訳が失敗し、例外を返すパターン」のテストコードです。\ntest(\u0026#39;翻訳 失敗で例外が返る\u0026#39;, function () { $content = \u0026#39;こんにちは\u0026#39;; $errorMessage = \u0026#39;API error occurred\u0026#39;; $this-\u0026gt;mock -\u0026gt;shouldReceive(\u0026#39;translate\u0026#39;) -\u0026gt;once() -\u0026gt;with($content) -\u0026gt;andThrow(new Exception($errorMessage)); $this-\u0026gt;testClass-\u0026gt;getTranslate($content); })-\u0026gt;throws(Exception::class, \u0026#39;翻訳に失敗しました: API error occurred\u0026#39;); 先ほどの成功のテストとは少し構成が変わりました。\nまずは前半のモック定義部分。ここは先ほどとほとんど同じですが、モックの戻り値の指定にandThrow(new Exception($errorMessage))として、例外を投げるように指定しています。\nそして検証部分はチェーンで繋いだthrows()メソッドで行います。throws()はPestが提供しているメソッドで、第一引数に期待する例外クラスを、第二引数では例外のメッセージを渡します。\nPHPUnitの場合、例外の検証時は以下のようにテストコードの冒頭に置かなければいけません。一方Pestではテスト実行後の最後にアサーションを配置できるので、テスト実行の流れと合っていてとても読みやすいと感じました。\npublic function test_翻訳失敗_PHPUnitの場合() { $this-\u0026gt;expectException(Exception::class); $this-\u0026gt;expectExceptionMessage(\u0026#39;翻訳に失敗しました: API error occurred\u0026#39;); // ... 以下でテスト実行 } 関連記事 現在LaravelでPHPUnitを使っていてPestを導入しようかなと考えている方は、よろしければ以下の移行手順もご覧ください。\nLaravelのPHPUnitテストをpest-plugin-driftでPestへ変換\n","date":"2024-12-04T07:44:19+09:00","permalink":"https://www.larajapan.com/2024/12/04/laravel-pest%E3%81%A7%E3%83%A2%E3%83%83%E3%82%AF%E3%83%BB%E4%BE%8B%E5%A4%96%E3%81%AE%E3%83%86%E3%82%B9%E3%83%88/","title":"Laravel Pestでモック・例外のテスト"},{"content":"composerは、phpのプロジェクトで使用されるパッケージを管理するコマンドです。昨今はcomposerなしではphpのプロジェクトは作成できないほど重要なものです。しかし、知らないことがたくさんあります。ということで「composerをもっと知ろう」シリーズです。composerに関するいろいろな面を紹介していきます。\ncomposerがインストールしたパッケージ まず、最新のLaravelのプロジェクトを作成してみましょう。 $ composer create-project laravel/laravel laravel-11x composer create-projejtに関しては違う機会に説明するとして、上のコマンド実行後に何のパッケージがインストールされたか見てみましょう。\n$ composer show を実行すると、以下のように107ものパッケージのリストが、パッケージ名、バージョン、説明を列として表示されます。\nbrick/math 0.12.1 Arbitrary-precision arithmetic library carbonphp/carbon-doctrine-types 3.2.0 Types to use Carbon in Doctrine dflydev/dot-access-data 3.0.3 Given a deep data structure, access data by dot notation. doctrine/inflector 2.0.10 PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words. doctrine/lexer 3.0.1 PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers. dragonmantank/cron-expression 3.4.0 CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due egulias/email-validator 4.0.2 A library for validating emails against several RFCs fakerphp/faker 1.24.0 Faker is a PHP library that generates fake data for you. filp/whoops 2.16.0 php error handling for cool kids fruitcake/php-cors 1.3.0 Cross-origin resource sharing library for the Symfony HttpFoundation graham-campbell/result-type 1.1.3 An Implementation Of The Result Type guzzlehttp/guzzle 7.9.2 Guzzle is a PHP HTTP client library guzzlehttp/promises 2.0.4 Guzzle promises library guzzlehttp/psr7 2.7.0 PSR-7 message implementation that also provides common utility methods guzzlehttp/uri-template 1.0.3 A polyfill class for uri_template of PHP hamcrest/hamcrest-php 2.0.1 This is the PHP port of Hamcrest Matchers laravel/framework 11.31.0 The Laravel Framework. laravel/pail 1.2.1 Easily delve into your Laravel application\u0026#39;s log files directly from the command line. laravel/pint 1.18.1 An opinionated code formatter for PHP. laravel/prompts 0.3.2 Add beautiful and user-friendly forms to your command-line applications. laravel/sail 1.38.0 Docker files for running a basic Laravel application. laravel/serializable-closure 1.3.6 Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP. laravel/tinker 2.10.0 Powerful REPL for the Laravel framework. league/commonmark 2.5.3 Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and GitHub-Flavored Markdown (GFM) .. 続く まだ開発を始めていなのにすでにこれだけのパッケージがインストールされます。上のリストを見ていて気付くのは、もちろんのことですが、Laravel自体もパッケージなのです。上のリストにあるlaravel/frameworkがそうです。他のもlaravelのレポからは６つのパッケージがインストールされます。私が大好きなlaravel/tinkerも含まれます。\ncomposer.jsonの中身を見ればわかりますが、そこでは上のすべてのパッケージを指定はしていません。以下を実行すると指定したパッケージがリストされます。\n$ composer show --self 上で、requiresの下にリストされているのが、ライブ環境で必要なパッケージです。phpはすでにインストールされているのでたったの２つです。 requires (dev)は、開発環境において必要なパッケージです。こちらもたったの７つです。\nさて、どうしてcomposer showでは107ものパッケージがリストされるのでしょうか？\nそれは、requiresあるいはrequires (dev)でリストされているそれぞのパッケージがまた違うパッケージを必要とするからです。さらにそのパッケージもまた他のを\u0026hellip;と。\n例えば、私が好きなtinkerのパッケージを見てみましょう。\n$ composer show --tree \u0026#34;laravel/tinker\u0026#34; ツリー構造はtinkerのパッケージが依存するパッケージを表示します。\nパッケージだけでなく、phpのバージョンやphpもモジュール（ext-json *など）の依存もあります。\ncomposerはこれらを全部チェックして、すべてのをまとめて必要なパッケージをインストールします。それぞれパッケージは違うバージョンを指定することもあるので、バージョンの整合性もチェックする必要あります。非常に大変に複雑な作業です。composerの偉大さが理解できますね。\nインストールされているパッケージは最新？ インストールしたパッケージたちは時間が経つと、バグの修正や新規のの追加など行われ、インストール時のバージョンと変わってきます。 以下のコマンドは、最新のパッケージの情報を取得してきて見せてくれます。それゆえにコマンド実行には時間がかかります。 $ composer show --latest それぞれの列は、パッケージ名、インストールされているバージョン、最新のバージョン、そして説明となります。 最新のパージョンに列において緑字は「最新」であり更新の必要はないということで、赤字は更新を「推薦」という意味です。\n黄色時は「メジャー」のバージョン更新がリリースされていることを示します。以下は違うプロジェクトからですが、前バージョンのLaravel 10xを使用しており、ご覧のようにLaravel 11xのメジャーリリースがあることを示しています。\n先のプロジェクトに戻って、全部ではなく更新を推薦するパッケージのみを見たいなら以下を実行してください。\u0026ndash;no-devのオプションは開発のパッケージを表示しません。\n$ composer show --outdated --no-dev ","date":"2024-11-23T06:51:48+09:00","permalink":"https://www.larajapan.com/2024/11/23/composer%E3%82%92%E3%82%82%E3%81%A3%E3%81%A8%E7%9F%A5%E3%82%8D%E3%81%86%EF%BC%88%EF%BC%91%EF%BC%89composer-show/","title":"composerをもっと知ろう（１）composer show"},{"content":"Laravel10から公式にテストフレームワークとしておすすめされるようになったPestですが、すでにPHPUnitで作成したテストファイルがたくさんある場合、それらを書き換えての移行は大変です。そこで、Pestが提供している変換用プラグインpestphp/pest-plugin-driftを試してみました。\nPest変換対象のLaravelプロジェクト 今回Pestへ変換するLaravelプロジェクトは以下の環境です。テスト用の小さいプロジェクトなので、テストファイルは9ファイルのみとなっています。\nLaravel Framework 11.29.0 PHPUnit 11.4.2 PHP 8.2.24\nPest・Pest変換用プラグインインストール では早速Pestからインストールします。Pest公式サイトの手順に従って進めます。\nまずはPHPunitを削除。\n$ composer remove phpunit/phpunit 次に、以下のコマンドでPestをインストール。\n$ composer require pestphp/pest --dev --with-all-dependencies Pestのバージョン3.5がインストールされました。続いてinitを実行します。\n$ ./vendor/bin/pest --init すると以下のファイルが作成されました。すでに存在しているファイルはパスされるので、実質Pest.phpのみが作成された形です。\nこれでPest本体のインストールは完了です。テストファイルの記述はまだPHPUnitのままですが、この段階でも./vendor/bin/pestが実行できます。\n続いて変換用プラグインをインストールしてゆきます。こちらも公式サイトの手順に従って、以下のコマンドを実行します。\n$ composer require pestphp/pest-plugin-drift --dev バージョン3.0.0がインストールされました。最後に、以下のコマンドでPHPUnitからPestへの変換を実行します。\n$ ./vendor/bin/pest --drift 既存のテストファイルが全て変換されました。\n今回は小さいシンプルなプロジェクトなので各種インストールはとくに問題なく進みましたが、大きなプロジェクトの場合依存関係の問題でインストールがスムーズにいかないこともあると思います。\nPHPUnit → Pest変換前後の比較 では、変換されたファイルを前後比較してみます。まず、以下が変換前のPHPUnit仕様のテストです。setUpやdataProviderも使用したよくあるテストコードです。\n/tests/Feature/Auth/AuthenticationTest.php namespace Tests\\Feature\\Auth; use App\\Models\\User; use Illuminate\\Foundation\\Testing\\RefreshDatabase; use Tests\\TestCase; class AuthenticationTest extends TestCase { use RefreshDatabase; protected $user; protected function setUp(): void { parent::setUp(); // 共通ユーザー作成 $this-\u0026gt;user = User::factory()-\u0026gt;create([ \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;test@example.com\u0026#39;, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;password\u0026#39; ]); } /** * @covers \\App\\Http\\Controllers\\Auth\\LoginController::showLoginForm */ public function test_login_screen_can_be_rendered(): void { $response = $this-\u0026gt;get(\u0026#39;/login\u0026#39;); $response-\u0026gt;assertStatus(200); } /** * @covers \\App\\Http\\Controllers\\Auth\\LoginController::login */ public function test_users_can_authenticate_with_valid_credentials(): void { $response = $this-\u0026gt;post(\u0026#39;/login\u0026#39;, [ \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;test@example.com\u0026#39;, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;password\u0026#39;, ]); $this-\u0026gt;assertAuthenticated(); $this-\u0026gt;assertEquals(\u0026#39;test@example.com\u0026#39;, auth()-\u0026gt;user()-\u0026gt;email); $response-\u0026gt;assertRedirect(route(\u0026#39;dashboard\u0026#39;, absolute: false)); } /** * @covers \\App\\Http\\Controllers\\Auth\\LoginController::login * @dataProvider invalidLoginDataProvider */ public function test_users_cannot_authenticate_with_invalid_credentials($email, $password): void { $response = $this-\u0026gt;post(\u0026#39;/login\u0026#39;, [ \u0026#39;email\u0026#39; =\u0026gt; $email, \u0026#39;password\u0026#39; =\u0026gt; $password, ]); $this-\u0026gt;assertGuest(); $response-\u0026gt;assertStatus(302); } public static function invalidLoginDataProvider(): array { return [ \u0026#39;間違ったパスワード\u0026#39; =\u0026gt; [\u0026#39;test@example.com\u0026#39;, \u0026#39;wrong-password\u0026#39;], \u0026#39;間違ったメールアドレス\u0026#39; =\u0026gt; [\u0026#39;wrong@example.com\u0026#39;, \u0026#39;password\u0026#39;], ]; } /** * @covers \\App\\Http\\Controllers\\Auth\\LoginController::logout */ public function test_users_can_logout(): void { $response = $this-\u0026gt;actingAs($this-\u0026gt;user)-\u0026gt;post(\u0026#39;/logout\u0026#39;); $this-\u0026gt;assertGuest(); $response-\u0026gt;assertRedirect(\u0026#39;/\u0026#39;); } } そして、Pestに変換された後のテストがこちらです。\n/tests/Feature/Auth/AuthenticationTest.php use App\\Models\\User; uses(\\Illuminate\\Foundation\\Testing\\RefreshDatabase::class); beforeEach(function () { // 共通ユーザー作成 $this-\u0026gt;user = User::factory()-\u0026gt;create([ \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;test@example.com\u0026#39;, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;password\u0026#39; ]); }); test(\u0026#39;login screen can be rendered\u0026#39;, function () { $response = $this-\u0026gt;get(\u0026#39;/login\u0026#39;); $response-\u0026gt;assertStatus(200); }); test(\u0026#39;users can authenticate with valid credentials\u0026#39;, function () { $response = $this-\u0026gt;post(\u0026#39;/login\u0026#39;, [ \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;test@example.com\u0026#39;, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;password\u0026#39;, ]); $this-\u0026gt;assertAuthenticated(); expect(auth()-\u0026gt;user()-\u0026gt;email)-\u0026gt;toEqual(\u0026#39;test@example.com\u0026#39;); $response-\u0026gt;assertRedirect(route(\u0026#39;dashboard\u0026#39;, absolute: false)); }); test(\u0026#39;users cannot authenticate with invalid credentials\u0026#39;, function ($email, $password) { $response = $this-\u0026gt;post(\u0026#39;/login\u0026#39;, [ \u0026#39;email\u0026#39; =\u0026gt; $email, \u0026#39;password\u0026#39; =\u0026gt; $password, ]); $this-\u0026gt;assertGuest(); $response-\u0026gt;assertStatus(302); })-\u0026gt;with(\u0026#39;invalidLoginDataProvider\u0026#39;); dataset(\u0026#39;invalidLoginDataProvider\u0026#39;, function () { return [ \u0026#39;間違ったパスワード\u0026#39; =\u0026gt; [\u0026#39;test@example.com\u0026#39;, \u0026#39;wrong-password\u0026#39;], \u0026#39;間違ったメールアドレス\u0026#39; =\u0026gt; [\u0026#39;wrong@example.com\u0026#39;, \u0026#39;password\u0026#39;], ]; }); test(\u0026#39;users can logout\u0026#39;, function () { $response = $this-\u0026gt;actingAs($this-\u0026gt;user)-\u0026gt;post(\u0026#39;/logout\u0026#39;); $this-\u0026gt;assertGuest(); $response-\u0026gt;assertRedirect(\u0026#39;/\u0026#39;); }); 変換後のPest仕様のコードではクラスの定義は削除され、それぞれのテストケースは関数宣言ではなくtest()関数として記述されています。また変換前は85行あったテストコードがPest変換後は57行とかなりコンパクトになりました。Docコメントが全て削除されているのが大きいのかもしれません。\nそれでは、移行するにあたり気になっていた箇所をピックアップしてみてみます。\ndataprovider PHPUnitのdataProviderは、Pest変換後は以下のようになりました。Docコメントとともに@dataProviderの記述は削除されて、代わりにwith()でデータセットを紐付けしています。テストとデータセットがバラバラなので、見た目にはPHPUnitとあまり変わりがありませんね。\ntest(\u0026#39;users cannot authenticate with invalid credentials\u0026#39;, function ($email, $password) { ・・・・・ })-\u0026gt;with(\u0026#39;invalidLoginDataProvider\u0026#39;); dataset(\u0026#39;invalidLoginDataProvider\u0026#39;, function () { return [ \u0026#39;間違ったパスワード\u0026#39; =\u0026gt; [\u0026#39;test@example.com\u0026#39;, \u0026#39;wrong-password\u0026#39;], \u0026#39;間違ったメールアドレス\u0026#39; =\u0026gt; [\u0026#39;wrong@example.com\u0026#39;, \u0026#39;password\u0026#39;], ]; }); データセットの紐付けは、以下のようにwith()内に記述される形に変換されることを期待したのですが、そうはなりませんでした。\ntest(\u0026#39;users cannot authenticate with invalid credentials\u0026#39;, function ($email, $password) { ・・・・・ })-\u0026gt;with([ [\u0026#39;test@example.com\u0026#39;, \u0026#39;wrong-password\u0026#39;], [\u0026#39;wrong@example.com\u0026#39;, \u0026#39;password\u0026#39;] ]); setUp関連 PHPUnitのsetUp()は、以下のようにbeforeEach()に変換されました。beforeEach()とは、それぞれのテストメソッドの実行前に毎回呼び出されるsetUp()と同じように共通の処理を行う際に使用されるメソッドです。\nbeforeEach(function () { // 共通ユーザー作成 $this-\u0026gt;user = User::factory()-\u0026gt;create([ \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;test@example.com\u0026#39;, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;password\u0026#39; ]); }); 他にも、PHPUnitで当然のように書いていたparent::setUp()やuse RefreshDatabase;はPestでは書く必要がなく、またprotected $user;といったプロパティ宣言も不要です。Pestの謳い文句の通り、テストコードはだいぶシンプルにできますね。\nテスト実行 では変換後のテストを実行してみましょう。\nテストはコマンドラインで、\n$ ./vendor/bin/pest あるいは、\n$ php artisan test で実行できます。\n以下のようにオプションを渡して実行ファイルを指定することもできます。\n$ ./vendor/bin/pest --filter=AuthenticationTest //ファイル名を指定する場合 実行結果は以下のように出力されました。\nテストにかかる時間はPestに変換しても速くなるということは特になく、PHPUnitと変わりなかったです。\n今回は自動で変換したためPestの記述に関しては要点のみとなりましたが、次回は手動でPestのテストコードを作成してゆきます。\n","date":"2024-11-10T01:38:41+09:00","permalink":"https://www.larajapan.com/2024/11/10/laravel%E3%81%AEphpunit%E3%83%86%E3%82%B9%E3%83%88%E3%82%92pest-plugin-drift%E3%81%A7pest%E3%81%B8%E5%A4%89%E6%8F%9B/","title":"LaravelのPHPUnitテストをpest-plugin-driftでPestへ変換"},{"content":"今までのカレンダーシリーズでは、タイムゾーンのことをまったく考えていませんでしたが、もちろん考える必要あります。\nLaravelのタイムゾーンは何？ Laravelのプロジェクトのタイムゾーンは、以下で設定されています。 config/app.php ... /* |-------------------------------------------------------------------------- | Application Timezone |-------------------------------------------------------------------------- | | Here you may specify the default timezone for your application, which | will be used by the PHP date and date-time functions. The timezone | is set to \u0026#34;UTC\u0026#34; by default as it is suitable for most use cases. | */ \u0026#39;timezone\u0026#39; =\u0026gt; env(\u0026#39;APP_TIMEZONE\u0026#39;, \u0026#39;UTC\u0026#39;), ... 見ての通り、デフォルトはUTCです。 UTCとは、協定世界時のことで国際協定により定められた世界共通の標準時です。\nこれを上書きしたいなら、.envで以下のように設定できます。\n.env ... APP_TIMEZONE=\u0026#34;Asia/Tokyo\u0026#34; ... 上の設定は日本時間ですが、その値はPHPでは以下でリストされていて、 https://www.php.net/manual/ja/timezones.php アジアは、以下でリストされています。 https://www.php.net/manual/ja/timezones.asia.php\nDateTimeZone アプリで固定のタイムゾーン（つまり、APP_TIMEZONEを指定）でなく、タイムゾーンをダイナミックに変えたいなら、 さきのタイムゾーンの定数を、以下のDateTimeZoneのクラスのインスタンス作成時に指定します。 https://www.php.net/manual/ja/class.datetimezone.php\nuse DateTimeZone; $tz = new DateTimeZone(\u0026#34;Asia/Tokyo\u0026#34;); Carbonではこのタイムゾーンのインスタンスを使って、UTCの時間を指定のタイムゾーンの時間に換算します。\nCarbon::now($tz); 直接、タイムゾーンの定数の指定も可能です。\nCarbon::now(\u0026#34;Asia/Tokyo\u0026#34;); OpeningHoursも同様にタイムゾーンの指定が可能です。\nOpeningHours::create($dates, $tz); Livewireのコンポーネントにタイムゾーンを 前回のコードをもとにタイムゾーンを取り入れました。以下で$this-\u003etzの箇所を見てください。 app/livewire/Calenar.php namespace App\\Livewire; use DateTimeZone; use Carbon\\Carbon; use Yasumi\\Yasumi; use Livewire\\Component; use Livewire\\Attributes\\Url; use Spatie\\OpeningHours\\OpeningHours; class Calendar extends Component { class Calendar extends Component { #[Url] public int $year; #[Url] public int $month; public $calendar = []; const TZ = \u0026#39;Asia/Tokyo\u0026#39;; private DateTimeZone $tz;//プロパティではないので、privateで宣言 public function boot() { $this-\u0026gt;tz = new DateTimeZone(self::TZ); } public function mount() { $this-\u0026gt;year ??= Carbon::now($this-\u0026gt;tz)-\u0026gt;year; $this-\u0026gt;month ??= Carbon::now($this-\u0026gt;tz)-\u0026gt;month; } public function previousMonth(): void { $this-\u0026gt;month--; if ($this-\u0026gt;month \u0026lt; 1) { $this-\u0026gt;month = 12; $this-\u0026gt;year--; } } public function nextMonth(): void { $this-\u0026gt;month++; if ($this-\u0026gt;month \u0026gt; 12) { $this-\u0026gt;month = 1; $this-\u0026gt;year++; } } public function render(): \\Illuminate\\View\\View { $openingHours = $this-\u0026gt;openingHours(); $this-\u0026gt;generateCalendar($openingHours); return view(\u0026#39;livewire.calendar\u0026#39;); } // アクションではないのでprivateとする private function openingHours(): OpeningHours　{ $yasumi = Yasumi::create(\u0026#39;Japan\u0026#39;, $this-\u0026gt;year, \u0026#39;ja_JP\u0026#39;); $holidays = collect($yasumi-\u0026gt;getHolidayDates()) -\u0026gt;mapWithKeys(function ($date) { return [$date =\u0026gt; []];}) -\u0026gt;all(); $dates = [ \u0026#39;monday\u0026#39; =\u0026gt; [\u0026#39;09:00-12:00\u0026#39;, \u0026#39;13:00-18:00\u0026#39;], \u0026#39;tuesday\u0026#39; =\u0026gt; [\u0026#39;09:00-12:00\u0026#39;, \u0026#39;13:00-18:00\u0026#39;], \u0026#39;wednesday\u0026#39; =\u0026gt; [\u0026#39;09:00-12:00\u0026#39;], \u0026#39;thursday\u0026#39; =\u0026gt; [\u0026#39;09:00-12:00\u0026#39;, \u0026#39;13:00-18:00\u0026#39;], \u0026#39;friday\u0026#39; =\u0026gt; [\u0026#39;09:00-12:00\u0026#39;, \u0026#39;13:00-20:00\u0026#39;], \u0026#39;saturday\u0026#39; =\u0026gt; [\u0026#39;09:00-12:00\u0026#39;, \u0026#39;13:00-16:00\u0026#39;], \u0026#39;sunday\u0026#39; =\u0026gt; [], \u0026#39;exceptions\u0026#39; =\u0026gt; $holidays, ]; return OpeningHours::create($dates, $this-\u0026gt;tz); } // アクションではないのでprivateとする private function generateCalendar(OpeningHours $openingHours): void { $startOfMonth = Carbon::createFromDate($this-\u0026gt;year, $this-\u0026gt;month, 1, $this-\u0026gt;tz); $endOfMonth = $startOfMonth-\u0026gt;copy()-\u0026gt;endOfMonth(); $startDayOfWeek = $startOfMonth-\u0026gt;dayOfWeek; $totalDays = $endOfMonth-\u0026gt;day; $this-\u0026gt;calendar = []; $week = []; for ($i = 0; $i \u0026lt; $startDayOfWeek; $i++) { $week[] = null; } $today = Carbon::today(); for ($day = 1; $day \u0026lt;= $totalDays; $day++) { $date = Carbon::createFromDate($this-\u0026gt;year, $this-\u0026gt;month, $day, $this-\u0026gt;tz); $dayInfo = [ \u0026#39;day\u0026#39; =\u0026gt; $day, \u0026#39;isHoliday\u0026#39; =\u0026gt; ! $openingHours-\u0026gt;isOpenOn($date-\u0026gt;toDateString()), ]; $dayInfo[\u0026#39;hours\u0026#39;] = []; if ($date-\u0026gt;greaterThanOrEqualTo($today)) { $hours = $openingHours-\u0026gt;forDate($date-\u0026gt;toDateTime()); foreach ($hours as $hour) { $dayInfo[\u0026#39;hours\u0026#39;][]= $hour-\u0026gt;format(); } } $week[] = $dayInfo; if (count($week) == 7) { $this-\u0026gt;calendar[] = $week; $week = []; } } if (count($week) \u0026gt; 0) { while (count($week) \u0026lt; 7) { $week[] = null; } $this-\u0026gt;calendar[] = $week; } } } 上のコードでタイムゾーンの対応だけでなく、以下のような前回のコードのリファクターもあるので注意してください。\nプロパティの$yearと$monthの宣言の上の行に、#[Url]のアトリビュートが使われています。これは、ブラウザのUrlのパラメータを反映させるためです（後に説明します）。 タイムゾーンの初期化は、mount()ではなくboot()で行っています。これは、mount()はLivewireのコンポーネントを含む画面がロードされるときに１回だけコールされますが、boot()は、コンポーネントのライフサイクルにおいて毎回コールされるからです（後に説明します）。 前回のコードでは、openHours()やgenerateCalendar()の関数の定義は、publicでしたが上のコードではprivateとしています。Livewireのコンポーネントのpublicの関数（例えば、previousMonth()）はアクションとしてクライアントからのアクセスが可能となるからです。 URLパラメータ 上のコードで、馴染みのないアトリビュートが登場しました。以下の#[Url]の部分です。 ... class Calendar extends Component { #[Url] public int $year; #[Url] public int $month; ... これは、php8から導入されたアトリビュートといわれるもので、Livewireではそのアトリビュートの次の行で宣言されている変数($yearや$month)のUrlにおけるパラメータを自動的に取り込むことを指示します。\n例えば、\nhttp://127.0.0.1:8001/calendar?month=1\u0026amp;year=2025 がブラウザのUrlとすると、コンポーネントの$yearと$monthのプロパティは、2025と1にそれぞれ初期化され、2025年の1月のカレンダーを表示してくれます。つまり、Urlにパラメータをつけることで欲しいの月のカレンダーを表示させることができるのです。便利ですね。\nhttp://127.0.0.:8001/calendar とパラメータがないなら、それらの変数は今日の年と月にmount()で初期化されます。\nLivewireのライフサイクル タイムゾーンの情報を保つ$tzの変数は、上のコードではmount()でなくboot()で初期化を行いましたが、どうしてそうなのでしょう。それには、コンポーネントで定義されている関数がコールされる順番の理解が必要です。 まず、ブラウザにおいて最初にカレンダーを表示したときは、以下の順番でコールされます。\nboot() → mount() → render() しかし、そのカレンダーにおいて、「次」のボタンを押して次の月へ移動するとき、つまりリアクティブのリクエストとなるときは、\nboot() → nextMonth() → render() の順番のコールとなり、mount()はコールされません。mount()で$tzを初期化してたら、render()でコールされるときにopeningHours()内で未初期化の変数のアクセスエラーとなります。\n","date":"2024-11-04T06:31:17+09:00","permalink":"https://www.larajapan.com/2024/11/04/%E3%82%AB%E3%83%AC%E3%83%B3%E3%83%80%E3%83%BC%EF%BC%88%EF%BC%94%EF%BC%89%E3%82%BF%E3%82%A4%E3%83%A0%E3%82%BE%E3%83%BC%E3%83%B3/","title":"カレンダー（４）タイムゾーン"},{"content":"Livewireを使用してカレンダーをリアクティブにしたところで、次はある店舗のカレンダーと見立てて休日と営業時間の表示をします。２つパッケージを使います。１つは、Laravelのパッケージをたくさん提供することで有名なspatieのopening-hoursのパッケージ、もう１つはいろいろな国の祭日のデータを提供するパッケージyasumiです。\n欲しいもの まず、完成品のカレンダーはこんなものです。\nこの店舗のカレンダーでは休日がピンクの背景色となっています。店舗は日曜日が定休日で祭日も休みとなります。さらに、このカレンダーを作成した当日を10/25としてそれ以降の営業日においての営業時間を表示しています。\nopening-hours：営業時間 営業時間のデータを扱うにはこのパッケージが最適です。 https://github.com/spatie/opening-hours\nこのパッケージでは以下のように、曜日ごとの営業時間の配列を渡してOpeningHoursのインスタンスを作成します。\nuse Spatie\\OpeningHours\\OpeningHours; $openingHours = OpeningHours::create([ \u0026#39;monday\u0026#39; =\u0026gt; [\u0026#39;09:00-12:00\u0026#39;, \u0026#39;13:00-18:00\u0026#39;], \u0026#39;tuesday\u0026#39; =\u0026gt; [\u0026#39;09:00-12:00\u0026#39;, \u0026#39;13:00-18:00\u0026#39;], \u0026#39;wednesday\u0026#39; =\u0026gt; [\u0026#39;09:00-12:00\u0026#39;], \u0026#39;thursday\u0026#39; =\u0026gt; [\u0026#39;09:00-12:00\u0026#39;, \u0026#39;13:00-18:00\u0026#39;], \u0026#39;friday\u0026#39; =\u0026gt; [\u0026#39;09:00-12:00\u0026#39;, \u0026#39;13:00-20:00\u0026#39;], \u0026#39;saturday\u0026#39; =\u0026gt; [\u0026#39;09:00-12:00\u0026#39;, \u0026#39;13:00-16:00\u0026#39;], \u0026#39;sunday\u0026#39; =\u0026gt; [], \u0026#39;exceptions\u0026#39; =\u0026gt; [ \u0026#39;2024-11-11\u0026#39; =\u0026gt; [\u0026#39;09:00-12:00\u0026#39;], \u0026#39;2024-12-25\u0026#39; =\u0026gt; [], \u0026#39;01-01\u0026#39; =\u0026gt; [], // 毎年の１月１日は休み \u0026#39;12-25\u0026#39; =\u0026gt; [\u0026#39;09:00-12:00\u0026#39;], // 毎年の１２月２５日は午前中だけ ], ]); 上では、\n毎週、月、火、木、金、土が同じ営業時間で、水曜日が朝だけの定期的な営業時間です。日曜は、空の配列ということで休みです 非定期は、exceptionsの配列に入れます。2024-11-11のように特定な日にちで指定できますし、01-01のように毎年繰り返す日にちの営業時間も指定が可能です さて、この営業時間を含むインスタンスをこしらえると、それに対していろいろなことを問うことが可能となります。\n例えば、\n月曜日は開いている？\n$openingHours-\u0026gt;isOpenOn(\u0026#39;monday\u0026#39;); true 日曜日は開いている？\n$openingHours-\u0026gt;isOpenOn(\u0026#39;sunday\u0026#39;); false 10/30日の19:00は開いている？\n$openingHours-\u0026gt;isOpenAt(new DateTime(\u0026#39;2024-10-30 19:00:00\u0026#39;)); false クリスマスの日は開いている？\n$openingHours-\u0026gt;isOpenOn(\u0026#39;2024-12-25\u0026#39;); false などなど、もっといろいろな関数があります。\nそしてもちろん、特定な日を指定して、その日の営業時間のデータの取得も可能です。\n$openHours = $openingHours-\u0026gt;forDate(new DateTime(\u0026#39;2024-10-25\u0026#39;)); $hours = []; foreach($openHours as $hour) { $hours[] = $hour-\u0026gt;format(); } print_r($hours); Array ( [0] =\u0026gt; 09:00-12:00 [1] =\u0026gt; 13:00-20:00 ) これが先のカレンダーで必要とされる関数となります。\n注意することは１つ、このopening-hoursで使用される関数に渡す日付は皆、馴染みのCarbonではなくphpのDateTimeであることです。 もちろん、CarbonもDateTimeのWrapperのようなもので、\nCarbon\\Carbon::create(\u0026#34;2025-10-25\u0026#34;)-\u0026gt;toDateTime(); DateTime @1761350400 {#3156 date: 2025-10-25 00:00:00.0 UTC (+00:00), } とDateTimeに簡単に変換が可能です。\nyasumi：祭日 祭日は国のよって違いますので、以下のパッケージを使います。\nhttps://github.com/azuyalabs/yasumi\nyasumiはもちろん日本語の「休み」からの由来です。\n早速、今年の日本の祭日を出してみましょう！\nuse Yasumi\\Yasumi; // 国名と年を渡すだけ $yasumi = Yasumi::create(\u0026#34;Japan\u0026#34;, \u0026#34;2024\u0026#34;); $holidays = $yasumi-\u0026gt;getHolidayDates(); print_r($holidays); Array ( [newYearsDay] =\u0026gt; 2024-01-01 [comingOfAgeDay] =\u0026gt; 2024-01-08 [nationalFoundationDay] =\u0026gt; 2024-02-11 [substituteHoliday:nationalFoundationDay] =\u0026gt; 2024-02-12 [emperorsBirthday] =\u0026gt; 2024-02-23 [vernalEquinoxDay] =\u0026gt; 2024-03-20 [showaDay] =\u0026gt; 2024-04-29 [constitutionMemorialDay] =\u0026gt; 2024-05-03 [greeneryDay] =\u0026gt; 2024-05-04 [childrensDay] =\u0026gt; 2024-05-05 [substituteHoliday:childrensDay] =\u0026gt; 2024-05-06 [marineDay] =\u0026gt; 2024-07-15 [mountainDay] =\u0026gt; 2024-08-11 [substituteHoliday:mountainDay] =\u0026gt; 2024-08-12 [respectfortheAgedDay] =\u0026gt; 2024-09-16 [autumnalEquinoxDay] =\u0026gt; 2024-09-22 [substituteHoliday:autumnalEquinoxDay] =\u0026gt; 2024-09-23 [sportsDay] =\u0026gt; 2024-10-14 [cultureDay] =\u0026gt; 2024-11-03 [substituteHoliday:cultureDay] =\u0026gt; 2024-11-04 [laborThanksgivingDay] =\u0026gt; 2024-11-23 ) 国名と年を渡すだけで祭日のデータ得られます。このデータを、先のopeningHoursのexceptionsに含んだら、店舗の休日となりますね。\n店舗のカレンダー ということで、これら２つのパッケージを使い以下のLivewireのコンポーネントとなります。\napp/Livewire/Calendar.php namespace App\\Livewire; use Carbon\\Carbon; use Yasumi\\Yasumi; use Livewire\\Component; use Spatie\\OpeningHours\\OpeningHours; class Calendar extends Component { public int $year; public int $month; public $calendar = []; public function mount() { $this-\u0026gt;year ??= Carbon::now()-\u0026gt;year; $this-\u0026gt;month ??= Carbon::now()-\u0026gt;month; } public function generateCalendar(OpeningHours $openingHours): void { $startOfMonth = Carbon::createFromDate($this-\u0026gt;year, $this-\u0026gt;month, 1); $endOfMonth = $startOfMonth-\u0026gt;copy()-\u0026gt;endOfMonth(); $startDayOfWeek = $startOfMonth-\u0026gt;dayOfWeek; $totalDays = $endOfMonth-\u0026gt;day; $this-\u0026gt;calendar = []; $week = []; for ($i = 0; $i \u0026lt; $startDayOfWeek; $i++) { $week[] = null; } $today = Carbon::today(); for ($day = 1; $day \u0026lt;= $totalDays; $day++) { $date = Carbon::createFromDate($this-\u0026gt;year, $this-\u0026gt;month, $day); $dayInfo = [ \u0026#39;day\u0026#39; =\u0026gt; $day, \u0026#39;isHoliday\u0026#39; =\u0026gt; ! $openingHours-\u0026gt;isOpenOn($date-\u0026gt;toDateString()), ]; $dayInfo[\u0026#39;hours\u0026#39;] = []; if ($date-\u0026gt;greaterThanOrEqualTo($today)) { $hours = $openingHours-\u0026gt;forDate($date-\u0026gt;toDateTime()); foreach ($hours as $hour) { $dayInfo[\u0026#39;hours\u0026#39;][]= $hour-\u0026gt;format(); } } $week[] = $dayInfo; if (count($week) == 7) { $this-\u0026gt;calendar[] = $week; $week = []; } } if (count($week) \u0026gt; 0) { while (count($week) \u0026lt; 7) { $week[] = null; } $this-\u0026gt;calendar[] = $week; } } public function previousMonth(): void { $this-\u0026gt;month--; if ($this-\u0026gt;month \u0026lt; 1) { $this-\u0026gt;month = 12; $this-\u0026gt;year--; } } public function nextMonth(): void { $this-\u0026gt;month++; if ($this-\u0026gt;month \u0026gt; 12) { $this-\u0026gt;month = 1; $this-\u0026gt;year++; } } public function openingHours(): OpeningHours { $yasumi = Yasumi::create(\u0026#39;Japan\u0026#39;, $this-\u0026gt;year); // 以下で祭日を取得して、[\u0026#39;2024-01-01\u0026#39; =\u0026gt; [], \u0026#39;2024-01-08\u0026#39; =\u0026gt; [] ...]の配列に変換 $holidays = collect($yasumi-\u0026gt;getHolidayDates()) -\u0026gt;mapWithKeys(function ($date) { return [$date =\u0026gt; []];}) -\u0026gt;all(); $dates = [ \u0026#39;monday\u0026#39; =\u0026gt; [\u0026#39;09:00-12:00\u0026#39;, \u0026#39;13:00-18:00\u0026#39;], \u0026#39;tuesday\u0026#39; =\u0026gt; [\u0026#39;09:00-12:00\u0026#39;, \u0026#39;13:00-18:00\u0026#39;], \u0026#39;wednesday\u0026#39; =\u0026gt; [\u0026#39;09:00-12:00\u0026#39;], \u0026#39;thursday\u0026#39; =\u0026gt; [\u0026#39;09:00-12:00\u0026#39;, \u0026#39;13:00-18:00\u0026#39;], \u0026#39;friday\u0026#39; =\u0026gt; [\u0026#39;09:00-12:00\u0026#39;, \u0026#39;13:00-20:00\u0026#39;], \u0026#39;saturday\u0026#39; =\u0026gt; [\u0026#39;09:00-12:00\u0026#39;, \u0026#39;13:00-16:00\u0026#39;], \u0026#39;sunday\u0026#39; =\u0026gt; [], \u0026#39;exceptions\u0026#39; =\u0026gt; $holidays, ]; return OpeningHours::create($dates); } public function render(): \\Illuminate\\View\\View { $openingHours = $this-\u0026gt;openingHours(); $this-\u0026gt;generateCalendar($openingHours); return view(\u0026#39;livewire.calendar\u0026#39;); } } コンポーネントのブレードは、以下となります。\n\u0026lt;div class=\u0026#34;container mx-auto p-4\u0026#34;\u0026gt; \u0026lt;h1 class=\u0026#34;text-2xl font-bold mb-4 text-center\u0026#34;\u0026gt;{{ $year }}年{{ $month }}月\u0026lt;/h1\u0026gt; \u0026lt;div class=\u0026#34;flex justify-between mb-4\u0026#34;\u0026gt; \u0026lt;button wire:click=\u0026#34;previousMonth\u0026#34; class=\u0026#34;bg-blue-500 text-white px-4 py-2 rounded\u0026#34;\u0026gt;前\u0026lt;/button\u0026gt; \u0026lt;button wire:click=\u0026#34;nextMonth\u0026#34; class=\u0026#34;bg-blue-500 text-white px-4 py-2 rounded\u0026#34;\u0026gt;次\u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;table class=\u0026#34;min-w-full bg-white border border-gray-200\u0026#34;\u0026gt; \u0026lt;thead\u0026gt; \u0026lt;tr\u0026gt; \u0026lt;th class=\u0026#34;py-2 px-4 border border-gray-200 bg-gray-100\u0026#34;\u0026gt;日\u0026lt;/th\u0026gt; \u0026lt;th class=\u0026#34;py-2 px-4 border border-gray-200 bg-gray-100\u0026#34;\u0026gt;月\u0026lt;/th\u0026gt; \u0026lt;th class=\u0026#34;py-2 px-4 border border-gray-200 bg-gray-100\u0026#34;\u0026gt;火\u0026lt;/th\u0026gt; \u0026lt;th class=\u0026#34;py-2 px-4 border border-gray-200 bg-gray-100\u0026#34;\u0026gt;水\u0026lt;/th\u0026gt; \u0026lt;th class=\u0026#34;py-2 px-4 border border-gray-200 bg-gray-100\u0026#34;\u0026gt;木\u0026lt;/th\u0026gt; \u0026lt;th class=\u0026#34;py-2 px-4 border border-gray-200 bg-gray-100\u0026#34;\u0026gt;金\u0026lt;/th\u0026gt; \u0026lt;th class=\u0026#34;py-2 px-4 border border-gray-200 bg-gray-100\u0026#34;\u0026gt;土\u0026lt;/th\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;/thead\u0026gt; \u0026lt;tbody\u0026gt; @foreach ($calendar as $week) \u0026lt;tr\u0026gt; @foreach ($week as $day) @if (is_null($day)) \u0026lt;td class=\u0026#34;py-2 px-4 border border-gray-200\u0026#34;\u0026gt;\u0026lt;/td\u0026gt; @else \u0026lt;td class=\u0026#34;py-2 px-4 border border-gray-200 align-top {{ $day[\u0026#39;isHoliday\u0026#39;] ? \u0026#39;bg-red-100\u0026#39; : \u0026#39;\u0026#39; }}\u0026#34;\u0026gt; {{ $day[\u0026#39;day\u0026#39;] }} \u0026lt;div class=\u0026#34;text-sm text-blue-500\u0026#34;\u0026gt; {!! implode(\u0026#39;\u0026lt;br\u0026gt;\u0026#39;, $day[\u0026#39;hours\u0026#39;]) !!} \u0026lt;/div\u0026gt; \u0026lt;/td\u0026gt; @endif @endforeach \u0026lt;/tr\u0026gt; @endforeach \u0026lt;/tbody\u0026gt; \u0026lt;/table\u0026gt; \u0026lt;/div\u0026gt; もちろん、以下の作業を忘れずに。\n$ composer require spatie/opening-hours $ composer require azuyalabs/yasumi ","date":"2024-10-29T11:47:49+09:00","permalink":"https://www.larajapan.com/2024/10/29/%E3%82%AB%E3%83%AC%E3%83%B3%E3%83%80%E3%83%BC%EF%BC%88%EF%BC%93%EF%BC%89%E4%BC%91%E6%97%A5%E3%81%A8%E5%96%B6%E6%A5%AD%E6%99%82%E9%96%93%E3%81%AE%E8%A1%A8%E7%A4%BA/","title":"カレンダー（３）休日と営業時間の表示"},{"content":"AI(Copilot)でコードを作成してカレンダーを表示できましたが、今度は今月の月間カレンダーだけなく前月と翌月のボタンを追加してクリックしたら表示をリアクティブに変更できるようにします。\nLivewireに変換 前回で作成したカレンダー生成の関数を用いることになりますが、今回もCopilotを使って開発します。 さあ、働いてもらいましょう！\n\u0026gt; Laravel Livewireを用いて、先月と翌月を表示できるようにしてください この結果としてLivewireのコンポーネント、それが使用するブレード、そのコンポーネントを含むブレード、とルートの定義が出力されるのですが、残念なことにこれがLivewireの最新のバージョン３ではなく、バージョン２の出力なのです。\nおかしいと思い、問いかけます。\n\u0026gt; どのバージョンのLivewireを知っていますか？ 私はLivewireの最新バージョンであるLivewire 3を知っています。Livewire 3は、リアクティブなUIを構築するための最新の機能と改善を提供しています。 そうなら、\n\u0026gt; そうなら、Livewire3を使って書き直してください とプッシュ。しかし残念なことに出力される結果は先ほどと同じとなりました。\nどうやらLivewireのバージョン３の知識はないみたいです。\nということで、Copilotで出力されたコードと私のLivewireの学習、つまり人間とのコラボにより以下のコード生成となりました。\nコンポーネントの作成 最新のバージョンのLaravelでプロジェクトを作成して、以下で最新（バージョン３ですよ）のLivewireのパッケージをインストールします。 $ composer require livewire/livewire 以下でコンポーネントを作成します。\n$ php artisan make:livewire Calendar 作成されるファイルは２つあります。\nnew file: app/Livewire/Calendar.php new file: resources/views/livewire/calendar.blade.php まず、Calendar.phpを編集します。ここではCopilotで出力されたコードを入れますが、注意するのは、namespaceがバージョン３のLivewireでは、App\\Http\\LivewireからApp\\Livewireに変わっています。\napp/Livewie/Calendar.php namespace App\\Livewire; use Livewire\\Component; use Carbon\\Carbon; class Calendar extends Component { public $year; public $month; public $calendar = []; public function mount($year = null, $month = null) { $this-\u0026gt;year = $year ?? Carbon::now()-\u0026gt;year; $this-\u0026gt;month = $month ?? Carbon::now()-\u0026gt;month; $this-\u0026gt;generateCalendar(); } public function generateCalendar() { $startOfMonth = Carbon::createFromDate($this-\u0026gt;year, $this-\u0026gt;month, 1); $endOfMonth = $startOfMonth-\u0026gt;copy()-\u0026gt;endOfMonth(); $startDayOfWeek = $startOfMonth-\u0026gt;dayOfWeek; $totalDays = $endOfMonth-\u0026gt;day; $this-\u0026gt;calendar = []; $week = []; for ($i = 0; $i \u0026lt; $startDayOfWeek; $i++) { $week[] = null; } for ($day = 1; $day \u0026lt;= $totalDays; $day++) {\u0026lt;strong\u0026gt; $week[] = $day; if (count($week) == 7) { $this-\u0026gt;calendar[] = $week; $week = []; } } if (count($week) \u0026gt; 0) { while (count($week) \u0026lt; 7) { $week[] = null; } $this-\u0026gt;calendar[] = $week; } } public function previousMonth() { $this-\u0026gt;month--; if ($this-\u0026gt;month \u0026lt; 1) { $this-\u0026gt;month = 12; $this-\u0026gt;year--; } $this-\u0026gt;generateCalendar(); } public function nextMonth() { $this-\u0026gt;month++; if ($this-\u0026gt;month \u0026gt; 12) { $this-\u0026gt;month = 1; $this-\u0026gt;year++; } $this-\u0026gt;generateCalendar(); } public function render() { return view(\u0026#39;livewire.calendar\u0026#39;); } } コンポーネントに含まれる、generateCalendar()は前回と似ていますが、このクラスのプロパティの$month, $yearにより返す値が変わるようになっています。また、前月を表示するpreviousMonth()と次月を表示するnextMonth()のメソッドが追加されています。\n今度はブレードですが、これもCopilotの出力ををもとに英語から日本語に私が編集しました。wire:clickにおいて先ほど追加関数名が指定されています。これらはLivewireのためのコードで、これによりカレンダーで表示する月を変えることができます。\n/resources/views/livewire/calendar.blade.php \u0026lt;div\u0026gt; \u0026lt;table\u0026gt; \u0026lt;caption\u0026gt; \u0026lt;button wire:click=\u0026#34;previousMonth\u0026#34;\u0026gt;前\u0026lt;/button\u0026gt; \u0026lt;strong\u0026gt;{{ $year }}年{{ $month }}月\u0026lt;/strong\u0026gt; \u0026lt;button wire:click=\u0026#34;nextMonth\u0026#34;\u0026gt;次\u0026lt;/button\u0026gt; \u0026lt;/caption\u0026gt; \u0026lt;thead\u0026gt; \u0026lt;tr\u0026gt; \u0026lt;th\u0026gt;日\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;月\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;火\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;水\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;木\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;金\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;土\u0026lt;/th\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;/thead\u0026gt; \u0026lt;tbody\u0026gt; @foreach ($calendar as $week) \u0026lt;tr\u0026gt; @foreach ($week as $day) @if (is_null($day)) \u0026lt;td\u0026gt;\u0026lt;/td\u0026gt; @else \u0026lt;td\u0026gt;{{ $day }}\u0026lt;/td\u0026gt; @endif @endforeach \u0026lt;/tr\u0026gt; @endforeach \u0026lt;/tbody\u0026gt; \u0026lt;/table\u0026gt; \u0026lt;/div\u0026gt; ルートの定義は以下となりますが、これはコンポーネントを全画面として表示する設定です。\nroutes/web.php use App\\Livewire\\Calendar; use Illuminate\\Support\\Facades\\Route; Route::get(\u0026#39;/calendar\u0026#39;, Calendar::class); そして、この全画面表示のためにレイアウトのブレードが必要です。\nresources/views/components/layouts/app.blade.php \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;{{ str_replace(\u0026#39;_\u0026#39;, \u0026#39;-\u0026#39;, app()-\u0026gt;getLocale()) }}\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;utf-8\u0026#34;\u0026gt; \u0026lt;meta name=\u0026#34;viewport\u0026#34; content=\u0026#34;width=device-width, initial-scale=1.0\u0026#34;\u0026gt; \u0026lt;title\u0026gt;{{ $title ?? \u0026#39;Page Title\u0026#39; }}\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; {{ $slot }} \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; これで完了です。\nう～ん。ちょっと醜いですね。Copilotに少し恰好良くしてもらいましょう。\n\u0026gt; コンポーネントのブレードをTailwind cssを利用してベターなルックにしてください 以下のようにブレードは変わります。\n/resources/views/livewire/calendar.blade.php \u0026lt;div class=\u0026#34;container mx-auto p-4\u0026#34;\u0026gt; \u0026lt;h1 class=\u0026#34;text-2xl font-bold mb-4 text-center\u0026#34;\u0026gt;{{ $year }}年{{ $month }}月\u0026lt;/h1\u0026gt; \u0026lt;div class=\u0026#34;flex justify-between mb-4\u0026#34;\u0026gt; \u0026lt;button wire:click=\u0026#34;previousMonth\u0026#34; class=\u0026#34;bg-blue-500 text-white px-4 py-2 rounded\u0026#34;\u0026gt;前\u0026lt;/button\u0026gt; \u0026lt;button wire:click=\u0026#34;nextMonth\u0026#34; class=\u0026#34;bg-blue-500 text-white px-4 py-2 rounded\u0026#34;\u0026gt;次\u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;table class=\u0026#34;min-w-full bg-white border border-gray-200\u0026#34;\u0026gt; \u0026lt;thead\u0026gt; \u0026lt;tr\u0026gt; \u0026lt;th class=\u0026#34;py-2 px-4 border border-gray-200 bg-gray-100\u0026#34;\u0026gt;日\u0026lt;/th\u0026gt; \u0026lt;th class=\u0026#34;py-2 px-4 border border-gray-200 bg-gray-100\u0026#34;\u0026gt;月\u0026lt;/th\u0026gt; \u0026lt;th class=\u0026#34;py-2 px-4 border border-gray-200 bg-gray-100\u0026#34;\u0026gt;火\u0026lt;/th\u0026gt; \u0026lt;th class=\u0026#34;py-2 px-4 border border-gray-200 bg-gray-100\u0026#34;\u0026gt;水\u0026lt;/th\u0026gt; \u0026lt;th class=\u0026#34;py-2 px-4 border border-gray-200 bg-gray-100\u0026#34;\u0026gt;木\u0026lt;/th\u0026gt; \u0026lt;th class=\u0026#34;py-2 px-4 border border-gray-200 bg-gray-100\u0026#34;\u0026gt;金\u0026lt;/th\u0026gt; \u0026lt;th class=\u0026#34;py-2 px-4 border border-gray-200 bg-gray-100\u0026#34;\u0026gt;土\u0026lt;/th\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;/thead\u0026gt; \u0026lt;tbody\u0026gt; @foreach ($calendar as $week) \u0026lt;tr\u0026gt; @foreach ($week as $day) @if (is_null($day)) \u0026lt;td class=\u0026#34;py-2 px-4 border border-gray-200\u0026#34;\u0026gt;\u0026lt;/td\u0026gt; @else \u0026lt;td class=\u0026#34;py-2 px-4 border border-gray-200\u0026#34;\u0026gt; {{ $day }} \u0026lt;/td\u0026gt; @endif @endforeach \u0026lt;/tr\u0026gt; @endforeach \u0026lt;/tbody\u0026gt; \u0026lt;/table\u0026gt; \u0026lt;/div\u0026gt; レイアウトの方は、以下となります。\nresources/views/components/layouts/app.blade.php \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;{{ str_replace(\u0026#39;_\u0026#39;, \u0026#39;-\u0026#39;, app()-\u0026gt;getLocale()) }}\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;utf-8\u0026#34;\u0026gt; \u0026lt;meta name=\u0026#34;viewport\u0026#34; content=\u0026#34;width=device-width, initial-scale=1.0\u0026#34;\u0026gt; \u0026lt;title\u0026gt;{{ $title ?? \u0026#39;Page Title\u0026#39; }}\u0026lt;/title\u0026gt; \u0026lt;link href=\u0026#34;https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css\u0026#34; rel=\u0026#34;stylesheet\u0026#34;\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body class=\u0026#34;bg-gray-100\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;container mx-auto p-4\u0026#34;\u0026gt; {{ $slot }} \u0026lt;/div\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 以下となりました。レスポンシブ対応ともなっています。\n以上、Copilotと私のコラボの作品です。CopilotのLivewireの知識が最新でないのは残念ですが、知ったふりをしては欲しくないですね。\n","date":"2024-10-20T11:24:17+09:00","permalink":"https://www.larajapan.com/2024/10/20/%E3%82%AB%E3%83%AC%E3%83%B3%E3%83%80%E3%83%BC%EF%BC%88%EF%BC%92%EF%BC%89livewire%E3%82%92%E4%BD%BF%E3%81%84%E3%83%AA%E3%82%A2%E3%82%AF%E3%83%86%E3%82%A3%E3%83%96%E3%81%A8%E3%81%99%E3%82%8B/","title":"カレンダー（２）Livewireを使いリアクティブとする"},{"content":"カレンダーに関連するプロジェクトは、そのダイナミックなデータの性質上とても面白いです。また、店舗休日を表示するための月間カレンダーの生成とか、場所や部屋や物の予約のためのカレンダーのアプリとか実用的なものが多いです。私のお客さんのサイトでもチェックアウト画面でお客さんが届け日を指定するために月間のカレンダーが表示されます。カレンダーを題材にLaravelでどんなことが簡単にできるかをいくつかの投稿に分けて紹介します。\n欲しいもの 簡単なところからということで、まず今月のカレンダーの表示から始めます。これを書いているのは2024年の10月なので、以下のような表示です。 Copilotでカレンダーの生成 私が生きている間にAIが開発者をヘルプできる時代になるとはまったく信じられなかったことですが、私も時代の流れに逆らえず、開発者のツールとしてマイクロソフトのCopilotを常時使用しています。Copilotは現時点ではChatGPT4を使用しており、VSCODEエディターの中からAIとの対話が可能です。 まず、以下の質問をCopilotに問います。\n\u0026gt; 今月の月間のカレンダーを週と日にちの配列として生成するphp関数をCarbonのパッケージを使用して作成してください 以下のコードを作成してくれました。\n以下は、Carbonパッケージを使用して今月の月間カレンダーを週と日にちの配列として生成するPHP関数です。 use Carbon\\Carbon; function getMonthlyCalendar() { // 現在の月と年を取得 $now = Carbon::now(); $startOfMonth = $now-\u0026gt;copy()-\u0026gt;startOfMonth(); $endOfMonth = $now-\u0026gt;copy()-\u0026gt;endOfMonth(); // 月の最初の日の曜日を取得 $startDayOfWeek = $startOfMonth-\u0026gt;dayOfWeek; $totalDays = $endOfMonth-\u0026gt;day; // カレンダーを作成する配列 $calendar = []; $week = []; // 最初の週の空の日を追加 for ($i = 0; $i \u0026lt; $startDayOfWeek; $i++) { $week[] = null; // 空のセル } // 日付を週に追加 for ($day = 1; $day \u0026lt;= $totalDays; $day++) { $week[] = $day; // 7日ごとに新しい週を作成 if (count($week) == 7) { $calendar[] = $week; $week = []; } } // 最後の週を追加 if (count($week) \u0026gt; 0) { while (count($week) \u0026lt; 7) { $week[] = null; // 空のセル } $calendar[] = $week; } return $calendar; } 説明のコメントつきでわかりやすいです。月間カレンダーはたいてい7x5あるいは7x4のグリッドですが、先月となる日や翌月となる日を非表示とする配慮が必要です。 この関数が返すデータを見せてもらいましょう。\n\u0026gt; 今月は2024年10月として、上の関数が返す配列を見せてください [ [null, null, 1, 2, 3, 4, 5], // 1週目 [6, 7, 8, 9, 10, 11, 12], // 2週目 [13, 14, 15, 16, 17, 18, 19], // 3週目 [20, 21, 22, 23, 24, 25, 26], // 4週目 [27, 28, 29, 30, 31, null, null] // 5週目 ] 今月というのを説明しないと2023年の10月となってしまいますので注意を。 期待していた週と日の２次元の配列です。先月や翌月となっているところは、皆nullとなっています。\nここまでできるなら、後全部やってもらいましょう！\n\u0026gt; それを利用したLaravelのコントローラとブレードを作成してください 以下はCopilotからの出力です。\n以下は、提供されたコードを利用してLaravelのコントローラとブレードテンプレートを作成する方法です。 コントローラの作成 まず、CalendarControllerを完成させます。 app/Http/Controllers/CalendarController.php namespace App\\Http\\Controllers; use Illuminate\\Http\\Request; use Carbon\\Carbon; class CalendarController extends Controller { public function show() { $calendar = $this-\u0026gt;getMonthlyCalendar(); return view(\u0026#39;calendar\u0026#39;, compact(\u0026#39;calendar\u0026#39;)); } function getMonthlyCalendar() { // 現在の月と年を取得 $now = Carbon::now(); $startOfMonth = $now-\u0026gt;copy()-\u0026gt;startOfMonth(); $endOfMonth = $now-\u0026gt;copy()-\u0026gt;endOfMonth(); // 月の最初の日の曜日を取得 $startDayOfWeek = $startOfMonth-\u0026gt;dayOfWeek; $totalDays = $endOfMonth-\u0026gt;day; // カレンダーを作成する配列 $calendar = []; $week = []; // 最初の週の空の日を追加 for ($i = 0; $i \u0026lt; $startDayOfWeek; $i++) { $week[] = null; // 空のセル } // 日付を週に追加 for ($day = 1; $day \u0026lt;= $totalDays; $day++) { $week[] = $day; // 7日ごとに新しい週を作成 if (count($week) == 7) { $calendar[] = $week; $week = []; } } // 最後の週を追加 if (count($week) \u0026gt; 0) { // 残りの空の日を追加 for ($i = count($week); $i \u0026lt; 7; $i++) { $week[] = null; } $calendar[] = $week; } return $calendar; } } ルートの定義 次に、ルートを定義します。 routes/web.php use App\\Http\\Controllers\\CalendarController; Route::get(\u0026#39;/calendar\u0026#39;, [CalendarController::class, \u0026#39;show\u0026#39;]); ブレードテンプレートの作成 最後に、カレンダーを表示するためのブレードテンプレートを作成します。 resources/views/calendar.blade.php \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;title\u0026gt;Calendar\u0026lt;/title\u0026gt; \u0026lt;img src=\u0026#34;data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7\u0026#34; data-wp-preserve=\u0026#34;%3Cstyle%3E%0A%20%20%20%20%20%20%20%20table%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20width%3A%20100%25%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20border-collapse%3A%20collapse%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20th%2C%20td%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20border%3A%201px%20solid%20black%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20text-align%3A%20center%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20padding%3A%2010px%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20th%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20background-color%3A%20%23f2f2f2%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%3C%2Fstyle%3E\u0026#34; data-mce-resize=\u0026#34;false\u0026#34; data-mce-placeholder=\u0026#34;1\u0026#34; class=\u0026#34;mce-object\u0026#34; width=\u0026#34;20\u0026#34; height=\u0026#34;20\u0026#34; alt=\u0026#34;\u0026amp;lt;style\u0026amp;gt;\u0026#34; title=\u0026#34;\u0026amp;lt;style\u0026amp;gt;\u0026#34; /\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h1\u0026gt;Calendar for {{ \\Carbon\\Carbon::now()-\u0026gt;format(\u0026#39;F Y\u0026#39;) }}\u0026lt;/h1\u0026gt; \u0026lt;table\u0026gt; \u0026lt;tr\u0026gt; \u0026lt;th\u0026gt;Sunday\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;Monday\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;Tuesday\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;Wednesday\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;Thursday\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;Friday\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;Saturday\u0026lt;/th\u0026gt; \u0026lt;/tr\u0026gt; @foreach ($calendar as $week) \u0026lt;tr\u0026gt; @foreach ($week as $day) \u0026lt;td\u0026gt;{{ $day ?? \u0026#39;\u0026#39; }}\u0026lt;/td\u0026gt; @endforeach \u0026lt;/tr\u0026gt; @endforeach \u0026lt;/table\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 曜日は英語ですが、あっという間にできてしまいました。\n次の投稿では、Livewireを使ってカレンダーをリアクティブにします。\n","date":"2024-10-15T11:24:06+09:00","permalink":"https://www.larajapan.com/2024/10/15/%E3%82%AB%E3%83%AC%E3%83%B3%E3%83%80%E3%83%BC%EF%BC%88%EF%BC%91%EF%BC%89ai%E3%81%A7%E7%94%9F%E6%88%90/","title":"カレンダー（１）AIで生成"},{"content":"ここまで４回の投稿でCypressテストの設定・テストコードなどをご紹介してきました。５回目の今回は、laracasts/cypressというLaravelでCypressを使用するために作成されたパッケージをご紹介します。Laravel10.xのプロジェクトにインストールして使ってみました。\nCypressのセットアップや基本的なテストなど、以前の記事は以下からご覧いただけます。\nCypressセットアップ・実行方法 Cypress画面表示のテスト Cypressログイン・ログアウトのテスト Cypressパスワードリセットのテスト\nlaracasts/cypressインストール 今までご紹介したCypressのテストでは、テスト対象プロジェクトとCypressが全く別のディレクトリでも問題なかったのですが、今回ご紹介するlaracasts/cypressはLaravelのartisanコマンドやfactoryなどを使用するため、テスト対象のLaravelプロジェクト内にインストールする必要があります。\nではまずはCypressをインストールします。Laravelのプロジェクトルートで、以下のコマンドを実行します。\n% npm install cypress --save-dev 執筆時点で、cypress13.14.2がインストールされました。次にlaracasts/cypressのインストールです。以下のコマンドを実行します。\n% composer require laracasts/cypress --dev このコマンドで、laracasts/cypress3.0.2と関連パッケージがインストールされました。\n最後に、以下のコマンドで初期ファイルを作成します。コマンドを実行するとcypressのディレクトリをどこに作成するか？を質問されるので、任意のディレクトリを指定してください。ここではコマンドラインで提案された通りのtests/配下を指定しました。\n% php artisan cypress:boilerplate Where should we put the cypress directory? [tests/cypress]: \u0026gt; Updated tests/cypress/support/index.js Updated tests/cypress/plugins/index.js Created tests/cypress/plugins/swap-env.js Created tests/cypress/integration/example.cy.js Created tests/cypress/support/laravel-commands.js Created tests/cypress/support/laravel-routes.js Created tests/cypress/support/assertions.js Created tests/cypress/support/index.d.ts Created cypress.config.js Created .env.cypress ファイルが作成されました、さっそくCypressを立ち上げてみます。以下のコマンドを実行してください。\n% npx cypress open ここで、require is not defined in ES module scopeというエラーが発生しました。\nrequire is not defined の修正 Laravel10ではデフォルトでpackage.jsonに\u0026ldquo;type\u0026rdquo;: \u0026ldquo;module\u0026rdquo;が設定されているのですが、これとlaracasts/cypressで使用されているCommonJS形式の記述がマッチしないことが原因でエラーとなっているようです。\nこのエラーの修正方法には何パターンかありますが、ここではLaravelのデフォルトを変更せず、またできるだけ簡単にエラーを解消できるよう以下の修正を行います。\n１：ファイルの拡張子を.cjsに変更\nまず、Cypress実行の際にrequireされる以下の２ファイルの拡張子を.jsから.cjsへ変更します。\n/cypress.config.cjs // .jsから.cjsへ /tests/cypress/plugins/swap-env.cjs // .jsから.cjsへ ２：configファイルを修正\n次に、cypress.config.cjsファイルのe2e内、setupNodeEvents()に以下のtaskを追加します。\ncypress.config.cjs const { defineConfig } = require(\u0026#39;cypress\u0026#39;) module.exports = defineConfig({ ..... e2e: { setupNodeEvents(on, config) { // ここにtaskを追加（plugins/index.jsの内容を移動） on(\u0026#39;task\u0026#39;, require(\u0026#39;./tests/cypress/plugins/swap-env.cjs\u0026#39;)); //ここを追加 return config　//ここを追加 }, ..... }, }) ３：index.jsを削除\n２の変更により/tests/cypress/plugins/index.jsは不要になるので削除します。\nここまででエラー修正は完了です。\n４：baseUrlの指定\nconfigファイルを開いているので、テストの実行に必要なbaseUrlも設定しておきましょう。 同じくconfigファイルのe2e内にLaravelプロジェクトのURLをセットします。\ncypress.config.cjs const { defineConfig } = require(\u0026#39;cypress\u0026#39;) module.exports = defineConfig({ ..... e2e: { setupNodeEvents(on, config) { // return require(\u0026#39;./tests/cypress/plugins/index.js\u0026#39;)(on, config) // ここに元のplugins/index.jsの内容を移動 on(\u0026#39;task\u0026#39;, require(\u0026#39;./tests/cypress/plugins/swap-env.cjs\u0026#39;)); return config }, baseUrl: \u0026#39;http://127.0.0.1:8000\u0026#39;, //プロジェクトURLの指定 ..... }, }) ここまで進めたら、再度以下のコマンドでCypressを立ち上げます。\n% npx cypress open 問題なく起動したらダッシュボードを開いて、laracasts/cypressインストール時に自動生成されたexample.cy.jsを選択し、実行してみます。\nこちらもエラーなく成功したら準備は完了です。\nlaracasts/cypressのコマンド laracasts/cypressでは、Laravelプロジェクトのテストに便利なコマンドが用意されています。その中からいくつかテストでよく使うものをご紹介します。\nまずはcreate()から。factoyと連動して、テスト用データを作成できます。以下の例ではUserデータを作成しています。\ncy.create(\u0026#39;App\\\\Models\\\\User\u0026#39;, { name: \u0026#39;ユーザー名\u0026#39;, email: \u0026#39;login-test@example.com\u0026#39;, }); 次に、login()・logout()です。コマンド名の通りログイン・ログアウトの処理を実行するコマンドで、PHPUnitでauth()-\u0026gt;login($user)、auth()-\u0026gt;logout()と記述した時と同じ動作をしてくれます。以下のように、引数にユーザーの情報を渡して使用します。\ncy.login({ name: \u0026#39;ユーザー名\u0026#39; }); cy.logout({ email: \u0026#39;test@example.com\u0026#39;\u0026#39; }); そして次はcurrentUser()です。これはLaravelのauth()-\u0026gt;user()と同じく、現在認証されているユーザーを取得してくれます。取得したユーザー情報は、Cypressコマンドのits()と組み合わせることで任意のプロパティが取り出せます。以下の例では取得したユーザーのemailデータを取り出し、期待通りか確認しています。\ncy.currentUser().its(\u0026#39;email\u0026#39;).should(\u0026#39;eq\u0026#39;, \u0026#39;test@example.com\u0026#39;); 最後に、refreshDatabase()です。Laravelのmigrate:refreshをコールします。このコマンドは任意の場所で使用できますが、CypressのbeforeEach()コマンドと組み合わせて使用すると、各テストケースの前にDBをリフレッシュしてくれるので便利です。\nbeforeEach(() =\u0026gt; { cy.refreshDatabase(); }); テストコード 今回は新規会員登録・ログイン・ログアウトのテストを、ここまでご紹介したlaracasts/cypressのコマンドを使用して作成しました。\nLaracastsCypressTest.cy.js describe(\u0026#39;laracasts/cypress Test\u0026#39;, () =\u0026gt; { beforeEach(() =\u0026gt; { cy.refreshDatabase(); }); it(\u0026#39;新規会員登録テスト\u0026#39;, () =\u0026gt; { cy.visit(\u0026#39;/register\u0026#39;); // フォームに情報を入力・送信 cy.get(\u0026#39;input[name=name]\u0026#39;).type(\u0026#39;新規登録ユーザー\u0026#39;); cy.get(\u0026#39;input[name=email]\u0026#39;).type(\u0026#39;test@example.com\u0026#39;); cy.get(\u0026#39;input[name=password]\u0026#39;).type(\u0026#39;password\u0026#39;); cy.get(\u0026#39;input[name=password_confirmation]\u0026#39;).type(\u0026#39;password\u0026#39;); cy.get(\u0026#39;button[type=\u0026#34;submit\u0026#34;]\u0026#39;).click() // ダッシュボードにリダイレクトされたことを確認 cy.url().should(\u0026#39;eq\u0026#39;, Cypress.config().baseUrl + \u0026#39;/dashboard\u0026#39;); // 認証中のユーザーを確認 cy.currentUser().its(\u0026#39;name\u0026#39;).should(\u0026#39;eq\u0026#39;, \u0026#39;新規登録ユーザー\u0026#39;); }); it(\u0026#39;ログインテスト\u0026#39;, () =\u0026gt; { // ユーザーを作成 cy.create(\u0026#39;App\\\\Models\\\\User\u0026#39;, { name: \u0026#39;ログインテスト\u0026#39;, email: \u0026#39;login-test@example.com\u0026#39;, }); cy.visit(\u0026#39;/login\u0026#39;); cy.get(\u0026#39;input[name=email]\u0026#39;).type(\u0026#39;login-test@example.com\u0026#39;); cy.get(\u0026#39;input[name=password]\u0026#39;).type(\u0026#39;password\u0026#39;); cy.get(\u0026#39;button[type=\u0026#34;submit\u0026#34;]\u0026#39;).click() // ダッシュボードに遷移したことを確認 cy.url().should(\u0026#39;eq\u0026#39;, Cypress.config().baseUrl + \u0026#39;/dashboard\u0026#39;); // 認証中のユーザーを確認 cy.currentUser().its(\u0026#39;email\u0026#39;).should(\u0026#39;eq\u0026#39;, \u0026#39;login-test@example.com\u0026#39;); }); it(\u0026#39;ログアウトテスト\u0026#39;, () =\u0026gt; { // ユーザーを作成 cy.create(\u0026#39;App\\\\Models\\\\User\u0026#39;, { name: \u0026#39;ログアウトテスト\u0026#39;, email: \u0026#39;logout-test@example.com\u0026#39;, }); //ログインを実行 cy.login({ name: \u0026#39;ログアウトテスト\u0026#39; }); cy.visit(\u0026#39;/dashboard\u0026#39;) //ドロップダウンメニューを開くためのボタンクリック・ログアウトボタンをクリック cy.get(\u0026#39;.relative\u0026#39;).click() cy.contains(\u0026#39;a\u0026#39;, \u0026#39;ログアウト\u0026#39;).click() cy.visit(\u0026#39;/dashboard\u0026#39;).assertRedirect(\u0026#39;/login\u0026#39;); // 認証中のユーザーがいないことを確認 cy.currentUser().should(\u0026#39;not.exist\u0026#39;); }); }); では、テストを実行してみましょう。Cypressのブラウザでテストファイルをクリックします。まだCypressを立ち上げていない場合はnpx cypress openを実行してCypressを立ち上げてくださいね。\n全て成功しました！今回Cypressコマンドの説明は省きましたが、以下の記事で実行方法や代表的なコマンドをご紹介していますのでぜひご覧ください。\nCypressセットアップ・実行方法 Cypress画面表示のテスト Cypressログイン・ログアウトのテスト Cypressパスワードリセットのテスト\n","date":"2024-10-09T12:42:04+09:00","permalink":"https://www.larajapan.com/2024/10/09/cypress%E3%81%A7e2e%E3%83%86%E3%82%B9%E3%83%88%E3%82%92%E8%87%AA%E5%8B%95%E5%8C%96%EF%BC%88%EF%BC%95%EF%BC%89laracasts-cypress%E3%81%A7laravel%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88/","title":"CypressでE2Eテストを自動化（５）laracasts/cypressでLaravelプロジェクトをテスト"},{"content":"ウェブソケットと同様にリアルタイム（まがい）で画面の一部を更新する方法に、ポーリングがあります。ウェブソケットではサーバーから更新情報をブラウザ（クライアント）へプッシュするのに対して、ポーリングは逆にブラウザからサーバーに情報を一定の時間で取得（ポーリング）しに行きます。それゆえに、PusherやReverbのようなウェブソケットサーバーは必要でなく簡単なセットアップとなります。このポーリングをとても人気があるLivewireを使用してプログラムしてみます。\n欲しいもの 実用的なものでないですが、とても簡単なデモとします。\n上の画面のように、ログイン画面が表示されたらログインできる残りの秒数を１０秒からカウントダウンします。１０秒過ぎたからと言ってログインが無効になるわけではありません。単に残りの秒数を全ページ更新せずに表示するだけです。\n通常なら単にjavascriptでsetTimeout()あるいはsetInterval()を駆使して実現できることを、javascriptを一切使用せずにサーバーから残り秒数を取得して表示します。\nLivewireのインストール まず、Livewireのパッケージのインストール。\n$ composer require livewire/livewire 今回はこれだけ。設定は何も要りません。\nコンポーネントの作成 次にタイマーのコンポーネントを作成します。artisanでモデルを作成するようにコンポーネントを作成してくれます。\n$ php artisan make:livewire Timer これで作成されるファイルは２つあります。１つはコンポーネントでもう１つはそれに使用されるブレードです。\nnew file: app/Livewire/Timer.php new file: resources/views/livewire/timer.blade.php コンポーネントのコードを以下のように編集します。\napp/Livewire/Timer.php namespace App\\Livewire; use Livewire\\Component; class Timer extends Component { public $remaining = 11; public function render() { $this-\u0026gt;remaining--; return view(\u0026#39;livewire.timer\u0026#39;, [ \u0026#39;remaining\u0026#39; =\u0026gt; $this-\u0026gt;remaining, ]); } } $remainingの変数が残り秒数の値となりますが、最初に11秒と設定しておき、そこから１を引いた10秒から表示です。 正確に行うには開始時点での日時をキープしてrender()がコールされるたびにその時点での日時から経過秒数を計算ですが、ここは１秒おきに関数がコールされるとしてシンプルにします。ちなみに、Livewireではサーバーでの複数のリクエストを通して同じオブジェクトにアクセスが可能で、$remainingの値は毎回初期化されずステートをキープします。\n次はコンポーネントのブレードの編集です。先のコンポーネントのクラス名と関連させてtimer.blade.phpとなっています。\nresources/views/livewire/timer.blade.php \u0026lt;div wire:poll.1s\u0026gt; 残りの秒数：{{ $remaining }} \u0026lt;/div\u0026gt; 上のコードで大事なのは、wire:poll.1sの部分です。これがポーリングを指示しています。.1sは、１秒ごとにポーリングです。これがないとデフォルトは、2.5秒でポーリングとなります。\nさて、このコンポーネントをログインのブレードに入れます。見てのとおり\u0026lt;livewire:timer /\u0026gt;だけとシンプルです。\nresources/views/auth/login.blade.php \u0026lt;x-guest-layout\u0026gt; \u0026lt;!-- Session Status --\u0026gt; \u0026lt;x-auth-session-status class=\u0026#34;mb-4\u0026#34; :status=\u0026#34;session(\u0026#39;status\u0026#39;)\u0026#34; /\u0026gt; \u0026lt;form method=\u0026#34;POST\u0026#34; action=\u0026#34;{{ route(\u0026#39;login\u0026#39;) }}\u0026#34;\u0026gt; @csrf \u0026lt;livewire:timer /\u0026gt; \u0026lt;!-- ここでコンポーネントを使用 --\u0026gt; \u0026lt;!-- Email Address --\u0026gt; ... この時点で不思議に思うのは、Livewireのおかげでjavascriptのコードは一切も書かないのですが、どこかでLivewireのライブラリ（javascript）をコールされているはずですよね。今のところブレードには前もって含まれてはいません。しかし、レンダーされた画面のHTMLソースを見ると以下のように、自動でスクリプトがインジェクトされています。\n.... \u0026lt;!-- Livewire Scripts --\u0026gt; \u0026lt;script src=\u0026#34;/livewire/livewire.js?id=cc800bf4\u0026#34; data-csrf=\u0026#34;ys60l3TQzop1efcgNKZjtUlZ4mX9WxZzlpiVjoUN\u0026#34; data-update-uri=\u0026#34;/livewire/update\u0026#34; data-navigate-once=\u0026#34;true\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 実際にログイン画面を表示してブラウザーのインスペクターでポーリングが行われているが見てみましょう。\nたくさんPOSTのリクエストが実行されています。リクエストの詳細では、 http://127.0.0.1:8000/livewire/update にアクセスしているのが分かります。\nこのルートも手動で設定してはいないのに自動設定されています。チェックしてみましょう。\n$ php artisan route:list | grep livewire GET|HEAD livewire/livewire.js ......................... Livewire\\Mechanisms › FrontendAssets@returnJavaScriptAsFile GET|HEAD livewire/livewire.min.js.map ................................... Livewire\\Mechanisms › FrontendAssets@maps GET|HEAD livewire/preview-file/{filename} livewire.preview-file › Livewire\\Features › FilePreviewController@handle POST livewire/update ...................... livewire.update › Livewire\\Mechanisms › HandleRequests@handleUpdate POST livewire/upload-file .............. livewire.upload-file › Livewire\\Features › FileUploadController@handle livewire関連がいろいろとありますが、先ほどのルートも含まれていました。\nタイマーを止める さて、最後にもう１つやることあります。先ほどのプログラムでは、ほったらかしにすると残りの秒数がマイナスになってしまいます。\n残り秒数が０以下となるなら、終了としてポーリングも停止したいです。そうするには、ブレードを編集して、０秒以下となったら、ポーリングの代わりに「終了しました！」の表示を条件文と使って入れ替えます。簡単ですね。\nresources/views/livewire/timer.blade.php \u0026lt;div\u0026gt; @if ($remaining \u0026lt;= 0) \u0026lt;div class=\u0026#34;text-red-600 font-bold\u0026#34;\u0026gt;終了しました！\u0026lt;/div\u0026gt; @else \u0026lt;div class=\u0026#34;font-bold\u0026#34; wire:poll.1s\u0026gt; 残りの秒数：{{ $remaining }} \u0026lt;/div\u0026gt; @endif \u0026lt;/div\u0026gt; 以下のようになりました。\n最後に Livewireを使うと実に簡単にポーリングができてしまいます、しかし、ポーリングはこのデモでは毎秒サーバーにリクエストするので、ユーザー数が多くなるとサーバーにはかなりな負荷となります。スケールが小さめのプロジェクト向きと思ってください。スケールが大きいプロジェクトではウェブソケットの使用を考えてください。\nウェブソケットに関しては、以下のご参照を。\n会員チャットの解説（１）Websocket\n","date":"2024-09-18T12:47:18+09:00","permalink":"https://www.larajapan.com/2024/09/18/livewire%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E3%83%9D%E3%83%BC%E3%83%AA%E3%83%B3%E3%82%B0/","title":"Livewireを使ってポーリング"},{"content":"今回はパスワードリセットのテストです。リセットリンクをクリックしてメールアドレスを入力、MailHogに届いたメールの確認、パスワードリセット実行、という一連のユーザーの操作をCypress13.xで実装します。\nCypressのセットアップや画面表示テストなど、以前の記事は以下からご覧いただけます。\nCypressセットアップ・実行方法 Cypress画面表示のテスト Cypressログイン・ログアウトのテスト\nCypressでパスワード再設定のテスト 引き続きLaravel Breezeのログイン画面を使ってテストします。テストの流れは、少し項目が多いですが以下のような形で進めようと思います。\nログイン画面で「パスワードを忘れた？」というリンクをクリック パスワードリセットのリクエスト画面でメールアドレスを入力・送信 受信したメールのリンクからパスワードリセット画面へ遷移 パスワードリセットを実行 旧パスワードでログインが失敗することを確認 新しいパスワードでログインが成功することを確認 受信メールの確認にはMailHogを使用しますので、もしインストールがまだの場合は環境に合わせてインストールくださいね。\nLaravelの.envは以下のように設定しています。\n.env ... MAIL_MAILER=smtp MAIL_HOST=localhost MAIL_PORT=1025 MAIL_USERNAME=null MAIL_PASSWORD=null MAIL_ENCRYPTION=null MAIL_FROM_ADDRESS=\u0026#34;local@example.com\u0026#34; MAIL_FROM_NAME=メールテスト ... 事前準備１： cypress-mailhog CypressからMailHogのデータを取得するための便利なコマンドが提供されているパッケージ、cypress-mailhogを使用します。\n以下のコマンドでインストールできます。執筆時点で最新の2.4.0がインストールされました。\n$ npm install --save-dev cypress-mailhog また、以下の設定が必要です。まずはcypress/support/commands.jsファイルにインポート。\ncypress/support/commands.js import \u0026#39;cypress-mailhog\u0026#39;; ... そして、cypress.config.jsファイルのenvにMailHogのURLを追記します。私の環境ではhttp://localhost:8025にてMailHogのUIが確認できます。\ncypress.config.js import { defineConfig } from \u0026#34;cypress\u0026#34;; export default defineConfig({ env: { mailHogUrl: \u0026#34;http://localhost:8025\u0026#34;, // MailHogのURLをここに追記 }, e2e: { baseUrl: \u0026#39;http://127.0.0.1:8000\u0026#39;, }, }); これでCypressのテスト内でcypress-mailhogのコマンドが使用できるようになりました。\n具体的には以下のように、cy.に続けてCypressコマンドのように使用できます。mhGetAllMails()はメールボックスのメールを最大50件取得するコマンドで、mhFirst()は、直前のコマンドで取得したメールから最初の１件を抽出するコマンドです。\ncy.mhGetAllMails() //メールを最大50件取得 cy.mhGetAllMails().mhFirst() //メールの最初の１件を取得 事前準備２： mailparser 次に、mailparserをインストールします。今回は届いたメールの本文を確認する必要があるのですが、先ほどのcypress-mailhogで取得したメールの本文を見てみると、以下のようにテキストとして読めない状態です。\nそのためmailparserを使用して、人間が読めるように解析する必要があります。\n以下のコマンドでインストール。\n$ npm install mailparser --save-dev こちらも2ファイルに設定が必要です。まずcypress/support/commands.jsにカスタムコマンドを追加します。先ほどインストールしたcypress-mailhogのimport文も含めると、以下のようになります。\ncypress/support/commands.js import \u0026#39;cypress-mailhog\u0026#39;; // cypress-mailhog 用の記述 ... Cypress.Commands.add(\u0026#39;mhParseMail\u0026#39;, { prevSubject: true }, (mail) =\u0026gt; { return cy.task(\u0026#39;parse-mail\u0026#39;, { mail: mail.Raw.Data }) }) ... カスタムコマンド名は任意ですが、mhParseMailとしました。\nそして、cypress.config.jsにmailparserのimportとtaskを追加します。taskとは、テストコード内からブラウザ外で必要な作業を呼び出すための機能です。taskのドキュメントはこちら\n先ほどcypress-mailhogで同ファイルに書き込んだものと合わせると、最終的には以下のようになります。\ncypress.config.js import { defineConfig } from \u0026#34;cypress\u0026#34;; import { simpleParser as parser } from \u0026#39;mailparser\u0026#39;; // ここでmailparserをインポート export default defineConfig({ env: { mailHogUrl: \u0026#34;http://localhost:8025\u0026#34; }, e2e: { baseUrl: \u0026#39;http://127.0.0.1:8000\u0026#39;, setupNodeEvents(on, config) { // タスク用の追記ここから on(\u0026#39;task\u0026#39;, { \u0026#39;parse-mail\u0026#39;: ({ mail }) =\u0026gt; parser(mail), // タスクを定義 }) }, // タスク用の追記ここまで }, }); on(\u0026rsquo;task\u0026rsquo;, {\u0026hellip;})がタスクを定義している箇所です。task名は任意ですが、ここではparse-mailとしました。カスタムコマンドmhParseMail()を実行するとparse-mailというtaskが呼ばれ、mailparserによりメール解析が実行される、という流れです。\nここまで設定できたらテストコードからの呼び出しは簡単で、cypress-mailhogで提供されているmhGetAllMails().mhFirst()に繋いで以下のように使用します。\ncy.mhGetAllMails().mhFirst().mhParseMail().then((mail) =\u0026gt; { mail.subject // 件名を取得 mail.from.value[0].address // Fromを取得 mail.text // 本文を取得 } 取得したメールの１通目のデータをmhParseMail()で解析してからthen()に渡します。受け取ったmailオブジェクトはmailparserの形式になっているため、上の例のようにmail.subjectで件名を、mail.from.value[0].addressでメールのfromを、mail.textで本文を取得できます。\n他にもプロパティが色々ありますのでmailparserのドキュメントをご確認ください。\nwrap ここまでで事前準備は完了ですが、最後に今回新しく使用するコマンドwrap()をご紹介します。\ncypress-mailhogやmailparserから受け取ったメールデータはJavaScriptのオブジェクトです。このデータをCypressのアサーションコマンドで扱うために、以下のようにwrap()コマンドでラップします。\ncy.mhGetAllMails().mhFirst().mhParseMail().then((mail) =\u0026gt; { cy.wrap(mail.subject).should(\u0026#39;eq\u0026#39;, \u0026#39;パスワードのリセット方法について\u0026#39;); } これで、Cypressコマンドを繋いでメールの件名が期待した内容がどうかを検証できるようになります。\nCypressでパスワードリセットのテスト 以下がパスワードリセットのテストコード全文になります。\ncypress/e2e/PasswordResetTest.cy.js it(\u0026#39;パスワードリセットのテスト\u0026#39;, () =\u0026gt; { cy.visit(\u0026#39;/login\u0026#39;); cy.contains(\u0026#39;a\u0026#39;, \u0026#39;パスワードを忘れた\u0026#39;).click(); // パスワードリセットページに遷移したことを確認 cy.url().should(\u0026#39;include\u0026#39;, \u0026#39;/forgot-password\u0026#39;); // メールアドレス入力 cy.get(\u0026#39;input[name=\u0026#34;email\u0026#34;]\u0026#39;).type(Cypress.env(\u0026#39;email\u0026#39;)); // パスワードリセットメール送信ボタンをクリック cy.contains(\u0026#39;button\u0026#39;, \u0026#39;パスワードリセットメールを送信\u0026#39;).click(); // 取得した一覧のうち、１件目（最新）のメール本文を取得 cy.mhGetAllMails().mhFirst().mhParseMail().then((mail) =\u0026gt; { cy.wrap(mail.subject).should(\u0026#39;eq\u0026#39;, \u0026#39;パスワードのリセット方法について\u0026#39;); cy.wrap(mail.from.value[0].address).should(\u0026#39;eq\u0026#39;, \u0026#39;from@example.com\u0026#39;); // メールの本文からパスワードリセットのためのリンクを抽出 const hrefRegex = /https?:\\/\\/[^\\s/\u0026#34;\u0026#39;]+\\/reset-password\\/[^\\s/\u0026#34;\u0026#39;]+/; let match = hrefRegex.exec(mail.text) // パスワードリセットの画面へ遷移 cy.visit(match[0]) cy.get(\u0026#39;input[name=\u0026#34;email\u0026#34;]\u0026#39;).type(Cypress.env(\u0026#39;email\u0026#39;)); // メールアドレス入力 cy.get(\u0026#39;input[name=\u0026#34;password\u0026#34;]\u0026#39;).type(\u0026#39;new-password\u0026#39;); // 新しいパスワード cy.get(\u0026#39;input[name=\u0026#34;password_confirmation\u0026#34;]\u0026#39;).type(\u0026#39;new-password\u0026#39;); // パスワードの再入力 // パスワードリセットメール送信ボタンをクリック cy.contains(\u0026#39;button\u0026#39;, \u0026#39;パスワードのリセット\u0026#39;).click(); // ログイン画面へ遷移したことを確認 cy.url().should(\u0026#39;eq\u0026#39;, Cypress.config().baseUrl + \u0026#39;/login\u0026#39;); // 旧パスワードでログインすると失敗する cy.get(\u0026#39;input[name=\u0026#34;email\u0026#34;]\u0026#39;).type(Cypress.env(\u0026#39;email\u0026#39;)); cy.get(\u0026#39;input[name=\u0026#34;password\u0026#34;]\u0026#39;).type(Cypress.env(\u0026#39;password\u0026#39;)); cy.get(\u0026#39;button[type=\u0026#34;submit\u0026#34;]\u0026#39;).click(); cy.get(\u0026#39;input[name=\u0026#34;email\u0026#34;]\u0026#39;) .next(\u0026#39;ul\u0026#39;) .should(\u0026#39;contain\u0026#39;, \u0026#39;ログイン情報が存在しません。\u0026#39;); // 新しいパスワードで再度ログイン試行（メールアドレスの入力値は残っているので不要） cy.get(\u0026#39;input[name=\u0026#34;password\u0026#34;]\u0026#39;).type(\u0026#39;new-password\u0026#39;); // 新しいパスワード cy.get(\u0026#39;button[type=\u0026#34;submit\u0026#34;]\u0026#39;).click(); // ダッシュボードに遷移したことを確認 cy.url().should(\u0026#39;eq\u0026#39;, Cypress.config().baseUrl + \u0026#39;/dashboard\u0026#39;); }) }) ではテストを実行してみます。npx cypress openでCypressを立ち上げて、対象ファイルをクリックします。\n成功しました！ですが、このテストではDBのパスワードを変更してしまうため、どこかでパスワードをデフォルトに戻すなどの処理を行う必要があります。\nCypressでDBをリセットするには、テスト対象のプロジェクトにDBリセット用のAPIを用意する、DBリセット用のシェルスクリプトを作成してテストから呼び出す・・・などの方法が考えられますが、今回はLaravelのプロジェクトがテスト対象ということもあり、ユニットテストのようにrefreshDatabase()が実行できれば便利ですよね。\nということで、次回はLaravelプロジェクトをCypressでテストするときに便利なパッケージをご紹介します。\n参照 Cypressセットアップ・実行方法 Cypress画面表示のテスト Cypressログイン・ログアウトのテスト","date":"2024-09-03T23:36:11+09:00","permalink":"https://www.larajapan.com/2024/09/03/cypress%E3%81%A7e2e%E3%83%86%E3%82%B9%E3%83%88%E3%82%92%E8%87%AA%E5%8B%95%E5%8C%96%EF%BC%88%EF%BC%94%EF%BC%89%E3%83%91%E3%82%B9%E3%83%AF%E3%83%BC%E3%83%89%E3%83%AA%E3%82%BB%E3%83%83%E3%83%88%E3%81%AE/","title":"CypressでE2Eテストを自動化（４）パスワードリセットのテスト"},{"content":"前回に紹介したLaravel Zeroはとても有用そうです。しかし、データベースの操作を行うことができなければですね。ということで、今回はその機能を追加します。さらに、もちろん私の好きなtinkerも使えるようにします。\nデータベース機能の追加 前回と同じプロジェクトの続きとして、データベース機能を追加します。前回では、applicationをartisanに改名したことをお忘れずに。\n$ php artisan app:install database Installing database component... ... Require package via Composer: ✔ Creating a default SQLite database: ✔ Creating migrations folder: ✔ Creating seeders folders and files: ✔ Creating factories folder: ✔ Creating default database configuration: ✔ Updating .gitignore: ✔ ... 上の実行により、Laravelでお馴染みのconfig/database.phpも追加されるので、.envに以下を追加します。\n.env ... DB_CONNECTION=mysql DB_HOST=localhost DB_DATABASE=zero DB_USERNAME=root DB_PASSWORD=password 準備が整ったら、これまたLaravelでお馴染みのEloquentのクエリの実行などが使えます。\nuse App\\User; use Illuminate\\Support\\Facades\\DB; $users = User::all(); // Eloquent $users = DB::table(\u0026#39;users\u0026#39;)-\u0026gt;get(); // Query Builder Tinker 私には大好きなツールですが、これもデフォルトではLaravel Zeroにはデフォルトで装備されていません。 外部パッケージとなっているので、composerでインストールします。\n$ composer require intonate/tinker-zero さらに、config/app.phpを編集して以下の行を追加します。\nconfig/app.php \u0026#39;providers\u0026#39; =\u0026gt; [ App\\Providers\\AppServiceProvider::class, Intonate\\TinkerZero\\TinkerZeroServiceProvider::class, // これを追加！ ], 実行してみましょう。こちらも、前回にapplicationをartisanに改名したことをお忘れずに。\n$ php artisan tinker Psy Shell v0.12.4 (PHP 8.2.15 — cli) by Justin Hileman \u0026gt; use App\\User; \u0026gt; User::count(); \u0026gt; 4 参照 Laravel Zero（１）コマンドの作成\n","date":"2024-08-21T06:45:57+09:00","permalink":"https://www.larajapan.com/2024/08/21/laravel-zero%EF%BC%88%EF%BC%92%EF%BC%89%E3%83%87%E3%83%BC%E3%82%BF%E3%83%99%E3%83%BC%E3%82%B9%E3%82%92%E4%BD%BF%E3%81%86/","title":"Laravel Zero（２）データベースを使う"},{"content":"今でも覚えています。Laravelを使い始めた時にとても気に入ったのがartisanコマンドであったことを。その当時の他のPHPフレームワークと異なり、Laravelでは最初からコマンドの開発がMVCのUIの開発と肩を並べて上級扱いされていました。Unix系のコマンドに慣れている私には感激でした。しかし、コマンドだけの開発しか必要ないときがあります。つまりブラウザの画面を必要としない開発です。そのようなときにLaravelをインストールすると、ウェブのためのコントローラなどが一緒についてくるので煩わしく思えます。そこで登場するのが、Laravel Zeroです。\nLaravel Zeroとは Laravel Zero（ゼロ）は、Laravelのコアの開発者であるNuno Maduros氏が開発したコマンド開発に特化したフレームワークです。ここでは、以前に記事としたGoogle AnalyticsのデータをAPIで取得（２）コマンドを作成のコマンドを、Laravel Zeroでその説明とともに書き直します。 インストール Laravelと同様に、Laravel Zeroはcomposerを利用してプロジェクトを作成します。\n$ composer create-project --prefer-dist laravel-zero/laravel-zero ga4 現時点では、Laravel 11xをもとにしているのでPHP 8.2が必要ですが、まだその環境が用意されていないなら、例えばLaravel 10xのPHP 8.1なら、以下のようにブランチを指定してインストールも可能です。\n$ composer create-project --prefer-dist laravel-zero/laravel-zero:10.x ga4 インストール後に、ディレクトリを見てみましょう。\n$ ls -al app/ bootstrap/ config/ tests/ vendor/ application box.json composer.json composer.lock .editorconfig .gitattributes .gitignore phpunit.xml.dist README.md Laravelがインストールされたディレクトリと似ていますが、database, public, resources, routes, storageなどのディレクトリがありません。また、artisanや.envのファイルもありません。\nappのディレクトリの中身を見てみると、\n$ tree app app ├── Commands │ └── InspireCommand.php └── Providers └── AppServiceProvider.php すっきりしてこれだけなのです。しかも、InsupireCommand.phpはデモのコマンドです。\n最初のコマンドの作成 早速、Google Analyticsからデータを取得するコマンドを作成しましょう。\nコマンドの作成のためのコマンドはLaravelと似ていますが、以下のようにartisanの代わりにapplicationを使います。\n$ php application make:command Ga4Command 上で作成されるファイルは、app/Console/Commandsではなくapp/Commandsに置かれます。 これを編集します。\napp/Commands/Ga4Command.php namespace App\\Commands; use Google\\Analytics\\Data\\V1beta\\Metric; use Google\\Analytics\\Data\\V1beta\\OrderBy; use Google\\Analytics\\Data\\V1beta\\DateRange; use Google\\Analytics\\Data\\V1beta\\Dimension; use Google\\Analytics\\Data\\V1beta\\OrderBy\\MetricOrderBy; use Google\\Analytics\\Data\\V1beta\\BetaAnalyticsDataClient; use Illuminate\\Support\\Str; use LaravelZero\\Framework\\Commands\\Command; class Ga4Command extends Command { /** * The name and signature of the console command. * * @var string */ protected $signature = \u0026#39;ga4\u0026#39;; /** * The console command description. * * @var string */ protected $description = \u0026#39;Get GA4 data\u0026#39;; /** * Execute the console command. */ public function handle() { $client = new BetaAnalyticsDataClient([ \u0026#39;credentials\u0026#39; =\u0026gt; storage_path(\u0026#39;ga4.key\u0026#39;), ]); $response = $client-\u0026gt;runReport([ \u0026#39;property\u0026#39; =\u0026gt; \u0026#39;properties/\u0026#39;.config(\u0026#39;google.ga4.property_id\u0026#39;), \u0026#39;dateRanges\u0026#39; =\u0026gt; [ new DateRange([ \u0026#39;start_date\u0026#39; =\u0026gt; \u0026#39;28daysAgo\u0026#39;, \u0026#39;end_date\u0026#39; =\u0026gt; \u0026#39;today\u0026#39;, ]), ], \u0026#39;dimensions\u0026#39; =\u0026gt; [ new Dimension([\u0026#39;name\u0026#39; =\u0026gt; \u0026#39;pageTitle\u0026#39;]), new Dimension([\u0026#39;name\u0026#39; =\u0026gt; \u0026#39;pagePath\u0026#39;]), ], \u0026#39;metrics\u0026#39; =\u0026gt; [ new Metric([\u0026#39;name\u0026#39; =\u0026gt; \u0026#39;screenPageViews\u0026#39;]), ], \u0026#39;orderBys\u0026#39; =\u0026gt; [ new OrderBy([ \u0026#39;metric\u0026#39; =\u0026gt; new MetricOrderBy([ \u0026#39;metric_name\u0026#39; =\u0026gt; \u0026#39;screenPageViews\u0026#39;, ]), \u0026#39;desc\u0026#39; =\u0026gt; true, ]), ], \u0026#39;limit\u0026#39; =\u0026gt; 100, ]); $rows = []; foreach ($response-\u0026gt;getRows() as $row) { $title = $row-\u0026gt;getDimensionValues()[0]-\u0026gt;getValue() ?? null; $url = $row-\u0026gt;getDimensionValues()[1]-\u0026gt;getValue() ?? null; $count = $row-\u0026gt;getMetricValues()[0]-\u0026gt;getValue() ?? null; if ($url === \u0026#39;/\u0026#39;) { continue; } $rows[] = (object) [ \u0026#39;title\u0026#39; =\u0026gt; Str::before($title, \u0026#39; – ララジャパン\u0026#39;), \u0026#39;url\u0026#39; =\u0026gt; $url, \u0026#39;views\u0026#39; =\u0026gt; $count, ]; } $html = view(\u0026#39;ranking\u0026#39;, compact(\u0026#39;rows\u0026#39;))-\u0026gt;render(); echo $html; return Command::SUCCESS; } } Google AnalyticsのデータをAPIで取得（２）コマンドを作成に掲載されているコードと比べると、唯一の違いはnamespaceのパスと継承するCommandのクラスがLaravelZero\\Framework\\Commands\\Commandと変わっただけです。\nこのコマンドを実行可能とさせるには、インストールすべきライブラリがいくつかあります。\nまず、Googleのライブラリです。\n$ composer require google/analytics-data そして、Google AnalyticsのプロパティIDを保存する、.envも必要なのでdotenvのライブラリを以下のようにインストールします。 しかし、composerではなく、ゼロのコマンドのapp:installオプションでインストールします。以下のように、.env, .env.exampleなどのLaravelではお馴染みのファイルがここで初めて作成されます。どうしてこれがプロジェクトを作成したときにインストールされないのはちょいと不思議。\n$ php application app:install dotenv Installing dotenv component... Creating .env: ✔ Creating .env.example: ✔ Updating .gitignore: ✔ さらに、Ga4Commandでは、出力にブレードを使うのでview()のヘルパーも必要です。こちらのインストールはこれもapp:installを使用します。\n$ php application app:install view Installing view component... ... Require package via Composer: ✔ Creating resources/views folder: ✔ Creating default view configuration: ✔ Creating cache storage folder: ✔ これで、resources/viewsのディレクトリや、storageのディレクトリも作成されます。だんだんとLaravelらしくなってきましたね。\n実行をartisanとする コマンドが作成されたところで、実行と行きましょう。 実行するには、以下のコマンドラインとなります。\n$ php application ga4 このapplicationが気に入らないなら違う名前にもできます。例えば、Laravelに合わせてartisanにしてみましょう。 以下の改名コマンドにおいてartisanを指定します。\n$ php application app:rename artisan Renaming the application... Renaming application to \u0026#34;artisan\u0026#34;: ✔ Updating config/app.php \u0026#34;name\u0026#34; property: ✔ Updating composer \u0026#34;bin\u0026#34;: ✔ 実行のコマンドラインは、以下のようになります。\n$ php artisan ga4 参照 Laravel Zero（２）データベースを使う\n","date":"2024-08-15T00:10:23+09:00","permalink":"https://www.larajapan.com/2024/08/15/laravel-zero%EF%BC%88%EF%BC%91%EF%BC%89%E3%82%B3%E3%83%9E%E3%83%B3%E3%83%89%E3%81%AE%E4%BD%9C%E6%88%90/","title":"Laravel Zero（１）コマンドの作成"},{"content":"以前にリファクタリングを自動化できるツール、Rectorを紹介しました。前回の記事ではRector側で用意された変換ルールに則ってリファクタする方法をお伝えしました。しかし、Rectorの真骨頂はむしろ自分で変換ルールを作成して各々の環境に合わせて自由にカスタマイズ出来る点にあります。直近の実務でそちらに触れる機会がありましたのでご紹介致します。\nRectorのインストール(おさらい） 前回の記事からだいぶ時間が経ってしまったのでプロジェクトにrectorをセットアップする手順からおさらいします。まず、以下でrectorをプロジェクトにinstallして下さい。\n$ composer require rector/rector --dev 2024/08/03時点でversion 1.2.1がinstallされました。次に、初回実行して設定ファイルを作成します。\n$ vendor/bin/rector No \u0026#34;rector.php\u0026#34; config found. Should we generate it for you? [yes]: \u0026gt; yes rector.phpがルートディレクトリに作成されました。そちらの編集については後ほど解説致します。\n自作ルールでリファクタリングを自動化 例えば、直近で私が携わった案件ではそれまで定数で管理していた値をPHP8.1の導入と共にEnumへ切り替えるリファクタをRectorで自作ルールを作成して行いました。今回はそちらのケースを例として紹介します。\n定数をEnumに変換する 以下の様なモデルクラスがあったとします。\napp/Models/Order.php namespace App\\Models; use Illuminate\\Database\\Eloquent\\Model; use Illuminate\\Database\\Eloquent\\Factories\\HasFactory; class Order extends Model { use HasFactory; // order status constants const ORDER_STATUS_ORDERED = 0; const ORDER_STATUS_SHIPPED = 1; const ORDER_STATUS_CANCELED = 2; protected $table = \u0026#39;order\u0026#39;; protected $primaryKey = \u0026#39;order_id\u0026#39;; protected $guarded = [\u0026#39;order_id\u0026#39;, \u0026#39;created_at\u0026#39;, \u0026#39;updated_at\u0026#39;]; // キャンセルされた？ public function isCanceled() { return $this-\u0026gt;status === self::ORDER_STATUS_CANCELED; } // 売上として計上された注文 public function scopeAsRevenue($query) { return $query-\u0026gt;whereIn(\u0026#39;status\u0026#39;, [self::ORDER_STATUS_ORDERED, self::ORDER_STATUS_SHIPPED]); } } このクラスではorder_statusというプロパティの値を定数で管理しています。そして、それらを以下のOrderStatusというEnumに切り替えたいとします。\napp/Enums/OrderStatus.php namespace App\\Enums; enum OrderStatus: int { case ORDERED = 1; case SHIPPED = 2; case CANCELED = 3; } 具体的にはコードを以下の様に書き換えます。\nself::ORDER_STATUS_CANCELED ↓ OrderStatus::CANCELED-\u0026gt;value カスタムルール作成 こちらの公式ドキュメントを参考に進めます。まず、カスタムルールを定義するクラスを作成します。\n$ vendor/bin/rector custom-rule // 作成するカスタムルール名を指定（ReplaceConstantToEnumValueとしました） What is the name of the rule class (e.g. \u0026#34;LegacyCallToDbalMethodCall\u0026#34;)?: \u0026gt; ReplaceConstantToEnumValue Generated files =============== * utils/rector/tests/Rector/ReplaceConstantToEnumValueRector/Fixture/some_class.php.inc * utils/rector/tests/Rector/ReplaceConstantToEnumValueRector/config/configured_rule.php * utils/rector/src/Rector/ReplaceConstantToEnumValueRector.php * utils/rector/tests/Rector/ReplaceConstantToEnumValueRector/ReplaceConstantToEnumValueRectorTest.php [OK] Base for the \u0026#34;ReplaceConstantToEnumValueRector\u0026#34; rule was created. Now you can fill the missing parts [OK] We also update composer.json autoload-dev, to load Rector rules. Now run \u0026#34;composer dump-autoload\u0026#34; to update paths [OK] We also update /***/rector/phpunit.xml, to add a rector test suite. You can run the rector tests by running: phpunit --testsuite rector 実行が完了するとルートディレクトリにutilsディレクトリが作成されます。utilsディレクトリの構造は以下の様になっているはずです。\n$ tree utils utils └── rector ├── src │ └── Rector │ └── ReplaceConstantToEnumValueRector.php └── tests └── Rector └── ReplaceConstantToEnumValueRector ├── ReplaceConstantToEnumValueRectorTest.php ├── Fixture │ └── some_class.php.inc └── config └── configured_rule.php composer.jsonにutils配下のクラスをオートロードする設定が追加されていますので反映させておきましょう。\n$ composer dump-autoload カスタムルールを定義 作成されたReplaceConstantToEnumValueRector.phpを編集してカスタムルールを定義していきましょう。このクラスにはデフォルトで３つのメソッドが定義されています。\ngetRuleDefinition() getNodeTypes() refactor() １つずつ解説します。\ngetRuleDefinition() こちらのメソッドではリファクタの前後でコードがどの様に変更されるか、を定義します。こちらのメソッドは他の開発者がルールを理解しやすくする為です。また、ドキュメントを作成する際に使える様ですが、そちらはまだ深掘りしていないので今回は割愛します。getRuleDefinition()は以下の様に実装しました。\nutils/rector/src/Rector/ReplaceConstantToEnumValueRector.php ... public function getRuleDefinition(): RuleDefinition { return new RuleDefinition(\u0026#39;Replace Constant to Enum\u0026#39;, [ new CodeSample( // BEFORE \u0026lt;\u0026lt;\u0026lt;\u0026#39;CODE_SAMPLE\u0026#39; ORDER_STATUS_ORDERED CODE_SAMPLE , // AFTER \u0026lt;\u0026lt;\u0026lt;\u0026#39;CODE_SAMPLE\u0026#39; OrderStatus::ORDERED-\u0026gt;value CODE_SAMPLE ), ]); ... getNodeTypes() こちらはリファクタの対象となるnodeを指定するメソッドです。nodeとはソースコードの構成要素の事で、クラスやメソッド、変数、条件式などです。Rectorはソースコードをnodeから成るツリー構造（AST）として解析し、nodeを操作する事でコードを変換しています。指定可能なnodeはこちらにまとめられています。例えば、今回は定数を操作したいのでClassConstFetch::classを指定します。\nutils/rector/src/Rector/ReplaceConstantToEnumValueRector.php ... /** * @return array\u0026lt;class-string\u0026lt;Node\u0026gt;\u0026gt; */ public function getNodeTypes(): array { // select node type return [ClassConstFetch::class]; } ... 今回はClassConstFetchのみですが、複数指定する事も可能です。ここで指定したnodeがfetchされ次に解説するrefactor()の引数に渡されます。\nrefactor() fetchしたnodeに変更を加えてコードをリファクタします。まず、引数で渡された$nodeがどういうものなのか見てみましょう。refactor()内にeval(\\Psy\\sh())を挿入してrectorを実行してみると以下の様に出力されました。（rectorの実行方法については後で解説します。）\nFrom utils/rector/src/Rector/ReplaceConstantToEnumValueRector.php:48: 46: public function refactor(Node $node): ?Node 47: { \u0026gt; 48: eval(\\Psy\\sh()); 49: 50: $constName = $this-\u0026gt;getName($node-\u0026gt;name); \u0026gt; $node = PhpParser\\Node\\Expr\\ClassConstFetch {#24984 +class: PhpParser\\Node\\Name {#24985 +parts: [ \u0026#34;self\u0026#34;, ], }, +name: PhpParser\\Node\\Identifier {#24986 +name: \u0026#34;ORDER_STATUS_CANCELED\u0026#34;, }, } getNodeTypes()で指定した通り、ClassConstFetchクラスのインスタンスが代入されていますね。ここでfetchしたnodeはself::ORDER_STATUS_CANCELEDに当たる部分なので、クラス名部分がself、変数名部分がORDER_STATUS_CANCELEDとしてそれぞれclass, nameプロパティから辿れます。それによって、更にリファクタしたい対象を絞り込む事ができます、以下の様に。\nutils/rector/src/Rector/ReplaceConstantToEnumValueRector.php ... /** * @param ClassConstFetch $node */ public function refactor(Node $node): ?Node { $constName = $this-\u0026gt;getName($node-\u0026gt;name); // ORDER_STATUS_* の場合のみリファクタ if (Str::startsWith($constName, \u0026#39;ORDER_STATUS_\u0026#39;)) { // リファクタ処理 } return $node; } ... $this-\u0026gt;getName($node-\u0026gt;name)の部分で変数名部分を取得し、それがORDER_STATUS_から始まるならリファクタ処理を実行、としました。\n次にリファクタ処理を実装します。リファクタ処理はOrderStatus::XXX-\u0026gt;valueを表現するnodeを新たに作成して返却します。nodeの作成は$this-\u0026gt;nodeFactoryのメソッドをcallして行います。（実行可能なメソッドはRector\\PhpParser\\Node\\NodeFactoryをご覧ください。）以下の様に実装しました。\nutils/rector/src/Rector/ReplaceConstantToEnumValueRector.php ... /** * @param ClassConstFetch $node */ public function refactor(Node $node): ?Node { $constName = $this-\u0026gt;getName($node-\u0026gt;name); // ORDER_STATUS_* if (Str::startsWith($constName, \u0026#39;ORDER_STATUS_\u0026#39;)) { // ORDER_STATUS_以降を取得 $case = Str::after($constName, \u0026#39;ORDER_STATUS_\u0026#39;); // OrderStatus::XXX のnodeを作成 $constFetch = $this-\u0026gt;nodeFactory-\u0026gt;createClassConstFetch(\u0026#39;App\\\\Enums\\\\OrderStatus\u0026#39;, $case); // OrderStatus::XXX にvalueプロパティを参照させる（OrderStatus::XXX-\u0026gt;value） return $this-\u0026gt;nodeFactory-\u0026gt;createPropertyFetch($constFetch, \u0026#39;value\u0026#39;); } return $node; } ... これでカスタムルールの作成が完了です。\nリファクタ実行 最後にrector.phpを設定してリファクタを実行してみましょう。rector.phpは以下の様に設定しました。\nrector.php declare(strict_types=1); use Rector\\Config\\RectorConfig; use Utils\\Rector\\Rector\\ReplaceConstantToEnumValueRector; return RectorConfig::configure() -\u0026gt;withPaths([ __DIR__ . \u0026#39;/app/Models/Order.php\u0026#39;, ]) -\u0026gt;withRules([ ReplaceConstantToEnumValueRector::class, ]) -\u0026gt;withImportNames(); withPaths()にてリファクタを適用するファイルを指定、withRules()にて適用するルールを指定しています。また、OrderStatusが完全修飾名となっているのでwithImportNames()を追加して名前空間をインポート（use App\\Enums\\OrderStatus;を追加）するようにしています。これでrectorを実行してみます。実際にリファクタを適用する前にコードがどの様に変化するのか、dry runで確認してみましょう。\n$ vendor/bin/rector --dry-run 1/1 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100% 1 file with changes =================== 1) app/Models/Order.php:1 ---------- begin diff ---------- @@ @@ namespace App\\Models; +use App\\Enums\\OrderStatus; use Illuminate\\Database\\Eloquent\\Model; use Illuminate\\Database\\Eloquent\\Factories\\HasFactory; @@ @@ // キャンセルされた？ public function isCanceled() { - return $this-\u0026gt;status === self::ORDER_STATUS_CANCELED; + return $this-\u0026gt;status === OrderStatus::CANCELED-\u0026gt;value; } // 売上として計上された注文 public function scopeAsRevenue($query) { - return $query-\u0026gt;whereIn(\u0026#39;status\u0026#39;, [self::ORDER_STATUS_ORDERED, self::ORDER_STATUS_SHIPPED]); + return $query-\u0026gt;whereIn(\u0026#39;status\u0026#39;, [OrderStatus::ORDERED-\u0026gt;value, OrderStatus::SHIPPED-\u0026gt;value]); } } ----------- end diff ----------- Applied rules: * ReplaceConstantToEnumValueRector [OK] 1 file would have been changed (dry-run) by Rector 差分を見ての通り、self::ORDER_STATUS_XXXだった箇所がOrderStatus::XXX-\u0026gt;valueに変換されていますね。問題なさそうなのでdry runオプションを外して実行します。\n$ vendor/bin/rector 以下が変換後のOrder.phpです。\napp/Models/Order.php namespace App\\Models; use App\\Enums\\OrderStatus; use Illuminate\\Database\\Eloquent\\Model; use Illuminate\\Database\\Eloquent\\Factories\\HasFactory; class Order extends Model { use HasFactory; // order status constants const ORDER_STATUS_ORDERED = 0; const ORDER_STATUS_SHIPPED = 1; const ORDER_STATUS_CANCELED = 2; protected $table = \u0026#39;order\u0026#39;; protected $primaryKey = \u0026#39;order_id\u0026#39;; protected $guarded = [\u0026#39;order_id\u0026#39;, \u0026#39;created_at\u0026#39;, \u0026#39;updated_at\u0026#39;]; // キャンセルされた？ public function isCanceled() { return $this-\u0026gt;status === OrderStatus::CANCELED-\u0026gt;value; } // 売上として計上された注文 public function scopeAsRevenue($query) { return $query-\u0026gt;whereIn(\u0026#39;status\u0026#39;, [OrderStatus::ORDERED-\u0026gt;value, OrderStatus::SHIPPED-\u0026gt;value]); } } 変換後のファイルを見ると定数の定義部分が残っていますね。こちらもまた別のカスタムルールを作成して削除できます。こうして１つずつ作業を棚卸ししてRectorのカスタムルールを作成していく事で膨大なリファクタ作業を大幅に短縮させる事が可能です。是非試してみて下さい。\n参照 Rectorでリファクタリングを自動化　その１ Rectorでリファクタリングを自動化　その２\n","date":"2024-08-04T06:58:49+09:00","permalink":"https://www.larajapan.com/2024/08/04/rector%E3%81%A7%E3%83%AA%E3%83%95%E3%82%A1%E3%82%AF%E3%82%BF%E3%83%AA%E3%83%B3%E3%82%B0%E3%82%92%E8%87%AA%E5%8B%95%E5%8C%96%E3%80%80%E3%81%9D%E3%81%AE%EF%BC%93/","title":"Rectorでリファクタリングを自動化　その３"},{"content":"前回のCypressでの画面表示テストに続き、今回はログイン・ログアウトのテストです。カスタムコマンドの作り方やHTTPリクエストの実行など、Cypressのテストに欠かせないアプローチが出てきますので参考になれば嬉しいです。\nCypressのセットアップや画面表示テストなど、以前の記事は以下からご覧いただけます。\nCypressセットアップ・実行方法 Cypress画面表示のテスト\nフォーム入力・ボタンクリック ログインのテストは前回に続きLaravel Breezeのログイン画面で行います。フォーム画面のテストには、テキスト入力・ボタンクリックの操作が必要ですので、それぞれCypressのtype()とclick()コマンドを使用します。\nどちらのコマンドもcy.type()\u0026hellip;のようにcyに直接繋げるのではなく、操作対象の要素を取得した後に繋いで使用します。例えばメールアドレス入力欄にメールアドレスを入力、またsubmitボタンをクリックといった操作は、以下のように記述します。\ncy.get(\u0026#39;input[name=\u0026#34;email\u0026#34;]\u0026#39;).type(\u0026#39;test@example.com\u0026#39;) cy.get(\u0026#39;button[type=\u0026#34;submit\u0026#34;]\u0026#39;).click() cypress.env.jsonに環境変数をセット ログイン情報など、テストの実行環境によって異なる値はcypress.env.jsonを作成してそちらにまとめておくと便利です。\n今回使用するユーザーのメールアドレス・パスワードは、以下のように定義しました。\ncypress.env.json { \u0026#34;email\u0026#34;: \u0026#34;test@example.com\u0026#34;, \u0026#34;password\u0026#34;: \u0026#34;password\u0026#34; } テストコードの中でこの値を呼び出すには、Cypress.env()を使います。\ncy.get(\u0026#39;input[name=\u0026#34;email\u0026#34;]\u0026#39;).type(Cypress.env(\u0026#39;email\u0026#39;)); cy.get(\u0026#39;input[name=\u0026#34;password\u0026#34;]\u0026#39;).type(Cypress.env(\u0026#39;password\u0026#39;)); ログイン成功・失敗のテストコード type()・click()を使用した、ユーザーがログインに成功するケースとエラーが発生するケースのテストコードは以下のようになります。アサーションは前回と同じくshould()を使って検証します。\nit(\u0026#39;正しいメールアドレス・パスワードでログイン成功\u0026#39;, () =\u0026gt; { cy.visit(\u0026#39;/login\u0026#39;) cy.get(\u0026#39;input[name=\u0026#34;email\u0026#34;]\u0026#39;).type(Cypress.env(\u0026#39;email\u0026#39;)) cy.get(\u0026#39;input[name=\u0026#34;password\u0026#34;]\u0026#39;).type(Cypress.env(\u0026#39;password\u0026#39;)) cy.get(\u0026#39;button[type=\u0026#34;submit\u0026#34;]\u0026#39;).click() // ダッシュボードに遷移 cy.url().should(\u0026#39;eq\u0026#39;, Cypress.config().baseUrl + \u0026#39;/dashboard\u0026#39;) }) it(\u0026#39;パスワード間違いでログイン失敗\u0026#39;, () =\u0026gt; { cy.visit(\u0026#39;/login\u0026#39;) cy.get(\u0026#39;input[name=\u0026#34;email\u0026#34;]\u0026#39;).type(Cypress.env(\u0026#39;email\u0026#39;)) cy.get(\u0026#39;input[name=\u0026#34;password\u0026#34;]\u0026#39;).type(\u0026#39;wrong-password\u0026#39;) //間違ったパスワード cy.get(\u0026#39;button[type=\u0026#34;submit\u0026#34;]\u0026#39;).click() //ログイン画面のまま cy.url().should(\u0026#39;include\u0026#39;, \u0026#39;/login\u0026#39;) // 画面のエラー表示を確認 cy.get(\u0026#39;input[name=\u0026#34;email\u0026#34;]\u0026#39;) .next(\u0026#39;ul\u0026#39;) .should(\u0026#39;contain\u0026#39;, \u0026#39;ログイン情報が存在しません。\u0026#39;) }) ログイン失敗のテストでは、「ログイン情報が存在しません。」のエラーメッセージが存在することを確認しています。このエラーメッセージはLaravel Breezeでは以下のHTMLで出力されます。\n\u0026lt;input class=\u0026#34;border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm block mt-1 w-full\u0026#34; id=\u0026#34;email\u0026#34; type=\u0026#34;email\u0026#34; name=\u0026#34;email\u0026#34; value=\u0026#34;test@example.com\u0026#34; required=\u0026#34;required\u0026#34; autofocus=\u0026#34;autofocus\u0026#34; autocomplete=\u0026#34;username\u0026#34;\u0026gt; \u0026lt;ul class=\u0026#34;text-sm text-red-600 space-y-1 mt-2\u0026#34;\u0026gt; \u0026lt;li\u0026gt;ログイン情報が存在しません。\u0026lt;/li\u0026gt; \u0026lt;/ul\u0026gt; メッセージ出力箇所のul・liには要素特定に使えそうなidやclassが割り当てられていないため、input[name=\u0026ldquo;email\u0026rdquo;]要素の次のul要素内をチェックします。\nrequest・thenコマンド さて次はログアウトですが、テストを作成する前にrequest()とthen()コマンドをご紹介します。\nrequest()は、HTTPリクエストを送信するために使用するコマンドです。以下のように記述すると、引数のURLに対してGETリクエストを送信します。\ncy.request(\u0026#39;/login\u0026#39;) POST送信の場合はオブジェクト形式でオプションを渡す必要があります。以下のように記述すると、POSTメソッドでURL/loginに対してリクエストを送信、かつリクエストbodyにはパスワード情報を含める。という意味になります。\ncy.request({ method: \u0026#39;POST\u0026#39;, url: \u0026#39;/login\u0026#39;, body: { password:\u0026#39;password\u0026#39;, } }) では次にthen()コマンドです。then()は単独で使うのではなく他のコマンドにチェーンして、前のコマンドからの結果を受け取る形で記述します。具体的な使用方法として、先ほどのPOST送信にthen()を繋げました。\ncy.request({ method: \u0026#39;POST\u0026#39;, url: \u0026#39;/login\u0026#39;, body: { password:\u0026#39;password\u0026#39;, } }).then((response) =\u0026gt; { expect(response.status).to.eq(200) }) このコードでは、HTTPリクエストが完了するのを待ってから、そのレスポンスをresponseという引数で受け取ります。その後、コールバック関数内でレスポンスのステータスコードを検証しています。\nthen()は他にもHTML要素や任意の値やオブジェクトなども扱えます。詳しくはドキュメントをご参照ください。\nログアウトのテスト ではログアウトのテストを作成してゆきましょう。画像のように、ドロップダウンを開いてログアウトリンクをクリックするとログアウトすることをテストします。\nこういったログアウトのようなテストでは、ユーザーがすでにログインしている状態を準備する必要があります。これを、直接フォームに入力してボタンをクリックする方法で行なっても良いのですが、それでは画面操作の分余計な時間がかかってしまいます。\nそこで、先ほどご紹介したrequest()を使用してHTTPリクエストでログインすることにします。これにより、UIを介さずに効率的にログイン処理を完了できます。\nログイン処理のカスタムコマンドを作成 テスト内で繰り返し使用する処理はカスタムコマンドとして定義します。これにより、どのテストからでも簡単に呼び出して使用できるようになります。\ncypress/support/commands.jsファイルにカスタムコマンドを追加し、その際Cypressが定めた以下の構文を使用します。\nCypress.Commands.add(\u0026#39;カスタムコマンド名\u0026#39;, (引数) =\u0026gt; { }) 以下が作成したカスタムコマンドです。ユーザー画面のログインなのでコマンド名はloginAsUserとしました。\ncypress/support/commands.js Cypress.Commands.add(\u0026#39;loginAsUser\u0026#39;, (email, password) =\u0026gt; { // Step 1: ログインページにアクセスしてCSRFトークンを取得 cy.visit(\u0026#39;/login\u0026#39;) cy.get(\u0026#39;input[name=\u0026#34;_token\u0026#34;]\u0026#39;).invoke(\u0026#39;val\u0026#39;).then((csrfToken) =\u0026gt; { // Step 2: 取得したCSRFトークンを使用してログインリクエストを送信 cy.request({ method: \u0026#39;POST\u0026#39;, url: \u0026#39;/login\u0026#39;, form: true, // フォームデータとして送信 body: { email: email, password: password, _token: csrfToken // CSRFトークンをリクエストに含める } }).then((response) =\u0026gt; { // Step 3: ログインが成功したかステータスコードで確認 expect(response.status).to.eq(200); }); }); }) ログインするだけなのに少し長いコマンドになっていますが、内容は大きく以下の３ステップに分かれています。\nログインに必要なCSRFトークンを取得する処理 cy.requestを使用してログインリクエストを実行 ログイン成功のステータス確認 まず、ステップ１ではログイン画面へアクセスし、トークンのデータを含むHTML要素を取得。その要素に対しinvoke(\u0026lsquo;val\u0026rsquo;)を使用し、トークンの値だけを抜き出しています。invoke(\u0026lsquo;val\u0026rsquo;)とはjQueryのval()メソッドを呼び出すということを意味します。なぜjQueryが使えるのか?というと、これはCypressがjQueryを内包しているためこのような値の取得方法が可能になっています。\nさて、こうして取得したトークンは次のHTTPリクエストで使用します。そのためthen()で繋ぎ、トークンをcsrfTokenとして受け取ります。\nそしてステップ２は、request()でのHTTPリクエスト実行です。リクエストメソッド、URL、リクエストデータなどを引数に渡しています。_tokenとしてcsrfTokenも含まれていますね。\n最後のステップ３はレスポンスの検証です。then()でレスポンスを受け取り、expect()を使ってHTTPステータスを確認しています。expect()はCypressが内部で使用しているChai.jsというアサーションライブラリのメソッドであるため、cy.に繋げず単独で使用できます。\nコマンドが完成しました！これをテストの中では、以下のように呼び出して使います。\ncy.loginAsUser() ログアウトのテストコード カスタムコマンドを使用したログアウトのテストコードは以下のようになります。\nit(\u0026#39;ログアウトリンククリックでログアウト\u0026#39;, () =\u0026gt; { //カスタムコマンドでログイン cy.loginAsUser(Cypress.env(\u0026#39;email\u0026#39;), Cypress.env(\u0026#39;password\u0026#39;)) //テスト対象画面へ遷移 cy.visit(\u0026#39;/dashboard\u0026#39;) //ドロップダウンメニューを開くためのボタンクリック・ログアウトボタンをクリック cy.get(\u0026#39;.relative\u0026#39;).click() cy.contains(\u0026#39;a\u0026#39;, \u0026#39;ログアウト\u0026#39;).click() //ホーム画面へ遷移したことを確認 cy.url().should(\u0026#39;eq\u0026#39;, Cypress.config().baseUrl + \u0026#39;/\u0026#39;) }); カスタムコマンドによるログイン処理、ログアウト実行、アサーション、という構成です。ログアウトテストに直接関係のないログイン処理を別ファイルに切り出しているので、テストコードがすっきりしていいですね。\nでは、ここまでのログイン成功・ログイン失敗・ログアウトのテストを全て実行してみます。\n無事成功しました！次回は、Cypressでメール確認のテストを作成します。\n参照 CypressでE2Eテストを自動化（１）セットアップ CypressでE2Eテストを自動化（２）画面表示のテスト","date":"2024-08-04T06:19:43+09:00","permalink":"https://www.larajapan.com/2024/08/04/cypress%E3%81%A7e2e%E3%83%86%E3%82%B9%E3%83%88%E3%82%92%E8%87%AA%E5%8B%95%E5%8C%96%EF%BC%88%EF%BC%93%EF%BC%89%E3%83%AD%E3%82%B0%E3%82%A4%E3%83%B3%E3%83%BB%E3%83%AD%E3%82%B0%E3%82%A2%E3%82%A6%E3%83%88/","title":"CypressでE2Eテストを自動化（３）ログイン・ログアウト"},{"content":"長年管理しているお客さんのプロジェクトのユニットテスの数が増えてきました。Featureテストも含めてテストケースの数は2,400以上あります。私の古めのWindowsのマシン（i7-9700CPU @ 3.00GHz）のWSL2環境では、7分51秒かかります。今回はこのテスト実行所有時間をほぼ1/10の50秒以下に縮めた話です。\nmysqlをメモリー内で実行 DBを使用するテストが多いので最初に考えたのは、使用しているmysqlサーバーのエンジンをInnoDBからMemoryとすることです。しかしこれはすぐに不可能とわかりました。MemoryのエンジンではTEXTのデータタイプを対応していないからです。\nどうしたらよいかといろいろ調査すると、mysqlのDBを保存しているボリュームをハードディスクのストレージからtmpfsに変えれば良いらしい。tmpfsは以下に説明されるようにメモリに存在するファイルシステムです。 tmpfsとは\nしかし、ローカル環境（WSL2）で実行されているmysqlサーバーをtmpfsで走らせるとなると、mysqlサーバーがいろいろなプロジェクトで共有されているためにとても不都合であるし、マシンを落としたらそれでDB内のデータは消えてしまうとなるともっと困ります。\nということで、sailを使用してdocker内でmysqlサーバーをtmpfsで実行するのが理想となりました。\nsailのセットアップに関してはここでは説明しませんが、セットアップの産物としてdocker-compose.ymlが作成されます。\nこのdocker-compose.ymlのmysqlサーバーの定義において、以下のようにvolumesのsail-mysqlをコメントアウトして、tmpfsを追加します。\n... mysql: image: \u0026#39;mysql/mysql-server:8.0\u0026#39; ports: - \u0026#39;${FORWARD_DB_PORT:-3306}:3306\u0026#39; environment: MYSQL_ROOT_PASSWORD: \u0026#39;${DB_PASSWORD}\u0026#39; MYSQL_ROOT_HOST: \u0026#39;%\u0026#39; MYSQL_DATABASE: \u0026#39;${DB_DATABASE}\u0026#39; MYSQL_USER: \u0026#39;${DB_USERNAME}\u0026#39; MYSQL_PASSWORD: \u0026#39;${DB_PASSWORD}\u0026#39; MYSQL_ALLOW_EMPTY_PASSWORD: 1 volumes: # - \u0026#39;sail-mysql:/var/lib/mysql\u0026#39; # コメントアウト！ - \u0026#39;./vendor/laravel/sail/database/mysql/create-testing-database.sh:/docker-entrypoint-initdb.d/10-create-testing-database.sh\u0026#39; tmpfs: # ここを追加！ - /var/lib/mysql networks: - sail healthcheck: test: - CMD - mysqladmin - ping - \u0026#39;-p${DB_PASSWORD}\u0026#39; retries: 3 timeout: 5s ... 以下を実行して、\n$ sail up dockerを立ち上げて、docker内のmysqlサーバーのファイルシステムを見ると、\nsh-4.4# df -h Filesystem Size Used Avail Use% Mounted on overlay 251G 26G 213G 11% / tmpfs 64M 0 64M 0% /dev tmpfs 7.8G 0 7.8G 0% /sys/fs/cgroup shm 64M 0 64M 0% /dev/shm /dev/sdd 251G 25G 214G 11% /docker-entrypoint-initdb.d/10-create-testing-database.sh /dev/sde 251G 26G 213G 11% /etc/hosts tmpfs 7.8G 199M 7.6G 3% /var/lib/mysql tmpfs 7.8G 0 7.8G 0% /proc/acpi tmpfs 7.8G 0 7.8G 0% /sys/firmware /var/lib/mysqlがtmpfsとなっていますね。\nテストを実行してみましょう。\n$ sail artisan test テスト実行の所有時間は、7分51秒から219秒から3分39秒と一気に半分以下となりました。\n並行処理でテストを実行 今度は、パラレルテストを利用してもっと高速化してみましょう。\nまず、必要なライブラリをインストールします。\n$ sail composer require brianium/paratest --dev その後、パラレルテストを実行します。\n$ sail artisan test --parallel tmpfsとパラレルを組み合わせたテスト実行となり、その結果、所有時間は驚異の49秒となりました。ほぼ1/10の時間です。\nDocker内のmysqlサーバーでデータベースを閲覧すると、以下のように、testingのデータベース７個新たに作成ｓれて、計８個となっています。これは私のマシンが８コアのためです。パラレルテストは使用のマシンのコア数でデータベースの数が決まります。\nmysql\u0026gt; show databases; +--------------------+ | Database | +--------------------+ | information_schema | | laravel | | mysql | | performance_schema | | sys | | testing | | testing_test_1 | | testing_test_2 | | testing_test_3 | | testing_test_4 | | testing_test_5 | | testing_test_6 | | testing_test_7 | +--------------------+ ちなみに、tmpfsなしのパラレルテストの所要時間は、5分28秒でした。tmpfs + parallelが最速のチームということですね。\n","date":"2024-07-17T11:26:20+09:00","permalink":"https://www.larajapan.com/2024/07/17/%E3%83%A6%E3%83%8B%E3%83%83%E3%83%88%E3%83%86%E3%82%B9%E3%83%88%E3%81%AE%E8%B6%85%E9%AB%98%E9%80%9F%E5%8C%96/","title":"ユニットテストの超高速化"},{"content":"テスト自動化ツールCypressのセットアップに続いて、今回は画面表示のテストです。Cypressでの画面遷移や要素の取得、アサーションなど基本的な書き方をご紹介します。\nCypressの構文 Cypressの基本的な構文は、以下のように３つのグループに分かれています。describeとcontextはテストケースのグループを、itは１つ１つのテストケースを表しています。\ncontextはdescribeのエイリアスなので、機能は同じです。テストケースの大まかな定義や特定の条件でのグループ分けなど、テストのスタイルによって使い分けることができます。\ndescribe(\u0026#39;大項目\u0026#39;, () =\u0026gt; { context(\u0026#39;中項目\u0026#39;, () { it(\u0026#39;テスト１\u0026#39;, () =\u0026gt; { //テストの内容 }) it(\u0026#39;テスト２\u0026#39;, () =\u0026gt; { //テストの内容 }) }) }) テストを作成する際にこれらの３つの項目全てが必要ということはなく、最小単位であるitだけでテストを構成しても問題ありません。\nCypressの画面遷移・要素の取得 今回のテスト対象はLaravel Breezeのログイン画面です。\n・画面が表示できるか ・ダッシュボードにアクセスするとログイン画面にリダイレクトされるか\nをテストします。\nこれらのテストに必要なCypressのコマンドについて、１つずつ進めてゆきましょう。\nまずは画面遷移から。visit()コマンドを使用します。引数にアクセスしたいURL（ここではローカルのログイン画面のURL）を渡して以下のように記述します。\ncy.visit(\u0026#39;http://127.0.0.1:8000/login\u0026#39;) 最初にcy.とありますが、Cypressではこのようにcy.の後にメソッドを繋げて記述します。\nテスト対象画面に遷移できたら、次はメールアドレス入力欄やログインボタンの要素を取得します。画面の要素を取得するには、get()やcontains()を使用して以下のように書きます。\ncy.get(\u0026#39;input#email\u0026#39;) cy.get(\u0026#39;button[type=\u0026#34;submit\u0026#34;]\u0026#39;).contains(\u0026#39;ログイン\u0026#39;) get()の引数には取得したい要素のセレクタを渡します。上の例ではidが\u0026quot;email\u0026quot;であるinput要素、typeが\u0026quot;submit\u0026quot;であるbutton要素を指定しています。\nそしてcontains()は、対象のテキストを「含む」要素を取得します。上記の例では、まずget()でtypeが\u0026quot;submit\u0026quot;であるbutton要素を取得し、contains()をチェーンで繋いで\u0026quot;ログイン\u0026quot;というテキストが含まれる要素に絞り込んでいます。「含む」なのでもし\u0026quot;ログインする\u0026quot;などの他のテキストが含まれていても取得対象となります。\nCSSセレクタの指定については「○番目の要素」や「○というデータ属性」などの細かい指定も可能です。ドキュメントには色々な例が載っていますのでご参照ください。\nまた、URLの確認もテストではよくあるかと思いますが、こちらもコマンドが用意されています。\ncy.url() これだけで、現在のURLを文字列として取得してくれます。\nCypressのアサーション ここまででテスト対象の要素は取得できたので、検証に進みます。get()やcontains()の後にshould()をチェーンで繋ぎます。\ncy.get(\u0026#39;input#email\u0026#39;).should(\u0026#39;exist\u0026#39;) cy.get(\u0026#39;button[type=\u0026#34;submit\u0026#34;]\u0026#39;).contains(\u0026#39;ログイン\u0026#39;).should(\u0026#39;exist\u0026#39;) should(\u0026rsquo;exist\u0026rsquo;)のように、引数にはアサーション方法を渡します。上の例では要素が「存在すること」を確認しています。\n引数として渡すアサーション方法は色々あり、他にも「include（含む）」「eq（イコール）」などの確認ができます。\ncy.url().should(\u0026#39;include\u0026#39;, \u0026#39;login\u0026#39;) cy.url().should(\u0026#39;eq\u0026#39;, \u0026#39;http://127.0.0.1:8000/login\u0026#39;); baseUrlを設定 visit()の引数であるURLですが、対象のURLを毎回記述するのは不便です。以下のようにcypress.config.jsにベースURLを定義しておきましょう。\n以下のように、baseUrl: \u0026lsquo;対象のURL\u0026rsquo;をe2eの中に記述します。\ncypress.config.js import { defineConfig } from \u0026#34;cypress\u0026#34;; export default defineConfig({ e2e: { baseUrl: \u0026#39;http://127.0.0.1:8000\u0026#39;, //ここを追加 setupNodeEvents(on, config) { // implement node event listeners here }, }, }); これで先ほどの画面遷移は以下のようにすっきり書くことができます。\n//cy.visit(\u0026#39;http://127.0.0.1:8000/login\u0026#39;) cy.visit(\u0026#39;/login\u0026#39;) テストの実行 いよいよテスト実行です。ここまでで出てきたコマンドを使用して、テストコードの全体は以下のようになりました。\nit(\u0026#39;ログイン画面表示\u0026#39;, () =\u0026gt; { cy.visit(\u0026#39;/login\u0026#39;) cy.get(\u0026#39;input#email\u0026#39;).should(\u0026#39;exist\u0026#39;) cy.get(\u0026#39;input#password\u0026#39;).should(\u0026#39;exist\u0026#39;) cy.get(\u0026#39;button[type=\u0026#34;submit\u0026#34;]\u0026#39;).contains(\u0026#39;ログイン\u0026#39;).should(\u0026#39;exist\u0026#39;) }) it(\u0026#39;未ログイン時はログイン画面へリダイレクト\u0026#39;, () =\u0026gt; { cy.visit(\u0026#39;/dashboard\u0026#39;) cy.url().should(\u0026#39;include\u0026#39;, \u0026#39;login\u0026#39;) }) Cypressブラウザを立ち上げて、ファイル名をクリックするとテストが走ります。起動・テスト実行の詳細は前回の記事をご覧ください。\nテストが成功しました！テストケース名をクリックするとアサートの内容など、テストの詳細を見ることができます。\n次回は、要素の入力を伴うログイン関連のテストを作成します。\n参照 CypressでE2Eテストを自動化（１）セットアップ CypressでE2Eテストを自動化（３）ログイン・ログアウト","date":"2024-07-03T12:59:35+09:00","permalink":"https://www.larajapan.com/2024/07/03/cypress%E3%81%A7e2e%E3%83%86%E3%82%B9%E3%83%88%E3%82%92%E8%87%AA%E5%8B%95%E5%8C%96%EF%BC%88%EF%BC%92%EF%BC%89%E7%94%BB%E9%9D%A2%E8%A1%A8%E7%A4%BA%E3%81%AE%E3%83%86%E3%82%B9%E3%83%88/","title":"CypressでE2Eテストを自動化（２）画面表示のテスト"},{"content":"円安の世の中AWSのドル支払は痛いです。最近お客さんのためにコスト削減できないかと調査したところなんと１ヶ月に1,400ドル以上もコストを下げる結果となりました。年間にしたら16,800ドルの節約です！\nS3からの画像配信 コスト削減を思い立って、まずお客さんが使用しているAWSサービスのコストを再検討しました。EC2やRDSはもうすべて１年間のリザーブを購入しているので、オンデマンドに比べて30%以上の節約はしています。となると節約できるのは転送量や使用量ベースのコストです。ウェブのアクセスやS3の画像のアクセスを私がコントロールできないので、AWSジャパンの無料のコスト解析のおにいさんたちに相談してみました。しかし、セーブしているところはしっかりセーブしていますよ！と逆に褒められる始末。 そのときに、そのおにいさんに１つ勧められたのは、S3から直接画像を配信するのではなく、そこをオリジンとしてCloudFrontから配信すれば良いということです。CloudFrontを使えばユーザーから一番近いデータセンターから配信されます。でもTokyoとOsakaしか日本にはデータセンターないのではと尋ねると、公開できないけれどあちこちにあるのですよと。しかし小さい日本、そんなに変わるかなと思っていると、今度は１ヶ月の最初の1TBは無料なのですよ、とびきりいい話をくれました。ちなみに、私のお客さんのAWSのS3には、ウェブで閲覧できる画像だけでなく、配信するメルマガに含まれるリンク先の画像も置いています。ここ１年間メールの配信数が増加しているのでS3のアクセスがかなり多いのは確かです。\n早速、AWS Cost ExplorerでS3の使用量をチェックしたところ、なんと１ヶ月のS3の使用量は、13,000GB（12TB以上）ありました。最初の1TBが無料はいい話だけれど、削減率はたった10%以下ですか。\nということで、本格的にCDNの市場調査とあいなりました。\nBunny（ウサギ）のCDNサービス いろいろ調査してわかったのは、CDNに関しては世界的スケールなので英語圏のサービスとなるとAWSより安いのがたくさんあることです。問題は、お客さんのサイトは日本語で日本に住む人だけが対象なので（商品は日本しか発送しません）、CDNのデータセンターが日本にあることが必須条件です。その条件でとびきり安いのが今回紹介するBunnyのCDNサービスです。\nまず、どれだけ安いかというと、\nアジア向けのスタンダードプランは、１GBあたり0.003ドル！\nAWSのS3やCloundFrontは１GBあたり0.114ドルだから、すでに73%以上のディスカウント率です。\nさらに、月1TB以上の大量消費のお客様のためにボリュームプランもあります。\n超安の１GBあたり0.005ドル！\n再度、AWSと比べると95%以上のディスカウントです。\n彼らの計算機を使うと、１ヶ月のコストは、\nちなにに、AWS S3のコストは、1,482ドルです。\nPOPS CDNのクォリティの基準として、POP (Points of Presence)の数、つまりデータを配信するデータセンターの数の多さというものがあります。\nBunnyのスタンダードプランを見てみましょう。\n世界中に123のデータセンターが散らばっています。日本周辺では、Tokyo, Osaka, Seoul, Hong Kong, Taipei, Singaporeがロケーションです。\nボリュームプランでは、POP数はぐっと少なり10となりました。しかし、日本ではTokyoが含まれているではないですか（2023年かららしい）！\n日本周辺では、Tokyo, Hong Kong, Singaporeがデータセンターのロケーションです。 先に話したように日本に住む日本の対象ならこれでも十分ですね。もちろん、どこのデータセンターもBunnyが一部をどこかから借りているのは確かです。\nCDNの設定 早速、登録して使用してみましょう。 登録して14日間は無料で使用可能なので試してみましょう。\n登録したと仮定して、すでにAWSのS3バケットをお持ちなら、CDNの設定はいたって簡単です。以下のように、Bunnyで希望するCDNのアドレスとS3のアドレスを入れて、High Volumeを選択するだけです。\nこれで、例えば、https://larajapan.b-cnd.net/test.jpgにアクセスが可能となります。\nCDNのPull Zoneを作成すれば、キャッシュの期間の設定、オリジンの認証、画像のクォリティの自動操作、Jpeg画像のwebpへの自動変換、など細かな設定がたくさんありますが、いろいろな対応が可能です。もちろん、CDN Pull Zoneは複数作成可能で、S3バケットごとに作成も可能です。\nCDNのキャッシュの削除 お客さんのサイトでBunnyのCDNを使用してみました。使用の前にプログラムが必要だったのは、サイトの商品画像の差し替えでした。以前はS3の画像を取り換えるだけでしたが、今度はそれとともに、Bunnyで対応する画像のキャッシュの削除です。これをやらないとBunnyのキャッシュ保持の期間があるために、すぐには画像が変わりません。もちろん、ブラウザでのキャッシュの問題もありますが。\nBunnyでのキャッシュの削除のプログラムは簡単です。\nまず、APIキーが必要です。以下の矢印の部分をクリックしドロップダウンメニューから登録した自分の名前をクリックします。そこのAccount Settingの画面でAPI Keyのメニューをクリックすれば以下のようにキーの取得が可能となります。シンプルなことにBunnyの他のサービスはすべてこのキーひとつで操作可能です。\nキーを取得しところで、パージの削除は、Laravelなら以下のようにHttpのファサードを使用します。\n$url = \u0026#39;https://larajapan.b-cdn.net/test.jpg\u0026#39;; //キャッシュ削除の対象のurl $purgeUrl = \u0026#39;https://api.bunny.net/purge\u0026#39;;　// Bunnyの削除のためのurl $response = Http::withHeaders([ \u0026#39;AccessKey\u0026#39; =\u0026gt; \u0026#39;ここにAPIキーを入れる\u0026#39;, \u0026#39;accept\u0026#39; =\u0026gt; \u0026#39;application/json\u0026#39;, ])-\u0026gt;get($purgeUrl, [ \u0026#39;url\u0026#39; =\u0026gt; $url, \u0026#39;async\u0026#39; =\u0026gt; \u0026#39;false\u0026#39;, ]); 使用した感想は？ ここが、皆さんが一番知りたいところでしょうね。\nお客さんのサイトでS3からの画像配信をBunnyのCDNへ変えて、かれこれ３ヶ月になります。良いニュースは、冒頭で書いた通りの凄いコスト削減となりました。不思議なのは、すべて日本のTokyoのデータセンターから配信されると思いきや、以下のようにTokyoとSingaporeからの配信が半々くらいです。アメリカでの配信があるのは、どうもメルマガに含まれる画像リンク（画像ではない）の画像をGoogleが彼らのデータセンターでキャッシュするからのようです。\nさて良いニュースがあれば悪いニュースもあります。それほど悪いニュースという訳でもないのですが、困ったのは、先に書いたBunnyのCDNのキャッシュの削除がうまく行かないときが数回ありました。つまり、お客さんのところでS3の画像の差し替え（同じファイル名で画像の内容を変える）の前に、既存の画像をS3から削除しBunnyのCDNのキャッシュの削除を行っても、このキャッシュがすぐに削除されないのです。ひどいときは削除まで１日近くかかりました。この間差し替えたのにも関わらず、以前の画像が表示されるのです。\nこの件に関してサポートへ連絡しました。受け答えは即座でいいのですが、いつまでも解決されない。しつこく尋ねると、「海底ケーブルが切れて障害が生じている」と壮大なことを言ってきます。画像の差し替えの頻度はとても低いのですが、お客さんが困っているのでS3からの画像配信に戻しました。サポートからの連絡がなかったのですが、１週間くらい経過してから問題が解決したのでBunnyのCDNに戻しました。もちろんその間はコストありましたが、いざという時に切り替えるようにプログラムを設定しておくのはいいことです。\nしかし、それで解決と思いきや最近また発生しました。私の方も準備万端ですでにキャッシュの削除の問題をモニターするプログラムも作成してありますので問題が発生すればすぐわかります。今回は前回よりはベターでキャッシュは最高１時間くらいで削除されました。しかしそれでも１時間はかかりすぎです。今回も対応されないと思いましたが、ダメもとでサポートに連絡しました。しかし今度は担当がいいのか、まじめに取り組んでくれてサーバーの調整をしてくれました。さらに、私がどうモニターしているかも尋ねてくれて、最終的には彼ら自身もTokyoにS3バケットを持ちTokyoのサーバーでキャッシュの削除時間をモニター作成したということです。すぐには復旧となりませんでしたが24時間以内に復旧となりました。前回に比べてかなりのサービスの改善です。まあ、将来はわからないので私のモニターは続きます。\n最後に AWSに比べて削減できたコストは信じられないレベルです。私のお客さんはEコマースのサイトなので配信速度に関してはそうセンシティブはありませんゆえに、速度の比較はしていません。ここはケースバイケースでしょう。私は経験がありませんが、BunnyのCDNにはビデオのストリーミングやS3と似たようなファイル保存のサービスも提供しています。コストが気になるなら無料で試してみるのがベストです。\n","date":"2024-06-25T03:58:47+09:00","permalink":"https://www.larajapan.com/2024/06/25/%E3%81%A8%E3%81%A6%E3%82%82%E5%AE%89%E3%81%84%E3%82%A6%E3%82%B5%E3%82%AE%E3%81%AEcdn-bunny-net/","title":"とても安いウサギのCDN - Bunny.net"},{"content":"流行し始めた当初は何て事ない個人の呟きが主流だったのが、あれよあれよと著名人が使い出し、いつの間にか近代SNSの代名詞へと変貌を遂げたTwitterもといXですが、実はこのララジャパンもアカウントを持っていて時折最新記事が投稿されたらお知らせをツイートしています。しかし、何分手動で行なっているのでついつい忘れてしまいサボり気味です。（ごめんなさい！）何とか改善しなければ、と思いX APIを活用して自動ツイートが出来ないか試してみる事にしました。\nX API Xは旧来よりAPIで豊富な機能を解放しており、自身のツイートの操作やそれらへのアクセス解析、または広くツイッター上から関連するツイートを検索したり、と色々な用途に活用されているようです。現在はv1.1とv2が提供されおり、後者の利用が推奨されています。無料で利用できる機能には制限がありますが今回はAPI経由でツイートをするだけなのでFreeプランで十分です。有料プランとの違いについてはこちらのページのX API access levels and versionsに詳細があります。本記事ではLaravelのプログラムからTwitter APIを叩いて任意のテキストをツイートする、までをゴールとして以下の順序で解説します。Twitterのアカウントをお持ちでない方は予め作成しておいてください。\n開発者プラットフォームにサインアップ API Keyとアクセストークンを取得 Laravel側でX APIを叩くプログラムを実装 開発者プラットフォームにサインアップ X APIを利用するにはまず開発者プラットフォームにてサインアップする必要があります。こちらのページのSign up for Free Accountから進んでください。\n次のページでは利用上のポリシーへの同意と利用目的を250文字以上で入力します。「ブログが更新された際のお知らせを自動でツイートする為」等の趣旨を記載しました。\n登録が完了すると開発者プラットフォームのダッシュボードページへ遷移します。\nAPI Keyとアクセストークンの取得 ダッシュボードページを開くと自動でDefaultプロジェクトが作成されているかと思います。\n（2024-06-26追記） APIの叩く為の認証情報を取得するのですが、デフォルトでは発行される鍵の権限がRead Onlyとなっています。ツイートを投稿する処理はWrite権限も必要となるので設定を変更しましょう。歯車マークをクリックして設定ページを開きます。\n次にUser authentication settingsという項目があるのでそちらのSet upボタンを押し認証設定ページへ遷移します。App permissionsがデフォルトでReadとなっているのでRead and writeへ変更します。今回の実装はWebアプリなのでType of App欄はWeb App, Automated App or Botを選択しました。\nApp info欄にはAPIを認証後のリダイレクト先の指定が必須となっていますが、今回はコマンドからの実行のみで認証後のリダイレクトは不要なのでhttps://x.comとしました。Website URL欄も今回は特定のWebアプリへインストールするわけでは無いのでhttps://x.comとしました。\n設定を保存するとOAuth2.0用にClient IDなどが発行されますが、今回は使用しないので無視してOKです。続いて、API Keyとアクセストークンを発行します。プロジェクトページへ戻り鍵マークをクリックしてKeys and tokensのページへ遷移します。\n以下からAPIキーとそのSecret、アクセストークンとそのSecretをそれぞれ取得して下さい。後にアプリ側からAPIを叩く際に必要となります。\nここまで完了したら次はLaravelアプリ側の実装を進めましょう。\nLaravelからAPIを叩くプログラムの実装 既にLaravelがインストールされている状態からの解説です。コマンドラインからX APIを叩ける様にXApiCommandを実装していきます。　APIを叩く際の認可方式としてOAuth1.0aかOAuth2.0がサポートされていますが、それらのフローを１から実装するのはとても大変なので既存のライブラリを使用します。こちらのページのPHP欄から利用可能なライブラリ一覧が確認できます。一覧の中で、twitteroauthが飛び抜けて星の数が多かったのでそちらを試してみました。まずは以下でパッケージをインストールします。\ncomposer require abraham/twitteroauth こちらのパッケージを使用してX APIへリクエストを投げる際の基本操作は以下です。tinkerを使用して説明します。\n// クラス初期化時に認証情報をセットする（前項で取得したもの） $connection = new TwitterOAuth($apiKey, $apiSecret, $accessToken, $accessTokenSecret); = Abraham\\TwitterOAuth\\TwitterOAuth {#5026} // v2を使用する場合はバージョン指定 $connection-\u0026gt;setApiVersion(\u0026#39;2\u0026#39;); = null // GETリクエストを投げる時 // $connnection-\u0026gt;get($endpoint, $params); // 以下は自身の情報を取得するエンドポイント $connection-\u0026gt;get(\u0026#39;users/me\u0026#39;); = {#5083 +\u0026#34;data\u0026#34;: {#5046 +\u0026#34;id\u0026#34;: \u0026#34;{your twitter id}\u0026#34;, +\u0026#34;name\u0026#34;: \u0026#34;{your twitter name}\u0026#34;, +\u0026#34;username\u0026#34;: \u0026#34;{your twitter username}\u0026#34;, }, } // POSTリクエストを投げる時 // $connection-\u0026gt;post($endpoint, $params); // 以下はツイートを投稿するエンドポイント $connection-\u0026gt;post(\u0026#39;tweets\u0026#39;, [\u0026#39;text\u0026#39; =\u0026gt; \u0026#39;Tweet through API\u0026#39;]); = {#5044 +\u0026#34;data\u0026#34;: {#5047 +\u0026#34;edit_history_tweet_ids\u0026#34;: [ \u0026#34;{your tweet ids}\u0026#34;, ], +\u0026#34;id\u0026#34;: \u0026#34;{your tweet id}\u0026#34;, +\u0026#34;text\u0026#34;: \u0026#34;Tweet through API\u0026#34;, }, } 各エンドポイントの詳細ついては開発者プラットフォームのAPI reference indexをご参照ください。\n次に設定ファイルを追加して前項で取得したAPI Keyやアクセストークンをプログラム内で扱えるようにします。config/twitter.phpファイルを追加して以下の様にセットしてください。\nconfig/twitter.php return [ \u0026#39;api_key\u0026#39; =\u0026gt; env(\u0026#39;TWITTER_API_KEY\u0026#39;), \u0026#39;api_secret\u0026#39; =\u0026gt; env(\u0026#39;TWITTER_API_SECRET\u0026#39;), \u0026#39;access_token\u0026#39; =\u0026gt; env(\u0026#39;TWITTER_ACCESS_TOKEN\u0026#39;), \u0026#39;access_token_secret\u0026#39; =\u0026gt; env(\u0026#39;TWITTER_ACCESS_TOKEN_SECRET\u0026#39;), ]; それぞれ.envから取得する様にしました。.envに以下の変数を追加して値をセットしていきましょう。\n.env TWITTER_API_KEY=（前項で取得したAPI Key） TWITTER_API_SECRET=（前項で取得したAPI Secret） TWITTER_ACCESS_TOKEN=（前項で取得したAccess Token） TWITTER_ACCESS_TOKEN_SECRET=（前項で取得したAccess Token Secret） これでプログラム内ではconfig(\u0026rsquo;twitter\u0026rsquo;)で認可に必要な情報を配列で取得できるようになりました。\n次にTwitterCommandクラスを追加し、X APIを叩いてツイートする処理を実装します。先ほどインストールしたパッケージを使用して以下の様に実装しました。\napp/Console/Commands/XApiCommand.php namespace App\\Console\\Commands; use Illuminate\\Console\\Command; use Abraham\\TwitterOAuth\\TwitterOAuth; class XApiCommand extends Command { /** * The name and signature of the console command. * * @var string */ protected $signature = \u0026#39;x_api {text}\u0026#39;; /** * The console command description. * * @var string */ protected $description = \u0026#39;call x api to post a tweet.\u0026#39;; /** * Execute the console command. */ public function handle() { $text = $this-\u0026gt;argument(\u0026#39;text\u0026#39;); $config = config(\u0026#39;twitter\u0026#39;); // keyをセット $connection = new TwitterOAuth( $config[\u0026#39;api_key\u0026#39;], $config[\u0026#39;api_secret\u0026#39;], $config[\u0026#39;access_token\u0026#39;], $config[\u0026#39;access_token_secret\u0026#39;], ); // Twitter Api V2 を指定 $connection-\u0026gt;setApiVersion(\u0026#39;2\u0026#39;); // ツイート $res = $connection-\u0026gt;post(\u0026#39;tweets\u0026#39;, compact(\u0026#39;text\u0026#39;)); // エラー処理 if (in_array ($connection-\u0026gt;getLastHttpCode(), [200, 201])) { $this-\u0026gt;info(\u0026#39;tweet posted successfully.\u0026#39;); return self::SUCCESS; } else { $this-\u0026gt;error(\u0026#39;failed to post tweet. \u0026#39;.json_encode($res)); return self::FAILURE; } } } ターミナルで以下を実行するとツイートが投稿されます。\nphp artisan x_api \u0026#34;test post\u0026#34; ツイッター上で投稿が確認できるかと思います。 まとめ 以上、開発者プラットフォームへ登録するところから実際にプログラムを走らせてツイートを投稿するまでの流れを解説しました。完全自動化するにはChatGPTと組み合わせてツイートの見出しを自動生成するなど、もう少し工夫すれば実現できそうです。\n","date":"2024-06-21T08:03:15+09:00","permalink":"https://www.larajapan.com/2024/06/21/x%E6%97%A7twitter-api%E3%82%92%E5%8F%A9%E3%81%84%E3%81%A6%E3%83%84%E3%82%A4%E3%83%BC%E3%83%88%E3%81%97%E3%81%A6%E3%81%BF%E3%81%9F/","title":"X(旧Twitter) APIを叩いてツイートしてみた"},{"content":"Webアプリの動作確認時、ブラウザをポチポチ手動でテストをするのも良いですが、可能な限り自動化すればテストに割く時間や手間を大きく削減できます。\nテスト自動化ツールは色々なタイプがありますが、導入がシンプル・JavaScrirptベースなCypressを試してみました。\nCypressとは Cypressとは、Webアプリケーション向けに開発されたフロントエンドの自動テストツールです。\nテスト自動化というとSeleniumが有名ですが、Seleniumはブラウザ操作全般（スクレイピングなど）に使えるのに対し、Cypressはテスト用に特化して作られています。そのためスナップショット・自動待機など便利な機能が最初から用意されており、GUIを操作してテスト実行が可能なのでエンジニアでなくともとっつきやすいのではないでしょうか。\nドキュメントには使い方が詳しく解説されているので、導入前にざっと見てみるのも面白いと思います。現時点で日本語非対応ですが翻訳機能を使えば問題ありません。\nまた、テスト結果を保存したりチームで共有するためのクラウド機能は有料ですが、Cypressの大部分であるテスト機能そのものは無料で使えるのも嬉しいポイント。\nCypressインストールとセットアップ CypressのインストールにはNode.js（現時点ではNode.js 18.x、もしくは20.x以上）が必要ですので、バージョンを確認。\n% node -v v18.17.1 次にインストール先のディレクトリへ移動して、以下のコマンドを実行します。今回私はテスト対象のプロジェクトルートにインストールしましたが、異なるディレクトリへインストールしてもCypressのテスト実行には問題ありません。\n% cd /your/project/path % npm install cypress --save-dev 実行後、現時点で最新のCypress13.9.0がインストールされました。では以下のコマンドでCypressを起動します。\n% npx cypress open 起動直後の画面です。ここからはターミナルではなくブラウザを操作して設定を進めます。E2Eテスト、コンポーネントテストの選択肢があるので、ここではE2Eテストを選択。後からでもこの選択は変更可能です。\n次に、構成ファイルの一覧が表示されます。ここもContinueをクリック。\n次に、テストに使用するブラウザを選択。ここではChromeを選択します。\nいよいよ最初のテスト作成へ進みます。Create new specをクリック。\nここではテストファイルの名前を編集できます。今回はデフォルトのままとして、Create spec をクリック。\n作成されたテストファイルが表示されました。ファイルにはサンプルコードが記述されています。このままテストを実行することもできますが、一旦×をクリックしてダッシュボードを見てみます。\n以下がダッシュボードです。テストはまだ１つしかないので一覧には先ほど作ったファイルだけがリストされていますね。\nここまでで、インストールディレクトリには以下のディレクトリ・ファイルが作成されました。\nCypress/e2e/spec.cy.js Cypress/fixtures/example.json Cypress/support/commands.js Cypress/support/e2e.js cypress.config.js Cypress自動テスト実行 サンプルコードではありますが、さっそくテストを実行してみましょう。テストの一覧から先ほど作成したspec.cy.jsをクリックします。\nすると画面が変わり、テストコードが実行されます。テストの内容はCypressのサンプルサイト（https://example.cypress.io）へアクセスするだけなので問題なく成功し、緑のOKマークが表示されています。\nテスト失敗のパターンも確認してみましょう。visit()とは引数のURLへアクセスするコマンドですので、ここを立ち上げていないローカルサーバーのアドレスに変更しました。どうなるでしょうか。\nCypress/e2e/spec.cy.js describe(\u0026#39;template spec\u0026#39;, () =\u0026gt; { it(\u0026#39;passes\u0026#39;, () =\u0026gt; { cy.visit(\u0026#39;http://127.0.0.1:8000\u0026#39;) //ここを編集 }) }) テストコードを編集すると自動でテストが走り、以下のようにエラーメッセージが表示されました。コードのどこで失敗したのかも示してくれるのでわかりやすいですね。\n少し長くなってしまったので、次回から具体的なブラウザテストを実装してゆきます。\n参照 CypressでE2Eテストを自動化（２）画面表示のテスト CypressでE2Eテストを自動化（３）ログイン・ログアウト\n","date":"2024-05-28T23:57:55+09:00","permalink":"https://www.larajapan.com/2024/05/28/cypress%E3%81%A7e2e%E3%83%86%E3%82%B9%E3%83%88%E3%82%92%E8%87%AA%E5%8B%95%E5%8C%96%E3%82%BB%E3%83%83%E3%83%88%E3%82%A2%E3%83%83%E3%83%97/","title":"CypressでE2Eテストを自動化（１）セットアップ"},{"content":"以前、Laravel Excelというpackageを使用してExcelやCSVにデータを出力する方法について解説しました。Laravel Excelはinterfaceが充実しており、それに則ってコードを書くと体系的かつ統一的に少ない記述量で実装できるので気に入っていました。しかし、大きなデータを扱う際などはなぜか消費メモリが徐々に増えていく為（今は改善されているかもしれませんが）、\nその場合はPHPのファイル関数（fopen(), fputcsv(), fclose()など）を用いていました。\nそうすると、今度はBOMの処理やヘッダの取得、読み込んだ行毎の処理など、その場その場で独自に必要な処理を追加していくのでコードが肥大化しがちです。適宜リファクタしていけば良いのですが、これら汎用的な処理はビジネスの肝では無いので他に良いパッケージがあれば導入したいところ。そんな折、Kenji氏からspatieのsimple-excelを使ってみれば？と提案いただき試してみたところ良い感触を得られたので記事にする事にしました。\nsimple-excel GitHub - spatie/simple-excel\nその名の通り、シンプルにExcelやCSVファイルの読み書きができるパッケージです。裏ではジェネレータを使用しており大きなファイルを扱う際もメモリの消費を抑えてくれるそうです。早速、以下のコマンドを実行してインストールしてみましょう。\ncomposer require spatie/simple-excel ファイル出力 simple-excelを使用してcsvファイルを出力してみましょう。ファイル出力を行う際は以下の様にSimpleExcelWriterクラスを使用します。\nuse Spatie\\SimpleExcel\\SimpleExcelWriter; $writer = SimpleExcelWriter::create(\u0026#39;member_list.csv\u0026#39;); ヘッダを追加する場合はaddHeader()を使用します。\n$writer-\u0026gt;addHeader([\u0026#39;性\u0026#39;, \u0026#39;名\u0026#39;, \u0026#39;性別\u0026#39;, \u0026#39;年齢\u0026#39;]); １行ずつデータを追加する場合はaddRow()です。\n$writer-\u0026gt;addRow([\u0026#39;山田\u0026#39;, \u0026#39;太郎\u0026#39;, \u0026#39;M\u0026#39;, 35]); addRows()を使用すれば複数行まとめて追加できます。\n$writer-\u0026gt;addRows([ [\u0026#39;山田\u0026#39;, \u0026#39;花子\u0026#39;, \u0026#39;F\u0026#39;, 29], [\u0026#39;鈴木\u0026#39;, \u0026#39;一郎\u0026#39;, \u0026#39;M\u0026#39;, 56], ]); 出力されたcsvファイルを確認してみましょう。以下の様になっていると思います。 因みに、SimpleExcelWriterを使用したcsvファイルの出力はデフォルトでBOM付きとなっている為、問題なくExcelで開けます。BOMを付与したく無い場合は$writerを取得する際にcreate()ではなくcreateWithoutBom()を使用します。\n$writer = SimpleExcelWriter::createWithoutBom(\u0026#39;member_list.csv\u0026#39;); ファイル読み込み 今度は先ほど出力したファイルを読み込んでみましょう。読み込みにはSimpleExcelReaderを使用します。\nuse Spatie\\SimpleExcel\\SimpleExcelReader; $reader = SimpleExcelReader::create(\u0026#39;member_list.csv\u0026#39;); ヘッダを取得する場合にはgetHeaders()を使用します。\n$header = $reader-\u0026gt;getHeaders(); = [ \u0026#34;性\u0026#34;, \u0026#34;名\u0026#34;, \u0026#34;性別\u0026#34;, \u0026#34;年齢\u0026#34;, ] また、データ部分を取得するにはgetRows()を使用します。getRows()はLazyCollectionインスタンスを返します。そちらは裏ではジェネレータを使用して反復処理を行うごとにファイルから１行ずつデータを取得します。取得した行データはヘッダをキーとした連想配列になっています。LazyCollectionは通常のCollectionと同様に扱う事ができるのでeach()やforeachループで処理する事ができます。\n$rows = $reader-\u0026gt;getRows(); = Illuminate\\Support\\LazyCollection {#5116 +source: Closure() {#5115 …4}, } // 最初の１行取得 $rows-\u0026gt;first(); = [ \u0026#34;性\u0026#34; =\u0026gt; \u0026#34;テスト\u0026#34;, \u0026#34;名\u0026#34; =\u0026gt; \u0026#34;太郎\u0026#34;, \u0026#34;性別\u0026#34; =\u0026gt; \u0026#34;M\u0026#34;, \u0026#34;年齢\u0026#34; =\u0026gt; \u0026#34;35\u0026#34;, ] // each()で行毎の処理実行 $rows-\u0026gt;each(function ($row) { printf(\u0026#34;%s %s (%d)\\n\u0026#34;, $row[\u0026#39;性\u0026#39;], $row[\u0026#39;名\u0026#39;], $row[\u0026#39;年齢\u0026#39;]); }); テスト 太郎 (35) 山田 花子 (29) 鈴木 一郎 (56) 時としてヘッダは日本語だがコードで処理する際は英語で扱いたい、という場合があるかと思います。先ほど、「取得した行データはヘッダをキーとした連想配列になっています。」と述べましたが、useHeaders()を使用して行データのキーを指定する事ができます。また、以下に示す例ではメソッドチェーンとしてコンパクトに記述してみました。\n$header = [\u0026#39;last_name\u0026#39;, \u0026#39;first_name\u0026#39;, \u0026#39;gender\u0026#39;, \u0026#39;age\u0026#39;]; $rows = SimpleExcelReader::create(\u0026#39;member_list.csv\u0026#39;) -\u0026gt;useHeaders($header) -\u0026gt;getRows(); $rows-\u0026gt;first(); = [ \u0026#34;last_name\u0026#34; =\u0026gt; \u0026#34;テスト\u0026#34;, \u0026#34;first_name\u0026#34; =\u0026gt; \u0026#34;太郎\u0026#34;, \u0026#34;gender\u0026#34; =\u0026gt; \u0026#34;M\u0026#34;, \u0026#34;age\u0026#34; =\u0026gt; \u0026#34;35\u0026#34;, ] まとめ やっている事はfopen()でファイルを開いてwhileループで回して処理するのと同じなのですが、Collectionと同様の操作で扱えるので複雑な処理が必要な場合でも可読性を保つ事ができそうですね。 参照 そうだ、Laravel Excelを使ってみよう（１）セットアップ そうだ、Laravel Excelを使ってみよう（２）ヘッダ, map() そうだ、Laravel Excelを使ってみよう（３）日付のフォーマット そうだ、Laravel Excelを使ってみよう（４）保存先の指定 そうだ、Laravel Excelを使ってみよう（５）画像を挿入する そうだ、Laravel Excelを使ってみよう（６）顔写真付き名簿を作成しよう\n","date":"2024-05-21T03:31:13+09:00","permalink":"https://www.larajapan.com/2024/05/21/spatie%E3%81%AEsimple-excel%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E3%81%BF%E3%81%9F%E3%80%82/","title":"メモリーに優しいsimple-excel"},{"content":"さあ大変です。PHP 8.2において動的プロパティが廃止(Deprecated）となりました。PHP 9.0ではErrorExceptionが投げられるようです。動的プロパティをたくさん使っているLaravelのプログラムは大丈夫なのでしょうか？\n動的プロパティが廃止とはどういうこと？ 空のクラスを作成します。\napp/Models/Person.php namespace App\\Models; class Person { // } PHP 8.1では、好き勝手に動的プロパティを作成できます。\n\u0026gt; use App\\Models\\Person; \u0026gt; $person = new Person; = App\\Models\\Person {#5068} \u0026gt; $person-\u0026gt;first_name = \u0026#39;Joe\u0026#39;; = \u0026#34;Joe\u0026#34; \u0026gt; $person-\u0026gt;first_name; = \u0026#34;Joe\u0026#34; しかし、同様なコードの実行をPHP 8.2下で行うと、\n\u0026gt; use App\\Models\\Person; \u0026gt; $person = new Person; = App\\Models\\Person {#5068} \u0026gt; $person-\u0026gt;first_name = \u0026#39;Joe\u0026#39;; DEPRECATED Creation of dynamic property App\\Models\\Person::$first_name is deprecated. = \u0026#34;Joe\u0026#34; と、しっかり廃止のエラーが表示されます。Deprecated（廃止）なのでDisabled（無効）になったわけではなくエラーとはならずに警告のみです。 つまり、実行はされているので値はGETできます。\n\u0026gt; $person-\u0026gt;first_name; = \u0026#34;Joe\u0026#34; もちろん、動的プロパティでなくプロパティがクラスで予め宣言されているなら問題ありません。\napp/Models/Person.php namespace App\\Models; // 以下はModelを継承していないことに注意 class Person { public string $first_name; } と定義したなら、以下のようにfirst_nameをセットしても、警告なしです。\n\u0026gt; use App\\Models\\Person; \u0026gt; $person = new Person; = App\\Models\\Person {#5068 +first_name: ? string, } \u0026gt; $person-\u0026gt;first_name = \u0026#39;Joe\u0026#39;; = \u0026#34;Joe\u0026#34; PHP 8.2以降で動的プロパティに対応するには、どうしたらよいのでしょう？\nマジックメソッドを使う 前回説明したマジックメソッドを使用するなら問題ありません。\napp/Models/Person.php namespace App\\Models; // 以下はModelを継承していないことに注意 class Person { protected array $attributes = []; //　プロパティの値を取得 public function __get(string $key ) : ?mixed { if (isset($this-\u0026gt;attributes[$key])) { return $this-\u0026gt;attributes[$key]; } return null; } //　プロパティを設定 public function __set(string $key, mixed $value) { $this-\u0026gt;attributes[$key] = $value; } } ということは、LaravelのEloquentやCollectionを使用したコードを書き換える必要はなしです。良かったです。安堵！\nstdClassを使う stdClassを使っても動的プロパティの作成が警告なしに可能となります。\n\u0026gt; $person = new StdClass; = {#5072} \u0026gt; $person-\u0026gt;first_name = \u0026#39;Joe\u0026#39;; = \u0026#34;Joe\u0026#34; さらに、stdClassを継承してクラスを作成してもOKです。\napp/Models/Person.php namespace App\\Models; use stdClass; class Person extends stdClass; { // } Attributeを使う PHP8.0 から導入されたAttributeを使用しても動的プロパティの作成が可能です。\nつまり、\napp/Models/Person.php namespace App\\Models; // 以下は、動的プロパティ許可としています #[\\AllowDynamicProperties] class Person { // } 参照 混乱してはいけないLaravelの動的プロパティ – Eloquent編 混乱してはいけないLaravelの動的プロパティ – Collection編 混乱してはいけないLaravelの動的プロパティ – 裏側編","date":"2024-05-20T08:31:31+09:00","permalink":"https://www.larajapan.com/2024/05/20/%E6%B7%B7%E4%B9%B1%E3%81%97%E3%81%A6%E3%81%AF%E3%81%84%E3%81%91%E3%81%AA%E3%81%84laravel%E3%81%AE%E5%8B%95%E7%9A%84%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3-php8-2%E3%81%A7%E5%BB%83%E6%AD%A2/","title":"混乱してはいけないLaravelの動的プロパティ - PHP8.2で廃止となった編"},{"content":"今回は、Laravelの動的プロパティの裏側を見てみます。つまり、どうしてインスタンスのプロパティがメソッドのコールとなるのか。\nマジックメソッド 知って入ればなんのことはないですが、phpではマジックメソッドという関数をクラスで定義すれば、クラスで定義されているメソッドを、インスタンスのプロパティとしてアクセスすることができます。 簡単にマジックメソッドの説明をしましょう。\nまずは、Personというクラスを作成します。Modelsのネームスペースに入れてありますが、デモのためにEloquentのModelクラスは継承しません。\napp/Models/Person.php namespace App\\Models; // 以下はModelを継承していないことに注意 class Person { protected array $attributes = []; //　プロパティの値を取得 public function __get(string $key ) : ?mixed { if (isset($this-\u0026gt;attributes[$key])) { return $this-\u0026gt;attributes[$key]; } return null; } //　プロパティを設定 public function __set(string $key, mixed $value) { $this-\u0026gt;attributes[$key] = $value; } //　すべてのプロパティをリスト public function getAttributes(): array { return $this-\u0026gt;attributes; } } ２つの_（アンダースコア）で始まるメソッド名、__get()と__set()がマジックメソッドです。\ntinkerで実行すると、以下のようにインスタンスに好きなプロパティ名で値を与えることができます。\n\u0026gt; use App\\Models\\Person; \u0026gt; $person = new Person; = App\\Models\\Person {#5077} \u0026gt; $person-\u0026gt;first_name = \u0026#39;Joe\u0026#39;; = \u0026#34;Joe\u0026#34; \u0026gt; $person-\u0026gt;last_name = \u0026#39;Biden\u0026#39;; = \u0026#34;Biden\u0026#34; 裏では、上のクラスの定義の__set()、つまりミューテーターが実行されています。その証拠に、$attributeの中身を見ると、\n\u0026gt; $person-\u0026gt;getAttributes(); = [ \u0026#34;first_name\u0026#34; =\u0026gt; \u0026#34;Joe\u0026#34;, \u0026#34;last_name\u0026#34; =\u0026gt; \u0026#34;Biden\u0026#34;, ] と連想配列に値が入っています。\nまた、__get()の定義のために、\n\u0026gt; $person-\u0026gt;first_name; = \u0026#34;Joe\u0026#34; \u0026gt; $person-\u0026gt;last_name; = \u0026#34;Biden\u0026#34; とそれぞれのプロパティの値を取り出すことが可能です。\nしかし、$attributesにキーがないプロパティにアクセスすると、\n\u0026gt; $person-\u0026gt;full_name; = null とnullを返します。\nプロパティがなかったらメソッドコール さて、今度はプロパティが存在しないときにメソッドをコールしてみましょう。 プロパティがfull_nameならfullName()をコールするように対応してみました。\nnamespace App\\Models; use Illuminate\\Support\\Str; class Person { protected array $attributes = []; public function __get(string $key ) : mixed { if (isset($this-\u0026gt;attributes[$key])) { return $this-\u0026gt;attributes[$key]; } // full_nameがキーなら、fullName()をコール if (method_exists($this, Str::camel($key))) { return $this-\u0026gt;{Str::camel($key)}(); } return null; } ... // 新規に定義 protected function fullName() { return $this-\u0026gt;attributes[\u0026#39;first_name\u0026#39;].\u0026#39; \u0026#39;.$this-\u0026gt;attributes[\u0026#39;last_name\u0026#39;]; } } 実行すると、full_nameの動的プロパティの値が返ります。\n\u0026gt; $person-\u0026gt;full_name; = \u0026#34;Joe Biden\u0026#34; もちろん、LaravelのModelのクラスではいろいろなケースがあるので、その動的プロパティのメカニズムはこれよりはるかに複雑ですが、マジックメソッドを使用して対応するのは同じです。\nLaravelでのマジックメソッド さて、Laravelで本当にマジックメソッドが使用されているか確認してみましょう。\nまずは、EloquentのModelクラスでは、以下のように、マジックメソッドが定義されています。\nvendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php namespace Illuminate\\Database\\Eloquent; ... abstract class Model implements Arrayable, ArrayAccess, CanBeEscapedWhenCastToString, HasBroadcastChannel, Jsonable, JsonSerializable, QueueableEntity, Stringable, UrlRoutable { ... /** * Dynamically retrieve attributes on the model. * * @param string $key * @return mixed */ public function __get($key) { return $this-\u0026gt;getAttribute($key); } /** * Dynamically set attributes on the model. * * @param string $key * @param mixed $value * @return void */ public function __set($key, $value) { $this-\u0026gt;setAttribute($key, $value); } ... このコードレベルではすっきりしていますが、興味ある方は奥までコードを辿ってみてください。いろいろなケースの対応がされています。\nそして、Collectionでは、HigherOrderCollectionProxyのクラスの中で、マジックメソッドが定義されています。\nvendor/laravel/framework/src/Illuminate/Collections/HigherOrderCollectionProxy.php namespace Illuminate\\Support; /** * @mixin \\Illuminate\\Support\\Enumerable */ class HigherOrderCollectionProxy { ... /** * Proxy accessing an attribute onto the collection items. * * @param string $key * @return mixed */ public function __get($key) { return $this-\u0026gt;collection-\u0026gt;{$this-\u0026gt;method}(function ($value) use ($key) { return is_array($value) ? $value[$key] : $value-\u0026gt;{$key}; }); } /** * Proxy a method call onto the collection items. * * @param string $method * @param array $parameters * @return mixed */ public function __call($method, $parameters) { return $this-\u0026gt;collection-\u0026gt;{$this-\u0026gt;method}(function ($value) use ($method, $parameters) { return $value-\u0026gt;{$method}(...$parameters); }); } } 他のどのクラスでマジックメソッドが使用されているか、vendor/laravel/のディレクトリで、grepしてみました。いろいろありますね。\nframework/src/Illuminate/Routing/Route.php framework/src/Illuminate/View/InvokableComponentVariable.php framework/src/Illuminate/Database/Eloquent/Model.php framework/src/Illuminate/Database/Eloquent/Builder.php framework/src/Illuminate/Bus/Batch.php framework/src/Illuminate/Conditionable/HigherOrderWhenProxy.php framework/src/Illuminate/Auth/GenericUser.php framework/src/Illuminate/Mail/Events/MessageSent.php framework/src/Illuminate/Queue/Jobs/DatabaseJobRecord.php framework/src/Illuminate/Support/ViewErrorBag.php framework/src/Illuminate/Support/ValidatedInput.php framework/src/Illuminate/Support/Optional.php framework/src/Illuminate/Support/Fluent.php framework/src/Illuminate/Support/Stringable.php framework/src/Illuminate/Http/Request.php framework/src/Illuminate/Http/Resources/DelegatesToResource.php framework/src/Illuminate/Testing/TestResponse.php framework/src/Illuminate/Testing/TestComponent.php framework/src/Illuminate/Container/Container.php framework/src/Illuminate/Collections/HigherOrderCollectionProxy.php framework/src/Illuminate/Collections/Enumerable.php framework/src/Illuminate/Collections/Traits/EnumeratesValues.php 参照 混乱してはいけないLaravelの動的プロパティ – Eloquent編 混乱してはいけないLaravelの動的プロパティ – Collection編 混乱してはいけないLaravelの動的プロパティ – PHP8.2で廃止となった編","date":"2024-05-15T23:27:32+09:00","permalink":"https://www.larajapan.com/2024/05/15/%E6%B7%B7%E4%B9%B1%E3%81%97%E3%81%A6%E3%81%AF%E3%81%84%E3%81%91%E3%81%AA%E3%81%84laravel%E3%81%AE%E5%8B%95%E7%9A%84%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3-%E8%A3%8F%E5%81%B4%E7%B7%A8/","title":"混乱してはいけないLaravelの動的プロパティ - 裏側編"},{"content":"動的プロパティは、EloquentだけでなくCollectionにもHigher Order Message（より上級のメッセージ？）という名前で颯爽と登場します。これも最初に見たら、こんなことできるの、という感じです。混乱してはいけません。Eloquentと同様に複雑なことをしないときのコード表現に便利なメソッドの短縮形です。\n以下の前回の話も参照してください。 混乱してはいけないLaravelの動的プロパティ - Eloquent編\nCollectionメソッドを動的プロパティとして扱う Laravelのマニュアルと似たような例を使います。\nここでは、スポーツ選手のプレイヤーをクラス（Player）とします。クラスにはプロパティとして、名前、人気の投票数、そしてVIPかどうかの３つの項目があります。例としてtinkerで表示されるようにプロパティは皆publicとしています。さらに、投票数が500以上ならVIPを正とする関数も定義されています。\napp/Models/Player.php namespace App\\Models; Class Player { function __construct( public string $name,　// 名前 public int $votes, // 投票数 public bool $isVip = false)　// VIP？ { // } public function markAsVip(): void { if ($this-\u0026gt;votes \u0026gt;= 500) { // 500数以上ならVIPに設定 $this-\u0026gt;isVip = true; } } } まず、２つインスタンスを含むCollectionを作成します。\n\u0026gt; use App\\Models\\Player; \u0026gt; $players = collect([ new Player(\u0026#34;LeBron James\u0026#34;, 500), new Player(\u0026#34;Stephen Curry\u0026#34;, 400) ]); = Illuminate\\Support\\Collection {#5066 all: [ App\\Models\\Player {#5096 +name: \u0026#34;LeBron James\u0026#34;, +votes: 500, +isVip: false, }, App\\Models\\Player {#5067 +name: \u0026#34;Stephen Curry\u0026#34;, +votes: 400, +isVip: false, }, ], } Collectionに含まれるプレイヤーのインスタンスにおいて、人気票に基づいてVIPかどうかを決めるとします。\n通常なら、以下のようにCollectionのeach()のメソッドを使い、それぞれのインスタンスにおいてmarkAsVip()をコールバックします。\n\u0026gt; $players-\u0026gt;each(fn (Player $player) =\u0026gt; $player-\u0026gt;markAsVip()); = Illuminate\\Support\\Collection {#5101 all: [ App\\Models\\Player {#5089 +name: \u0026#34;LeBron James\u0026#34;, +votes: 500, +isVip: true, }, App\\Models\\Player {#5102 +name: \u0026#34;Stephen Curry\u0026#34;, +votes: 400, +isVip: false, }, ], } しかし、なんとこれと同じことが、Collectionの動的プロパティとして以下のように短縮して実行も可能なのです。\n\u0026gt; $players-\u0026gt;each-\u0026gt;markAsVip(); = Illuminate\\Support\\Collection {#5101 all: [ App\\Models\\Player {#5089 +name: \u0026#34;LeBron James\u0026#34;, +votes: 500, +isVip: true, }, App\\Models\\Player {#5102 +name: \u0026#34;Stephen Curry\u0026#34;, +votes: 400, +isVip: false, }, ], } 凄いですね。\nCollectionの他のメソッドも使えます。VIPだけのプレイヤーを抽出したいとすると、filter()を動的プロパティとします。、\n\u0026gt; $players-\u0026gt;filter-\u0026gt;isVip; = Illuminate\\Support\\Collection {#5102 all: [ App\\Models\\Player {#5089 +name: \u0026#34;LeBron James\u0026#34;, +votes: 500, +isVip: true, }, ], } もう１つ例を見てみましょう。今度は、sum()を使ってプレーヤーの総投票数の計算もできてしまいます。\n\u0026gt; $players-\u0026gt;sum-\u0026gt;votes; = 900 ちなみに、このような短縮形が使えるのは、Collectionのすべてのメソッドとはいきませんが、以下のメソッドの使用が現在可能です。\naverage, avg, contains, each, every, filter, first, flatMap, groupBy, keyBy, map, max, min, partition, reject, skipUntil, skipWhile, some, sortBy, sortByDesc, sum, takeUntil, takeWhile, unique\n参照 混乱してはいけないLaravelの動的プロパティ – Eloquent編 混乱してはいけないLaravelの動的プロパティ – 裏側編 混乱してはいけないLaravelの動的プロパティ – PHP8.2で廃止となった編","date":"2024-05-07T22:49:37+09:00","permalink":"https://www.larajapan.com/2024/05/07/%E6%B7%B7%E4%B9%B1%E3%81%97%E3%81%A6%E3%81%AF%E3%81%84%E3%81%91%E3%81%AA%E3%81%84laravel%E3%81%AE%E5%8B%95%E7%9A%84%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3-collection%E7%B7%A8/","title":"混乱してはいけないLaravelの動的プロパティ - Collection編"},{"content":"LaravelではModelやCollectionやRequestなどのクラスにおいて動的プロパティがコードの短縮形としてよく使われます。しかし、同じクラスでメソッドとして定義したものがいきなりプロパティとして使われるので、私は昔よく混乱したものです。今回はまずEloquent編として代表的な動的プロパティの活用を混乱しないように説明します。\nEloquentのアクセッサー まず、動的プロパティの例として、DBには対応する項目は存在しないのに、あたかも存在する項目のように振る舞うEloquentのアクセッサーです（ミューテーターも同じですが）。 DBのテーブルにfirst_name（名前）とlast_name（苗字）が存在するとして、姓名（full_name）を返すアクセッサーメソッドを定義します。\nnamespace App\\Models; use Illuminate\\Database\\Eloquent\\Casts\\Attribute; use Illuminate\\Database\\Eloquent\\Model; class User extends Model { ... /** * 姓名を返すアクセッサー * * @return \\Illuminate\\Database\\Eloquent\\Casts\\Attribute */ protected function fullName(): Attribute { return Attribute::make( get: fn ($value, $attribute) =\u0026gt; $attribute[\u0026#39;last_name\u0026#39;].\u0026#39; \u0026#39;.$attribute[\u0026#39;first_name\u0026#39;], ); } } これを使用するときは、動的プロパティとして以下のように使います。\n\u0026gt; User::find(1); [!] Aliasing \u0026#39;User\u0026#39; to \u0026#39;App\\Models\\User\u0026#39; for this Tinker session. = App\\Models\\User {#5393 id: 1, created_at: \u0026#34;2024-04-16 03:42:31\u0026#34;, updated_at: \u0026#34;2024-04-16 03:42:31\u0026#34;, first_name: \u0026#34;直子\u0026#34;, last_name: \u0026#34;山田\u0026#34;, } \u0026gt; User::find(1)-\u0026gt;full_name; = \u0026#34;山田 直子\u0026#34; 先ほどのアクセッサーのメソッドfullName()をコールして姓名full_nameを動的プロパティとして返します。\nちなみに、fullName()のメソッドはprotectedで宣言されているので直接はコールできません。\n\u0026gt; User::find(1)-\u0026gt;fullName(); BadMethodCallException Call to undefined method App\\Models\\User::fullName(). EloquentのRelationship 今度は、EloquentのRelationshipの動的プロパティです 典型的なブログを例として、ユーザーとブログの投稿を使います。それぞれのDBテーブルを代表してUserとPostがModelのクラスとなります。そして、Userのクラスでは、それらの１対多の関係を以下のように定義します。\nnamespace App\\Models; use Illuminate\\Database\\Eloquent\\Model; class User extends Model { /** * ユーザー１に対して複数のブログの投稿 */ public function posts() { return $this-\u0026gt;hasMany(Post::class); } } この関係を利用して、特定のユーザーのブログのレコードを取得するのは通常以下のようになります。\n$user = User::find(1); $posts = $user-\u0026gt;posts()-\u0026gt;get(); // Collectionを返す しかし、動的プロパティを使うと、\n$posts = $user-\u0026gt;posts; と短縮形で書くことができます。先の非短縮形と同じ結果となります。便利ですね。\nしかし、この短縮形、非短縮形でできることがなんでもできる訳ではありません\n例えば、非短縮形のメソッド方式では、\n$user-\u0026gt;posts()-\u0026gt;orderBy(\u0026#39;created_at\u0026#39;)-\u0026gt;get(); とレコードをソートできますが、短縮形の動的プロパティの使用ではエラーとなります。\n$user-\u0026gt;posts-\u0026gt;orderBy(\u0026#39;created_at\u0026#39;); BadMethodCallException Method Illuminate\\Database\\Eloquent\\Collection::orderBy does not exist. これは、$user-\u0026gt;posts()がIlluminate\\Database\\Eloquent\\Relations\\HasManyを返すのに、$user-\u0026gt;postではIlluminate\\Database\\Eloquent\\Collectionを返すからです。Collectionには、orderBy()というメソッドはないのです。しかし、前者にはあります。\nちなみに後者では、CollectionのメソッドsortBy()を使用すると同じ結果が得られます。\n$user-\u0026gt;posts-\u0026gt;sortBy(\u0026#39;created_at\u0026#39;); もう１つ、より混乱しそうな例を挙げてみましょう。\nまずは非短縮形から、\n$user-\u0026gt;posts()-\u0026gt;where(\u0026#39;title\u0026#39;, \u0026#39;=\u0026#39;, \u0026#39;ブログタイトル\u0026#39;)-\u0026gt;get(); タイトルに特定の条件で絞ったクエリーです。ちなみに、実行されたSQLは、以下となります。\nselect * from `post` where `user`.`id` = 1 and `user`.`id` is not null and `title` = \u0026#39;ブログタイトル\u0026#39;; さて、これを短縮形の動的プロパティで書くと、\n$user-\u0026gt;posts-\u0026gt;where(\u0026#39;title\u0026#39;, \u0026#39;=\u0026#39;, \u0026#39;ブログタイトル\u0026#39;); -\u0026gt;get()がないだけの違いですが、これはまったく正規です。返す結果も前者と同じです。where()はCollectionのメソッドにもあるのですね。\n実行されたSQLも見てみましょう。\nselect * from `post` where `user`.`id` = 1 and `user`.`id` is not null; 違いわかりますか？タイトルを制限するwhereの条件文がSQL文にありません。つまり、ユーザーが投稿したレコードをいったん全部取得して、Collectionのwhere()のメソッドでフィルターしているのです。\n両者ともに実行結果は同じですが、どこまでがデータベースにより実行されるか、どこからがCollectionのメソッドで実行するかにおいての違いです。この違いでパフォーマンスが違ってきます。一般的には前者のようにDBにたくさん仕事をしてもらうのが効率的です。注意が必要ですね。\n参照 混乱してはいけないLaravelの動的プロパティ – Collection編 混乱してはいけないLaravelの動的プロパティ – 裏側編 混乱してはいけないLaravelの動的プロパティ – PHP8.2で廃止となった編","date":"2024-04-19T05:45:38+09:00","permalink":"https://www.larajapan.com/2024/04/19/%E6%B7%B7%E4%B9%B1%E3%81%97%E3%81%A6%E3%81%AF%E3%81%84%E3%81%91%E3%81%AA%E3%81%84laravel%E3%81%AE%E5%8B%95%E7%9A%84%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3-eloquent%E7%B7%A8/","title":"混乱してはいけないLaravelの動的プロパティ - Eloquent編"},{"content":"Laravelユーザー認証のテスト、５回目となる今回はスロットル機能についてです。ログイン画面でEメール・パスワードを入力し一定回数以上ログインに失敗するとロックがかかる便利機能ですが、ユニットテストはどのように書くのでしょうか。\nこの記事では、Laravel10xにBreezeをインストールした環境でテストを作成しています。テスト環境構築を含む以前の記事は、以下のリンクからご覧いただけます。\n（１）環境構築・画面表示のテスト （２）エラーメッセージの日本語化・ログインのテスト （３）ログアウトのテスト （４）パスワードリセットのテスト テスト対象 デフォルトではパスワードを５回連続で間違えると６０秒ロックがかかり、ロック中はこの画像のような状態となり正しいパスワードを入力してもログインができません。 実際には、このロック機能はEメール+IPをキーとして働いており、同じIPからでもメールアドレスを変えればログイン試行が可能です。こういったことを踏まえると様々なテストケースが考えられますが、今回はシンプルに以下の項目をテストします。\n・正しいEメールとパスワードでログインできる ・Eメールと誤ったパスワードを入力、５回失敗までは通常のエラーメッセージが返る ・６０秒経過するまでは正しいパスワードを入力してもログイン不可 ・６０秒経過後はログインできる\nログインスロットルのテスト まずは正しいEメールとパスワードで問題なくログインできることのテストです。このテストはBreezeが提供してくれているテストコードに含まれており、以前にログイン機能のテストでもご紹介しています。 tests/Feature/Auth/AuthenticationTest.php public function test_users_can_authenticate_using_the_login_screen(): void { $user = User::factory()-\u0026gt;create(); $response = $this-\u0026gt;post(\u0026#39;/login\u0026#39;, [ \u0026#39;email\u0026#39; =\u0026gt; $user-\u0026gt;email, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;password\u0026#39;, ]); $this-\u0026gt;assertAuthenticated(); $response-\u0026gt;assertRedirect(RouteServiceProvider::HOME); } ここは特に問題ないと思いますのでスロットル機能のテストに進みます。スロットルに関してはBreezeのテストコードが無かったので新規に作成しました。少し長いですが、以下が一連のテストコードになります。\ntests/Feature/Auth/AuthenticationTest.php public function test_login_throttle(): void { $user = User::factory()-\u0026gt;create( [\u0026#39;password\u0026#39; =\u0026gt; \u0026#39;password\u0026#39;] ); //5回の失敗までは通常のエラーメッセージが返る for ($i = 0; $i \u0026lt; 5; ++$i) { $response = $this-\u0026gt;post(\u0026#39;/login\u0026#39;, [ \u0026#39;email\u0026#39; =\u0026gt; $user-\u0026gt;email, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;wrong-password\u0026#39;, //異なるパスワード ]); $response-\u0026gt;assertSessionHasErrors([\u0026#39;email\u0026#39; =\u0026gt; \u0026#39;ログイン情報が存在しません。\u0026#39;]); }; //5回失敗後、正しいパスワードでログイン試行 $response = $this-\u0026gt;post(\u0026#39;/login\u0026#39;, [ \u0026#39;email\u0026#39; =\u0026gt; $user-\u0026gt;email, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;password\u0026#39;, ]); $response-\u0026gt;assertSessionHasErrors([ \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;ログイン試行の規定数に達しました。59秒後に再度お試しください。\u0026#39; ]); //5０秒経過 sleep(50); //まだ60秒経過していないので正しいパスワードでもログイン不可 $response = $this-\u0026gt;post(\u0026#39;/login\u0026#39;, [ \u0026#39;email\u0026#39; =\u0026gt; $user-\u0026gt;email, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;password\u0026#39;, ]); //メッセージの部分比較 $response-\u0026gt;assertInvalid([ \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;ログイン試行の規定数に達しました。\u0026#39; ]); sleep(10); //60秒経過後はログインできる $response = $this-\u0026gt;post(\u0026#39;/login\u0026#39;, [ \u0026#39;email\u0026#39; =\u0026gt; $user-\u0026gt;email, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;password\u0026#39;, ]); $this-\u0026gt;assertAuthenticated(); $response-\u0026gt;assertRedirect(RouteServiceProvider::HOME); } まずは、正しいEメールと間違ったパスワードで５回ログインを試行しています。当然ログインは失敗しますが、その際のメッセージは通常のログイン失敗のエラーメッセージであることを確認しています。\n次に、６回目に正しいEメールとパスワードでログインします。ですがロックがかかっているためログインはできません。その際のエラーメッセージは「ログイン試行の規定数に達しました。・・・」であることを確認しています。\nつづいて５０秒後にも、正しいEメールとパスワードで再度ログインを試行して、ログイン不可の確認をします。\n上のコードでは「５０秒後」の時間設定にはsleep(50)を使用していますが、sleep()を使うと実際にその秒数待ち時間が入るため、待ち時間が長い場合は以下のようにCarbonを使うことで実際に待つことなくテストが実行できます。\n... use Carbon\\Carbon; ... // 50秒経過 Carbon::setTestNow(Carbon::now()-\u0026gt;addSeconds(50)); // sleep(50); ... また、「ログイン試行の規定数に達しました。**秒後に再度・・・」のエラーメッセージのアサートですが、秒数部分はどうしてもテストごとに差異が出てしまいます。エラーメッセージのアサートとして馴染みのあるassertSessionHasErrors()は全文比較するコマンドのため、メッセージの一部のみを確認したい場合はLaravel8から追加されたassertInvalid()が便利です。\n使い方はassertSessionHasErrors()と同じですが、以下のようにメッセージの部分比較が可能です。\n$response-\u0026gt;assertSessionHasErrors([ \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;ログイン試行の規定数に達しました。59秒後に再度お試しください。\u0026#39; ]); $response-\u0026gt;assertInvalid([ \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;ログイン試行の規定数に達しました。\u0026#39; ]); 最後に、さらに１０秒待って、今度は６０秒経過したのでログイン可能なことを確認です。ここでも、sleep()の代わりに先ほどのCarbonのメソッドを使用して効率的にテストを進めることが可能です。\nでは、テストを実行してみます。\n$ php artisan test --filter=AuthenticationTest PASS Tests\\Feature\\Auth\\AuthenticationTest ✓ users can authenticate using the login screen 0.28s ✓ login throttle 61.16s Tests: 2 passed (20 assertions) 問題なくテストが成功しました。画面でのテストでは少し手間がかかる機能もこうやってユニットテストで確認できるのは便利ですね。\n","date":"2024-04-15T23:01:09+09:00","permalink":"https://www.larajapan.com/2024/04/15/%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E8%AA%8D%E8%A8%BC%E3%81%AE%E3%83%86%E3%82%B9%E3%83%88%EF%BC%88%EF%BC%95%EF%BC%89laravel-10%E3%80%80%E3%82%B9%E3%83%AD%E3%83%83%E3%83%88%E3%83%AB/","title":"ユーザー認証のテスト（５）Laravel 10　スロットル"},{"content":"早いものでL10がリリースされてからもう１年が経ちました。個人的な感想ですがL10では正直なところそこまで大きな変更が無かった様に思います。しかし大抵その様な場合は水面下で激しい議論が酌み交わされており次バージョンにてドラスティックな変更がやってくるもの。さてさてL11はどうなるやら、一緒に見ていきましょう！\nサポート いつも通りサポート表の確認から。まず、注意すべき点としてL11ではPHP8.2以上が必須となっています。前回のL9からL10にアップグレードする際もPHP8.0から8.1へPHPのVer要件が引き上げられました。忙しない感じもしますが、今回もPHP8.2が未インストールの場合は予め環境にインストールしておきましょう。macOSの方はphpbrewを使うと複数バージョンをインストールして切替られるので便利ですよ。\n新規プロジェクト作成 早速L11をインストールしてみましょう。\n$ composer create-project laravel/laravel L11.x インストール完了後にプロジェクトディレクトリに移動し、バージョンを確認してみます。\n$ cd L11.x $ php artisan --version \u0026gt;\u0026gt; Laravel Framework 11.0.7 2024年3月18日現在、11.0.7がインストールされました。続いて、ビルドインサーバを起動してみます。\nphp artisan serve お馴染みのメニューが表示されました。\nL10と比べて凝ったデザインになりましたね。続いて従来のバージョンからの変更点についてチェックしていきます。\nファイル構成の変更 Streamlined Application Structureというのが今回のアップデートの目玉のようです。要はファイル構成の変更ですが、方向性としてはより簡素で合理的な構造を目指す意図があるようです。その為、app配下の更新頻度が低いと判断されたファイル群が省かれ、各ファイルで行っていた設定はbootstrap/app.phpに集約されメソッドベースで設定する事となりました。以下がインストール直後のbootstrap/app.phpです。\nbootstrap/app.php use Illuminate\\Foundation\\Application; use Illuminate\\Foundation\\Configuration\\Exceptions; use Illuminate\\Foundation\\Configuration\\Middleware; return Application::configure(basePath: dirname(__DIR__)) -\u0026gt;withRouting( web: __DIR__.\u0026#39;/../routes/web.php\u0026#39;, commands: __DIR__.\u0026#39;/../routes/console.php\u0026#39;, health: \u0026#39;/up\u0026#39;, ) -\u0026gt;withMiddleware(function (Middleware $middleware) { // }) -\u0026gt;withExceptions(function (Exceptions $exceptions) { // })-\u0026gt;create(); withMiddleware() ミドルウェアの設定に関するファイル群、app/Http/Middleware（ディレクトリごと）とapp/Http/Kernel.phpが削除され、それらで行っていた設定はwithMiddleware()で行います。\nwithExceptions() 以前はapp/Exceptions/Handler.phpにて、アプリケーション上で発生するあらゆる例外を検知しレポート（ログや通知など）やレンダリング（エラー画面の表示）の制御を行っていましたが、こちらも削除されwithExceptions()にまとめられました。\nwithCommands() 上に表示したbootstrap/app.phpにはありませんがwithCommands()というメソッドもあります。こちらのメソッドでは元々app/Console/Kernel.phpにて行っていたCommandクラスのディレクトリを指定できます。従来通り、app/Console/Commands配下のコマンドクラスは何も設定せずとも自動でロードされますが、もし他のディレクトリに分けて運用している場合はこのメソッドを使って追加登録する必要があります。\nその他、bootstrap/app.php にて行える設定についてはvendor/laravel/framework/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php をご確認下さい。\nServiceProviderの統合 app/Providers配下もとてもスッキリしたものになりました。AppServiceProviderクラスを除き以下の４つのServiceProviderクラスが削除されました。\nAuthServiceProvider BroadcastServiceProvider EventServiceProvider RouteServiceProvider 従来はAuthServiceProviderではModelとPolicyの紐付けを、EventServiceProviderではEventとListenerの紐付けを行なっていましたが、L11.xではそれらはAppServiceProviderのboot()にてGate::policy()やEvent::listen()を用いて行う様です。\napp/Providers/AppServiceProvider.php namespace App\\Providers; use Illuminate\\Support\\Facades\\Gate; use Illuminate\\Support\\Facades\\Event; use Illuminate\\Support\\ServiceProvider; class AppServiceProvider extends ServiceProvider { /** * Bootstrap any application services. */ public function boot(): void { // ModelクラスとPolicyクラスの紐付け Gate::policy( YourModel::class, YourPolicy::class, ); // EventクラスとListenerクラスの紐付け Event::listen( YourEvent::class, YourListener::class, ); } } 集約されてしまうことでAppServiceProviderが肥大化してしまうのでは？と心配になりますが、Laravel標準のディレクトリ構造、且つ、標準の命名規則に従って開発していれば大抵はLaravelが自動でクラス同士の紐付けを行なってくれるので手動で設定が必要なケースはそう多くなさそうです。（ModelクラスとPolicyクラスの自動紐付けについてはPolicyで認可チェックにて解説しているので是非そちらもチェックしてみてください。）\nシンプルになったControllerの基底クラス Controllerクラスの基底クラスであるApp\\Http\\Controllers\\Controller.phpもシンプルになりました。\napp/Http/Controllers/Controller.php namespace App\\Http\\Controllers; abstract class Controller { // } 只の真っさらなabstractクラスですね。以下のL10.xのと見比べてみてください。以前はIlluminate\\Routing\\Controllerを継承し、２つのトレイトAuthorizesRequestsとValidatesRequestsをuseしていました。\napp/Http/Controllers/Controller.php namespace App\\Http\\Controllers; use Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests; use Illuminate\\Foundation\\Validation\\ValidatesRequests; use Illuminate\\Routing\\Controller as BaseController; class Controller extends BaseController { use AuthorizesRequests, ValidatesRequests; } これらのトレイトは削除された訳では無く「必要あれば適宜使ってね」という事の様です。例えば以前にPolicyで認可チェックにて紹介したコントローラヘルパのauthorize()を使用して認可チェックを行う場合はAuthorizesRequestsが必要となります。\nonce() 最後にリリースノートを読んでいてこれは便利そうだ、と思った新しいヘルパ関数once()を紹介します。この関数は引数に指定したコールバックの結果をそのプロセスの間中キャッシュしてくれるというものです。これは例えばDBに保存しているマスタデータを取得する際などに便利そうです。例として以下にDBに保存された都道府県名を取得するコードを書いてみました。\napp/Models/Prefecture.php namespace App\\Models; use Illuminate\\Database\\Eloquent\\Factories\\HasFactory; use Illuminate\\Database\\Eloquent\\Model; class Prefecture extends Model { use HasFactory; protected $table = \u0026#39;prefecture\u0026#39;; public static function list(): array { return once(fn () =\u0026gt; self::pluck(\u0026#39;name\u0026#39;)-\u0026gt;all()); } } once()を使って一度データを取得すれば2回目以降の呼び出しではキャッシュから参照されるのでクエリが発行されず効率的です。同一プロセス上で何度も参照が必要な場合に有効ですね。因みに以前は以下の様にstaticプロパティに退避するなどして対応していました。once()なら1行で書ける処理ですが、以前は5行も必要でした。\n... static $list; public static function list(): array { if (! isset(self::$list)) { self::$list = self::pluck(\u0026#39;name\u0026#39;)-\u0026gt;all(); } return self::$list; } ","date":"2024-04-06T07:52:04+09:00","permalink":"https://www.larajapan.com/2024/04/06/laravel-11-x%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB/","title":"Laravel 11.xのインストール"},{"content":"artisan dbコマンドはLaravelのプロジェクトのデータベースの情報を表示してくれるコマンドでしたが、artisan modelはLaravelのModelのためのコマンドです。見てみましょう。\nmodel:show ※初めての方は、まずartisan dbコマンド読んでください。準備が要ります。 すでに準備がされた方は、早速artisan modelを実行しましょう。\n$ php artisan model:show User と実行すると、\n有用な情報がいっぱいありますが、説明が必要ですね。\nAttributes artisan db:table usersの実行においてもDBテーブルの項目やデータタイプが表示されますが、上ではそれらに加えて、fillable、hidden、castの属性もいくつかの項目で表示されています。\n以下のModelの定義を見るとそれらの属性が上で表示されているがわかります。\napp/Models/User.php ... class User extends Authenticatable { use HasApiTokens, HasFactory, Notifiable; /** * The attributes that are mass assignable. * * @var array\u0026lt;int, string\u0026gt; */ protected $fillable = [ \u0026#39;name\u0026#39;, \u0026#39;email\u0026#39;, \u0026#39;password\u0026#39;, ]; /** * The attributes that should be hidden for serialization. * * @var array\u0026lt;int, string\u0026gt; */ protected $hidden = [ \u0026#39;password\u0026#39;, \u0026#39;remember_token\u0026#39;, ]; /** * The attributes that should be cast. * * @var array\u0026lt;string, string\u0026gt; */ protected $casts = [ \u0026#39;email_verified_at\u0026#39; =\u0026gt; \u0026#39;datetime\u0026#39;, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;hashed\u0026#39;, ]; } さらに、accessorの定義を加えてみましょう。\napp/Models/Post.php ... class User extends Authenticatable { ... public function getNameAttribute() { return Str::upper($this-\u0026gt;attributes[\u0026#39;name\u0026#39;]); // 大文字に変換 } } 再度実行すると、\n表示されていますね。\nRelations 上のRelationsのセクションでは、HasApiTokensとNotifiableのトレイトで定義されているRelationが表示されていますが、それでは少しわかりづらいので、PostsのModelを作成して、以下のようにUsersにRelationを定義します。\napp/Models/Post.php ... class User extends Authenticatable { ... public function posts(): HasMany { return $this-\u0026gt;hasMany(Post::class); } } 再度実行すると、\nしっかり表示されていますね。\nObservers これは、モデルに関連するDB処理イベントのメソッドを表示します。例えば、以下のようにDBレコード作成後の処理を定義して、\napp/Models/Post.php ... class User extends Authenticatable { ... public static function booted() { static::created(function ($user) { // レコードが作成された後に実行する処理 }); } } 再度実行すると、\n最も便利なartisan dbコマンドの使い方 前回の説明において、このコマンドで一番大事なことを忘れていました。というか、最近になってたまたまオプションなしで実行して気づいたことです。\nphp artisan dbとオプションなしで実行すると、\n$ php artisan db mysql: [Warning] Using a password on the command line interface can be insecure. Reading table information for completion of table and column names You can turn off this feature to get a quicker startup with -A Welcome to the MySQL monitor. Commands end with ; or \\g. Your MySQL connection id is 369 Server version: 8.0.33 MySQL Community Server - GPL Copyright (c) 2000, 2023, Oracle and/or its affiliates. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type \u0026#39;help;\u0026#39; or \u0026#39;\\h\u0026#39; for help. Type \u0026#39;\\c\u0026#39; to clear the current input statement. mysql\u0026gt; そうなんです。mysqlのコマンドを.envで指定したログインとパスワードを使って実行してくれるのです。便利ですね。\n最後に artisanコマンドはLaravelを使う開発者にとってなくてはならない重要なツールですが、機能が多すぎてとてもオプションを覚えることができません。まして毎回バージョンごとに数が増えるし。\nしかし、とても便利なサイト見つけました。\nhttps://artisan.page/\nドロップダウンで、バージョンも変えることもできます。\n","date":"2024-03-22T09:30:22+09:00","permalink":"https://www.larajapan.com/2024/03/22/artisan-model%E3%82%B3%E3%83%9E%E3%83%B3%E3%83%89/","title":"artisan modelコマンド"},{"content":"Laravelユーザー認証のテスト4回目、今回はパスワードリセットについてです。ユーザーがパスワードを忘れた時、「パスワードを忘れた？」などのリンクからパスワードリセットリクエストを送信する、よく実装されている機能です。\nこの記事では、Laravel10xにBreezeをインストールした環境でテストを作成しています。テスト環境構築を含む以前の記事は、以下のリンクからご覧いただけます。\n（１）環境構築・画面表示のテスト （２）エラーメッセージの日本語化・ログインのテスト （３）ログアウトのテスト テスト対象画面とテスト項目 パスワードリセットの機能をテストするにあたり、テスト対象はパスワードリセットのリクエスト画面・パスワードリセット画面の２つがあります。それぞれの画面のテスト項目にはさまざまなケースが考えられますが、今回は以下としました。\nパスワードリセットのリクエスト画面\nパスワードリセットリクエスト画面にアクセスできる メールアドレスを入力し、登録済みのメールアドレスならユーザー宛てにリセットメールが送信される 未登録のメールアドレスであればメールは送信されない パスワードリセット画面\nメールに記載されたリセット用のリンクからパスワードリセット画面へアクセスできる パスワードを新しいものに変更、その後新しいパスワードでログインができる パスワードリセットリクエスト画面のテスト ではまずは、「パスワードリセットリクエスト画面にアクセスできる」のテストからです。\ntests/Feature/Auth/PasswordResetTest.php public function test_reset_password_link_screen_can_be_rendered(): void { $response = $this-\u0026gt;get(\u0026#39;/forgot-password\u0026#39;); $response-\u0026gt;assertStatus(200); } ここは特に変わった記述はありません。未ログイン状態でアクセスすることが前提なので、テストコードは２行で完成です。\n次は「メールアドレスを入力し、登録済みのメールアドレスならユーザー宛てにリセットメールが送信される」のテストですが、その前にポスト送信時に実行されるコードを確認してみると、以下のようになっていました。\napp/Http/Controllers/Auth/PasswordResetLinkController.php namespace App\\Http\\Controllers\\Auth; use App\\Http\\Controllers\\Controller; use Illuminate\\Http\\RedirectResponse; use Illuminate\\Http\\Request; use Illuminate\\Support\\Facades\\Password; use Illuminate\\View\\View; class PasswordResetLinkController extends Controller { ... public function store(Request $request): RedirectResponse { $request-\u0026gt;validate([ \u0026#39;email\u0026#39; =\u0026gt; [\u0026#39;required\u0026#39;, \u0026#39;email\u0026#39;], ]); // We will send the password reset link to this user. Once we have attempted // to send the link, we will examine the response then see the message we // need to show to the user. Finally, we\u0026#39;ll send out a proper response. $status = Password::sendResetLink( $request-\u0026gt;only(\u0026#39;email\u0026#39;) ); return $status == Password::RESET_LINK_SENT ? back()-\u0026gt;with(\u0026#39;status\u0026#39;, __($status)) : back()-\u0026gt;withInput($request-\u0026gt;only(\u0026#39;email\u0026#39;)) -\u0026gt;withErrors([\u0026#39;email\u0026#39; =\u0026gt; __($status)]); } } こちらを踏まえて、Breezeが提供してくれているテストにリダイレクト先・セッションメッセージのアサートを追加しました。以下がそのテストコードになります。\ntests/Feature/Auth/PasswordResetTest.php public function test_reset_password_link_can_be_requested(): void { Notification::fake(); //メールが実際に送信されないように $user = User::factory()-\u0026gt;create(); $response = $this-\u0026gt;from(\u0026#39;forgot-password\u0026#39;) -\u0026gt;post(\u0026#39;/forgot-password\u0026#39;, [\u0026#39;email\u0026#39; =\u0026gt; $user-\u0026gt;email]); $response-\u0026gt;assertRedirect(\u0026#39;forgot-password\u0026#39;) -\u0026gt;assertSessionHas(\u0026#39;status\u0026#39;, \u0026#39;パスワードリセットメールを送信しました。\u0026#39;); Notification::assertSentTo($user, ResetPassword::class); } ステータスメッセージとともに前画面にリダイレクトされ、ユーザー宛てにメールが正常に送信されることを確認しています。\n$this-\u0026gt;from(\u0026lsquo;forgot-password\u0026rsquo;)ですが、これは何をしているかというとリファラーをセットしています。 テストでは、ブラウザ操作時のように「前のURL」という概念が存在しません。そのためfrom()関数を使ってポスト送信時のリファラをセットしてあげる必要があります。\n３つ目は、「未登録のメールアドレスであればメールは送信されない」のテストです。このケースはBreezeのテストコードには含まれていなかったため新規に作成しています。\ntests/Feature/Auth/PasswordResetTest.php public function test_reset_password_link_request_fails_for_unregistered_email(): void { Notification::fake(); $response = $this-\u0026gt;from(\u0026#39;forgot-password\u0026#39;) -\u0026gt;post(\u0026#39;/forgot-password\u0026#39;, [\u0026#39;email\u0026#39; =\u0026gt; \u0026#39;unregistered@example.com\u0026#39;]); $response-\u0026gt;assertRedirect(\u0026#39;forgot-password\u0026#39;) -\u0026gt;assertSessionHasErrors(\u0026#39;email\u0026#39;, [\u0026#39;このメールアドレスに一致するユーザーは存在しません。\u0026#39;]); Notification::assertNothingSent(); } from()を使ってリファラをセット・ポスト送信をするところまでは先ほどと同じです。もちろんメールアドレスは未登録のものを使用してくださいね。ポスト送信の結果からassertSessionHasErrors()を使用して、エラーメッセージemailが期待通りであることをアサートしています。\n最後のassertNothingSent()は、通知が何も送信されていないことをアサートしています。\nではここまでで作成したテストを実行してみましょう。\n$ php artisan test --filter=PasswordResetTest PASS Tests\\Feature\\Auth\\PasswordResetTest ✓ reset password link screen can be rendered 0.57s ✓ reset password link can be requested 0.16s ✓ reset password link request fails for unregistered email 0.17s Tests: 3 passed (10 assertions) Duration: 1.14s 問題なさそうです。\nパスワードリセット画面でのテスト 次は、パスワードリセット実行画面のテストです。まずは「メールに記載されたリセット用のリンクからパスワードリセット画面へアクセスできる」をテストします。\ntests/Feature/Auth/PasswordResetTest.php public function test_reset_password_screen_can_be_rendered(): void { Notification::fake(); $user = User::factory()-\u0026gt;create(); $this-\u0026gt;post(\u0026#39;/forgot-password\u0026#39;, [\u0026#39;email\u0026#39; =\u0026gt; $user-\u0026gt;email]); Notification::assertSentTo($user, ResetPassword::class, function ($notification) { $response = $this-\u0026gt;get(\u0026#39;/reset-password/\u0026#39; . $notification-\u0026gt;token); $response-\u0026gt;assertStatus(200); return true; }); } 上記はBreezeのテストコードから特に変更はしていません。コードの前半部分はユーザーの作成とポスト送信に関する記述なので先ほどまでのテストと同じで、後半のNotification::assertSentToの部分がこのテストのメインとなります。\nクロージャーの引数として、$notificationを受け取っています。これはユーザーへ送信される通知メールであるResetPasswordクラスのインスタンスなので、これを使って「メールが送信されていること」だけでなく「通知メールに含まれるトークンを使ってパスワードリセット画面（/reset-password/{token}）にアクセスし、そのページが正常にレンダリングされる」ことの一連のテストが可能になります。\nでは次が最後のテスト項目です。「パスワードを新しいものに変更、その後新しいパスワードでログインができる」をテストします。Breezeのテストコードをベースに、成功時のセッションメッセージと新しいパスワードでログインできることのアサートを追加しました。\ntests/Feature/Auth/PasswordResetTest public function test_password_can_be_reset_with_valid_token(): void { Notification::fake(); $user = User::factory()-\u0026gt;create(); $this-\u0026gt;post(\u0026#39;/forgot-password\u0026#39;, [\u0026#39;email\u0026#39; =\u0026gt; $user-\u0026gt;email]); Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) { $response = $this-\u0026gt;post(\u0026#39;/reset-password\u0026#39;, [ \u0026#39;token\u0026#39; =\u0026gt; $notification-\u0026gt;token, \u0026#39;email\u0026#39; =\u0026gt; $user-\u0026gt;email, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;new_password\u0026#39;, //新しいパスワード \u0026#39;password_confirmation\u0026#39; =\u0026gt; \u0026#39;new_password\u0026#39;, ]); $response-\u0026gt;assertRedirect(\u0026#39;login\u0026#39;) -\u0026gt;assertSessionHas(\u0026#39;status\u0026#39;, \u0026#39;パスワードをリセットしました。\u0026#39;); return true; }); $this-\u0026gt;assertGuest(); //この時点ではまだ未ログイン状態であることを確認 $this-\u0026gt;post(\u0026#39;/login\u0026#39;, [ \u0026#39;email\u0026#39; =\u0026gt; $user-\u0026gt;email, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;new_password\u0026#39;, //新しいパスワードでログイン ]); $this-\u0026gt;assertAuthenticated(); //ログインできたことを確認 } このテストでもNotification::assertSentToが使用されていますが、今度はクロージャーの中でパスワードリセットのポスト送信を実行し、リダイレクト先・ステータスメッセージが期待通りであることを確認しています。\nまた、パスワードリセット完了直後はユーザーはまだ未ログイン状態となっています。そのため新しいパスワード（new_password）を使ってログインを実行し、ログインが成功することを確認しています。\nではここで作成した２つのテストを実行してみます。\n$ php artisan test --filter=PasswordResetTest PASS Tests\\Feature\\Auth\\PasswordResetTest ✓ reset password screen can be rendered 0.60s ✓ password can be reset with valid token 0.10s Tests: 2 passed (8 assertions) Duration: 0.89s うまくいきました！次回は、ログインスロットルに関するテストをご紹介します。\n","date":"2024-03-19T12:01:40+09:00","permalink":"https://www.larajapan.com/2024/03/19/%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E8%AA%8D%E8%A8%BC%E3%81%AE%E3%83%86%E3%82%B9%E3%83%88%EF%BC%88%EF%BC%94%EF%BC%89laravel-10%E3%80%80%E3%83%91%E3%82%B9%E3%83%AF%E3%83%BC%E3%83%89%E3%83%AA%E3%82%BB/","title":"ユーザー認証のテスト（４）Laravel 10　パスワードリセット"},{"content":"前回の記事でPolicyを使った認可チェックの実装方法について紹介しました。その中でGateファサードのメソッドを使うことで色々応用が効く点に触れました。今回はその辺を少し掘り下げてどんなケースで応用が効くのか紹介したいと思います。\n不認可の理由を表示したい 時としてユーザの操作が制限されている（つまり特定のアクションが不認可である）場合にその理由を表示したいケースがあります。例えば以下の様な商品一覧ページがあったとして、何かしらの理由で商品が購入不可の場合に購入ボタンを無効とし、その理由を赤字で表示したい場合など。もちろん、このケースでもPolicyが適用できます。\nこちらのページのPolicyの実装は以下のようになります。認可がdenyされるケースが複数あり、それぞれ異なるメッセージが返されます。\napp/Policies/ProductPolicy.php namespace App\\Policies; use App\\Models\\User; use App\\Models\\Product; use Illuminate\\Auth\\Access\\Response; class ProductPolicy { public function available(?User $user, Product $product): Response { $err = match (true) { ($product-\u0026gt;vip_only === \u0026#39;Y\u0026#39; \u0026amp;\u0026amp; $user?-\u0026gt;vip_flag !== \u0026#39;Y\u0026#39;) =\u0026gt; \u0026#39;VIPのみ購入可能な商品です\u0026#39;, $user?-\u0026gt;balance \u0026lt; $product-\u0026gt;price =\u0026gt; \u0026#39;残高が不足しています\u0026#39;, default =\u0026gt; \u0026#39;\u0026#39;, }; return empty($err) ? Response::allow() : Response::deny($err); } } さて、これらのメッセージを先の画面のように表示するにはどうすればよいでしょうか？\n前回の記事において、Policyを使った判別処理の実装方法をblade側、controller側、middleware側に分けて紹介しました。それらの中でcontroller側のauthorize()やmiddleware側のcan()ではdenyの時にResponse::deny()に指定したエラー文言を表示する事ができます。しかし、403ページへ強制的にリダイレクトされてからの表示です。403ページへのリダイレクト無しにエラー文言だけ取得して表示に利用することができないでしょうか？\nGate::inspect()を使ってエラー表示 Gate::inspect()を使うとPolicyを介して認可チェックを行った際に返されるレスポンスを取得する事ができます。そちらからResponse::deny()に指定したエラー文言を以下のように取得する事ができます。\n\u0026gt; $response = Gate::inspect(\u0026#39;available\u0026#39;, Product::first()) = Illuminate\\Auth\\Access\\Response {#6114} // allowか判別 \u0026gt; $response-\u0026gt;allowed() = false // denyか判別 \u0026gt; $response-\u0026gt;denied() = true // denyのメッセージ取得 \u0026gt; $response-\u0026gt;message(); = \u0026#34;残高が不足しています\u0026#34; // Response自体を文字列にキャストしてもメッセージが取得できる \u0026gt; (string) $response = \u0026#34;残高が不足しています\u0026#34; ということで、以下のようにbladeにGate::inspect()を埋め込むと、PolicyがResponse::deny()を返した場合に冒頭の商品ページのエラー表示とすることができます。（購入ボタンの部分のみ抜粋しています）\nresources/views/product_index.blade.php ... \u0026lt;div class=\u0026#34;mt-2\u0026#34;\u0026gt; @can(\u0026#39;available\u0026#39;, $product) \u0026lt;button class=\u0026#34;button is-small is-success\u0026#34;\u0026gt; 購入する \u0026lt;/button\u0026gt; @else \u0026lt;button class=\u0026#34;button is-small is-success\u0026#34; disabled\u0026gt; 購入する \u0026lt;/button\u0026gt; \u0026lt;p class=\u0026#34;has-text-weight-bold has-text-danger\u0026#34;\u0026gt;{{ Gate::inspect(\u0026#39;available\u0026#39;, $product)-\u0026gt;message() }}\u0026lt;/p\u0026gt; @endcan \u0026lt;/div\u0026gt; ... @reason 先のようなエラー表示のケースは良く使われそうですがパッと見て何を表示しているのか分かりづらいと思いませんか？そんな時こんな記事を見つけました。@reasonというディレクティブを追加するというものです。AppServiceProviderのboot()に以下を追加してみましょう。 app/Providers/AppServiceProvider.php ... public function boot(): void { // @reason を追加 Blade::directive(\u0026#39;reason\u0026#39;, function ($expression) { return \u0026#34;\u0026lt;?php echo Gate::inspect($expression)-\u0026gt;message() ?\u0026gt;\u0026#34;; }); } ... すると先ほどのblade側でGate::inspect()をcallしていた部分を次のように書き換えられます。\nresources/views/product_index.blade.php ... \u0026lt;div class=\u0026#34;mt-2\u0026#34;\u0026gt; @can(\u0026#39;available\u0026#39;, $product) \u0026lt;button class=\u0026#34;button is-small is-success\u0026#34;\u0026gt; 購入する \u0026lt;/button\u0026gt; @else \u0026lt;button class=\u0026#34;button is-small is-success\u0026#34; disabled\u0026gt; 購入する \u0026lt;/button\u0026gt; \u0026lt;p class=\u0026#34;has-text-weight-bold has-text-danger\u0026#34;\u0026gt;@reason(\u0026#39;available\u0026#39;, $product)\u0026lt;/p\u0026gt; @endcan \u0026lt;/div\u0026gt; ... より分かりやすくなりましたね。\n","date":"2024-03-13T11:21:52+09:00","permalink":"https://www.larajapan.com/2024/03/13/%E3%80%90policy%E3%80%91gateinspect%E3%81%A7%E4%B8%8D%E8%AA%8D%E5%8F%AF%E3%81%AE%E7%90%86%E7%94%B1%E3%82%92%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B/","title":"【Policy】Gate::inspect()で不認可の理由を取得する"},{"content":"認証済みのユーザが持つ権限に応じて実行可能な操作を制限したいケースがよくあります。例えば、ブログアプリにおいて記事の編集・削除が可能なのは投稿者に限定するなど。所謂、アクセス権の制御が必要となるケースです。Laravelではそれら「認可」の実装方法としてPolicyが提供されています。\n認証済みユーザに応じて表示を出し分ける 冒頭で紹介したブログアプリの例について考えてみましょう。以下のように会員が投稿した記事が一覧で表示されているページがあるとします。\n現在、「ひかる」というユーザでログインしており、自身の投稿した記事については編集のアクションが認可されているの編集ボタンが表示されています。Policyを使わない場合は@ifなどで以下の様に表示を出し分けるかと思います。\nresources/views/article_index.blade.php ... @if (auth()-\u0026gt;user()?-\u0026gt;id == $article-\u0026gt;author_id) \u0026lt;div class=\u0026#34;buttons mt-2\u0026#34;\u0026gt; \u0026lt;a class=\u0026#34;button is-small is-success\u0026#34; href=\u0026#34;{{ route(\u0026#39;article.edit\u0026#39;, $article) }}\u0026#34;\u0026gt; 編集 \u0026lt;/a\u0026gt; \u0026lt;/div\u0026gt; @endif ... しかし、大抵この様な表示の出し分けは１箇所に留まらず、あちこちで必要となるケースが多いです。そして都度この様な条件式を記述するとコードの見通しが悪くなるし、また判別式の条件に変更があった場合にそれら全てを更新するのは面倒です。更に他にも記事に関する権限（例えば閲覧、作成、削除など）をコントロールしたい、となるかもしれません。よって関連する認可処理をどこか適切な場所にまとめておきたいです。そうです、その適切な場所というのがPolicyという訳です。\nPolicyで実装してみよう 先の例をPolicyを使って書き換えてみましょう。実装の流れとしては３ステップです。\nPolicyクラスの作成 Policyクラスを登録 Policyを使った判別処理を実装 Policyクラスの作成 Policyクラスの作成は以下のコマンドで実行できます。\nphp artisan make:policy ArticlePolicy すると app/Policies/ArticlePolicy.php が作成されます。デフォルトでは以下の通り、何の変哲もない只のクラスです。\napp/Policies/ArticlePolicy.php namespace App\\Policies; use App\\Models\\User; class ArticlePolicy { /** * Create a new policy instance. */ public function __construct() { // } } ここにArticleの編集や削除が可能か判別するupdateメソッドを追加します。以下のように\napp/Policies/ArticlePolicy.php namespace App\\Policies; use App\\Models\\User; use App\\Models\\Article; class ArticlePolicy { public function update(?User $user, Article $article): bool { return $user?-\u0026gt;id == $article-\u0026gt;author_id; } } 追加したupdate()を見てみましょう、第一引数には認証済みのUserモデルインスタンスが渡されます。ユーザがログインしていないゲストユーザの場合はnullとなる為、\u0026rsquo;?\u0026lsquo;を付けてnullableとしています。\nPolicyクラスを登録する 作成したPolicyクラスを使う為にはAuthServiceProviderの$policiesに登録する必要があります。そうする事でLaravelがどのModelクラスにどのPolicyクラスが紐づいているのか判別できるようになります。\n（修正：2024-03-24） 作成したPolicyクラスがLaravel標準の命名規則に則っている場合は自動でモデルに紐づくポリシークラスを見つけてくれるので登録する必要はありません。例えば次の条件を満たす場合は自動で解決されます。\nモデル名がArticle、かつ、ポリシークラス名がArticlePolicy Articleモデルがapp/Models配下にある ArticlePolicyがapp/Policies or app/Models/Policies配下にある 上記の条件を満たさない場合、つまり、標準の命名規則外である場合や独自のディレクトリ構造で運用している場合などはAuthServiceProviderの$policiesに手動で登録しましょう。以下の例では本来は登録せずとも自動解決されますが、説明の為に敢えて$policiesにてArticleとArticlePolicyの紐付けを行っています。\napp/Providers/AuthServiceProvider.php namespace App\\Providers; use App\\Models\\Article; use App\\Policies\\ArticlePolicy; use Illuminate\\Foundation\\Support\\Providers\\AuthServiceProvider as ServiceProvider; class AuthServiceProvider extends ServiceProvider { /** * The model to policy mappings for the application. * * @var array\u0026lt;class-string, class-string\u0026gt; */ protected $policies = [ Article::class =\u0026gt; ArticlePolicy::class, // ←追加 ]; /** * Register any authentication / authorization services. */ public function boot(): void { // } } Policyを使った判別処理を実装 作成したArticlePolicyを使って冒頭の編集ボタンの出し分け処理を実装してみましょう。前述した通り、こうした認可の判別処理というのはアプリの至る箇所で必要となり、それぞれの呼び出し元において適した実装方法が提供されています。以下にblade側、controller側、middleware側における例を掲載します\nblade側で表示の出し分け blade側では@canディレクティブを使用する事ができます。冒頭の@ifで表示を出し分けていた処理を@canで書き換えると以下のようになります。\nresources/views/article_index.blade.php ... @can(\u0026#39;update\u0026#39;, $article) \u0026lt;div class=\u0026#34;buttons mt-2\u0026#34;\u0026gt; \u0026lt;a class=\u0026#34;button is-small is-success\u0026#34; href=\u0026#34;{{ route(\u0026#39;article.edit\u0026#39;, $article) }}\u0026#34;\u0026gt; 編集 \u0026lt;/a\u0026gt; \u0026lt;/div\u0026gt; @endcan ... @canは第一引数にPolicyクラスのメソッド名、第二引数に判別対象のモデルを指定します。するとLaravelは指定されたモデルを元にAuthServiceProviderで指定した$policiesを見てどのポリシークラスを参照すべきか判断します。そして、紐づくポリシークラス内から第一引数で指定されたメソッドを見つけて実行してくれます。因みに、@canは@if同様に@can\u0026hellip;@else\u0026hellip;@canとする事もできます。また、@cannotというディレクティブもあるので適宜使い分けてみてください。\ncontroller側で判別する 続いて、controller側でPolicyを使った判別処理の書き方を紹介します。blade側で編集ボタンが非表示となっていても、悪質なユーザがURLを直接参照して編集画面を開いてしまうかもしれません。記事の編集画面のcontroller側にも判別処理を実装してそれを防ぎましょう。３つ方法があります。\ncan()/cannot() authorize() Gate::allow()/Gate::denies() １つずつみていきましょう。まずcan()から。こちらはUserモデルのcan()をコールして判別する方法です。もしUser以外の認証モデルで使用する場合はAuthorizableトレイトがuseされている必要があるのでご注意を。ArticleControllerのedit()にて以下のように実装しました。\napp/Http/Controllers/ArticleController.php ... public function edit(Article $article) { $user = auth()-\u0026gt;user(); if ($user-\u0026gt;cannot(\u0026#39;update\u0026#39;, $article)) { abort(403, \u0026#39;他人の記事は編集できません！\u0026#39;); } return $user-\u0026gt;name.\u0026#39;の記事の編集ページ\u0026#39;; } ... ユーザが記事をupdateできるなら「○○の記事の編集ページ」と表示され、不可なら「他人の記事は編集できません！」と表示されます。口語的なコードで分かりやすいですね。因みに上記ではcannot()を使用していますが、cant()としてもOKです。\n次にcontrollerクラスのauthorize()を使用する方法です。認可されない場合は403ページが返されます。こちらはcan()の様に細かい指定ができませんが、短く書くことができます。\napp/Http/Controllers/ArticleController.php ... public function edit(Article $article) { $this-\u0026gt;authorize(\u0026#39;update\u0026#39;, $article); return auth()-\u0026gt;user()-\u0026gt;name.\u0026#39;の記事の編集ページ\u0026#39;; } ... もし、can()の時の様にエラー文言を指定したい場合はArticlePolicyのupdate()にてResponseクラスを返却するようにします。\napp/Policies/ArticlePolicy.php ... public function update(?User $user, Article $article): Response { return ($user?-\u0026gt;id == $article-\u0026gt;author_id) ? Response::allow() : Response::deny(\u0026#39;他の人の記事は編集できません！\u0026#39;); } ... 最後にGateファサードを使用する例です。実はPolicyにおけるコアな処理はGateが担っています。can()やauthorize()も内部的にはGateのメソッドをcallしています。その為、Gateのallows()やdenies()を直接呼び出して判別する事もできます。以下の様に。\napp/Http/Controllers/ArticleController.php ... public function edit(Article $article) { if (Gate::denies(\u0026#39;update\u0026#39;, $article)) { abort(403, \u0026#39;他の人の記事は編集できません！\u0026#39;); } return auth()-\u0026gt;user()-\u0026gt;name.\u0026#39;の記事の編集ページ\u0026#39;; } ... allows(),denies()以外にもGateには様々なメソッドが用意されていて応用が効きます。それらについては長くなってしまうので次回に紹介したいと思います。\nmiddlewareで判別する Controller側で判別する方法を紹介しましたが、middlewareで判別する方法もあります。そちらの実装はRouteを定義する際にメソッドチェーンでcan()が使用できます。以下の様に。\nroutes/web.php ... Route::get(\u0026#39;article/{article}/edit\u0026#39;, [ArticleController::class, \u0026#39;edit\u0026#39;]) -\u0026gt;can(\u0026#39;update\u0026#39;, \u0026#39;article\u0026#39;) -\u0026gt;name(\u0026#39;article.edit\u0026#39;); ... こちらもController側の判別処理で紹介したauthorize()と同様に、不認可であれば403ページを返します。\n","date":"2024-02-28T13:12:19+09:00","permalink":"https://www.larajapan.com/2024/02/28/policy%E3%81%A7%E8%AA%8D%E5%8F%AF%E3%83%81%E3%82%A7%E3%83%83%E3%82%AF/","title":"Policyで認可チェック"},{"content":"前回のログイン・ログイン失敗のテストに続き、今回はログアウトのテストをご紹介します。また認証機能にBreezeを使用した環境で、ログアウト時のリダイレクト先の変更も行います。\nこの記事では、Laravel10xにBreezeをインストールした環境でテストを作成しています。テスト環境構築を含む以前の記事は、以下のリンクからご覧いただけます。\n（１）環境構築・画面表示のテスト （２）エラーメッセージの日本語化・ログインのテスト ログアウトのテスト 前回と同じくBreezeが提供してくれているテストをベースに、テスト名はそのままでアサーションとポスト送信の記述を少し変更しています。\ntests/Feature/Auth/AuthenticationTest.php public function test_users_can_logout(): void { $user = User::factory()-\u0026gt;create(); $this-\u0026gt;actingAs($user); //$userを認証済み状態に $this-\u0026gt;assertAuthenticated(); //ログインしていることを確認 $response = $this-\u0026gt;post(\u0026#39;/logout\u0026#39;); $this-\u0026gt;assertGuest(); //ログアウトしていることを確認 $response-\u0026gt;assertRedirect(\u0026#39;/\u0026#39;); } POST送信でログアウトを実行し、正しく認証が削除されているか、リダイレクトされているかをチェックしています。またactingAs()を使うことで、ログイン処理を記述せずに対象のユーザーを認証済み状態に設定できます。\nこのテストでは分かりやすいようにログアウト動作の前後で認証状態をチェックしていますが、ログアウト前のチェックが不要な場合は以下のように、actingAs()とpost()を１行で書くこともできます。\n$response = $this-\u0026gt;actingAs($user)-\u0026gt;post(\u0026#39;/logout\u0026#39;); では、テストを実行してみます。\n$ php artisan test --filter=test_users_can_logout PASS Tests\\Feature\\Auth\\AuthenticationTest ✓ users can logout 0.63s Tests: 1 passed (4 assertions) Duration: 0.93s 問題なさそうですね。\nログアウト後の遷移先を変更（Breeze使用） デフォルトでログアウト後のリダイレクト先はルート画面となっていますが、異なる画面へ遷移するよう変更したいケースも多いと思います。\nBreezeでログアウトの処理がどのように定義されているのか見てみます。まずはルーティングから。\nroutes/auth.php ... Route::post(\u0026#39;logout\u0026#39;, [AuthenticatedSessionController::class, \u0026#39;destroy\u0026#39;]) -\u0026gt;name(\u0026#39;logout\u0026#39;); ... AuthenticatedSessionControllerのdestroy()に定義されていることが分かりました。同ファイルを一部抜粋したものが以下になります。\napp/Http/Controllers/Auth/AuthenticatedSessionController.php namespace App\\Http\\Controllers\\Auth; use App\\Http\\Controllers\\Controller; use App\\Http\\Requests\\Auth\\LoginRequest; use App\\Providers\\RouteServiceProvider; use Illuminate\\Http\\RedirectResponse; use Illuminate\\Http\\Request; use Illuminate\\Support\\Facades\\Auth; use Illuminate\\Support\\Facades\\Route; use Inertia\\Inertia; use Inertia\\Response; class AuthenticatedSessionController extends Controller { ... /** * Destroy an authenticated session. */ public function destroy(Request $request): RedirectResponse { Auth::guard(\u0026#39;web\u0026#39;)-\u0026gt;logout(); $request-\u0026gt;session()-\u0026gt;invalidate(); $request-\u0026gt;session()-\u0026gt;regenerateToken(); return redirect(\u0026#39;/\u0026#39;); } } ログアウトの処理と、最後にリダイレクト先が指定されていますね。redirect()の部分を、ログイン画面にリダイレクトするよう修正しました。\nreturn redirect(\u0026#39;/login\u0026#39;); ここで、ログアウトのテストコードに戻ります。\nまだテストコードへはリダイレクト先の変更が反映されていないので、テスト失敗が正しい挙動です。念の為テストを実行して確認してみましょう。\n$ php artisan test --filter=test_users_can_logout FAIL Tests\\Feature\\Auth\\AuthenticationTest ⨯ users can logout 0.51s ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── FAILED Tests\\Feature\\Auth\\AuthenticationTest \u0026gt; users can logout Failed asserting that two strings are equal. -\u0026#39;http://localhost\u0026#39; +\u0026#39;http://localhost/login\u0026#39; ちゃんとリダイレクト先相違のエラーが出ました。コントローラーの変更が反映されていることが分かったので、現在の仕様に合わせてテストを書き換えます。\npublic function test_users_can_logout(): void { $user = User::factory()-\u0026gt;create(); $response = $this-\u0026gt;actingAs($user)-\u0026gt;post(\u0026#39;/logout\u0026#39;); $this-\u0026gt;assertGuest(); $response-\u0026gt;assertRedirect(\u0026#39;/login\u0026#39;); //リダイレクト先を修正 } テストを実行します。\n$ php artisan test --filter=test_users_can_logout PASS Tests\\Feature\\Auth\\AuthenticationTest ✓ users can logout 0.49s Tests: 1 passed (3 assertions) Duration: 0.72s 成功しました！次回は、パスワードのリセットに関するテストをご紹介します。\n","date":"2024-02-17T10:13:04+09:00","permalink":"https://www.larajapan.com/2024/02/17/%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E8%AA%8D%E8%A8%BC%E3%81%AE%E3%83%86%E3%82%B9%E3%83%88%EF%BC%88%EF%BC%93%EF%BC%89laravel-10%E3%83%AD%E3%82%B0%E3%82%A2%E3%82%A6%E3%83%88/","title":"ユーザー認証のテスト（３）Laravel 10　ログアウト"},{"content":"「LaravelでメールをDKIM署名」で作成したメールのDKIM署名が正しいかどうかを判断するには、Gmailアプリなら受信したメールの「メッセージのソースの表示」でDKIMがPASSあるいはFAILで判定が表示されるのでわかります。しかし、ユニットテストでそれと同様な判断するにはどうしたらよいのか、というのが今回のお話です。\nメールのDKIM署名をチェックするツール さきに話したようにGmailでは、以下のように「メッセージのソースの表示」でソースを見ると、DKIMの署名が正しいならPASSと出ます。 しかし、ユニットテストでDKIM署名のチェックするには、配信したメールをファイルに落としてそれをチェックできるコマンドが必要です。\n運よくそのツールを見つけることができました。ツールは残念ながらphpではなくpythonで書かれたものですが、以下の実行でインストールできます。\n$ pip install dkimpy このコマンドを利用するとDKIM署名のチェックの実行はとても簡単です。先ほどのGmailからダウンロードしたファイルをtest.emlとしてチェックしてみましょう。\n$ dkimverify \u0026lt; test.eml signature ok 署名OKです！\nテストしたメールの送り元（From）はhello@lotsofbytes.comで、lotsofbytes.comのドメインには前回説明したようにAmazonのRoute53のDNSサーバーにTXTに公開鍵のエントリーが登録されています。このツールはそれを取得してメールのDKIM署名が正しいかどうかチェックしてくれます。\n配信するメールをファイルに落とす DKIM署名の正負を判断してくれるコマンドを見つけたところで、こんどはそのコマンドにフィードするためのメールデータが必要です。メールを作成するのは前回に開発したDKIM署名処理を行うMailableなので、それで生成されたメールのデータをファイルに落とす必要あります。しかし、Mailableから直接出力できるメソッドがあれば良いのですが、Mailableを使用するメーラー（smtpとかの）との関わりのため、そのようなメソッドはMailableには存在しません。 そこで考えるのは、メーラーをログとして、つまり.envでMAIL_MAILER=logと設定してファイルに落とすことです。しかし、ファイルはデフォルトではlaravel.logとなるし、出力はログなので記録時の日時の余計な文字列が含まれ、加工が必要となります。\nちなみに、ユニットテストの設定のXMLファイルでは、以下のようにメーラーは配列に落とすのがデフォルトとなっています。\nphpunit.xml \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;phpunit xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:noNamespaceSchemaLocation=\u0026#34;vendor/phpunit/phpunit/phpunit.xsd\u0026#34; bootstrap=\u0026#34;vendor/autoload.php\u0026#34; colors=\u0026#34;true\u0026#34; \u0026gt; ... \u0026lt;php\u0026gt; ... \u0026lt;env name=\u0026#34;MAIL_MAILER\u0026#34; value=\u0026#34;array\u0026#34;/\u0026gt; ... \u0026lt;/php\u0026gt; \u0026lt;/phpunit\u0026gt; ということは、メールの送信のテストがあれば、メモリにその内容が入ることになります。つまり、メール配信後にメモリから送信した内容を取得すればよいのです。\nこれは意外に簡単で、通常のメール送のコードにtoString()の文字列化のメソッドを追加するだけです。以下、tinkerで見てみましょう。\nまず、ユニットテストのようにメーラーを配列とします。\n\u0026gt; config([\u0026#39;mail.default\u0026#39; =\u0026gt; \u0026#39;array\u0026#39;]); = null 宛先を変数として、\n\u0026gt; use App\\Mail\\Newsletter; \u0026gt; $email = \u0026#39;test@example.com\u0026#39;; = \u0026#34;test@example.com\u0026#34; NewsletterのMailableを初期化してMailのファサードで送信すると、\n\u0026gt; Mail::to($email)-\u0026gt;send(new Newsletter($email))-\u0026gt;toString(); DKIM署名のメールのデータが返ってきます。\n= \u0026#34;\u0026#34;\u0026#34; From: Mailable Test \u0026lt;hello@lotsofbytes.com\u0026gt;\\r\\n To: test@example.com\\r\\n Subject: =?utf-8?Q?=E3=83=A1=E3=83=AB=E3=83=9E=E3=82=AC?=\\r\\n =?utf-8?Q?=E3=81=AE=E9=85=8D=E4=BF=A1=E3=81=A7=E3=81=99?=\\r\\n MIME-Version: 1.0\\r\\n Date: Sun, 04 Feb 2024 21:58:58 +0000\\r\\n Message-ID: \u0026lt;bfe853f9bd87f57ab1f02e48f3465bd9@lotsofbytes.com\u0026gt;\\r\\n DKIM-Signature: v=1; q=dns/txt; a=rsa-sha256;\\r\\n bh=Ueb0qJ5i58RAOT361r7NWPIoj8dj5uSTjaEHFZSj/p8=; d=lotsofbytes.com; h=From:\\r\\n To: Subject: MIME-Version: Date: Message-ID; i=@lotsofbytes.com; s=default;\\r\\n t=1707083938; c=relaxed/relaxed;\\r\\n b=EiklbRI4G7Q8VpIM4LAXfyCJKJveXZQMdRza/Q1g/XyAG0wEp5HfXa33RnylIB27C1gozstjp\\r\\n InPV6cqTrWI1SF0xQaBjknHqVIR+eh/Eh+zEidYgmP4Ocs2QC/6iAtx1UpCvgIk4AnvgM/y3q\\r\\n RW+3DkVmIu79nGSRzBtwG3BlG5iA1Eo4NGZt//k70Qi8Ngkl33ce0VeyYZ5EzHVokupnzJiB3\\r\\n yZeWtUEBERog12/kKeLYSLLN8nvGRY1mByWLwPuWF3zYVX3Luji9nMmYRqNKCWZbhe+SFTxwJ\\r\\n yWLYYgYXqZDkw0pdgI2g62Xb9xfsySAIIbcphrceDUM1E82w5A==\\r\\n List-Unsubscribe: \u0026lt;http://localhost/unsubscribe?token=eyJpdiI6IjRhRGNvdnJHWEdFd1pRWmZhNmhBbFE9PSIsInZhbHVlIjoiSit0WmxBWE5JSTdvK3FCNjFJempDUWY3SmVEWFo0NXZhTjRDc215cmxUUT0iLCJtYWMiOiJhNmJkNmQyMmVlMzI3YjJmZDdkMGMyMGRiNjhhYjhiNTgyOTEyZGFjYTFhNzkzZWIxOTQ5MzU1MTlhZDJkMjYzIiwidGFnIjoiIn0%3D\u0026gt;\\r\\n List-Unsubscribe-Post: List-Unsubscribe=One-Click\\r\\n Content-Type: text/html; charset=utf-8\\r\\n Content-Transfer-Encoding: quoted-printable\\r\\n \\r\\n \u0026lt;html\u0026gt;\\r\\n \u0026lt;body\u0026gt;\\r\\n \u0026lt;h1\u0026gt;=E3=83=A1=E3=83=AB=E3=83=9E=E3=82=AC=E3=81=AE=E9=85=8D=E4=BF=\\r\\n =A1=E3=81=A7=E3=81=99\u0026lt;/h1\u0026gt;\\r\\n \u0026lt;p\u0026gt;=E3=81=84=E3=81=A4=E3=82=82=E3=83=A1=E3=83=AB=E3=83=9E=E3=82=\\r\\n =AC=E3=82=92=E8=B3=BC=E8=AA=AD=E3=81=97=E3=81=A6=E9=A0=82=E3=81=84=E3=81=\\r\\n =A6=E3=81=82=E3=82=8A=E3=81=8C=E3=81=A8=E3=81=86=E3=81=94=E3=81=96=E3=81=\\r\\n =84=E3=81=BE=E3=81=99=E3=80=82\u0026lt;/p\u0026gt;\\r\\n \u0026lt;/body\u0026gt;\\r\\n \u0026lt;/html\u0026gt;\\r\\n \u0026#34;\u0026#34;\u0026#34; \u0026gt; この文字列をファイルに保存して、dkimverifyのコマンドにフィードすればユニットテストの作成が可能ですね。\nユニットテストを作成 以下、Dkim署名のユニットテストを作成してみました。\ntests/Unit/NewsletterTest namespace Tests\\Unit; use Tests\\TestCase; use App\\Mail\\Newsletter; use Illuminate\\Support\\Facades\\Mail; class NewsletterTest extends TestCase { protected $exportFileName, $emlPath; protected function setUp(): void { parent::setUp(); // テスト用のファイル名を設定 $this-\u0026gt;exportFileName = \u0026#39;NewsletterTest.eml\u0026#39;; $this-\u0026gt;emlPath = storage_path(\u0026#39;app/\u0026#39;.$this-\u0026gt;exportFileName); // テスト用のファイルが存在する場合は削除 @unlink($this-\u0026gt;emlPath); } /** * @test * * @covers App\\Mail\\NewsletterMessage */ public function buildWithDkim(): void { $email = \u0026#39;test@example.com\u0026#39;; $mailable = new Newsletter($email); // メール送信して文字列化 $data = Mail::to($email)-\u0026gt;send($mailable)-\u0026gt;toString(); // メールをファイルに保存 file_put_contents($this-\u0026gt;emlPath, $data); //DKIM情報チェック $output = shell_exec(\u0026#39;/usr/local/bin/dkimverify \u0026lt; \u0026#39;.$this-\u0026gt;emlPath); $this-\u0026gt;assertEquals(\u0026#34;signature ok\\n\u0026#34;, $output); } } テストを実行してみましょう。\n$ php artisan test --filter NewsletterTest PASS Tests\\Unit\\NewsletterTest ✓ build with dkim 0.33s Tests: 1 passed (1 assertions) Duration: 0.37s 問題なくパスです。\n","date":"2024-02-06T12:51:34+09:00","permalink":"https://www.larajapan.com/2024/02/06/dkim%E7%BD%B2%E5%90%8D%E3%82%92%E5%90%AB%E3%82%80%E3%83%A1%E3%83%BC%E3%83%AB%E3%81%AE%E3%83%A6%E3%83%8B%E3%83%83%E3%83%88%E3%83%86%E3%82%B9%E3%83%88/","title":"DKIM署名を含むメールのユニットテスト"},{"content":"前回においてGoogle Cloudを通じてGoogle Analyticsへのアクセス認証のお膳立てができたとろで、今回は目的のデータ取得のコマンドの開発です。\n認証に必要なデータを設定する 新規のLaravelのプロジェクトを作成した後に、まず前回で用意したアクセス認証のための、プロパティIDと秘密鍵を含むga4.keyファイルの取り込みの設定をします。\nプロパティID GA4のプロパティIDは、config(\u0026lsquo;services.ga4.property_id\u0026rsquo;)として取り込むので、以下のコードを追加します。\nconfig/services.php return [ /* |-------------------------------------------------------------------------- | Third Party Services |-------------------------------------------------------------------------- | | This file is for storing the credentials for third party services such | as Mailgun, Postmark, AWS and more. This file provides the de facto | location for this type of information, allowing packages to have | a conventional file to locate the various service credentials. | */ ... \u0026#39;ga4\u0026#39; =\u0026gt; [ \u0026#39;property_id\u0026#39; =\u0026gt; env(\u0026#39;GA4_PROPERTY_ID\u0026#39;), ], ]; もちろん、.envでは、以下のようにプロパティIDを指定します。\n.env ... GA4_PROPERTY_ID=2XXXXXX2 ga4.key サービスアカウントで作成した秘密鍵を含むjsonファイルga4.keyは、storage/ga4.keyに移します。 そこの場所にファイルを置く理由は、GITバージョン管理から外すためです。Laravelのデフォルトの.gitignoreの設定には、/storage/*.keyがあります。\n.gitignore /node_modules /public/build /public/hot /public/storage /storage/*.key /vendor .env .env.backup .env.production .phpunit.result.cache ... Laravelのコマンドを作成 いよいよコマンドの開発です。まず、必要なライブラリを以下の実行で追加します。\n$ composer require google/analytics-data そして、GA4のデータの取得のためのコマンドを作成します。\napp/Console/Commands/Ga4Command.php namespace App\\Console\\Commands; use Illuminate\\Support\\Str; use Illuminate\\Console\\Command; use Google\\Analytics\\Data\\V1beta\\BetaAnalyticsDataClient; use Google\\Analytics\\Data\\V1beta\\DateRange; use Google\\Analytics\\Data\\V1beta\\Dimension; use Google\\Analytics\\Data\\V1beta\\OrderBy; use Google\\Analytics\\Data\\V1beta\\OrderBy\\MetricOrderBy; use Google\\Analytics\\Data\\V1beta\\Metric; class Ga4Command extends Command { /** * The name and signature of the console command. * * @var string */ protected $signature = \u0026#39;ga4\u0026#39;; /** * The console command description. * * @var string */ protected $description = \u0026#39;Get GA4 data\u0026#39;; /** * Execute the console command. * * @return int */ public function handle() { $client = new BetaAnalyticsDataClient([ \u0026#39;credentials\u0026#39; =\u0026gt; storage_path(\u0026#39;ga4.key\u0026#39;), // GA4のデータのアクセスに必要なファイルへのパス ]); $response = $client-\u0026gt;runReport([ \u0026#39;property\u0026#39; =\u0026gt; \u0026#39;properties/\u0026#39;.config(\u0026#39;services.ga4.property_id\u0026#39;), // このプロパティIDのプロパティにアクセス \u0026#39;dateRanges\u0026#39; =\u0026gt; [ new DateRange([ // 今日から過去２８日間のデータに絞る \u0026#39;start_date\u0026#39; =\u0026gt; \u0026#39;28daysAgo\u0026#39;, \u0026#39;end_date\u0026#39; =\u0026gt; \u0026#39;today\u0026#39;, ]), ], \u0026#39;dimensions\u0026#39; =\u0026gt; [ // ページタイトルとページパスがディメンション new Dimension([\u0026#39;name\u0026#39; =\u0026gt; \u0026#39;pageTitle\u0026#39;]), new Dimension([\u0026#39;name\u0026#39; =\u0026gt; \u0026#39;pagePath\u0026#39;]), ], \u0026#39;metrics\u0026#39; =\u0026gt; [ // ページビューの数が指標 new Metric([\u0026#39;name\u0026#39; =\u0026gt; \u0026#39;screenPageViews\u0026#39;]), ], \u0026#39;orderBys\u0026#39; =\u0026gt; [ // ページビューが多い順から少ない順へ new OrderBy([ \u0026#39;metric\u0026#39; =\u0026gt; new MetricOrderBy([ \u0026#39;metric_name\u0026#39; =\u0026gt; \u0026#39;screenPageViews\u0026#39;, ]), \u0026#39;desc\u0026#39; =\u0026gt; true, ]), ], \u0026#39;limit\u0026#39; =\u0026gt; 10, //最高１０まで ]); // 出力をブレードでフォーマット $rows = []; foreach ($response-\u0026gt;getRows() as $row) { $title = $row-\u0026gt;getDimensionValues()[0]-\u0026gt;getValue() ?? null; $url = $row-\u0026gt;getDimensionValues()[1]-\u0026gt;getValue() ?? null; $count = $row-\u0026gt;getMetricValues()[0]-\u0026gt;getValue() ?? null; if ($url === \u0026#39;/\u0026#39;) { continue; } $rows[] = (object) [ \u0026#39;title\u0026#39; =\u0026gt; Str::before($title, \u0026#39; – ララジャパン\u0026#39;), \u0026#39;url\u0026#39; =\u0026gt; $url, \u0026#39;views\u0026#39; =\u0026gt; $count, ]; } $html = view(\u0026#39;ranking\u0026#39;, compact(\u0026#39;rows\u0026#39;))-\u0026gt;render(); echo $html; return Command::SUCCESS; } } 上のrunReport()に渡す配列のディメンションと指標は以下に説明があります。\nhttps://developers.google.com/analytics/devguides/reporting/data/v1/api-schema?hl=ja#dimensions\n上で使用したディメンションは、\n指標は、\nコマンドの実行 上のコマンドを実行すると、以下のような出力となります。これをコピペしてWordpressに入れ込みます。\n$ php artisan ga4 \u0026lt;div class=\u0026#34;mg-wid-title\u0026#34;\u0026gt;\u0026lt;h6 class=\u0026#34;wtitle\u0026#34;\u0026gt;Trending\u0026lt;/h6\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;ol\u0026gt; \u0026lt;li\u0026gt;\u0026lt;a href=\u0026#34;/2023/11/23/メルマガをワンクリックで登録解除/\u0026#34; class=\u0026#34;trending\u0026#34;\u0026gt;メルマガをワンクリックで登録解除\u0026lt;/a\u0026gt;\u0026lt;/li\u0026gt; \u0026lt;li\u0026gt;\u0026lt;a href=\u0026#34;/2021/05/03/bulk-insertで大量のデータをdbに登録する/\u0026#34; class=\u0026#34;trending\u0026#34;\u0026gt;bulk insertで大量のデータをDBに登録する\u0026lt;/a\u0026gt;\u0026lt;/li\u0026gt; \u0026lt;li\u0026gt;\u0026lt;a href=\u0026#34;/2020/02/08/phpunitの実行のあれこれ/\u0026#34; class=\u0026#34;trending\u0026#34;\u0026gt;phpunitの実行のあれこれ\u0026lt;/a\u0026gt;\u0026lt;/li\u0026gt; \u0026lt;li\u0026gt;\u0026lt;a href=\u0026#34;/2022/08/22/sqlのcount関数と条件/\u0026#34; class=\u0026#34;trending\u0026#34;\u0026gt;SQLのCOUNT()関数と条件\u0026lt;/a\u0026gt;\u0026lt;/li\u0026gt; \u0026lt;li\u0026gt;\u0026lt;a href=\u0026#34;/2020/10/03/withinput-witherrors-with/\u0026#34; class=\u0026#34;trending\u0026#34;\u0026gt;withInput(), withErrors(), with()\u0026lt;/a\u0026gt;\u0026lt;/li\u0026gt; \u0026lt;li\u0026gt;\u0026lt;a href=\u0026#34;/2022/01/24/laravel-collection（９）foreachの代わりにeach/\u0026#34; class=\u0026#34;trending\u0026#34;\u0026gt;Laravel Collection（９）foreachの代わりにeach\u0026lt;/a\u0026gt;\u0026lt;/li\u0026gt; \u0026lt;li\u0026gt;\u0026lt;a href=\u0026#34;/2016/06/19/eloquentでカウントするときの注意/\u0026#34; class=\u0026#34;trending\u0026#34;\u0026gt;Eloquentでカウントするときの注意\u0026lt;/a\u0026gt;\u0026lt;/li\u0026gt; \u0026lt;li\u0026gt;\u0026lt;a href=\u0026#34;/2023/09/25/laravelの新しいmailableでhtmlメールを送信/\u0026#34; class=\u0026#34;trending\u0026#34;\u0026gt;Laravelの新しいMailableでHTMLメールを送信\u0026lt;/a\u0026gt;\u0026lt;/li\u0026gt; \u0026lt;li\u0026gt;\u0026lt;a href=\u0026#34;/2020/07/25/abortを使ってリダイレクト/\u0026#34; class=\u0026#34;trending\u0026#34;\u0026gt;abort()を使ってリダイレクト\u0026lt;/a\u0026gt;\u0026lt;/li\u0026gt; \u0026lt;/ol\u0026gt; ちなみに、上のコマンドで使用されたブレードは、\nresources/views/ranking.blade.php \u0026lt;div class=\u0026#34;mg-wid-title\u0026#34;\u0026gt;\u0026lt;h6 class=\u0026#34;wtitle\u0026#34;\u0026gt;Trending\u0026lt;/h6\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;ol\u0026gt; @foreach ($rows as $row) \u0026lt;li\u0026gt;\u0026lt;a href=\u0026#34;{!! $row-\u0026gt;url !!}\u0026#34; class=\u0026#34;trending\u0026#34;\u0026gt;{{ $row-\u0026gt;title }}\u0026lt;/a\u0026gt;\u0026lt;/li\u0026gt; @endforeach \u0026lt;/ol\u0026gt; ","date":"2024-01-28T03:02:42+09:00","permalink":"https://www.larajapan.com/2024/01/28/google-analytics%E3%81%AE%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92api%E3%81%A7%E5%8F%96%E5%BE%97%EF%BC%88%EF%BC%92%EF%BC%89%E3%82%B3%E3%83%9E%E3%83%B3%E3%83%89%E3%82%92%E4%BD%9C%E6%88%90/","title":"Google AnalyticsのデータをAPIで取得（２）コマンドを作成"},{"content":"このブログサイトのページの右側には、TRENDINGと称してGoogle Analytics（GA4）から取得した過去２８日間の人気の記事のトップ１０を掲載しています。これは、Google Analytics Data APIを使用して取得したデータです。その取得方をいくつかに分けて説明します。Google Cloudの認証は複雑なので今回はその設定を説明し、次回にAPIを使用してデータを取得を説明します。\nGoogle Cloudでの設定 まず、Google Cloud（もちろんアカウントがありログインされているとして）において、\nリソースの管理に行き、新規のプロジェクトを作成します。 新規のプロジェクトは、Google Analyticsとします。 作成したプロジェクトにおいて、IAMと管理 \u003e サービスアカウントからサービスアカウントを作成します。 新規のサービスアカウントは、GA4 Accessとします。 サービスアカウントを作成すると、以下のようなメールアドレスが作成されます。このメールアドレスは後に登場するGA4のアカウントでのアクセス管理で使用されます。 作成したサービスアカウントのキーの画面へ行き、そこで秘密鍵を作成すると、jsonファイルとしてダウンロードされます。ダウンロードされたファイル名は、ga4.keyとして改名しておきます。 今度は、APIとサービス \u003e ライブラリの画面へ行きカテゴリで「その他」を選択し、Google Analytics Data APIを選択します。 有効とします。 Google Analyticsでの設定 今度は、今回作成するプログラムでデータを取得する対象のGoogle Analyticsのアカウントにログインし管理画面に行きます。\n管理 \u003e プロパティ \u003e プロパティの詳細で、プロパティIDを取得します。 プロパティ \u003e プロパティのアクセス管理からユーザーを追加します。 先にGoogle Cloudで作成したサービスアカウントのメールをここで使います。追加ボタンを押してアクセスの許可を与えます。 まとめ 以上でGA4から情報を取得するための設定ができました。 次のGA4のデータの取得をするLaravelのコマンドの作成には、以下の情報を使います。\nga4.keyとして保存した、サービスアカウントの秘密鍵の情報 Google AnalyticsからのプロパティID ","date":"2024-01-27T10:23:06+09:00","permalink":"https://www.larajapan.com/2024/01/27/google-analytics%E3%81%AE%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92api%E3%81%A7%E5%8F%96%E5%BE%97%EF%BC%88%EF%BC%91%EF%BC%89%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9%E8%AA%8D%E8%A8%BC%E3%81%AE%E8%A8%AD%E5%AE%9A/","title":"Google AnalyticsのデータをAPIで取得（１）アクセス認証の設定"},{"content":"ローカル環境におけるメール送信部分を確認する際に以前の記事でMailtrapやMailhogを活用した方法を紹介していますが、もっと原始的且つ手っ取り早い方法としてLogに出力して確認するというオプションもあります。今回はそちらの方法をご紹介致します。\nメールをログに出力する メールをログに出力するにはメーラーにlogを指定するだけです。特定の場面においてlog出力に切り替えたいなら以下のようにmailer()を使って指定できます。過去の記事で紹介されたraw()を使えばMailableクラスを作成せずに簡単にテスト用のメールが送信できますよ。\nMail::mailer(\u0026#39;log\u0026#39;)-\u0026gt;raw(\u0026#39;This ia a test mail.\u0026#39;, fn($m) =\u0026gt; $m-\u0026gt;subject(\u0026#39;Test mail\u0026#39;)-\u0026gt;to(\u0026#39;test@example.com\u0026#39;)); するとデフォルトの設定では、storage/logs/laravel.logに出力されるはずです。\nstorage/logs/laravel.log [2024-01-21 02:23:12] local.DEBUG: From: Example \u0026lt;hello@example.com\u0026gt; Subject: Test mail To: test@example.com MIME-Version: 1.0 Date: Sun, 21 Jan 2024 02:23:12 +0000 Message-ID: \u0026lt;2c6e264f57ffa38f94b5bee86bdb1493@example.com\u0026gt; Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: quoted-printable This ia a test mail. デフォルトのメーラーをlogに切り替えたい場合は.envのMAIL_MAILERにlogを指定すればOKです。\n.env ... #MAIL_MAILER=smtp // デフォルトではsmtpが設定されていますね MAIL_MAILER=log ... そうすれば、mailer()で指定せずともlogメーラーが採用されます。\nMail::raw(\u0026#39;This ia a test mail.\u0026#39;, fn($m) =\u0026gt; $m-\u0026gt;subject(\u0026#39;Test mail\u0026#39;)-\u0026gt;to(\u0026#39;test@example.com\u0026#39;)); 設定を理解する 何がどうしてlaravel.logに出力されたのか解説します。まず、MAIL_MAILERにlogを指定した場合、config/mail.phpにて以下の設定が使用されます。\nconfig/mail.php ... \u0026#39;mailers\u0026#39; =\u0026gt; [ ... \u0026#39;log\u0026#39; =\u0026gt; [ \u0026#39;transport\u0026#39; =\u0026gt; \u0026#39;log\u0026#39;, \u0026#39;channel\u0026#39; =\u0026gt; env(\u0026#39;MAIL_LOG_CHANNEL\u0026#39;), ], ... 出力先のログチャンネルを.envのMAIL_LOG_CHANNELから取得していますね。しかし、先ほどの実行時にはMAIL_LOG_CHANNELを定義していません。故にこちらの値はnullとなります。チャンネルの指定が無い場合、ログ出力においてはdefaultのチャンネルが採用されます。config/logging.phpを見てみましょう。\nconfig/logging.php ... \u0026#39;default\u0026#39; =\u0026gt; env(\u0026#39;LOG_CHANNEL\u0026#39;, \u0026#39;stack\u0026#39;), // ←① ... \u0026#39;channels\u0026#39; =\u0026gt; [ \u0026#39;stack\u0026#39; =\u0026gt; [ \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;stack\u0026#39;, \u0026#39;channels\u0026#39; =\u0026gt; [\u0026#39;single\u0026#39;], // ←② \u0026#39;ignore_exceptions\u0026#39; =\u0026gt; false, ], \u0026#39;single\u0026#39; =\u0026gt; [ \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;single\u0026#39;, \u0026#39;path\u0026#39; =\u0026gt; storage_path(\u0026#39;logs/laravel.log\u0026#39;), // ←③ \u0026#39;level\u0026#39; =\u0026gt; env(\u0026#39;LOG_LEVEL\u0026#39;, \u0026#39;debug\u0026#39;), \u0026#39;replace_placeholders\u0026#39; =\u0026gt; true, ], ... ①defaultのチャンネルはstackです。②stackチャンネルではstackドライバが指定されており、ログの出力先を複数指定したい場合にそれらをバインドして指定できます。デフォルトの設定ではsingleチャンネルのみが指定されていました。そして、③このsingleチャンネルの出力先がlaravel.logと言うわけです。文章にするとやや複雑に感じますが、実際にconfigを辿るとそれほどでもありません。\n出力先ログチャンネルを変更する logメーラーを使用した際の出力先ログチャンネルを変更したい場合は、.envにおいてMAIL_LOG_CHANNELを指定すればOKです。試しにmail_debugログチャンネルを新たに作成し、そちらに出力してみましょう。まずは、config/logging.phpにおいて、channelsにmail_debugチャンネルを追加します。\nconfig/logging.php ... \u0026#39;channels\u0026#39; =\u0026gt; [ ... \u0026#39;mail_debug\u0026#39; =\u0026gt; [ \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;single\u0026#39;, \u0026#39;path\u0026#39; =\u0026gt; storage_path(\u0026#39;logs/mail_debug.log\u0026#39;), \u0026#39;level\u0026#39; =\u0026gt; env(\u0026#39;LOG_LEVEL\u0026#39;, \u0026#39;debug\u0026#39;), ], ... 出力先のログファイルは、storage/logs/mail_debug.logとしました。次に、.envにおいてMAIL_LOG_CHANNELを指定します。\n.env ... MAIL_LOG_CHANNEL=mail_debug ... 設定はこれだけです。メールを送信してmail_debug.logに出力されるか確認してみましょう。\nMail::raw(\u0026#39;mail_log.logに出力されるはず。\u0026#39;, fn($m) =\u0026gt; $m-\u0026gt;subject(\u0026#39;Test mail\u0026#39;)-\u0026gt;to(\u0026#39;test@example.com\u0026#39;)); mail_debug.logを確認すると意図通りにログが出力されているのが確認できるかと思います。\nstorage/logs/mail_debug.log [2024-01-21 02:55:47] local.DEBUG: From: Example \u0026lt;hello@example.com\u0026gt; Subject: Test mail To: test@example.com MIME-Version: 1.0 Date: Sun, 21 Jan 2024 02:55:47 +0000 Message-ID: \u0026lt;bbe1cccad557301198e97d3badb384f0@example.com\u0026gt; Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: quoted-printable mail_log.logに出力されるはず。 nullチャンネルの活用 ここからは少し応用編です。config/logging.phpの設定にnullと言うチャンネルがあります。\nconfig/logging.php ... \u0026#39;null\u0026#39; =\u0026gt; [ \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;monolog\u0026#39;, \u0026#39;handler\u0026#39; =\u0026gt; NullHandler::class, ], ... これはログはどこにも出力せず破棄する設定です。以前から一体何に使うのだろう？と疑問でしたが先日、logメーラーと組み合わせる事で大量メールを送信する際のメール生成部分のパフォーマンステストに活用できる事が分かり、実際に以前の記事でkhino氏が紹介したDKIM署名のテストにおいてこれを活用しました。DKIM署名の記事はこちらです。\nDKIM署名はMailableクラスのbuild()内で行うので、DKIM署名を行う場合と行わない場合でどのようにパフォーマンスが変わるのかを実際のメルマガと同じ送信件数でテストする必要がありました。ここでテストしたかったのはDKIM署名によりメール生成部分がボトルネックにならないか、のチェックであり送信部分ではありません。故に実際にメールを送信する必要は無く、生成したメールはlogメーラーでnullチャンネルに吐き出して破棄する方法を試みました。nullチャンネルを指定するには.envのMAIL_LOG_CHANNELにnullを指定するのですが、ここでも１つ工夫が必要でした。\n.env ... MAIL_LOG_CHANNEL=null ... このように.envに指定した場合、nullは文字列ではなくnull型として扱われてしまうのです。故に、出力先のチャンネルが未指定となりdefaultチャンネルが採用されlaravel.logに送信したメールが出力されてしまいます。そこで、config/logging.phpにてnullチャンネルをコピーしたsilentチャンネルを追加し、そちらを.envで指定するようにしました。\nconfig/logging ... \u0026#39;silent\u0026#39; =\u0026gt; [ \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;monolog\u0026#39;, \u0026#39;handler\u0026#39; =\u0026gt; NullHandler::class, ], ... .env ... MAIL_LOG_CHANNEL=silent ... 今度は意図通り、送信したメールはどこにも出力されず破棄されました。かなりニッチな活用法ですが、覚えておくと他にも色々と応用が効くかもしれません。\n","date":"2024-01-24T07:40:40+09:00","permalink":"https://www.larajapan.com/2024/01/24/%E3%83%A1%E3%83%BC%E3%83%AB%E3%82%92%E3%83%AD%E3%82%B0%E3%81%AB%E5%87%BA%E5%8A%9B%E3%81%99%E3%82%8B/","title":"メールをログに出力する"},{"content":"前回の画面表示テストに続き、今回もLaravelユーザー認証のテストです。正しいＥメールとパスワードでログインが成功するケースと、間違ったパスワードでログインしログインに失敗する２つのケースについて、テストを作成します。\nエラーメッセージの日本語化 前回、Laravelと認証機能のBreezeをインストールした直後の環境でテストを作成しました。後でご紹介するログイン失敗のケースではエラーメッセージも含めてアサートしたいので、まずは日本語化の設定から進めてゆきます。\nGithubで公開されているBreezejpという日本語化パッケージを使わせていただきます。名前にBreezeとついていますがJetstream、Laravel UIなどの環境でも使用可能とのこと。適用方法もとても簡単で、以下の２つのコマンドを実行するだけです。\nまず、composerでパッケージをインストールします。\n$ composer require askdkc/breezejp --dev 次に以下のコマンドを実行。\n$ php artisan breezejp Laravel Breeze用に日本語翻訳ファイルを準備します config/app.phpのlocaleをjaにします GitHubリポジトリにスターの御協力をお願いします🙏 (yes/no) [yes]: \u0026gt; yes Thank you! / ありがとう💓 日本語ファイルのインストールが完了しました! これでプロジェクト直下にlangディレクトリと、日本語用ファイルが作成できました。また出力内容からも分かるように、言語ファイルの設置だけでなくconfig/app.phpのlocaleもjaに変更してくれています。\nこれで日本語化の設定は完了です。\nここまででBreezeと、Breezejpの２つが出てきました。名前が似ていて少しややこしいのですが、前者は認証機能を簡単に実装できるパッケージで（前回の記事でインストールしました）、後者は今回インストールした日本語化のパッケージになります。\nテスト用Userデータ 次に、テストに必要なダミーユーザーを作成するためのFactoryについてです。Laravelデフォルトですでに用意されているので、内容を確認してみます。\ndatabase/factories/UserFactory.php ... class UserFactory extends Factory { protected static ?string $password; /** * Define the model\u0026#39;s default state. * * @return array\u0026lt;string, mixed\u0026gt; */ public function definition(): array { return [ \u0026#39;name\u0026#39; =\u0026gt; fake()-\u0026gt;name(), \u0026#39;email\u0026#39; =\u0026gt; fake()-\u0026gt;unique()-\u0026gt;safeEmail(), \u0026#39;email_verified_at\u0026#39; =\u0026gt; now(), \u0026#39;password\u0026#39; =\u0026gt; static::$password ??= Hash::make(\u0026#39;password\u0026#39;), \u0026#39;remember_token\u0026#39; =\u0026gt; Str::random(10), ]; } ... パスワードには、”password”という文字をハッシュ化した値が保存されるようになっているようですね。\nログイン成功のテスト 準備ができたので、テストをしてゆきましょう。まずはログイン成功のテストです。前回と同じくBreezeが用意してくれているユニットテストを使用しますが、アサートを少し追加しています。\ntests/Feature/Auth/AuthenticationTest.php public function test_users_can_authenticate_using_the_login_screen(): void { $user = User::factory()-\u0026gt;create(); $this-\u0026gt;assertGuest(); //未ログイン状態であることをチェック $response = $this-\u0026gt;post(\u0026#39;/login\u0026#39;, [ \u0026#39;email\u0026#39; =\u0026gt; $user-\u0026gt;email, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;password\u0026#39;, ]); $this-\u0026gt;assertAuthenticated(); //ログインが成功したことをチェック $response-\u0026gt;assertRedirect(RouteServiceProvider::HOME); } このテストでは、Laravelが用意してくれているログイン認証のための便利なアサーションを２つ使用しています。assertGuest()は、ユーザーが未ログインであること、またassertAuthenticated()は、ユーザーが認証済みであることをアサートしています。ログイン前後の認証状態を効率的にチェックできてとても便利ですね。\nこの２つの関数は通常、guardを引数として受け取ります。上記のテストでは引数を省略していますが、その場合にはconfig/auth.phpのdefaultsセクションで定義されているwebガードがデフォルトとして使用されます。\nでは以下のように、guardsの定義にwebだけでなくadminも定義されている場合はどうでしょうか。\nconfig/auth.php ・・・ \u0026#39;defaults\u0026#39; =\u0026gt; [ \u0026#39;guard\u0026#39; =\u0026gt; \u0026#39;web\u0026#39;, \u0026#39;passwords\u0026#39; =\u0026gt; \u0026#39;users\u0026#39;, ], ・・・ \u0026#39;guards\u0026#39; =\u0026gt; [ \u0026#39;web\u0026#39; =\u0026gt; [ \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;session\u0026#39;, \u0026#39;provider\u0026#39; =\u0026gt; \u0026#39;users\u0026#39;, ], \u0026#39;admin\u0026#39; =\u0026gt; [ \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;session\u0026#39;, \u0026#39;provider\u0026#39; =\u0026gt; \u0026#39;admin\u0026#39;, ], ], ・・・ adminガードに対して認証確認を行いたい場合は、以下のように引数にadminを渡します。環境に併せて適宜ご変更ください。\n$this-\u0026gt;assertGuest(\u0026#39;admin\u0026#39;); $this-\u0026gt;assertAuthenticated(\u0026#39;admin\u0026#39;); ログイン失敗のテスト 次はログインに失敗する場合のテストです。正しいパスワードは先ほどのFactoryで設定されている通り\u0026quot;password\u0026quot;ですが、失敗するために\u0026quot;wrong-password\u0026ldquo;でログインしてみます。\ntests/Feature/Auth/AuthenticationTest.php public function test_users_can_not_authenticate_with_invalid_password(): void { $user = User::factory()-\u0026gt;create(); $this-\u0026gt;assertGuest(); //未ログイン状態であることをチェック $response = $this-\u0026gt;post(\u0026#39;/login\u0026#39;, [ \u0026#39;email\u0026#39; =\u0026gt; $user-\u0026gt;email, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;wrong-password\u0026#39;, ]); $response-\u0026gt;assertSessionHasErrors([ \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;ログイン情報が存在しません。\u0026#39; ]); $this-\u0026gt;assertGuest(); } エラーメッセージは先ほど日本語化したため、正しく設定されていれば日本語のメッセージが返るはずです。\nでは、ログイン成功・失敗のテストを実行してみましょう。\n$ php artisan test --filter=AuthenticationTest PASS Tests\\Feature\\Auth\\AuthenticationTest ✓ users can authenticate using the login screen 0.58s ✓ users can not authenticate with invalid password 0.27s Tests: 2 passed (8 assertions) Duration: 1.04s テストが成功しました！\n次回は、ログアウト関連のテストをご紹介します。\n","date":"2024-01-20T12:39:28+09:00","permalink":"https://www.larajapan.com/2024/01/20/%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E8%AA%8D%E8%A8%BC%E3%81%AE%E3%83%86%E3%82%B9%E3%83%88%EF%BC%88%EF%BC%92%EF%BC%89laravel-10%E3%83%AD%E3%82%B0%E3%82%A4%E3%83%B3/","title":"ユーザー認証のテスト（２）Laravel 10　ログイン"},{"content":"Laravelの最新バージョンは10xでもうすぐに11xがリリースされます。しかし、Laravel 9xでartisan dbコマンドにオプションが増えていることについ最近気づいたので、忘れずにここで紹介します。\nLaravel 9x以前から存在したdbコマンド db:seed まず、データベースにシードのレコードを作成するコマンドです。 $ php artisan db:seed と実行すると、以下のDatabaseSeederがデフォルトで使用されます。\ndatabase/seeders/DatabaseSeeder.php namespace Database\\Seeders; // use Illuminate\\Database\\Console\\Seeds\\WithoutModelEvents; use Illuminate\\Database\\Seeder; class DatabaseSeeder extends Seeder { /** * Seed the application\u0026#39;s database. */ public function run(): void { \\App\\Models\\User::factory(10)-\u0026gt;create();　// 10個のusersのレコードが作成されます // \\App\\Models\\User::factory()-\u0026gt;create([ // \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;Test User\u0026#39;, // \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;test@example.com\u0026#39;, // ]); } } 新規のプロジェクトでは、seedを実行する前には以下のようにmigrateを実行しましょう。\nしかし、プロダクションではこんなに簡単に実行できると危ないと思いませんか？ ちなみに、上の実行の環境では、.envでAPP_ENVはlocalで私の開発環境設定です。\n.env APP_NAME=Laravel APP_ENV=local ... この値が、productionのときには、上のコマンドを実行すると、\nと警告してくれて実行してOKかどうか質問されます。しかし、それでも実行は可能です。注意しましょう。\ndb:wipe db:seedは実行する環境によっては危険なコマンドでしたが、こちらはもっと危険です。以下は、APP_ENV=localでの実行ですが、有無も言わさずにDBのテーブルをすべて削除してしまいます。\nもちろん、APP_ENV=productionのときには、db:seedと同様に警告と確認のステップがあります。\nLaravel 9xで登場したdbコマンド 今度は、Laravel 9xで追加されたコマンドたちです。先のDB操作のコマンドとは違って今度はどれもDBの情報をすぐにチェックできる有用なコマンドです。\ndb:monitor 現在のDBの接続数を表示するコマンドです。\ndb:show DBの情報を表示するコマンドですが、Doctrine DBAL (doctrine/dbal)のパッケージがインストールされていないと、インストールするかどうかが問われます。YESとすると、必要なパッケージがインストールされます。 インストール完了後、再度実行すると、\ndb:table こちらは、DBテーブルの情報を表示するコマンドです。まず、どのテーブルの情報が必要か尋ねられます。\nusersを選択してENTERすると、\nDBテーブルの項目などテーブルの情報を表示してくれます。これはとても便利ですね。\n","date":"2024-01-08T02:33:53+09:00","permalink":"https://www.larajapan.com/2024/01/08/artisan-db%E3%82%B3%E3%83%9E%E3%83%B3%E3%83%89/","title":"artisan dbコマンド"},{"content":"Googleのメルマガ配信に対する要求は、前回の「メルマガをワンクリックで登録解除」だけでなく、配信するすべてのメールは、「ドメインに SPF および DKIM メール認証を設定します。」とあります。SPFはDNSでの設定のみですが、DKIMメール認証のためには、それぞれの配信メールの内容を元にDKIM署名のヘッダーの追加が必要です。私のクライアントの環境では、postfix + opendkimとシステムレベルで対応としました。ところが、\nそのパフォーマンスが非常に悪く配信時間が以前の何倍もかかるようになりました。パフォーマンスの悪さの原因がわからず悩んでいたところ、Laravelで（正確にはSymfonyで）、DKIM署名が可能なことを発見しました。試した結果、postfix + opendkimの半分以上の配信時間が可能となりました。\nDKIM認証の仕組み まず、DKIM認証の仕組みを簡単に説明します。DKIM認証のためには、配信側においてsshと同様に秘密キーと公開キーを作成します。公開キーはドメインのDNSにエントリーを作成して保存します。メール配信側（通常はMTA）は、送信メールのデータと秘密キーでDKIM署名を作成してメールのヘッダーに、DKIM-Signatureの値として追加します。メール送信先のMTAでは、このメールを受け取ると配信者のDNSから先の公開鍵を取得して、メールに含まれるDKIM署名の正当性を検証します。\n以下、DKIM認証されたメールのソースです。Dkim-Signatureヘッダーとしてあり、受け取り側のgoogle.comでDKIMの認証がOK（dkim=pass）なことが伺えます。\nDKIMのキーを作成 DKIM認証の対象となるドメイン（上の例では、lotsofbytes.com）のために、秘密キーと公開キーを以下のコマンド実行で作成します。\n$ openssl genrsa -out dkim_private.key 2048 Generating RSA private key, 2048 bit long modulus ............................+++ ......................................+++ e is 65537 (0x10001) 次に、それをもとに公開キーを作成します。\n$ openssl rsa -in dkim_private.key -pubout -outform der 2\u0026gt;/dev/null | openssl base64 -A MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2Em6C71Rcvf9qsRJciVRjuqcJQRzMeNRMK7Kizz2jfPxnx7XBu7hg2lJ3AvJj86pXJE2DASR0LmXMUfwOyXQ5dCJrBdfYK31ZCuBfVN1Vz/9utupyq1zPfAoteB8biR+mvc5MinrY2kj6LDBbuYU3Ff930AOVig/5ExAKfrSvt9gqrSSbZn36L/MnigcJWmB61mbCDXEPxTsZ8C+X7OsplpSdmHM3VpL9Dzo6FPyFwNnKMcGMBpgnTGo8PfN7H+Fh5n7QTv42f6BLEiWd47JoBU5yRnRqjVCFenTI1Gu/8RlwaWUJBZ7+eDeJmaPONb6WFjRlVD8ScU0qzEpKi71mQIDAQAB これをDNSに以下のように、TXTレコードとして追加します。公開キーは長いので１つの文字列で収めることができません。キーの適当な場所で引用符を使って２つの文字列にする必要があります。以下は、AWSでのRoute53でのDNSエントリーの例です。\nアプリでDKIM署名 DKIMの署名の機能はLaravelには存在しないので、Laravelが使用しているSymfony MailerのDKIM署名を使います。\n「メルマガをワンクリックで解除」で使用したMailableのコードを編集しました。以下では差分を掲載しています。\napp/Mail/Newsletter.php namespace App\\Mail; use Illuminate\\Bus\\Queueable; use Illuminate\\Mail\\Mailable; use Symfony\\Component\\Mime\\Email; use Illuminate\\Mail\\Mailables\\Content; use Illuminate\\Mail\\Mailables\\Headers; use Illuminate\\Queue\\SerializesModels; use Illuminate\\Mail\\Mailables\\Envelope; use Symfony\\Component\\Mime\\Crypto\\DkimSigner; class Newsletter extends Mailable { use Queueable, SerializesModels; /** * @var DkimSigner */ public static $signer; /** * Create a new message instance. */ public function __construct(public string $email) { $this-\u0026gt;email = $email; if (! self::$signer) { // 署名者のインスタンスを作成 self::$signer = new DkimSigner( \u0026#39;file://\u0026#39;.storage_path(\u0026#39;dkim_private.key\u0026#39;), // 秘密キーのパス config(\u0026#39;mail.dkim.domain\u0026#39;), // DKIM対象のドメイン名 \u0026#39;default\u0026#39; // DNSのセレクター ); } } /** * Build the message. */ public function build(): static { $html = view(\u0026#39;newsletter\u0026#39;)-\u0026gt;render(); // 以下で、メールにDKIM署名 $this-\u0026gt;withSymfonyMessage(function (Email $symfonyMessage) use($html) { $symfonyMessage-\u0026gt;html($html); // symfonyのコンポーネントにデータを入れる $signedEmail = self::$signer-\u0026gt;sign($symfonyMessage); // 署名 $symfonyMessage-\u0026gt;setHeaders($signedEmail-\u0026gt;getHeaders()); // ヘッダーに追加 }); return $this; } ... } コードの変更箇所は、２つあります。\nまずコンストラクターにおいて署名者のインスタンスを作成して、これをクラス変数として$signerに収めます。ここで必要な情報は、\n秘密キーのパス：これは先に作成したdkim_private.keyをstorageのディレクトリに置いておくことが必要。Laravelのプロジェクトの.gitignoreには、/storage/*.keyの行が含まれるので、バージョン管理からは除外となります。 DKIMの対象となるドメイン名：ここでは以下のように、congig/mail.phpに項目を追加しておき、.envにて、MAIL_DKIM_DOMAIN=で値を渡します。ちなみに、ここでドメインは、メールの差出人Fromで使われるドメイン（例えば、hello@lotsofbytes.comnならlotsofbytes.com）となります。 config/mail.php ```php return [ ... 'dkim' =\u003e [ 'domain' =\u003e env('MAIL_DKIM_DOMAIN'), ] ]; ``` 公開キーのレコード名のセレクター名：ここでは先にDNSに登録した名前の、default._domainkey.lotsofbytes.comのdefaultを指定。 次に、build()のメソッドを追加します。これは、Laravel 9x以前にEnvelopeやContentなどのコンポートがないときにメールを構築するために使われていたメソッドです。しかし、ここにおいてSymfony MailのモジュールにアクセスしてDKIM署名を実行する機能のために使います。ここで大事なのは、SymfonyのDKIM署名を使用するために、Symfonyのメールのインスタンス（Symfony\\Component\\Mime\\Email）においてメールの内容となるデータを先に注入することです。もちろん、後にLaravel側でも同様な作業が行われますが、それはメール配信時の作業で、この時点ではDKIM署名のために必要なメールの内容がありません。それゆえにです。\n最後に postfix+opendkimのDKIM署名のパフォーマンスが悪かったために、このような一時対応となりましたが、これが複数のMailableで必要となると管理は先のシステムレベルで行えることに越したことはありません（改善方法を知っているお客様がいれば是非ご連絡を！）。しかし、レンタルサーバーでpostfixなどの管理権限がないときは、今回の方法が役に立つと思います。\n","date":"2024-01-03T02:38:56+09:00","permalink":"https://www.larajapan.com/2024/01/03/laravel%E3%81%A7%E3%83%A1%E3%83%BC%E3%83%AB%E3%82%92dkim%E7%BD%B2%E5%90%8D/","title":"LaravelでメールをDKIM署名"},{"content":"従来はプログラムから特定のパスにディレクトリやファイルを追加する際、それらのパーミッションの変更にはPHPのchmod()を使用していました。しかし、最近ドキュメントを読み返してみてLaravelらしいやり方を学んだのでまとめます。\nデフォルトの権限 まずはStorageファサードを使用してstorage配下にディレクトリやファイルを追加した場合に、それらのパーミッションがどうなっているのか確認してみましょう。config/filesystems.phpの設定をデフォルトの状態から変更していない前提で話を進めます。tinkerで以下を実行してみてください。 // ディレクトリを作成 \u0026gt; Storage::makeDirectory(\u0026#39;memos\u0026#39;); = true // ファイルを作成 \u0026gt; Storage::put(\u0026#39;memos/todo.txt\u0026#39;, \u0026#39;今日のTODOリスト...\u0026#39;); = true ディスクを指定していないのでデフォルトのlocalディスクドライバが選択され、storage/app配下にmemos/todo.txtが追加されたはずです。追加されたディレクトリとファイルのパーミッションを確認してみましょう。\n$ ls -la storage/app/ | grep memos drwx------ 3 hikaru staff 96 12 12 22:53 memos $ ls -la storage/app/memos/todo.txt -rw-r--r-- 1 hikaru staff 20 12 12 23:12 storage/app/memos/todo.txt ※ユーザ名及びグループ名は実行環境毎に異なるので仮です。ご自身の環境の値に置き換えて読み進めてください。 上記の実行結果から分かる通りパーミッションは以下の通りになっていました。\nmemosディレクトリ（rwx------）: ユーザのみフルアクセス可能 todo.txt（rw-r--r--）: 誰でも閲覧可能、書き込みはユーザのみ可能 todo.txt自体は誰でも閲覧可能となっていますが、memosディレクトリ自体の権限がユーザにのみ付与されているので、他のユーザはその配下にアクセスする事ができません。アクセスしようとするとPermission deniedと表示されます。memosディレクトリも誰でも閲覧可能とするにはどうすれば良いでしょうか？\nvisibilityの設定 Laravelにおいてはパーミンションの事をvisibilityと呼ぶ様です。visibilityの設定値はpublicかprivateの２択に簡略化されており、ローカルであれクラウドであれ同じ操作でアクセス権の設定を行えます。詳しくは公式ドキュメントをご確認ください。visibilityを設定するにはsetVisibility()を使用します。第一引数にvisibilityを設定するターゲットを、第二引数にpublic or privateを指定します。例えば、前項のmemosディレクトリをpublicにするなら、\n\u0026gt; Storage::setVisibility(\u0026#39;memos\u0026#39;, \u0026#39;public\u0026#39;); = true 権限をチェックしてみましょう。\n$ ls -la storage/app/ | grep memos drwxr-xr-x 3 hikaru staff 96 12 12 23:39 memos 誰でも閲覧可能となりましたね。今度は逆にtodo.txtをprivateにしてみましょう。\n\u0026gt; Storage::setVisibility(\u0026#39;memos/todo.txt\u0026#39;, \u0026#39;private\u0026#39;); = true 権限を確認するとユーザhikaruのみ閲覧可能となっていました。\n$ ls -la storage/app/memos/todo.txt -rw------- 1 hikaru staff 25 12 12 23:43 todo.txt デフォルトの設定ではpublicとprivateを指定した場合のアクセス権についてまとめると以下の通りになっていました。\nfilesystems.phpで詳細設定 publicやprivateによって設定されるアクセス権をより細かくカスタマイズしたいケースがあるかもしれません。その場合はfilesystems.phpにて指定する事ができます。以下はデフォルトのlocalディスクドライバの設定です。\nconfig/filesystems.php ... \u0026#39;disks\u0026#39; =\u0026gt; [ \u0026#39;local\u0026#39; =\u0026gt; [ \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;local\u0026#39;, \u0026#39;root\u0026#39; =\u0026gt; storage_path(\u0026#39;app\u0026#39;), \u0026#39;throw\u0026#39; =\u0026gt; false, ], ... これを次のように変更してみます。\nconfig/filesystems.php ... \u0026#39;disks\u0026#39; =\u0026gt; [ \u0026#39;local\u0026#39; =\u0026gt; [ \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;local\u0026#39;, \u0026#39;root\u0026#39; =\u0026gt; storage_path(\u0026#39;app\u0026#39;), \u0026#39;throw\u0026#39; =\u0026gt; false, \u0026#39;directory_visibility\u0026#39; =\u0026gt; \u0026#39;public\u0026#39;, \u0026#39;visibility\u0026#39; =\u0026gt; \u0026#39;public\u0026#39;, \u0026#39;permissions\u0026#39; =\u0026gt; [ \u0026#39;file\u0026#39; =\u0026gt; [ \u0026#39;public\u0026#39; =\u0026gt; 0664, \u0026#39;private\u0026#39; =\u0026gt; 0600, ], \u0026#39;dir\u0026#39; =\u0026gt; [ \u0026#39;public\u0026#39; =\u0026gt; 0775, \u0026#39;private\u0026#39; =\u0026gt; 0700, ], ] ], ... directory_visibilityとvisibilityはディレクトリやファイルを追加した際のデフォルトのvisibilityを指定しています。前項ではディレクトリ追加時のvisibilityがprivateになっていましたが、publicに変更しました。そして、permissions内ではファイルとディレクトリにおけるpublicとprivateを指定した際に割り当てるパーミッションを8進数で指定します。上記ではpublic時にグループユーザにも書き込み権限を付与しました。設定通りにアクセス権が付与されるか、再度tinkerでチェックしてみましょう。\n\u0026gt; Storage::makeDirectory(\u0026#39;memos2\u0026#39;); = true 権限を確認すると\n$ ls -la storage/app/ | grep memos2 drwxr-xr-x 2 hikaru staff 64 12 21 20:27 memos2 あれれ、グループユーザに書き込み権限が付与されていません。何故でしょう？しばらくソースコードと睨めっこしてやっと気づきました。犯人はumaskです。Storage::makeDirectory()は内部的にPHPのmkdir()を使用しているのですが、そちらのドキュメントにはこう書かれています。\nまた permissions は、 現在設定されている umask の影響も受けます。 umask を変更するには umask() を使用します。 umaskとはUnix系OSにおいてディレクトリやファイルを追加した際のアクセス権を指定するコマンドです。例えば、私の環境、macOSではデフォルトでは標準的な022が指定されており、基本パーミッションである0777から022を引いた0755がデフォルトのパーミッションとなります。ドキュメントに書かれている通り、umask()で制限を解除してからディレクトリ作成コマンドを実行してみましょう。\n\u0026gt; umask(000); = 18 \u0026gt; Storage::makeDirectory(\u0026#39;memos3\u0026#39;); = true \u0026gt; Storage::put(\u0026#39;memos3/todo.txt\u0026#39;, \u0026#39;今日のTODOリスト...\u0026#39;); = true パーミッションを確認すると\n$ ls -la storage/app/ | grep memos3 drwxrwxr-x 3 hikaru staff 96 12 21 20:48 memos3 $ ls -la storage/app/memos3/todo.txt -rw-rw-r-- 1 hikaru staff 13 12 21 20:48 todo.txt config/filesystems.phpで指定した通り、ディレクトリもファイルもグループユーザにも書き込み権限が付与されました。\n","date":"2023-12-24T08:10:43+09:00","permalink":"https://www.larajapan.com/2023/12/24/laravel%E7%9A%84%E3%83%91%E3%83%BC%E3%83%9F%E3%83%83%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E8%A8%AD%E5%AE%9A%EF%BC%88visibility%EF%BC%89/","title":"Laravel的パーミッションの設定（visibility）"},{"content":"当サイトでご紹介しているユーザー認証のテストはLaravel5.4のもので、Laravel10となった今ではアサート関数やfactory関連の記述も変化しているため新しいものに書き換えることにしました。Laravelの新規インストールから、認証周りのテスト作成をご紹介します。また、認証機能にはBreezeを使用しました。\nBreezeとは? Breezeについては、Laravelのドキュメントにて以下のように紹介されています。 Laravel Breezeは、ログイン、ユーザー登録、パスワードリセット、メール確認、パスワード確認など、すべての認証機能を最小かつシンプルにLaravelへ実装したものです。 Laravel uiの後継という立ち位置で、Laravel7から採用されています。フロントの実装もついてくるのでインストールするだけですぐに認証機能を利用することができます。\nプロジェクト新規作成・Breezeインストール まずはLaravelをインストールします。本記事作成時のバージョンは10.2.9でした。インストール先のディレクトリ名は適宜書き換えてくださいね。\n$ composer create-project laravel/laravel test-project $ cd test-project 次に、.envを編集します。今回DBはsqliteを使用しますので、DB_CONNECTIONを編集し、以降はコメントアウトします。\n.env DB_CONNECTION=sqlite # DB_HOST=127.0.0.1 # DB_PORT=3306 # DB_DATABASE=laravel # DB_USERNAME=root # DB_PASSWORD= 次はマイグレーションを実行します。sqliteのデフォルトでは、/database/database.sqlite というファイルが作成されます。\n$ php artisan migrate ここまで進めたら、いよいよBreezeをインストールします。オプションについていくつか質問されますが、テスト用フレームワークはPHPUnitを選択してください。\n$ composer require laravel/breeze --dev $ php artisan breeze:install ┌ Which Breeze stack would you like to install? ───────────────┐ │ Blade with Alpine │ └──────────────────────────────────────────────────────────────┘ ┌ Would you like dark mode support? ───────────────────────────┐ │ No │ └──────────────────────────────────────────────────────────────┘ ┌ Which testing framework do you prefer? ──────────────────────┐ │ PHPUnit │ └──────────────────────────────────────────────────────────────┘ これでインストール関連は完了です。テストを書くだけなら不要ですが、せっかくなのでブラウザの表示も確認してみましょう。\n$ php artisan serve http://127.0.0.1:8000/register へアクセスすると、以下のようなユーザー登録画面が表示されます。\nテストの準備 phpunit.xmlを編集します。今回はテストでもsqliteを使用するので、以下のようにDB_CONNECTION、DB_DATABASEのコメントアウトを外すだけです。\nphpunit.xml \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;phpunit xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:noNamespaceSchemaLocation=\u0026#34;vendor/phpunit/phpunit/phpunit.xsd\u0026#34; bootstrap=\u0026#34;vendor/autoload.php\u0026#34; colors=\u0026#34;true\u0026#34; \u0026gt; \u0026lt;testsuites\u0026gt; \u0026lt;testsuite name=\u0026#34;Unit\u0026#34;\u0026gt; \u0026lt;directory\u0026gt;tests/Unit\u0026lt;/directory\u0026gt; \u0026lt;/testsuite\u0026gt; \u0026lt;testsuite name=\u0026#34;Feature\u0026#34;\u0026gt; \u0026lt;directory\u0026gt;tests/Feature\u0026lt;/directory\u0026gt; \u0026lt;/testsuite\u0026gt; \u0026lt;/testsuites\u0026gt; \u0026lt;source\u0026gt; \u0026lt;include\u0026gt; \u0026lt;directory\u0026gt;app\u0026lt;/directory\u0026gt; \u0026lt;/include\u0026gt; \u0026lt;/source\u0026gt; \u0026lt;php\u0026gt; \u0026lt;env name=\u0026#34;APP_ENV\u0026#34; value=\u0026#34;testing\u0026#34;/\u0026gt; \u0026lt;env name=\u0026#34;BCRYPT_ROUNDS\u0026#34; value=\u0026#34;4\u0026#34;/\u0026gt; \u0026lt;env name=\u0026#34;CACHE_DRIVER\u0026#34; value=\u0026#34;array\u0026#34;/\u0026gt; \u0026lt;env name=\u0026#34;DB_CONNECTION\u0026#34; value=\u0026#34;sqlite\u0026#34;/\u0026gt; //ここのコメントアウトを外す \u0026lt;env name=\u0026#34;DB_DATABASE\u0026#34; value=\u0026#34;:memory:\u0026#34;/\u0026gt; //ここのコメントアウトを外す \u0026lt;env name=\u0026#34;MAIL_MAILER\u0026#34; value=\u0026#34;array\u0026#34;/\u0026gt; \u0026lt;env name=\u0026#34;QUEUE_CONNECTION\u0026#34; value=\u0026#34;sync\u0026#34;/\u0026gt; \u0026lt;env name=\u0026#34;SESSION_DRIVER\u0026#34; value=\u0026#34;array\u0026#34;/\u0026gt; \u0026lt;env name=\u0026#34;TELESCOPE_ENABLED\u0026#34; value=\u0026#34;false\u0026#34;/\u0026gt; \u0026lt;/php\u0026gt; \u0026lt;/phpunit\u0026gt; これで準備は完了です。あとはテストを書いてゆくだけなのですが、なんとBreezeはインストール直後の状態でユーザー登録やログイン認証・パスワード変更のテストが一通り揃っています。\n以下はtestsディレクトリのスクリーンショットです。\nこれだけでテストは十分揃っているようですが、今回は少し付け足したいケースもあったのでこれらのテストを参考にしながら認証テストを作成することにします。\n画面表示のテスト テストを書くにあたりルーティングを確認します。php artisan route:listでリストを出力すると、以下のようになっていました。 このルーティングの中から、まずはログイン画面（login）の表示と、ログイン後のダッシュボード（dashboard）の表示をテストしてみます。AuthenticationTest.phpには初期状態で４つのテストが用意さてれていますが、まずは簡単なテストから、ということでシンプルに以下のように編集しました。１つ目はデフォルトで用意されているテストで、２つ目は新しく追加したものです。\ntests/Feature/Auth/AuthenticationTest.php namespace Tests\\Feature\\Auth; use Illuminate\\Foundation\\Testing\\RefreshDatabase; use Tests\\TestCase; class AuthenticationTest extends TestCase { use RefreshDatabase; public function test_login_screen_can_be_rendered(): void { $response = $this-\u0026gt;get(\u0026#39;/login\u0026#39;); $response-\u0026gt;assertStatus(200); } public function test_unauthenticated_user_cannot_view_dashboard(): void { $response = $this-\u0026gt;get(\u0026#39;dashboard\u0026#39;); $response-\u0026gt;assertRedirect(\u0026#39;/login\u0026#39;); } } １つ目のテストは、ログイン画面の表示を確認しています。問題なく表示されればステータスのコードは200となり、テストが通るはずです。\n２つ目は、認証が必要な画面にアクセスしています。まだログインしていない状態なので、ログイン画面にリダイレクトされる挙動を期待しています。\nテストを実行してみると、成功しました。\n$ vendor/bin/phpunit --filter AuthenticationTest --testdox PHPUnit 10.5.1 by Sebastian Bergmann and contributors. .. 2 / 2 (100%) Time: 00:01.134, Memory: 38.50 MB OK (2 tests, 3 assertions) アサートは２つなのに3 assertionsとなっているのは、２つ目のテストのassertRedirectが２つのアサートを内包しているためです。 １つは、リダイレクトされたこと。もう１つは、リダイレクトの先が/loginであることです。\n次回は、ログイン関連のテストをご紹介いたします。\n","date":"2023-12-21T08:43:51+09:00","permalink":"https://www.larajapan.com/2023/12/21/%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E8%AA%8D%E8%A8%BC%E3%81%AE%E3%83%86%E3%82%B9%E3%83%88%EF%BC%88%EF%BC%91%EF%BC%89laravel-10/","title":"ユーザー認証のテスト（１）Laravel 10"},{"content":"一度に大量のデータを更新する必要がある時などはArtisanコンソールコマンドでバッチを作成して実行します。その際に更新に時間が掛かって途中で何も出力が無いと処理が正常に進んでいるのか不安になってしまいます。そんな時はプログレスバーを表示すれば進捗が確認できて便利です。\n演習コマンド Laravelにデフォルトで用意されているusersテーブルを用いて演習用のコマンドを作成し、使い方を確認していきます。作成するのは登録されているemailからユーザー名部分を抜き出し、nameを上書きするコマンドです。コマンドの実行には更新対象となるusersのレコードが必要となるので先にダミーレコードを準備しましょう。DatabaseSeeder.phpのrun()を以下の様に編集してください。\ndatabase/seeders/DatabaseSeeder.php namespace Database\\Seeders; use Illuminate\\Database\\Seeder; class DatabaseSeeder extends Seeder { /** * Seed the application\u0026#39;s database. */ public function run(): void { \\App\\Models\\User::factory(1000)-\u0026gt;create(); } } そして以下を実行するとusersに1000件のダミーレコードが作成されます。\n$ php artisan db:seed 続いて演習用のUserNameUpdateコマンドを作成します。\n$ php artisan make:command UserNameUpdate UserNameUpdateコマンドは以下の様にしました。まだプログレスバーを実装する前の状態です。\napp/Console/Commands/UserNameUpdate.php namespace App\\Console\\Commands; use App\\Models\\User; use Illuminate\\Support\\Str; use Illuminate\\Console\\Command; class UserNameUpdate extends Command { /** * The name and signature of the console command. * * @var string */ protected $signature = \u0026#39;app:user-name-update\u0026#39;; /** * The console command description. * * @var string */ protected $description = \u0026#39;nameをemailのユーザー名に更新する\u0026#39;; /** * Execute the console command. */ public function handle() { $this-\u0026gt;info(\u0026#39;ユーザ名の更新を開始します。\u0026#39;); $users = User::all(); $users-\u0026gt;each(function ($user) use (\u0026amp;$updated) { $user-\u0026gt;name = Str::before($user-\u0026gt;email, \u0026#39;@\u0026#39;); $user-\u0026gt;save(); $updated++; }); $this-\u0026gt;info(\u0026#39;ユーザ名の更新が完了しました。（更新件数：\u0026#39; . $updated . \u0026#39;）\u0026#39;); } } 以下を実行するとemailの\u0026rsquo;@\u0026lsquo;より前の部分をnameに上書きします。\n$ php artisan app:user-name-update ユーザ名の更新を開始します。 ユーザ名の更新が完了しました。（更新件数：1000） コマンド実行後にtinkerで以下を実行してみて下さい。ランダムにピックアップした２０件ですが、意図通りに更新されているのが確認できると思います。\n\u0026gt; User::take(20)-\u0026gt;inRandomOrder()-\u0026gt;pluck(\u0026#39;email\u0026#39;, \u0026#39;name\u0026#39;); = Illuminate\\Support\\Collection {#7081 all: [ \u0026#34;marjorie.hagenes\u0026#34; =\u0026gt; \u0026#34;marjorie.hagenes@example.com\u0026#34;, \u0026#34;owen.sipes\u0026#34; =\u0026gt; \u0026#34;owen.sipes@example.org\u0026#34;, \u0026#34;leann10\u0026#34; =\u0026gt; \u0026#34;leann10@example.net\u0026#34;, \u0026#34;amy21\u0026#34; =\u0026gt; \u0026#34;amy21@example.net\u0026#34;, \u0026#34;brooks.leffler\u0026#34; =\u0026gt; \u0026#34;brooks.leffler@example.org\u0026#34;, \u0026#34;arianna89\u0026#34; =\u0026gt; \u0026#34;arianna89@example.com\u0026#34;, \u0026#34;ledner.teagan\u0026#34; =\u0026gt; \u0026#34;ledner.teagan@example.net\u0026#34;, \u0026#34;axel17\u0026#34; =\u0026gt; \u0026#34;axel17@example.org\u0026#34;, \u0026#34;ykoepp\u0026#34; =\u0026gt; \u0026#34;ykoepp@example.com\u0026#34;, \u0026#34;rita19\u0026#34; =\u0026gt; \u0026#34;rita19@example.net\u0026#34;, \u0026#34;genevieve98\u0026#34; =\u0026gt; \u0026#34;genevieve98@example.com\u0026#34;, \u0026#34;nathen.goldner\u0026#34; =\u0026gt; \u0026#34;nathen.goldner@example.net\u0026#34;, \u0026#34;xjones\u0026#34; =\u0026gt; \u0026#34;xjones@example.org\u0026#34;, \u0026#34;assunta57\u0026#34; =\u0026gt; \u0026#34;assunta57@example.com\u0026#34;, \u0026#34;swift.vena\u0026#34; =\u0026gt; \u0026#34;swift.vena@example.net\u0026#34;, \u0026#34;senger.maybell\u0026#34; =\u0026gt; \u0026#34;senger.maybell@example.org\u0026#34;, \u0026#34;jace.willms\u0026#34; =\u0026gt; \u0026#34;jace.willms@example.com\u0026#34;, \u0026#34;stephanie.berge\u0026#34; =\u0026gt; \u0026#34;stephanie.berge@example.org\u0026#34;, \u0026#34;acremin\u0026#34; =\u0026gt; \u0026#34;acremin@example.org\u0026#34;, \u0026#34;yaufderhar\u0026#34; =\u0026gt; \u0026#34;yaufderhar@example.org\u0026#34;, ], } 一度コマンドを実行してnameが更新されてしまったので、以下を実行してダミーデータを作り直しておきましょう。\n$ php artisan migrate:fresh --seed withProgressBar() それでは先ほどのコマンドを修正してプログレスバーを表示するようにしてみましょう。プログレスバーを表示するにはwithProgressBar()を使用します。withProgressBar()は第一引数にループ処理可能な値（iterable）を、第二引数にループ実行する処理をクロージャで指定します。UserNameUpdateコマンドに実装した例が以下になります。\napp/Console/Commands/UserNameUpdate.php ... public function handle() { $this-\u0026gt;info(\u0026#39;ユーザ名の更新を開始します。\u0026#39;); $users = $this-\u0026gt;withProgressBar(User::all(), function (User $user) { $user-\u0026gt;name = Str::before($user-\u0026gt;email, \u0026#39;@\u0026#39;); $user-\u0026gt;save(); usleep(10000); }); $this-\u0026gt;info(\u0026#39;\u0026#39;); // 改行用 $this-\u0026gt;info(\u0026#39;ユーザ名の更新が完了しました。（更新件数：\u0026#39; . $users-\u0026gt;count() . \u0026#39;）\u0026#39;); } ... コマンドを実行するとバーが表示され、User::all()の総数である１０００件の処理が完了すると100%となるようにループ毎にゲージが溜まっていきます。\n$ php artisan app:user-name-update ユーザ名の更新を開始します。 1000/1000 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100% ユーザ名の更新が完了しました。（更新件数：1000） 今回ダミーデータを1000件用意しましたが、私の環境では処理が一瞬で終了してしまい、プログレスバーのゲージが蓄積していく様子が確認できなかったのでusleep()を挟みループ毎に0.01秒スリープするようにしました。また、プログレスバー表示後に改行コードが含まれておらず、続く「ユーザ名の更新が完了しました。・・・」の表示がプログレスバーと同じ行に表示されてしまう為、$this-\u0026gt;info(\u0026rsquo;\u0026rsquo;)で改行を挟みました。\ncreateProgressBar() プログレスバーを表示するもう１つの方法としてcreateProgressBar()を使ってProgressBarインスタンスを生成する方法があります。こちらは前項のwithProgressBar()よりもバーを表示するタイミングやゲージを進めるタイミングを細かくコントロールできます。以下、UserNameUpdateコマンドに実装した例です。\napp/Console/Commands/UserNameUpdate.php ... public function handle() { $this-\u0026gt;info(\u0026#39;ユーザ名の更新を開始します。\u0026#39;); $users = User::all(); $bar = $this-\u0026gt;output-\u0026gt;createProgressBar($users-\u0026gt;count()); $bar-\u0026gt;start(); foreach ($users as $user) { $user-\u0026gt;name = Str::before($user-\u0026gt;email, \u0026#39;@\u0026#39;); $user-\u0026gt;save(); usleep(10000); $bar-\u0026gt;advance(); } $bar-\u0026gt;finish(); $this-\u0026gt;info(\u0026#39;\u0026#39;); $this-\u0026gt;info(\u0026#39;ユーザ名の更新が完了しました。（更新件数：\u0026#39; . $users-\u0026gt;count() . \u0026#39;）\u0026#39;); } ... 今回のUserNameUpdateコマンドの様に取得したデータに対してループでシンプルな処理を実行するパターンでは前項で紹介したwithProgressBar()の方がコードが簡潔ですね。ループ内で実行する処理が複雑でクロージャに渡す変数が多くなってしまう様なケースではcreateProgressBar()を使用するのが良いかもしれません。因みにですが、withProgressBar()はLaravel8から追加された関数の様です。Laravel7以前はcreateProgressBar()のみでした。\n","date":"2023-11-29T00:25:29+09:00","permalink":"https://www.larajapan.com/2023/11/29/%E3%82%B3%E3%83%9E%E3%83%B3%E3%83%89%E3%81%AB%E3%83%97%E3%83%AD%E3%82%B0%E3%83%AC%E3%82%B9%E3%83%90%E3%83%BC%E3%82%92%E8%BF%BD%E5%8A%A0%E3%81%99%E3%82%8B/","title":"コマンドにプログレスバーを追加する"},{"content":"Gmailは 2024 年 2 月以降、Gmail アカウントに 1 日あたり 5,000 件以上のメールを送信する送信者に対していくつかの義務付けを発表しました。その１つに、「受信者がメールの配信登録を容易に解除できるようにすること」とあります。毎日何十万というメルマガを送信する私のクライアントではメルマガの受信者の大半がGmailのメールアドレスを使用しています。メルマガはLaravelのプログラムから送信されます。対応しないと迷惑メールになりますよというGoogleの警告は恐ろしいですが、Laravelなら簡単に対応できます。\n※11/25：以下においてCSRFトークンに関して書き忘れがあったの追加しています。\nメールの配信登録を容易に解除できるとは？ Googleが発表したメール送信のガイドラインは以下で詳細が閲覧できます。 https://support.google.com/mail/answer/81126?sjid=16148365418482843036-NC\nサーバーでのSPFとDKIMの両方の対応はサーバーのシステムでの設定ですが、アプリ側では以下の要求があります。 そこで言及されているワンクリックとは、PCのブラウザで開いたウェブのGmailで、以下のように、受信メールの送信元の隣に表示される登録解除のリンクのことです。\nこれをクリックすると以下のようなポップアップの画面が表示され「登録解除」のボタンを押すと、送信者のサイトに遷移することなくメルマガの登録解除が実行されます。\nこれがどうして可能なのかなのですが、受信したメールのソースコードを見ると、\n先のガイドラインで指示したように、List-UnsubscribeとList-Unsubscribe-Postのヘッダーが含まれているからです。\nつまりそれらのヘッダーを含むメールを送信すれば、対応できるわけです。\nワンクリックのリンクを含むMailableを作成 もう１度、Googleが勧める追加すべきヘッダーを見てみましょう。\nList-Unsubscribe-Post: List-Unsubscribe=One-Click List-Unsubscribe: \u0026lt;https://solarmora.com/unsubscribe/example\u0026gt; 最初の方は固定のヘッダーですが、後者の方はリンクは購読解除のプログラムの作成が必要とされます。\nまず、このヘッダーを含むMailableを作成します。 以下のコードでは、 送信先のメールアドレスを暗号化して、List-Unsubscribeの登録解除のURLでtokenの引数とします。 実際には、暗号化にはメールアドレスだけでなく発行した日時やどのキャンペーンのメルマガかなどの他の情報と合わせて入れる必要があるかもしれません。\napp/Mail/Newsletter.php namespace App\\Mail; use Illuminate\\Bus\\Queueable; use Illuminate\\Mail\\Mailable; use Illuminate\\Mail\\Mailables\\Content; use Illuminate\\Mail\\Mailables\\Headers; use Illuminate\\Queue\\SerializesModels; use Illuminate\\Mail\\Mailables\\Envelope; class Newsletter extends Mailable { use Queueable, SerializesModels; /** * Create a new message instance. */ public function __construct(public string $email) { $this-\u0026gt;email = $email; } /** * Get the message headers. */ public function headers(): Headers { // 送信先のメールアドレスを暗号化する $url = route(\u0026#39;unsubscribe.store\u0026#39;,[\u0026#39;token\u0026#39; =\u0026gt; encrypt($this-\u0026gt;email)]); return new Headers( text: [ \u0026#39;List-Unsubscribe\u0026#39; =\u0026gt; \u0026#39;\u0026lt;\u0026#39;.$url.\u0026#39;\u0026gt;\u0026#39;, \u0026#39;List-Unsubscribe-Post\u0026#39; =\u0026gt;\u0026#39;List-Unsubscribe=One-Click\u0026#39;, ], ); } /** * Get the message envelope. */ public function envelope(): Envelope { return new Envelope( subject: \u0026#39;メルマガの配信です\u0026#39;, ); } /** * Get the message content definition. */ public function content(): Content { return new Content( view: \u0026#39;newsletter\u0026#39;, ); } } 上のMailableで使用されるブレードは以下です。\nresources/views/newsletter.blade.php \u0026lt;html\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h1\u0026gt;メルマガの配信です\u0026lt;/h1\u0026gt; \u0026lt;p\u0026gt;いつもメルマガを購読して頂いてありがとうございます。\u0026lt;/p\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; さてこれで、メルマガ登録解除のヘッダーを含むメールの送信が可能です。 tinkerを使い、メルマガを送信します。$emailの変数の値は自分のGmailのメールアドレスを使用してくださいね。\n\u0026gt; use App\\Mail\\Newsletter; \u0026gt; $email = \u0026#39;test@example.com\u0026#39;; = \u0026#34;test@example.com\u0026#34; \u0026gt; Mail::to($email)-\u0026gt;send(new Newsletter($email)); = Illuminate\\Mail\\SentMessage {#7251} さて、届いた部分をGmailで開くと、なんと「メールリスト登録解除」のリンクが表示されていません！\nしかし、ソースコードを見ると、必要なヘッダーはしっかり含まれています。\nこれではテストできませんね。ネットで調査したところ、メール送信元のサイトの評価と関係しているとかで、評価が良くないと逆に使用されたメールアドレスが正しいということでスパムをたくさん送信されるとかの理由らしいです。つまり開発環境などから送信したメールは評価がないので、リンクは表示されないのです。評価が良い私のクライアントからのメルマガでは、受信したメールに登録解除のリンクは表示されていました。\nしかしながら、どうテストしたらよいものか。\n登録解除のPOSTを受け取るコントローラ とりあえず、登録解除のPOSTを受け取るコントローラが必要です。以下に作成してみました。\napp/Http/Controllers/UnsubscribeController.php namespace App\\Http\\Controllers; use App\\Models\\User; use Illuminate\\Http\\Request; use Illuminate\\Support\\Facades\\Log; class UnsubscribeController extends Controller { /** * Store a newly created resource in storage. */ public function store(Request $request) { $email = decrypt($request-\u0026gt;token); //非暗号化してメールアドレスを取り出す // 実際はメールアドレスを将来メルマガに使用しないようなDB処理が必要なところ // ログファイルにメールアドレスを保存 Log::debug(\u0026#39;email: \u0026#39;.$email); } } POSTを受け取るのでこのデモではstore()だけにしています。\nそれを使うルートの方は、こんな感じ。\nroutes/web.php use App\\Http\\Controllers\\UnsubscribeController; use Illuminate\\Support\\Facades\\Route; Route::get(\u0026#39;/\u0026#39;, function () { return view(\u0026#39;welcome\u0026#39;); }); Route::resource(\u0026#39;unsubscribe\u0026#39;, UnsubscribeController::class)-\u0026gt;only(\u0026#39;store\u0026#39;); さらに、通常のCSRFトークンは意味がないので、以下で例外とします。\napp/Http/Middleware/VerifyCsrfToken.php namespace App\\Http\\Middleware; use Illuminate\\Foundation\\Http\\Middleware\\VerifyCsrfToken as Middleware; class VerifyCsrfToken extends Middleware { /** * The URIs that should be excluded from CSRF verification. * * @var array\u0026lt;int, string\u0026gt; */ protected $except = [ \u0026#39;unsubscribe\u0026#39;, ]; } さて、届いたメールに登録解除のリンクがなくて、どうこのPOSTを確認するかですが、これもtinkerで可能です。 ローカル環境でまずウェブサーバーを立ち上げます。\n$ php artisan serve INFO Server running on [http://127.0.0.1:8000]. tinkerで、以下のようにLaravelのHttp Clientを使用してPOSTを実行します。$tokenの値はさきほどのメールのソースコードから持ってきます。\n\u0026gt; $token=\u0026#39;eyJpdiI6IkxaaWhEeW5OSnl4OTZYWkJCMG5ZaEE9PSIsInZhbHVlIjoib2VCNFhBaWZCak1PUGZsN3Q2RFphTEZZc25TN2UwT29wOXZyMitrd0pPST0iLCJtYWMiOiJmODY4YjZmOGU1NzYxODg3NDJjZmRmM2JmMTA3YTdiMDQzNzQ4YTY1NTJmZmI0ZjZmZmIxMGY5NDNhMzExYTY5IiwidGFnIjoiIn0%3D\u0026#39;; \u0026gt; Http::post(\u0026#39;http://127.0.0.1:8000/unsubscribe?token=\u0026#39;.$token)-\u0026gt;body(); = \u0026#34;\u0026#34; 実行後にlaravel.logの中身を見ると、\n[2023-11-23 01:39:06] local.DEBUG: email: test@example.com しっかり、非暗号化してメールアドレスを取り出すことができました。\n最後に ワンクリックのメルマガ登録解除の簡単な対応を紹介しましたが、注意が必要なのは今回はPCのブラウザでGmailを開いたときの対応だけだということです。他のOutlookなどのメールクライアントには関係のないことです。また、スマホのGmailのアプリでも解除のリンクは表示されません。それゆえに、メールの内容にもメールの登録解除のリンクを入れて対応することが必要です。\n","date":"2023-11-23T12:10:48+09:00","permalink":"https://www.larajapan.com/2023/11/23/%E3%83%A1%E3%83%AB%E3%83%9E%E3%82%AC%E3%82%92%E3%83%AF%E3%83%B3%E3%82%AF%E3%83%AA%E3%83%83%E3%82%AF%E3%81%A7%E7%99%BB%E9%8C%B2%E8%A7%A3%E9%99%A4/","title":"メルマガをワンクリックで登録解除"},{"content":"先日携わっているプロジェクトにおいてとある不具合に遭遇しました。DBから取得したデータが一部欠損していたのです。調査したところデータを取得する際のクエリにおいてGROUP_CONCATが使用されており、そちらの上限をオーバーした事が原因でした。今回は直接Laravelと関係する訳ではありませんが、LAMP環境を運用する上で遭遇するかもしれない事象という事で備忘録としてまとめます。\nGROUP_CONCAT() GROUP_CONCATはmysqlの関数です。GROUP BYでまとめたデータをカンマ区切りなどで取得する際に使います。例えば、以下のようなfoodテーブルがあったとします。\n# テーブル作成クエリ CREATE TABLE `food` ( `id` int NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL DEFAULT \u0026#39;\u0026#39;, `category` varchar(255) NOT NULL DEFAULT \u0026#39;\u0026#39;, PRIMARY KEY (`id`) ); そして、そのテーブルに以下のようなデータが挿入されていたとします。\n# データ挿入クエリ INSERT INTO `food` (name, category) VALUES (\u0026#39;みかん\u0026#39;, \u0026#39;果物\u0026#39;), (\u0026#39;とまと\u0026#39;, \u0026#39;野菜\u0026#39;), (\u0026#39;玉ねぎ\u0026#39;, \u0026#39;野菜\u0026#39;), (\u0026#39;きゅうり\u0026#39;, \u0026#39;野菜\u0026#39;), (\u0026#39;りんご\u0026#39;, \u0026#39;果物\u0026#39;), (\u0026#39;ぶどう\u0026#39;, \u0026#39;果物\u0026#39;); GROUP_CONCATを使うと、カテゴリ毎に食べ物の名前をカンマ区切りでまとめて取得できます。以下のように。\nSELECT category, GROUP_CONCAT(name) FROM food GROUP BY category; \u0026gt;\u0026gt; +----------+----------------------+ | category | GROUP_CONCAT(name) | +----------+----------------------+ | 果物 | みかん,りんご,ぶどう | | 野菜 | とまと,玉ねぎ,きゅうり | +----------+----------------------+ 2 rows in set (0.02 sec) カンマ区切りの文字列なので、explode()などで配列に変換すれば使い勝手が良いですね。\nGROUP_CONCATの上限 既にお気づきかと思いますがGROUP BYでまとめたデータが多ければ多いほどGROUP_CONCATの結果も長くなります。前項の例で言えば、例えばカテゴリーが果物のレコードが1000件あったなら、1000件分の果物の名前をカンマ区切りで結合して文字列として取得する訳です。とは言え、そんな長い文字列を取得する訳にも行かないので、mysql側でgroup_concat_max_lenというシステム変数によって取得できる最大の長さが制限されています。group_concat_max_lenのデフォルトの設定値は1024バイトです。\nSHOW VARIABLES LIKE \u0026#39;group_concat_max_len\u0026#39;; \u0026gt;\u0026gt; +----------------------+-------+ | Variable_name | Value | +----------------------+-------+ | group_concat_max_len | 1024 | +----------------------+-------+ 1 row in set (0.04 sec) この制限値を超えた場合、超えた部分のデータは切り捨てられます。group_concat_max_lenの設定値を変えて、実際にこの挙動を確認してみましょう。\nまず、GROUP_CONCATの上限を20バイトに設定してみます。\nSET group_concat_max_len = 20; そして、前項で実行したクエリをもう一度実行してみましょう。\nSELECT category, GROUP_CONCAT(name) FROM food GROUP BY category; \u0026gt;\u0026gt; +----------+----------------------+ | category | GROUP_CONCAT(name) | +----------+----------------------+ | 果物 | みかん,りんご, | | 野菜 | とまと,玉ねぎ, | +----------+----------------------+ 2 rows in set, 2 warnings (0.00 sec) 果物はぶどうが、野菜はきゅうりのデータが欠損しています。そしてwarningが発生しています。\nSHOW WARNINGS; \u0026gt;\u0026gt; +---------+------+---------------------------------+ | Level | Code | Message | +---------+------+---------------------------------+ | Warning | 1260 | Row 3 was cut by GROUP_CONCAT() | | Warning | 1260 | Row 6 was cut by GROUP_CONCAT() | +---------+------+---------------------------------+ 2 rows in set (0.00 sec) 3行目と６行目のデータがGROUP_CONCATによって省かれた旨の警告でした。ここでいう3行目、６行目とはGROUP BYで処理される前の結果セットであると推察します。\n因みに文字列のバイト数を確認するにはLENGTH関数が使えます。\nSELECT LENGTH(\u0026#39;みかん,りんご,\u0026#39;); \u0026gt;\u0026gt; +--------------------------------+ | LENGTH(\u0026#39;みかん,りんご,\u0026#39;) | +--------------------------------+ | 20 | +--------------------------------+ 1 row in set (0.00 sec) 先ほど途中でカットされたデータのバイト数を確認すると丁度group_concat_max_lenに設定した値である20バイトとなっていますね。内訳は平仮名が１文字３バイト6文字、カンマが１バイト2文字で合計20バイトという訳です。この様にgroup_concat_max_lenで設定したバイト数以上の文字列を取得しようとした場合、制限を超えた分の文字列が欠損して取得されてしまうのです。そして、これがまさに今回私が遭遇した不具合の原因です。\nGROUP_CONCATの設定を変更する GROUP_CONCATの返却値の長さが決まっているなら、group_concat_max_lenを余裕を持たせた充分な値に設定する事で取得するデータの欠損を予防できます。アプリ内の特定の箇所にのみ設定値の引き上げが必要な場合はDB::statement()で一時的に設定すれば良さそうです。\n// デフォルトの1024 byteから２倍に引き上げ DB::statement(\u0026#39;SET group_concat_max_len = 2048;\u0026#39;) あるいはアプリ全体で上限を引き上げたいのであればmysqlの設定ファイル、my.cnfに設定を追加してあげればOKです。\nmy.cnf [mysqld] group_concat_max_len=2048 設定を反映させるにはmysqlの再起動が必要なのでお忘れなく。\nまとめ 原因が分かるとなんて事ない不具合ですが、直接的なエラーが発生しない為気付くのに少し時間が掛かりました。経験上GROUP_CONCATを使う頻度は多くありませんが、こういう事もあるということを頭の片隅においておきたいものです。\n","date":"2023-11-14T06:23:37+09:00","permalink":"https://www.larajapan.com/2023/11/14/group_concat%E3%81%AE%E4%B8%8A%E9%99%90/","title":"GROUP_CONCATの上限"},{"content":"前回の「特定の会員にリアルタイムでお知らせ」の記事は、ブラウザー内で表示するお知らせの話でした。つまり、ブラウザーを開いていないとお知らせは伝わりません。今回はブラウザーを開いてなくてもデスクトップ上でユーザーにお知らせします。\n欲しい機能 簡単なデモとして、tinkerで以下のように、登録しているユーザー（ここでは太郎さん）にHello!というメッセージをNotificationを使用して送信します。メッセージを日本語にしたいのですが、tinkerでは英語しか使えません。\ntinker \u0026gt; use App\\Notifications\\Toast; \u0026gt; $user = User::first(); \u0026gt; $user-\u0026gt;notify(new Toast(\u0026#39;Hello!\u0026#39;)); WindowsあるいはMacのデスクトップ上で右下にお知らせがスライドして登場します。\nお知らせをクリックすると、指定したサイトに飛ばすことも可能です。\nソースコード 今回の機能を追加したソースコードは、前回の会員チャットをもとにして違うgitブランチ（chat-notification-desktop）としました。\nレポジトリはこちらですが、すでにデモのソースコードをインストールしているなら、以下でスイッチできます。\n$ git fetch $ git checkout chat-notification-desktop を実行してブランチをゲットできます。\nchat-notificationとの差分は以下です。前回のブラウザーでのお知らせのToastのコンポーネントが廃止となり、後に説明するお知らせ許可のダイアログのコンポーネントが追加されています。\n$ git diff chat-notification --name-status M resources/js/chat.js D resources/js/components/ChatToast.vue A resources/js/components/ConfirmModal.vue M resources/views/chat.blade.php 前回と同様に以下のコマンドを実行すると、ブラウザでテストが可能です。\n$ npm run build $ php artisan serve お知らせ受信の許可 先のデモのように、ユーザーにお知らせするにはユーザーのデスクトップにおいて、ユーザーがブラウザーとサイトに許可（通知権限）を与える必要があります。サイトにアクセスするたびに毎回許可を問う必要はないです。１回許可（あるいは拒否）すればその後はその選択を尊重します。\nしかし、この通知許可はユーザーの操作での起動が必要（ブラウザにより異なる）となるので、こちらが作成したボタンやダイアログの表示で許可を問い、その後さらにブラウザの方でポップアックしたダイアログでさらに許可を問うというやや面倒な手順となっています。\nその部分のコードを見てみましょう。全体はこちらで見れます。\nまず、通知許可がすでに選択されているかどうかチェックします。\nchat.js ... createApp({ setup () { const messages = ref([]); const modalVisible = ref(false); ... // 通知権限が既に決定されているかどうかを調べる if (Notification.permission === \u0026#39;default\u0026#39;) { modalVisible.value = true; } Notificationはウェブの通知APIでたいていどのブラウザでも対応しています。このpermissionの値は、granted, denied, defaultの３つ文字列のどれかで、それぞれ、許可済み、拒否、未選択と意味です。未選択なら、以下のようなダイアログが表示されます。\nこのダイアログは、ConfirmDialogのコンポーネントでブレードでは以下のように使用されています。先ほどのmodalVisibleの値が更新されたゆえに、このダイアログが表示されます。\nchat.blade.php ... \u0026lt;confirm-modal :visible=\u0026#34;modalVisible\u0026#34; v-on:confirmed=\u0026#34;askPermission\u0026#34; question=\u0026#34;お知らせを受け取りますか？\u0026#34;\u0026gt; \u0026lt;/confirm-dialog\u0026gt; ... このダイアログが表示されて、ユーザーが「はい」をクリックしたら、askPermissionの以下のコードが実行され、ダイアログを閉じて今度はブラウザの許可選択のポップアップを表示します。\nchat.js ... // 通知権限許可のモーダルのダイアログで「はい」をクリックしたら、今度はブラウザの通知許可を表示 function askPermission() { modalVisible.value = false; Notification.requestPermission().then((permission) =\u0026gt; { console.log(permission); }); } ... こちらがそのポップアップです。\nここでユーザーが許可して初めて、こちらのプログラムからユーザーにデスクトップのお知らせを送信することが可能となります。\nお知らせの受信 お知らせを受け取り通知を作成する部分は前回と同様にLaravel Echoのnotificationを使用します。しかし、今度はウェブの通知APIで受け取ったメッセージを含めて通知を作成します。\nchat.js .... window.Echo.private(\u0026#39;App.Models.User.\u0026#39; + window.userId) .notification((n) =\u0026gt; { // 通知権限が既に付与されているなら、通知を作成 if (Notification.permission === \u0026#39;granted\u0026#39;) { const notification = new Notification( \u0026#39;ご連絡\u0026#39;, { icon: \u0026#39;https://upload.wikimedia.org/wikipedia/commons/thumb/9/9a/Laravel.svg/180px-Laravel.svg.png\u0026#39;, body: n.message } ); notification.onclick = () =\u0026gt; { window.open(\u0026#39;https://larajapan.com\u0026#39;); } } }); ... ","date":"2023-10-24T11:26:01+09:00","permalink":"https://www.larajapan.com/2023/10/24/%E7%89%B9%E5%AE%9A%E3%81%AE%E4%BC%9A%E5%93%A1%E3%81%AE%E3%83%87%E3%82%B9%E3%82%AF%E3%83%88%E3%83%83%E3%83%97%E3%81%AB%E3%81%8A%E7%9F%A5%E3%82%89%E3%81%9B/","title":"特定の会員のデスクトップにお知らせ"},{"content":"LaravelのMailableはメール送信に便利なクラスですが、テストでも同様にMailableをアサートするための便利なメソッドが用意されています。この記事では、Laravel10系の新しいMailableを使ったメール送信テストの書き方をご紹介します。\n新Mailableでのメール送信についてはこちらの記事をご覧ください。\nテスト対象 テスト対象は以下のクラスです。送信先のメールアドレスを受け取って送信するシンプルなものです。\nApp/Models/Message.php class Message extends Model { public static function sendMail($email) { Mail::to($email)-\u0026gt;send(new BuildMail); } } sendの引数に渡しているBuildMailがMailableを継承しているクラスとなり、以下のように定義しています。\nApp/Mail/BuildMail.php namespace App\\Mail; use Illuminate\\Bus\\Queueable; use Illuminate\\Contracts\\Queue\\ShouldQueue; use Illuminate\\Mail\\Mailable; use Illuminate\\Mail\\Mailables\\Attachment; use Illuminate\\Mail\\Mailables\\Content; use Illuminate\\Mail\\Mailables\\Envelope; use Illuminate\\Queue\\SerializesModels; class BuildMail extends Mailable { use Queueable, SerializesModels; /** * Create a new message instance. */ public function __construct() { // } /** * Get the message envelope. */ public function envelope(): Envelope { return new Envelope( subject: \u0026#39;Test Mail\u0026#39;, from: \u0026#39;from@example.com\u0026#39;, ); } /** * Get the message content definition. */ public function content(): Content { return new Content( html: \u0026#39;emails.test\u0026#39;, ); } /** * Get the attachments for the message. * * @return array\u0026lt;int, \\Illuminate\\Mail\\Mailables\\Attachment\u0026gt; */ public function attachments(): array { return [ Attachment::fromPath( storage_path(\u0026#39;app/img/testmail.jpg\u0026#39;) ), ]; } } メールの送信回数をテスト まず、メールが１回送信されたことをテストしてみましょう。テストコードは以下のようになります。\npublic function sendMailTest() { Mail::fake(); //テスト対象の関数を実行 Message::sendMail(\u0026#39;to@example.com\u0026#39;); Mail::assertSent(BuildMail::class, 1); } 最初にMail::fake()を使用しています。テストでは実際にメールを送信する必要はないので、メール送信をモック化し実際に送信されることを防いでいます。\nMail::assertSent()では、メールが送信されたかどうか、またその回数をアサートしています。第一引数がメール送信クラス、第二引数が期待する送信回数です。今回は１回送信されることを確認するので、１としています。\nこれでテストを実行すると、無事OKとなりました。\n% vendor/bin/phpunit --filter sendMailTest PHPUnit 10.4.1 by Sebastian Bergmann and contributors. . 1 / 1 (100%) Time: 00:00.447, Memory: 26.00 MB OK (1 test, 1 assertions) もし期待と異なりメールが２通送信された場合、以下のエラーが返ってきます。エラーメッセージにも送信数が違う旨が書いてあるので分かりやすいですね。\nThe expected [App\\Mail\\BuildMail] mailable was sent 2 times instead of 1 times. Failed asserting that 2 is identical to 1. 件名・宛先をテスト 件名や宛先など、より詳しく送信メールの情報をアサートしたい場合も同様にMail::assertSent()を使います。その際、以下のように第二引数のクロージャーでBuildMailクラスのインスタンスである$mail変数を受け取る必要があります。\nMail::assertSent(BuildMail::class, function ($mail) { return $mail-\u0026gt;hasTo(\u0026#39;to@example.com\u0026#39;) \u0026amp;\u0026amp; $mail-\u0026gt;hasFrom(\u0026#39;from@example.com\u0026#39;) \u0026amp;\u0026amp; $mail-\u0026gt;hasSubject(\u0026#39;Test Mail\u0026#39;); }); hasToで送信先のメールアドレスを、hasFromで送信元のメールアドレスを、またhasSubjectでメールの件名をそれぞれアサートしています。新・旧Mailableで関数名も同じですね。\nテストを実行してみると、こちらもOKが出ました。\n% vendor/bin/phpunit --filter sendMailTest PHPUnit 10.4.1 by Sebastian Bergmann and contributors. . 1 / 1 (100%) Time: 00:00.353, Memory: 26.00 MB OK (1 test, 1 assertions) このテストの際、新Mailableになって便利になったと感じたことがあります。\n旧Mailableでは、buildメソッドを使用しsubjectやfromを指定しているケースも少なくないと思います。その場合、テストコード内でもbuildの記述が必要でした。以下のようにです。\nMail::assertSent(BuildMail::class, function ($mail) { $mail-\u0026gt;build(); return $mail-\u0026gt;hasTo(\u0026#39;to@example.com\u0026#39;) \u0026amp;\u0026amp;.... }); ですが、buildを使用せず新Mailableで提供されるようになったenvelopeやcontentを使用していれば、特に何も気にしなくともアサートは成功します。これはテストを書く立場として少し嬉しいです。\n添付ファイルをテスト 次に、添付ファイルが期待通りか確認します。\nBuildMailの定義でも使用していたAttachmentクラスを使うので、useをお忘れなく。\n... use Illuminate\\Mail\\Mailables\\Attachment; ... public function sendMailTest() { ...//関数の実行など Mail::assertSent(BuildMail::class, function ($mail) { return $mail-\u0026gt;hasAttachment( Attachment::fromPath(storage_path(\u0026#39;app/img/testmail.jpg\u0026#39;)), ); }); } 複数のメールを確認 １回で複数件送信されるメールのアサートも簡単です。以下は、計３通のメールが送信されたか、その宛先がそれぞれ期待通りかをアサートする場合のテストコードです。\nMail::assertSent(BuildMail::class, 3); Mail::assertSent(BuildMail::class, function ($mail) { return $mail-\u0026gt;hasTo(\u0026#39;member1@example.com\u0026#39;); }); Mail::assertSent(BuildMail::class, function ($mail) { return $mail-\u0026gt;hasTo(\u0026#39;member2@example.com\u0026#39;); }); Mail::assertSent(BuildMail::class, function ($mail) { return $mail-\u0026gt;hasTo(\u0026#39;member3@example.com\u0026#39;); }); このように、それぞれ順番にアサートを記述するだけで大丈夫です。こちらも新・旧Mailableで特に変わりはありません。\n以上のように、新しいMailableでも旧とほとんど変わらない書き方でテストができます。どれもメールテストではよく使うものだと思いますので、ぜひご活用ください。\n","date":"2023-10-17T01:13:17+09:00","permalink":"https://www.larajapan.com/2023/10/17/laravel%E6%96%B0mailable%E3%81%AE%E3%83%A1%E3%83%BC%E3%83%AB%E9%80%81%E4%BF%A1%E3%83%A6%E3%83%8B%E3%83%83%E3%83%88%E3%83%86%E3%82%B9%E3%83%88/","title":"Laravel新Mailableのメール送信ユニットテスト"},{"content":"前回で作成したOpenAIチャットのデモはいろいろと改善点が盛りだくさんです。その１つは、OpenAIからのレスはテキストなのですがMarkdownの形式で来ます。今回はこの表示をHTMLに変換します。\nOpenAIからのレス OpenAIからのレスは以下のようなテキストできます。 2023年10月4日以降の日本の祝日は以下の通りです： - 体育の日（スポーツの日）：10月9日 - 文化の日：11月3日 - 勤労感謝の日：11月23日 12月から年末までの祭日はありません。 MarkdownのWikiを参照すると、 上で使用されているハイフン（‐）は、順序無しリストのアイテム、HTMLではのの使用ということになります。 しかし、これを無視すると以下のような表示（前回）となります。 無視しないでHTMLに変換して、 のように表示したいです。\nソースコード 今回の機能を追加したソースコードは、前回のOpenAIチャットをもとにして違うgitブランチ（chat-openai-markdown）としました。 レポジトリはこちらですが、すでにデモのソースコードをインストールしているなら、以下でスイッチできます。 $ git fetch $ git checkout chat-openai-markdown を実行してブランチをゲットできます。 前回のブランチのchat-openaiとの差分は以下です。\n$ git diff chat-openai --name-status M package-lock.json M package.json M resources/js/components/ChatMessages.vue MarkdownのテキストからHTMLの変換のために、今回はmarkdown-itのパッケージが必要です。それゆえに以下の実行が必要です。\n$ npm install 最後に、前回と同様に以下のコマンドを実行すると、ブラウザでテストが可能です。\n$ npm run build $ php artisan serve markdown-itをChatMessages.vueに取り込む 差分で見たように、今回コードの変更はChatmessages.vueのみです。サーバー側ではなくフロントエンドでHTML変換です。 以下のコードで変更の部分にコメントしました。非常に簡単です。 ChatMessages.vue ```js 私 {{ message.user.name }}さん {{ message.message }}\n// ここでHTMLに変換 // 以下は変換後のスタイル設定 以下は、コードのMarkdownをHTML変換した例です。 \u0026lt;a href=\u0026#34;markdown2.png\u0026#34;\u0026gt;\u0026lt;img src=\u0026#34;markdown2.png\u0026#34; alt=\u0026#34;\u0026#34; width=\u0026#34;932\u0026#34; height=\u0026#34;458\u0026#34; class=\u0026#34;alignnone size-full wp-image-8814\u0026#34; /\u0026gt;\u0026lt;/a\u0026gt; ","date":"2023-10-14T02:31:28+09:00","permalink":"https://www.larajapan.com/2023/10/14/openai%E3%81%AE%E3%83%AC%E3%82%B9%E3%82%92html%E8%A1%A8%E7%A4%BA/","title":"OpenAIのレスをHTML表示"},{"content":"OpenAIとのチャットを作成します。ChatGPTのように、ログインした会員がメッセージ（質問）を送信したら、Open AIが答えるというものです。メッセージはOpen AIのAPIを使用して送信しますが、AIのレスは時間がかかる（長いときは６０秒以上）ので、この部分を非同期の対応とします。非同期の対応にはいくつか方法がありますが、ここではシンプルにartisanのコマンドをバックグラウンドのプロセスとして実行します。そして、前回のNotificationを使って答えが来たらWebscoketを通して表示とします。\n欲しい機能 デモとして簡単にするために、今までのログインした会員の間でのチャットとは違い、１対１のチャット、ここではログインした会員（太郎くんが私）が質問者で、花子さんがAIとなります。 Open AIでアカウントを登録 Open AIのAPIを使うのでAPIのキーがまず必要となりますが、Open AIのサイトでアカウントの登録が必要です。Pusherの登録と違い、ステップが多いので辛抱を。基本的にアカウント情報（メールアドレス、パスワード）を入力して、Eメールをもらう。Eメールの確認リンクから、今度は名前と生年月日を入れて、さらに電話番号を入れてスマホでテキストコードを受け取り、それを入力して登録完了です。 まず、以下のリンクに行きます。\nhttps://openai.com\nLoginをクリックすると、\nSignupをクリックすると、\nさらに、\nパスワードの入力して完了すると、登録したメールアドレスに以下のようなメールが来ます。\nメール確認のボタンをクリックすると、今度は、\n名前と生年月日を入力すると（真ん中の組織名はオプションです）、\nここに電話番号を入力してボタンをクリックすると、あなたのスマホにショートメールでコードが送られます。それを正しく次の画面で入力するとやっと登録完了です。\nAPIのキーをゲット 次にAPIのキーの取得です。まず、以下へ行きます。 https://platform.openai.com/account/api-keys\nそこで「Create new secret key」のボタンをクリックすると、以下のダイアログが表示されます。\nそのダイアログで、キーのタイトルを入れて（オプションなので入れなくてもよい）、「Create secret key」のボタンを押すと、\nキーが作成されます。この値が後のプログラムの設定で必要となります。「Done」のボタンを押したらもうキーの値が見ることができないので必ずどこかにコピペで記録を。\nソースコード 今回の機能を追加したソースコードは、前回の会員チャットをもとにして違うgitブランチ（chat-openai）としました。 レポジトリはこちらですが、すでにデモのソースコードをインストールしているなら、以下でスイッチできます。\n$ git fetch $ git checkout chat-openai を実行してブランチをゲットできます。\nmasterとの差分は以下です。\n$ git diff master --name-status A app/Console/Commands/OpenAICommand.php D app/Events/MessageSent.php M app/Http/Controllers/ChatsController.php A app/Notifications/OpenAINotification.php M composer.json M composer.lock A config/openai.php M resources/js/chat.js M resources/views/layouts/app.blade.php M routes/channels.php 会員間のメッセージのやりとりはないので、MessageSentのイベントは削除しています。 また、以下のopenaiのphpのライブラリを使用するので、composer.jsonが更新されています。 https://github.com/openai-php/client\nそれゆえに、以下の実行が必要です。\n$ composer install そして、Open AIの設定のために、.envの編集も必要です。\n.env ... OPENAI_API_KEY= OPENAI_REQUEST_TIMEOUT=60 OPENAI_USER_ID=2 OPENAI_MODEL=gpt-4 OPENAI_API_KEYには上で取得したキーを入れてください。OPENAI_USER_IDは花子さんのusers.idです。OPEN_MODELは使用するOpen AIのモデルです。最新が、gpt-4ですがコストを考えるなら、gpt-3.5-turboをお勧めします。\n最後に、前回と同様に以下のコマンドを実行すると、ブラウザでテストが可能です。\n$ npm run build $ php artisan serve 必ずここを読む Open AIのアカウントの作成の際に、無料で５ドル分の３ヶ月有効なAPIの使用が可能となります（この記事を書いた時点ですがすでにアカウントを持っていた故に確認できず）。５ドルでも結構なテストはできます。しかし、コストは使用するモデル（gpt-3, gtp-4など）や送信と受信する文字数により変わります。実際は使用される言語のトークン数なので、事前にいくらとなるかの計算は簡単にできません。\n使用したコストは以下でチェックできます。 https://platform.openai.com/account/usage\n今回のデモは最新の質問だけをOpen AIに尋ねるという形式にしているので、ChatGPTのような複数のOpen AIとの対話とはなりません（以下のような）。APIではChatGPTのようにセッションを保つ機能がないのです。その代わりにこちらのプログラムで過去のすべての対話（メッセージ）とともに新たな質問（メッセージ）をAPIに送るということをする必要があります。そのコードもコメントしてOpenAICommand.phpのコードに入れてありますので、試すことも可能です。しかし、メッセージを作成するごとにトークン数が増えていくのでコストに注意をしてください。\n","date":"2023-10-10T00:22:02+09:00","permalink":"https://www.larajapan.com/2023/10/10/openai%E3%81%A8%E3%83%81%E3%83%A3%E3%83%83%E3%83%88/","title":"OpenAIとチャット"},{"content":"LaravelのNotificationはEメールやSMSなど多用なチャンネルへの情報発信に便利なものですが、その１つのチャンネルとしてbroadcastがあります。今回は、それを使用してチャットと同様にリアルタイムで情報をユーザーに発信します。\n欲しい機能 簡単なデモとして、tinkerで以下のように、登録しているユーザー（ここでは太郎さん）にHello!というメッセージをNotificationを使用して送信します。\ntinker \u0026gt; use App\\Notifications\\Toast; \u0026gt; $user = User::first(); \u0026gt; $user-\u0026gt;notify(new Toast(\u0026#39;Hello!\u0026#39;)); 太郎さんがログインしてチャット画面にいるとして、\nのように画面の右下隅にBootstrap５のToastがポップアップして情報を伝えます。\nソースコード 今回の機能を追加したソースコードは、前回の会員チャットをもとにして違うgitブランチ（chat-notification）としました。\nレポジトリはこちらですが、すでにデモのソースコードをインストールしているなら、以下でスイッチできます。\n$ git fetch $ git checkout chat-notification を実行してブランチをゲットできます。\nmasterとの差分は以下です。\n$ git diff master --name-status A app/Notifications/Toast.php M resources/js/bootstrap.js M resources/js/chat.js A resources/js/components/ChatToast.vue M resources/views/chat.blade.php M resources/views/layouts/app.blade.php M routes/channels.php 前回と同様に以下のコマンドを実行すると、ブラウザでテストが可能です。\n$ npm run build $ php artisan serve 送信側 特定のユーザーにNotificationを使用してEメールの送信の仕方は、過去にこちらで説明しました。\nWebsocketを使用して送信するには、以下のようにNotificationの定義においてbroadcastを指定します。\nToast.php namespace App\\Notifications; use Illuminate\\Bus\\Queueable; use Illuminate\\Notifications\\Notification; use Illuminate\\Notifications\\Messages\\BroadcastMessage; class Toast extends Notification { ... /** * Get the notification\u0026#39;s delivery channels. * * @return array\u0026lt;int, string\u0026gt; */ public function via(object $notifiable): array { return [\u0026#39;broadcast\u0026#39;]; } /** * Get the broadcastable representation of the notification. * * @param mixed $notifiable * @return BroadcastMessage */ public function toBroadcast($notifiable) { return new BroadcastMessage([ \u0026#39;name\u0026#39; =\u0026gt; $notifiable-\u0026gt;name, \u0026#39;message\u0026#39; =\u0026gt; $this-\u0026gt;message, ]); } ... そして送り先のユーザーのインスタンスのモデルでは、以下のようにNotifiableのトレイトがあることが必要です。\nnamespace App\\Models; // use Illuminate\\Contracts\\Auth\\MustVerifyEmail; use Illuminate\\Database\\Eloquent\\Factories\\HasFactory; use Illuminate\\Foundation\\Auth\\User as Authenticatable; use Illuminate\\Notifications\\Notifiable; use Laravel\\Sanctum\\HasApiTokens; class User extends Authenticatable { use HasApiTokens, HasFactory, Notifiable; ... 最終的に以下のようなコードで送信します。\nuse App\\Notifications\\Toast; $user = User::first(); $user-\u0026gt;notify(new Toast(\u0026#39;Hello!\u0026#39;)); ToastのNotificationはイベントとしてPusher側で受け取りクライアントに流します。 以下は、その直後のPusherのDebug Consoleでの画面です。Pusher側では、チャンネル名は private-App.Models.User.1となっていて、イベントは、Illuminate\\Notifications\\Events\\BroadcastNotificationCreatedとLaravelのイベントとなっていますね。\nクライアント クライアント側では、チャット同様にPusherからの情報を受け取るためのコードが必要となりますが、チャットのlistenではなく、notificationとなります。\nchat.js ... createApp({ setup () { const messages = ref([]); const toast = ref({}); fetchMessages(); window.Echo.private(\u0026#39;chat\u0026#39;) .listen(\u0026#39;MessageSent\u0026#39;, (e) =\u0026gt; { messages.value.push({ message: e.message.message, user: e.user }); }); window.Echo.private(\u0026#39;App.Models.User.\u0026#39; + window.userId) .notification((notification) =\u0026gt; { toast.value = notification; }); ... しかし、特定した以外のユーザーのサーバーからの情報が流れては困るので、チャット同様にこのためのプライベートチャンネル購読の認証が必要となります。プライベートチャンネル名は、App.Models.User.ユーザーID と特定のユーザーのためなユニークなチャンネル名となります。\nクライアントにおいてそのユーザーのIDをサーバーから取得するために、以下のようなレイアウトのテンプレートに工夫が必要となります。\napp.blade.php ... @auth \u0026lt;script\u0026gt; window.userId = {{ auth()-\u0026gt;user()-\u0026gt;id }}; \u0026lt;/script\u0026gt; @endauth \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; そして認証時には、以前にここで説明したように、サーバー側での認証となります。\nchannels.php use Illuminate\\Support\\Facades\\Auth; use Illuminate\\Support\\Facades\\Broadcast; /* |-------------------------------------------------------------------------- | Broadcast Channels |-------------------------------------------------------------------------- | | Here you may register all of the event broadcasting channels that your | application supports. The given channel authorization callbacks are | used to check if an authenticated user can listen to the channel. | */ // Notificationのプライベートチャンネルの認証 Broadcast::channel(\u0026#39;App.Models.User.{id}\u0026#39;, function ($user, $id) { return (int) $user-\u0026gt;id === (int) $id; }); // チャットのプライベートチャンネルの認証 Broadcast::channel(\u0026#39;chat\u0026#39;, function ($user) { return Auth::check(); }); 複数のユーザーにnotify Notificationは、以下のように複数のユーザーに一括しての情報送信も可能です。\n$users = User::all(); Notification::send($users, new Toast(\u0026#39;Hi!\u0026#39;)); しかし、以下のPusherのDebug Consoleで見られるようにユーザーの人数分だけウェブサーバーからPusherサーバーにイベント情報が流れます。それぞれで情報が異なる（今回の例では名前の情報が異なる）なら別ですが、まったく同じ情報を送信するなら効率的ではありません。そのようなときはチャットと同様にログインした会員すべてに対してのイベントとして送信した方がよいです。\n","date":"2023-10-04T23:08:18+09:00","permalink":"https://www.larajapan.com/2023/10/04/%E7%89%B9%E5%AE%9A%E3%81%AE%E4%BC%9A%E5%93%A1%E3%81%AB%E3%83%AA%E3%82%A2%E3%83%AB%E3%82%BF%E3%82%A4%E3%83%A0%E3%81%A7%E3%81%8A%E7%9F%A5%E3%82%89%E3%81%9B/","title":"特定の会員にリアルタイムでお知らせ"},{"content":"HTMLでデザインされたメールは読みやすく、視覚的に訴求しやすいというメリットがあります。ですが、多様なメーラーに対応するため通常のwebページとは異なるコーディングが必要であり、レスポンシブも考慮すると１通のメールを作成するのも大変です。\nそこで、簡単にレスポンシブなHTMLメールが作成できるMJMLというツールをLaravel10.xのプロジェクトで試してみました。\nMJMLをプロジェクトにインストール ベースとなるメールクラスは前回作成したものを使用し、MJMLを使えるよう修正を加えてゆきます。\nLaravel用にパッケージが用意されていますので、まずはこちらをインストールします。\n$ composer require asahasrabuddhe/laravel-mjml 次に、フロントエンド用のライブラリをインストールします。\nnpm install --save mjml ここまででインストールは完了です。\nビューファイル作成 次に、メールの本文となるビューファイルを作成します。\nMJMLではmj-bodyやmj-columnなどの独自タグを使って画面を組み立てます。テンプレートやドキュメントにたくさん使用例が載っていますので、ご参照ください。オンラインエディタではリアルタイムに表示を確認できるので便利です。\n今回はシンプルに、ナビゲーションとカラムだけのメールを作成しました。先にPCビューの完成形をお見せします。\nこのHTMLメールをMJMLで記述したものが、以下となります。\nresources/views/emails/mjml.blade.php \u0026lt;mjml\u0026gt; \u0026lt;mj-head\u0026gt; \u0026lt;mj-attributes\u0026gt; \u0026lt;mj-all font-family=\u0026#34;Ubuntu, Helvetica, Arial, sans-serif\u0026#34; /\u0026gt; \u0026lt;mj-class name=\u0026#34;nav-item\u0026#34; font-size=\u0026#34;14px\u0026#34; color=\u0026#34;#fff\u0026#34; /\u0026gt; \u0026lt;mj-class name=\u0026#34;column\u0026#34; font-size=\u0026#34;16px\u0026#34; color=\u0026#34;#333\u0026#34; align=\u0026#34;center\u0026#34; padding=\u0026#34;25px\u0026#34; font-weight=\u0026#34;bold\u0026#34; /\u0026gt; \u0026lt;/mj-attributes\u0026gt; \u0026lt;/mj-head\u0026gt; \u0026lt;mj-body background-color=\u0026#34;#FFC3BA\u0026#34;\u0026gt; \u0026lt;!-- header navigation --\u0026gt; \u0026lt;mj-section background-color=\u0026#34;#ef6451\u0026#34;\u0026gt; \u0026lt;mj-column\u0026gt; \u0026lt;mj-navbar base-url=\u0026#34;https://example.com\u0026#34; hamburger=\u0026#34;hamburger\u0026#34; ico-color=\u0026#34;#ffffff\u0026#34;\u0026gt; \u0026lt;mj-navbar-link mj-class=\u0026#34;nav-item\u0026#34; href=\u0026#34;/page1\u0026#34;\u0026gt;メニュー１\u0026lt;/mj-navbar-link\u0026gt; \u0026lt;mj-navbar-link mj-class=\u0026#34;nav-item\u0026#34; href=\u0026#34;/page2\u0026#34;\u0026gt;メニュー２\u0026lt;/mj-navbar-link\u0026gt; \u0026lt;mj-navbar-link mj-class=\u0026#34;nav-item\u0026#34; href=\u0026#34;/page3\u0026#34;\u0026gt;メニュー３\u0026lt;/mj-navbar-link\u0026gt; \u0026lt;mj-navbar-link mj-class=\u0026#34;nav-item\u0026#34; href=\u0026#34;/page4\u0026#34;\u0026gt;メニュー４\u0026lt;/mj-navbar-link\u0026gt; \u0026lt;/mj-navbar\u0026gt; \u0026lt;/mj-column\u0026gt; \u0026lt;/mj-section\u0026gt; \u0026lt;!-- ３column section --\u0026gt; \u0026lt;mj-section background-color=\u0026#34;#FAD74D\u0026#34; padding-bottom=\u0026#34;15px\u0026#34;\u0026gt; \u0026lt;mj-column\u0026gt; \u0026lt;mj-text mj-class=\u0026#34;column\u0026#34;\u0026gt;カラムA\u0026lt;/mj-text\u0026gt; \u0026lt;/mj-column\u0026gt; \u0026lt;mj-column\u0026gt; \u0026lt;mj-text mj-class=\u0026#34;column\u0026#34;\u0026gt;カラムB\u0026lt;/mj-text\u0026gt; \u0026lt;/mj-column\u0026gt; \u0026lt;mj-column\u0026gt; \u0026lt;mj-text mj-class=\u0026#34;column\u0026#34;\u0026gt;カラムC\u0026lt;/mj-text\u0026gt; \u0026lt;/mj-column\u0026gt; \u0026lt;/mj-section\u0026gt; \u0026lt;/mj-body\u0026gt; \u0026lt;/mjml\u0026gt; 大きく、mj-headとmj-bodyで構成されています。\n背景色や文字色など、個別に指定したいCSSはインラインでの記述が可能です。ですが、全てのCSSをインラインで書くとコードが読みにくくなったり修正が大変になりますので、繰り返しになるものは mj-head \u0026gt; mj-attributes 内でまとめて指定しました。\nその際２つのタグを使用しています。mj-allは、全体のフォントと文字色の指定に。またmj-classでは、クラスとCSSの定義をしています。これはメニューリストのような複数あるアイテムに便利です。\nmj-classの記述を上のコードから抜粋すると、mj-head内ではnav-itemクラスを以下のように定義しています。\nmj-head内 \u0026lt;mj-class name=\u0026#34;nav-item\u0026#34; font-size=\u0026#34;14px\u0026#34;/\u0026gt; そしてこのnav-itemをbody内で使用するには、適用させたいタグにmj-classを指定すればOKです。以下のようになります。\nmj-body内 \u0026lt;mj-navbar-link mj-class=\u0026#34;nav-item\u0026#34; href=\u0026#34;/page1\u0026#34;\u0026gt;メニュー1\u0026lt;/mj-navbar-link\u0026gt; また本文の要素は、セクションごとに mj-section（セクション） \u0026gt; mj-column（列） \u0026gt; 中身の要素　という形に配置してゆきます。mj-columnの中は、メニューボタンにはmj-navbar、テキストにはmj-text、のように要素に応じたタグが用意されているのでこれらを使って画面を構成します。\nレスポンシブなメールを送信 最後に、Mailableの修正です。修正箇所は２つで、１つはuseするMailableを今回インストールしたパッケージのクラスへ変更。２つめは、contentの記述です。\n以下は、修正前のコードです。\napp/Mail/TestMail.php ... use Illuminate\\Mail\\Mailable; ... public function content(): Content { return new Content( view: \u0026#39;view.name\u0026#39;, ); } ... 修正後は、以下のようになります。mjml・buildMjmlViewといった新しいメソッドが使えるようになるのでそちらを使用します。\n... use Asahasrabuddhe\\LaravelMJML\\Mail\\Mailable; ... public function content(): Content { return new Content( view: $this-\u0026gt;mjml(\u0026#39;emails.mjml\u0026#39;)-\u0026gt;buildMjmlView()[\u0026#39;html\u0026#39;], ); } ... この記述だけで、先ほどのMJMLタグがHTMLに変換されます。早速Tinkerでメールを送信してみましょう。\n$ php artisan tinker Psy Shell v0.11.20 (PHP 8.1.17 — cli) by Justin Hileman \u0026gt; use App\\Mail\\TestMail; \u0026gt; Mail::to(\u0026#39;mjml-test@example.com\u0026#39;)-\u0026gt;send(new TestMail); = Illuminate\\Mail\\SentMessage {#6325} PCビューは先ほどと同じなので省略です。モバイル表示では、どうでしょうか。\nPCビューでは横並びだったナビゲーションはハンバーガーメニューに、３カラムは縦並びに変わりました。ちゃんと画面サイズに最適化された表示になっていますね。\nソースを見てみると、以下のようなHTMLに変換されています。全体はもっと長いので、MJMLを使うことでコードをだいぶ簡略化できることがわかります。 ","date":"2023-10-04T00:06:23+09:00","permalink":"https://www.larajapan.com/2023/10/04/laravel-x-mjml%E3%81%A7%E3%83%AC%E3%82%B9%E3%83%9D%E3%83%B3%E3%82%B7%E3%83%96%E3%83%A1%E3%83%BC%E3%83%AB%E3%82%92%E9%80%81%E4%BF%A1/","title":"Laravel × MJMLでレスポンシブメールを送信"},{"content":"当サイトでも以前ご紹介しているLaravelのMailableですが、Laravel9.xから新しくなったようです。今までの書き方と何が違うのか？試してみました。使用バージョンはLaravel10.19になります。\n以前のMailableの記事はこちら\nHTMLメールを送信 まずは、新しいMailableクラスを作成します。以下のコマンドでTestMail.phpというファイルが作成されます。\n$ php artisan make:mail TestMail 作成されたファイルを見てみると、旧Mailableにはあったbuildが無くなっています。代わりにenvelope、content、attachmentsというメソッドが用意されていますね。\napp/Mail/TestMail.php namespace App\\Mail; use Illuminate\\Bus\\Queueable; use Illuminate\\Contracts\\Queue\\ShouldQueue; use Illuminate\\Mail\\Mailable; use Illuminate\\Mail\\Mailables\\Content; use Illuminate\\Mail\\Mailables\\Envelope; use Illuminate\\Queue\\SerializesModels; class TestMail extends Mailable { use Queueable, SerializesModels; /** * Create a new message instance. */ public function __construct() { // } /** * Get the message envelope. */ public function envelope(): Envelope { return new Envelope( subject: \u0026#39;Test Mail\u0026#39;, ); } /** * Get the message content definition. */ public function content(): Content { return new Content( view: \u0026#39;view.name\u0026#39;, ); } /** * Get the attachments for the message. * * @return array\u0026lt;int, \\Illuminate\\Mail\\Mailables\\Attachment\u0026gt; */ public function attachments(): array { return []; } } ここから、今回必要な箇所を編集してゆきます。\n１つ目のenvelopeでは、件名・送信者・返信先に関する情報を設定できます。新しくAddressクラスが使えるようになっており、名前つきメールアドレスが作成できます。\nAddressを使用する際は、クラスをuseしてくださいね。\nuse Illuminate\\Mail\\Mailables\\Address; public function envelope(): Envelope { return new Envelope( subject: \u0026#39;Test Mail\u0026#39;, from: new Address(\u0026#39;from@example.com\u0026#39;, \u0026#39;テスト送信者\u0026#39;), ); } 次のcontentでは本文の設定を行います。今回はHTMLメールを送信するので、簡単なものですが以下のようなブレードを作成しました。\nresources/views/emails/test.blade.php \u0026lt;html\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h1\u0026gt;新しいMailableです\u0026lt;/h1\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; このブレードのパスを、contentに記述します。パスはresources/views以下の部分のみでOKです。\napp/Mail/TestMail.php public function content(): Content { return new Content( html: \u0026#39;emails.test\u0026#39;, ); } 最後のAttachmentは、添付ファイルについて定義します。\nAttachmentのfromPathメソッドに、添付ファイルへのパスを渡します。あらかじめstorage/app/img内にtestmail.jpgという画像ファイルを用意したので、そちらを記述しました。\nuse Illuminate\\Mail\\Mailables\\Attachment; public function attachments(): array { return [ Attachment::fromPath(storage_path(\u0026#39;app/img/testmail.jpg\u0026#39;)), ]; } Attachmentを使用する場合も、useが必要ですのでお忘れなく。\nこれで設定は完了です！さっそくtinkerでメールを送信します。\n$ php artisan tinker Psy Shell v0.11.20 (PHP 8.1.17 — cli) by Justin Hileman \u0026gt; use App\\Mail\\TestMail; \u0026gt; Mail::to(\u0026#39;test@example.com\u0026#39;)-\u0026gt;send(new TestMail); = Illuminate\\Mail\\SentMessage {#6331} 受信をメールを確認してみましょう。私のローカル環境では、Mailtrapに届くように設定しています。\n件名、From、本文、いずれも設定した通りになっていますね。画像ファイルも添付されています。\n今回は新しくなったMailableを試してみました。8.x系で使用していたbuildは新しいMailableでも引き続き使用可能ですが、設定項目がそれぞれの関数に分かれている新しいMailableの方が、より分かりやすくなったように感じます。\n次回は、Mailableで簡単にレスポンシブなHTMLメールが作成できる、MJMLというライブラリをご紹介します。\n","date":"2023-09-25T23:38:49+09:00","permalink":"https://www.larajapan.com/2023/09/25/laravel%E3%81%AE%E6%96%B0%E3%81%97%E3%81%84mailable%E3%81%A7html%E3%83%A1%E3%83%BC%E3%83%AB%E3%82%92%E9%80%81%E4%BF%A1/","title":"Laravelの新しいMailableでHTMLメールを送信"},{"content":"会員チャットデモに機能を追加します。今度は違う機能で、現在参加している会員リストをリアルタイムで表示します。会員がログインしたらその会員名が追加され、会員がログアウトしたら削除されます。これもグループチャットではよくある機能です。\n欲しい機能 私が太郎さんとしてログインすると、上の右の場所に、現在チャットに参加している会員名をダイナミックでリストします。すでに花子さんがチャット画面を閲覧しています。\nここで、花子さんがログアウトすると上のリストから自動的に削除されます。今回は、この機能をPusherのプレゼンスチャンネル（後に説明）を使用して作成します。\nソースコード 今回の機能を追加したソースコードは、前回の会員チャットをもとにして違うgitブランチ（chat-presence）としました。 レポジトリはこちらですが、すでにデモのソースコードをインストールしているなら、以下でスイッチできます。\n$ git fetch $ git checkout chat-presence を実行してブランチをゲットできます。\nmasterとの差分は以下の４つのファイルのみです。\n$ git diff master --name-status M app/Events/MessageSent.php M resources/js/chat.js M resources/views/chat.blade.php M routes/channels.php 前回と同様に以下のコマンドを実行すると、ブラウザでテストが可能です。\n$ npm run build $ php artisan serve サーバー側の変更 今回は、Pusherで使用するチャンネルの形態が変わるために、ブロードキャストするチャンネルがプライベートチャンネルからプレゼンスチャンネルに変わります。プレゼンスチャンネルは基本的にはプライベートチャンネルと同じですが、プライベートチャンネルと違って現時点誰が参加しているかを追跡することが可能です。\nその変更のために以下の２つファイルが編集されています。\n使用するチャンネルのクラスを、PrivateChannelからPresenceChannelに変えます。\nMessageSent.php ... public function broadcastOn() { // return new PrivateChannel(\u0026#39;chat\u0026#39;); return new PresenceChannel(\u0026#39;chat\u0026#39;); } ... プレゼンスチャンネルでは、認証の返し値は、プライベートチャンネルのOKならブーリアンはなく、認証された会員名を属性とした配列を返します。\nchannels.php ... Broadcast::channel(\u0026#39;chat\u0026#39;, function ($user) { // return Auth::check(); if (Auth::check()) { return [\u0026#39;name\u0026#39; =\u0026gt; $user-\u0026gt;name]; } return false; }); クライアント側の変更 クライアントのchat.jsでも、プライベートチャンネル（private）からプレゼンスチャンネル（join）への変更あります。 また、会員の出入りの追跡のために、here(), joining(), leaving()が新たに使用されています。メッセージの受信（listen）は変わりません。\nchat.js ... createApp({ setup () { const messages = ref([]), members = ref([]);　// 参加会員リスト fetchMessages(); window.Echo.join(\u0026#39;chat\u0026#39;) // privateからjoinに .here(users =\u0026gt; { // 現在のリスト members.value = users; }) .joining(user =\u0026gt; { // 参加した会員を追加 members.value.push(user); }) .leaving(user =\u0026gt; { // 抜けた会員を削除 members.value.splice(members.value.indexOf(user), 1); }) .listen(\u0026#39;MessageSent\u0026#39;, (e) =\u0026gt; { // こちらはそのまま使用可能 messages.value.push({ message: e.message.message, user: e.user }); }); ... return { members, // コンポーネントで利用できるように messages, fetchMessages, addMessage } } }) .component(\u0026#39;chat-messages\u0026#39;, ChatMessages) .component(\u0026#39;chat-form\u0026#39;, ChatForm) .mount(\u0026#39;#app\u0026#39;); ブレードでは以下の部分を追加しています。\n... \u0026lt;div class=\u0026#34;col-md-4\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;card\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;card-header\u0026#34;\u0026gt;参加会員\u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;card-body\u0026#34;\u0026gt; \u0026lt;ul\u0026gt; \u0026lt;li v-for=\u0026#34;member in members\u0026#34; v-text=\u0026#34;member.name\u0026#34;\u0026gt;\u0026lt;/li\u0026gt; \u0026lt;/ul\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; ... プレゼンスチャンネル PusherのDebugコンソールで、イベントのリストを見てみましょう。表示は新から旧への順番です。\nリストを下から見ていくと、まず花子さんがログインすると、pusher_internal:member_addedとしてPusherのイベントが作成され接続している他の会員にWebsocketで知らせます。ログアウトすると、今度はpusher_internal:member_removedのイベントが作成されてPusherのサーバーが他の会員へ知らせます。\n","date":"2023-09-22T06:58:32+09:00","permalink":"https://www.larajapan.com/2023/09/22/%E4%BC%9A%E5%93%A1%E3%83%81%E3%83%A3%E3%83%83%E3%83%88%E3%80%80%E7%8F%BE%E5%9C%A8%E5%8F%82%E5%8A%A0%E3%81%97%E3%81%A6%E3%81%84%E3%82%8B%E4%BC%9A%E5%93%A1%E3%83%AA%E3%82%B9%E3%83%88/","title":"会員チャット　現在参加している会員リスト"},{"content":"会員チャットデモに機能を追加します。自分以外の誰かがタイプし始めたら、それをリアルタイムで知らせる機能です。チャットなら必ずある機能です。※9/23日にコード修正変更あります！！\n欲しい機能 私は太郎さんとして会員ログインをしてチャット画面にいます。花子さんが、送信メッセージをタイプし始めると以下のように、「花子さんがタイプしています…」のメッセージが表示されます。\n花子さんがタイプしているときには上の「タイプしています…」は表示されますが、暫くタイプを休憩したら上の「タイプしています…」は消えます。その後花子さんはメッセージを送信するかもしれませんし、しないかもしれません。\nこれが欲しい機能です。\nPusherでの設定 プログラムを始める前に、PusherのアカウントにおいてEnable client eventsをオンとする必要あります。 今回は、このクライアントイベント（後に説明）の機能を使います。\nソースコード 今回の機能を追加したソースコードは、前回の会員チャットをもとにして違うgitブランチ（chat-whisper）としました。\nレポジトリはこちらですが、すでにデモのソースコードをインストールしているなら、以下でスイッチできます。\n$ git fetch $ git checkout chat-whisper を実行してブランチをゲットできます。\nmasterとの差分は以下の３つのファイルのみです。\n$ git diff master --name-status M resources/js/chat.js M resources/js/components/ChatForm.vue M resources/views/chat.blade.php 前回と同様に以下のコマンドを実行すると、ブラウザでテストが可能です。\n$ npm run build $ php artisan serve Whisper まずクライアントのchat.jsでの変更を見てみましょう（最後に9/23の修正のコードがあるので比べてみてください）。\nchat.js createApp({ setup () { const messages = ref([]), typer = ref(false), typing = ref(false); fetchMessages(); window.Echo.private(\u0026#39;chat\u0026#39;) .listen(\u0026#39;MessageSent\u0026#39;, (e) =\u0026gt; { messages.value.push({ message: e.message.message, user: e.user }); }) .listenForWhisper(\u0026#39;typing\u0026#39;, (e) =\u0026gt; { // タイプしている会員名を受信して、「〇〇さんがタイプしています」を表示 typer.value = e.typer; typing.value = true; // ２秒経過したら、「〇〇さんがタイプしています」のメッセージを非表示 setTimeout(() =\u0026gt; { typer.value = false; typing.value = false; }, 2000); }); ... function isTyping(e) { // タイプを開始して300ms経過したら、タイプした会員名を送信 // 300msごとにイベントを送信することで、Pusherへの送信の回数を減らしています // Pusherの制限は１秒に最高１０まで setTimeout(() =\u0026gt; { window.Echo.private(\u0026#39;chat\u0026#39;) .whisper(\u0026#39;typing\u0026#39;, { typer: e.user.name }); }, 300); } ... 前回チャットのソースコードと違うのは、プライベートチャンネルのchatのリスナーにおいて、自分宛てのメッセージのイベント（MessageEvent）だけでなく、listenForWhisper()の関数を用いて、typingのイベントも聞いていることです。そこでは、typingのイベントがあれば、画面に「〇〇さんがタイプしています」を表示します。\nまた、自身のキーボートのタイプのイベントをPusherのサーバーに送るために、isTyping()の関数の定義も追加されています。この関数はキーボードのタイプのイベントにバインドされていて（後に説明）、タイプを開始するとPusherのサーバーにタイプしている会員名を含んでtypingイベントとして送ります。\n以下は、Pusher側でのDebug Consoleのログですが、client-typingのイベントをたくさん受信しているのがわかります。花子さんがタイプしていることがわかりますね。イベント名の、client-typingのclient-はLaravel Echoがプリフィックスしています。\n掘り下げてvuejsの世界へ chat.jsのisTypingでタイプしているイベントをPusherのサーバーに送っているのはわかりました。さて、その関数はどのように使われているか、掘り下げて見てみます。今度は、vuejsの世界です。\nまず、以下のブレードで先のisTypingの関数がchat-formのコンポーネントのistypingというvueのカスタム定義のイベントにバインドされています。\nchat.blade.php ... \u0026lt;chat-form v-on:messagesent=\u0026#34;addMessage\u0026#34; v-on:istyping=\u0026#34;isTyping\u0026#34; :user=\u0026#34;{{ auth()-\u0026gt;user() }}\u0026#34;\u0026gt;\u0026lt;/chat-form\u0026gt; ... そして、chat-formのコンポーネントのソースChatForm.vueを見ると、input文内で@keydown = \u0026ldquo;isTyping\u0026rdquo;でキーボードをタイプするイベント（keydown）とバインドしています。ややこしいですが、そこでのisTypingは先のとは違いコンポーネント内で定義の関数で、タイプしている会員オブジェクトをデータとしてistypingのイベントとして送信しています。ちなみに、@keydown.enter=\u0026ldquo;sendMessage\u0026rdquo;の方は、メッセージをタイプした後に送信するためにEnterキーを押したときのイベントです。\nChatForm.vue ... function isTyping(e) { emit(\u0026#34;istyping\u0026#34;, { user: props.user }); } \u0026lt;/script\u0026gt; \u0026lt;template\u0026gt; \u0026lt;div class=\u0026#34;input-group\u0026#34;\u0026gt; \u0026lt;input type=\u0026#34;text\u0026#34; class=\u0026#34;form-control\u0026#34; placeholder=\u0026#34;メッセージをタイプしてください...\u0026#34; v-model=\u0026#34;newMessage\u0026#34; @keydown = \u0026#34;isTyping\u0026#34; @keydown.enter=\u0026#34;sendMessage\u0026#34; /\u0026gt; ... \u0026lt;/template\u0026gt; クライアントイベント 今回は、Pusherのクライアントイベントを使用した機能です。クライアントイベントでは、クライアント（太郎や花子のブラウザ）とPusherサーバーのみの間でのWebsocketの通信で、ウェブサーバーを通してLaravelのプログラムにアクセスする通信（Http）がまったくありません。 修正 読者からの指摘により、9/23日以前にアップしたコードにおいて、chat.js（上に掲載したコード）に問題あることがわかりました。以下のように修正されています。以前のコードでも動作には問題ないですが、以下２点の問題ありました。 送り側がタイプし続けると、受け取り側の「〇〇さんがタイプしています」の表示において、表示と非表示がちらつく受け取り側の問題。解決には、設定したタイマーをクリアーして非表示にするタイマーを再度設定します。そうすることでいったん表示したメッセージをキープします。上の旧コードではその処理がありませんでした。 送り側がタイプし続けると、Pusherへの送信のイベント数の制限（１秒に１０まで）を超える、送り側の問題。解決には、最初のタイプのイベントで１回Pusherに送信したら、その後300ms待ちます。そして、その間にはタイマーは作成しません。上の旧のコードではタイプする度にタイマーを作成していて送信回数の抑制とはなっておらず、Pusherでは上限を超えるイベントエラーとなっていました。 すでにコードをgithubからダウンロードしていたなら、ローカルのブランチを削除して再度ダウンロードをお願いします。\nchat.js createApp({ setup () { const messages = ref([]), typer = ref(false), typing = ref(false); fetchMessages(); let listenTimer = false; window.Echo.private(\u0026#39;chat\u0026#39;) .listen(\u0026#39;MessageSent\u0026#39;, (e) =\u0026gt; { messages.value.push({ message: e.message.message, user: e.user }); }) .listenForWhisper(\u0026#39;typing\u0026#39;, (e) =\u0026gt; { // タイプしている会員名を受信して、「〇〇さんがタイプしています」を表示 typer.value = e.typer; typing.value = true; // ２秒待つ前にタイプされたら、前回のタイマーをクリアーして // 表示・非表示の入れ替えを抑制します if (listenTimer) { clearTimeout(listenTimer); } // ２秒経過したら、表示を非表示に listenTimer = setTimeout(() =\u0026gt; { typer.value = false; typing.value = false; }, 2000); }); ... let whisperTimer = false; function isTyping(e) { if (whisperTimer === false) { // タイプ開始したらすぐにタイプした会員名送信して、300ms待ちます // その間のタイプのイベントは、上の条件文のためここのコードが実行されずに無視します // こうすることで、Pusherへの送信の回数を減らしています // Pusherの制限は１秒に最高１０まで window.Echo.private(\u0026#39;chat\u0026#39;).whisper(\u0026#39;typing\u0026#39;, { typer: e.user.name }); whisperTimer = setTimeout(() =\u0026gt; { whisperTimer = false; }, 300); } } ... ","date":"2023-09-20T01:27:24+09:00","permalink":"https://www.larajapan.com/2023/09/20/%E4%BC%9A%E5%93%A1%E3%83%81%E3%83%A3%E3%83%83%E3%83%88%E3%80%80%E3%80%87%E3%80%87%E3%81%95%E3%82%93%E3%81%8C%E3%82%BF%E3%82%A4%E3%83%97%E3%81%97%E3%81%A6%E3%81%84%E3%81%BE%E3%81%99/","title":"会員チャット　〇〇さんがタイプしています…"},{"content":"前回に引き続いての会員チャットの解説です。少々ややこしい、クライアントとPusherのサーバー間のWebsocketのプライベートチャンネルの購読認証の仕組みを説明します。\nデモの会員のチャットプログラムのインストール手順はこちらから。 会員チャットの解説（１）Websocket　はこちらから。\nプライベートチャンネルが必要なのは？ プライベートチャンネルがあるということは、誰でもアクセスできるパブリックチャンネルもあります。しかし、チャットは個人あるいはグループ間でのプライベートな通信ゆえに、セキュリティが重要となってきます。\n例えば、chatチャンネルにいきなり会員登録やログインをしていないユーザーがチャンネルでの通信内容を聞けたり、メッセージを送信できたら問題です。また、会員ログインをしていても、chatチャンネルでなく自分が属さない違うチャンネルの通信内容を聞けても困ります。\nそれゆえに、プライベートチャンネルでは、チャンネルを使用する前に、ある条件に適うユーザーのみが使用できる購読認証が必要となります。\nチャンネルの購読認証 理解のためにプライベートチャンネルの全体の流れをシーケンス図としてみました。\nウェブサーバーはLaravelのプログラムを実行するアプリサーバーです。クライアントは太郎さんや花子さんのブラウザで、PusherがWebsocketのサーバーです。\n上の図では、接続のイベントがまず起こり、次にウェブサーバーを伴く購読のイベントが起こります。購読のイベントでは、認証にはウェブサーバーのHttpが使用されることに注意してください。\n接続（Websocket） 最初の図において、まずWebscoketの接続が、クライアントとPusherのサーバーの間で確立します。前回も書いたように、これはbootstrap.jsの以下でのLaravel Echoの初期化で行われます。\nbootstrap.js ... import Echo from \u0026#39;laravel-echo\u0026#39;; import Pusher from \u0026#39;pusher-js\u0026#39;; window.Pusher = Pusher; window.Echo = new Echo({ broadcaster: \u0026#39;pusher\u0026#39;, key: import.meta.env.VITE_PUSHER_APP_KEY, wsHost: import.meta.env.VITE_PUSHER_HOST ?? `ws-${import.meta.env.VITE_PUSHER_APP_CLUSTER}.pusher.com`, wsPort: import.meta.env.VITE_PUSHER_PORT ?? 80, wssPort: import.meta.env.VITE_PUSHER_PORT ?? 443, forceTLS: (import.meta.env.VITE_PUSHER_SCHEME ?? \u0026#39;https\u0026#39;) === \u0026#39;https\u0026#39;, enabledTransports: [\u0026#39;ws\u0026#39;, \u0026#39;wss\u0026#39;], cluster:import.meta.env.VITE_PUSHER_APP_CLUSTER, }); 設定したPusherのキーなどの情報に間違いがない限り、接続は問題なく確立します。\n購読の認証（Http） 特定のプライベートチャンネル購読（ここではchat）は、権限のないアクセスが起こらないように、アプリ（Laravelのプログラム）において認証が必要とされます。\n上のシーケンス図のように、まず、クライアントのchat.jsの以下の初期化から始まります。\nchat.js ... createApp({ setup () { const messages = ref([]); fetchMessages(); window.Echo.private(\u0026#39;chat\u0026#39;) .listen(\u0026#39;MessageSent\u0026#39;, (e) =\u0026gt; { messages.value.push({ message: e.message.message, user: e.user }); }); ... window.Echo.private(\u0026lsquo;chat\u0026rsquo;)では、Pusherのサーバーとプライベートチャンネルの購読が確立されていないなら、http://127.0.0.1:8000/broadcasting/authにPOSTします。POSTに送信する値は、接続で得たsocket_idの値と、channel_nameとしてここではprivate-chatです。\nさて、この/broadcasting/authのルートですが、通常のroutes/web.phpでは定義されていません。しかし、以下を実行すると、\n$ php artisan route:list とあります。Laravelが自動で挿入したルートのようです。\n会員チャットのデモでは、config/app.phpにおいて、デフォルトではコメントされているApp\\Providers\\BroadcastServiceProvider::classの行のコメントが解除されています。\nconfig/app.php ... \u0026#39;providers\u0026#39; =\u0026gt; ServiceProvider::defaultProviders()-\u0026gt;merge([ /* * Package Service Providers... */ /* * Application Service Providers... */ App\\Providers\\AppServiceProvider::class, App\\Providers\\AuthServiceProvider::class, App\\Providers\\BroadcastServiceProvider::class, //デフォルトではここがコメントされている App\\Providers\\EventServiceProvider::class, App\\Providers\\RouteServiceProvider::class, ])-\u0026gt;toArray(), ... コメントを解除した段階で先ほどのルートが作成されます。\nさて、このPOSTで何が起こるかというと、以下のroutes/channels.phpにアクセスして、現在のユーザー（ここではログインした会員）がプライベートチャンネルのchatにアクセスできるかどうか認証するのです。\nroutes/channes.php ... Broadcast::channel(\u0026#39;chat\u0026#39;, function ($user) { return Auth::check(); }); Auth::check()は会員がログインされているかどうかの関数で、会員がログインされているなら、trueをそうでないならfalseを返します。trueなら、認証されたトークンとしてのkeyを作成して、チャンネル名とともにWebsocketでPusherサーバーに購読リクエストとして送ります。\n今回は会員ログインがあればだれでも参加できるチャットですが、例えば、会員でも特定のグループに属さなければ、チャットができないとかのケースではこの認証の条件が違ってきます。\n購読（Websocket) ウェブサーバー（つまりLaravel）で購読の認証が完了したら、そこで発生したデータをPusherサーバーに送り、チャンネルの購読作業が完了します。完了したら、ウェブサーバーを介せずにクライアントとPusherサーバー間でのWebsocketの通信が開始ます。\n太郎さんがメッセージを送信したら、Pusherサーバーを通じて、chatチャンネルを認証購読されたユーザーのみに、メッセージが流れます。\nブラウザのインスペクター プライベートチャンネルの認証のための一連のイベントは、ブラウザ（ここではFirefoxを使用）のインスペクターで見ることができます。\nインスペクターのNetworkのタブで、HTML、XHR(ajax)、WS(websocket)をオンにして、http://127.0.0.1:8000/chatページにアクセスします。\nまず、認証のためのauthへのPOSTですが、レスとしてauthの値が返されていることがわかります。値はコロンで分割されていて最初はPusherで取得したkeyの値で後ろが生成されたsignatureの値です。\nWebsocketのイベントは、以下のハイライトされている行でリアルタイムで見ることができます。先のPOSTのレスの値をPusherのサーバーに送信しているのがわかりますね。この行が、認証のPOSTより前にあるのはすでにPusherサーバーとの接続が先に起こっているからです。WebsocketのイベントはHttpのイベントと違い、将来に通信（例えばメッセージの受信）が起こってもこの行だけのレスにイベントが追加されます。\n以下では、Pusherのサーバーから購読確立成功（subscription_succeeded）が返ってきています。\n","date":"2023-09-06T04:29:56+09:00","permalink":"https://www.larajapan.com/2023/09/06/%E4%BC%9A%E5%93%A1%E3%83%81%E3%83%A3%E3%83%83%E3%83%88%E3%81%AE%E8%A7%A3%E8%AA%AC%EF%BC%88%EF%BC%92%EF%BC%89%E8%AA%8D%E8%A8%BC/","title":"会員チャットの解説（２）プライベートチャンネル"},{"content":"前回作成したWebsocketを使用した会員チャット。Laravelのお陰でプログラムはシンプルなのですが、裏で何か起こっているかいまひとつ。ということで、仕組みに関して私なりの理解で説明します。\nデモの会員のチャットプログラムのインストール手順はこちらから。\nHttpとWebsocketの違い まず、チャットで使用されている通信のプロトコルに関して。 Websocketは、Httpと同様にインターネットのアプリレイヤーのプロトコルの１つですが、以下のような違いがあります。\nHttpがリクエストとレスポンスで通信が完了するのと違い、Websocketは一度接続が確立するとクローズ（サーバーあるいはクライアントが）するまで双方向の通信が可能となり、とても高速の通信が可能となります。\nメッセージを送信すると 前回開発した会員チャットのデモにおいては、これらの２つの通信プロトコルがどう使用されているか見てみましょう。\nクライアントのブラウザからメッセージを送信したときの流れの図を作成してみました。クライアント（ブラウザー）とサーバー（ウェブサーバーとPusherのサーバー）を結ぶ線において、通信のプロトコルが異なることに注意してください。黒線はHttpで、赤線はWebSocketです。\nHttp まず、クライアントとウェブサーバーとPushサーバー間のHttp/Httpsの通信を見てみます。\n上の図では、チャットの画面（太郎のブラウザなど）で、メッセージを入力して「送信」ボタンをクリックすると、同じページ上に含まれるchat.jsの以下のaddMessage()が実行されます。\nchat.js ... createApp({ setup () { ... function addMessage(message) { messages.value.push(message); axios.post(\u0026#39;/messages\u0026#39;, message).then(response =\u0026gt; { // success }); } ... そこでは、入力した自分のメッセージを画面に反映させるとともに、ajaxで/messageにデータをPOSTします。 つまり、以下のChatController.phpのsendMessage()が実行されます。\nChatcontroller.php ... class ChatsController extends Controller { ... public function sendMessage(Request $request) { $user = Auth::user(); // DBにレコードを作成 $message = $user-\u0026gt;messages()-\u0026gt;create([ \u0026#39;message\u0026#39; =\u0026gt; $request-\u0026gt;message, ]); // Pusherにブロードキャストに必要な情報を送信 broadcast(new MessageSent($user, $message))-\u0026gt;toOthers(); return [\u0026#39;status\u0026#39; =\u0026gt; \u0026#39;Message Sent!\u0026#39;]; } ... 上のメソッドでは、受信したデータをDBに記録して、ヘルパーのbroadcast()をコールして、Pusherのサーバーにブロードキャストするデータを送ります。データは、MessageSentのイベントインスタンスとして渡し、toOthers()で、にブロードキャストの対象には自分が含まれないことを指定します。ここまででは、まだHttpのプロトコルを通信の枠内です。\nMessageSentイベントの定義を見てみましょう。\nMessageSent.php ... class MessageSent implements ShouldBroadcast { use Dispatchable, InteractsWithSockets, SerializesModels; public $user; public $message; /** * Create a new event instance. * * @return void */ public function __construct(User $user, Message $message) { $this-\u0026gt;user = $user; $this-\u0026gt;message = $message; } /** * Get the channels the event should broadcast on. * * @return \\Illuminate\\Broadcasting\\Channel|array */ public function broadcastOn() { return new PrivateChannel(\u0026#39;chat\u0026#39;); } } このイベントはLaravelで通常に使われるイベントと同じですが、ShouldBroadcastをインターフェースとしているところが違います。それゆえに、broadcastOn()のメソッドが定義されています。そのメソッドにおいて、chatという名前のプライベートチャンネルが指定されていることに注意してください。\nWebsocket 今度は、クライアントとPusherサーバー間のWebsocketの通信について見てみます。\nまず、クライアントとPusherサーバーのWebsocketの接続は、以下のbootstrap.jsのコードのnew Echo()の初期化の実行で確立します。\nbootstrap.js ... import Echo from \u0026#39;laravel-echo\u0026#39;; import Pusher from \u0026#39;pusher-js\u0026#39;; window.Pusher = Pusher; window.Echo = new Echo({ broadcaster: \u0026#39;pusher\u0026#39;, key: import.meta.env.VITE_PUSHER_APP_KEY, wsHost: import.meta.env.VITE_PUSHER_HOST ?? `ws-${import.meta.env.VITE_PUSHER_APP_CLUSTER}.pusher.com`, wsPort: import.meta.env.VITE_PUSHER_PORT ?? 80, wssPort: import.meta.env.VITE_PUSHER_PORT ?? 443, forceTLS: (import.meta.env.VITE_PUSHER_SCHEME ?? \u0026#39;https\u0026#39;) === \u0026#39;https\u0026#39;, enabledTransports: [\u0026#39;ws\u0026#39;, \u0026#39;wss\u0026#39;], cluster:import.meta.env.VITE_PUSHER_APP_CLUSTER, }); 次に、chat.jsの以下でvueのアプリが初期化され、setup()がコールされます。そこでは、window.Echo.private(\u0026lsquo;chat\u0026rsquo;).listenにおいて、\nクライアントとPusherサーバーでchatチャンネルの購読を確立する（最初の１回のみ） そのチャンネルにおいて、PushサーバーからMessageSentイベントを受信したらクライアントの画面に表示する 作業を行います。ここで使用するチャンネル名（chat）とイベント名（MessageSent）は、Http側の通信で使用されているものと同じであることが必要です。 chat.js ... createApp({ setup () { const messages = ref([]); fetchMessages(); window.Echo.private(\u0026#39;chat\u0026#39;) .listen(\u0026#39;MessageSent\u0026#39;, (e) =\u0026gt; { messages.value.push({ message: e.message.message, user: e.user }); }); ... 例えば、花子さんのクライアントでPusherのサーバーとチャンネルの購読が確立すると、太郎さんが送信したメッセージ（つまり、MessageSentイベント）は、PusherサーバーからWebsocketを通じて送られてきます。そして、それがMessageSentイベントなら、花子さんのブラウザにリアルタイムで太郎さんのメッセージが表示されます。\nPusherのデバッグ画面 Pusherのサイトの管理画面では、上で説明したWebsocketの一連のイベントを、以下のDebug Consoleのメニューにおいてすべて閲覧することができます。\nそこでは、太郎さんの接続と購読、花子さんの接続と購読、そして太郎さんが送信したメッセージの受信などのWebsocketイベントが最新なものからリスト表示されます。ちなみに、太郎さんが送信してPusherが受信したイベントでは、チャンネル名が、private-chatと表示されていますが、これはLaravel Echoが、MessageSentで指定したchatがプライベートチャンネルゆえに、private-をプレフィックスしているからです。\n次回は HttpとWebsocketを使用したチャットの仕組みを説明をしましたが、次回はクライアントとPusherサーバー間でのWebsocketでのセキュリティ、つまりプライベートチャンネルの仕組みを説明する予定です。","date":"2023-09-04T04:50:28+09:00","permalink":"https://www.larajapan.com/2023/09/04/%E4%BC%9A%E5%93%A1%E3%83%81%E3%83%A3%E3%83%83%E3%83%88%E3%81%AE%E8%A7%A3%E8%AA%AC/","title":"会員チャットの解説（１）Websocket"},{"content":"WebSocketを使用したリアルタイムのアプリケーションを作りたいのだけれど、WebSocketのサーバーの設定や管理は面倒、という方にお勧めが、Pusherのサービスです。無料プランで、１日200Kまでのメッセージ、100人までの同時接続が可能という寛大さ。ちょっとした小さいサイトなら無料プランで十分かもしれません。今回はそのサービスの紹介とともに、会員チャットをLaravel 10xとVue 3で作成します。\n会員チャットのデモ まず、完成品を見てみましょう。 上は、「太郎さん」がログインして、同じサイトにログインしている「花子さん」とチャットしているところです。「私」というのがログインしている「太郎さん」。\n逆に「花子さん」でログインすると、「私」は花子さんです。\nもちろん、「太郎さん」と「花子さん」だけでなく、他にも会員が登録していれば、すべての会員とチャットが可能です。 デモということで画面はシンプルですが、デザインを工夫すればよりチャットらしいもの変えることができます。\nPusherで登録してキーをゲット チャットのデモをインストールを説明する前に、PusherのWebsocketのサーバーを使用するために以下の作業が必要です。英語の画面のみなので、画像で説明します。 まず、以下のリンクに行きます。 https://pusher.com/\nそこから、登録（Sign Up）の画面へ。 ここでメールアドレスとパスワードを入力して登録するか、すでにGoogleやGithubのアカウントがあるなら、それらからも接続で登録可能です。\n登録すると、以下のように画面が変わり、 先に使用したメールアドレスへ、以下の確認のメールが送信されます。\n確認メールのリンクをクリックすると以下の画面へ飛びます。\nそこにおいて、「Get Started]をクリックすると、以下のポップアップが表示され、App（アプリ）の作成となります。 そこでは、Appの名前とクラスターのデータセンターの指定が必要です。他は選択しなくても構いません。「Create App」のボタンを押すと、\n以下の画面に変わり、この画面での情報が後に、.envでの編集において必要となります。\n会員チャットデモのレポジトリ PusherのサイトにはLaravelとVueを使用した親切なチュートリアルが掲載されています。しかし、Laravelは8x、VueはVue 2とやや古めということで、Laravel 10xと Vue 3で私が書き直したものをGithubのリポジトリとしました。\nまず、これをローカル環境にクローンします。\n$ git clone git@github.com:lotsofbytes/laravel-chat.git laravel-chat このレポジトリでは、Laravelの10xの新規のプロジェクトに、Laravel UIのVueバージョンをインストールして、その上にチャットに必要なプログラムの変更を行いました。gitのコミットには以下のように３つのコミットとしましたので、差分を見てもらえればどのファイルをチャットのために変更したのが分かります。\n840583a (HEAD -\u0026gt; master, origin/master) setup pusher 978ef3b install laravel/ui vue 62cad23 init $ git diff 978ef3b --name-status M README.md A app/Events/MessageSent.php A app/Http/Controllers/ChatsController.php A app/Models/Message.php M app/Models/User.php M composer.json M composer.lock M config/app.php A database/migrations/2021_09_08_142412_create_messages_table.php M database/seeders/DatabaseSeeder.php M package-lock.json M package.json M resources/js/app.js M resources/js/bootstrap.js A resources/js/chat.js A resources/js/components/ChatForm.vue A resources/js/components/ChatMessages.vue A resources/views/chat.blade.php M resources/views/home.blade.php M resources/views/layouts/app.blade.php M routes/channels.php M routes/web.php M vite.config.js インストールの手順 ローカル環境にレポジトリをクローンしたところで、まず必要なライブラリのインストールです。\n$ composer install さらに、フロントエンドのライブラリのために、\n$ npm install 次に、\n$ cp .env.example .env を実行して、.envを作成。\n$ php artisan key:generate で.envにおけるキーを作成します。 この時点でデータベースも作成してください。\n.envをエディタで以下の変数を編集します。\n# ローカル環境のDBの設定 DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=laravel_chat DB_USERNAME=root DB_PASSWORD= # 以下はpusherに設定 BROADCAST_DRIVER=pusher # Pusherのサイトから取得してきた値に設定（＊以下の値はデモ用で使用はできません） PUSHER_APP_ID=1657179 PUSHER_APP_KEY=dea19d8ae29b0823c91c PUSHER_APP_SECRET=1b4c2c87d40f99389e2f PUSHER_APP_CLUSTER=ap3 PUSHER_HOST= PUSHER_PORT=443 PUSHER_SCHEME=https 次にデータベースにDBテーブルを作成して、太郎さんと花子さんのユーザーを作成しておきます。\n$ php artisan migrate $ php artisan db:seed 今回はviteを使用したので、使用されているjsのコンパイルが必要です（レポジトリにはコンパイルされたファイルは含まれていません）。以下の実行で、DeprecationのWarningが出るかもしれませんが、気にせずに。\n$ npm run build public/build/assetsにコンパイルされたjsとcssのファイルが作成されます。\n最後にLaravelの簡易ウェブサーバーを立ち上げます。\n$ php artisan serve INFO Server running on [http://127.0.0.1:8000]. Press Ctrl+C to stop the server これで以下にアクセスして、ログインすると最初に紹介した画面となります。 http://127.0.0.1:8000/chat\n太郎さんのログインは、tarou@example.com、花子さんは、hanako@example.comです。 どちらもパスワードは、passwordです。\n異なる２つのブラウザ（例えば、ChromeとFirefoxとか）を立ち上げて、違うユーザーでログインして確認してみてください。\n最後に 会員チャットデモの解説は以下にあります。 会員チャットの解説（１）Websocket　はこちらから。 会員チャットの解説（２）プライベートチャンネル　はこちらから。\nそれから、以下が現時点での無料プラン（SandBox）プランの制限です。開発またはライブでばんばん使える上限であります。\n","date":"2023-09-01T07:23:00+09:00","permalink":"https://www.larajapan.com/2023/09/01/pusher%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E3%83%AA%E3%82%A2%E3%83%AB%E3%82%BF%E3%82%A4%E3%83%A0%E3%81%AE%E4%BC%9A%E5%93%A1%E3%83%81%E3%83%A3%E3%83%83%E3%83%88%E3%82%92%E4%BD%9C%E6%88%90/","title":"Pusherを使ってリアルタイムの会員チャットを作成"},{"content":"前回は、Mockeryを使用したテストの基本的な書き方について解説しました。今回はモックしにくいケースでどのようにテストを書くのか、overloadを使った方法をご紹介します。\n前回はこちらから閲覧できます。\nモック対象を new しているケース テスト対象クラスは以下のようになっています。外部APIを介してデータを取得しリストを返却、という部分は前回と同じですが、constructの部分は少し違います。\nclass TestApi { private $connection; public function __construct() { $this-\u0026gt;connection = new TestOAuth; } public function getList(string $param) { $list = $this-\u0026gt;connection-\u0026gt;get($param); return $list-\u0026gt;statuses; } } モック対象であるTestOAuthを、インジェクションではなくconstructの中でnewしています。これではTestOAuthのモックを作成しても、前回のように引数として渡すことができません。\noverloadを付けてモックを作成 そこで使用されるのがoverloadです。\nモック作成時、以下のようにoverloadプレフィックスを付けることでプロダクトコードを変更することなく対象をモックできます。\n\\Mockery::mock(\u0026#39;overload:\u0026#39; . TestOAuth::class); テストコード全体は以下のようになります。モックオブジェクト作成時にoverloadを使用している以外は、ほぼ前回と同じように記述しています。\npublic function getListTest() { //モックを作成 $mock = \\Mockery::mock(\u0026#39;overload:\u0026#39; . TestOAuth::class); $mock-\u0026gt;shouldReceive(\u0026#39;get\u0026#39;) -\u0026gt;once() -\u0026gt;with(\u0026#39;test\u0026#39;) -\u0026gt;andReturn((object) [ \u0026#39;statuses\u0026#39; =\u0026gt; [ (object)[\u0026#39;mockからのレスポンス１\u0026#39;], (object)[\u0026#39;mockからのレスポンス２\u0026#39;] ] ]); //テストここから $testApi = new TestApi(); $actual = $testApi-\u0026gt;getList(\u0026#39;test\u0026#39;); $this-\u0026gt;assertCount(2, $actual); } onceでメソッドが１度だけ呼ばれることを、withでメソッドに渡される引数が「test」であることを、期待値としてモックオブジェクトに設定しています。\nまたandReturnで、APIからのレスポンスが2つのオブジェクトを含む配列を持つstatusesプロパティが返るようにしました。\nこれでテストを実行すると、\n% vendor/bin/phpunit --filter getListTest PHPUnit 9.5.16 by Sebastian Bergmann and contributors. . 1 / 1 (100%) Time: 00:00.420, Memory: 22.00 MB 問題なく通りました。\nちなみに、overloadするだけで本当にモックが使用されているのか？と心配になるかと思いますので、モックに差し代わっているかどうかチェックしてみます。\nプロダクトコード内、モックからのレスポンスを受け取っている変数をダンプして中身を確認します。以下の箇所ですね。\n$list = $this-\u0026gt;connection-\u0026gt;get($param); テストを動かしながら$listの中身を確認すると、以下のようになりました。\n{#727 \u0026#34;statuses\u0026#34;: array:2 [ 0 =\u0026gt; {#328 \u0026#34;0\u0026#34;: \u0026#34;mockからのレスポンス１\u0026#34; } 1 =\u0026gt; {#333 \u0026#34;0\u0026#34;: \u0026#34;mockからのレスポンス２\u0026#34; } ] } ちゃんとモックオブジェクトに設定した値が返ってきています。\n実際の呼び出しがモックの期待値と異なるケース モックオブジェクトに設定した期待値と、実際の結果が異なった場合どのような挙動になるのでしょうか。エラーケースを見てみましょう。\nまずは、呼び出し回数が異なる場合です。今回のテストではgetメソッドが１回呼ばれることを期待していました。\nしかし実際にはgetが２回呼ばれたとします。テストを実行すると、以下のエラーとなりました。\nMockery\\Exception\\InvalidCountException: Method get(\u0026lt;Any Arguments\u0026gt;) from Abraham\\TestOAuth should be called exactly 1 times but called 2 times. エラーメッセージにも、２度呼ばれた旨が書いてあって分かりやすいですね。\n次は、引数が期待と異なった場合です。引数が「test」であることを期待していたところ、実際に使用された引数は「テスト」だったとします。\nテストを実行すると・・・\nMockery\\Exception\\NoMatchingExpectationException: No matching handler found for Abraham\\TestOAuth::get(\u0026#39;テスト\u0026#39;). Either the method was unexpected or its arguments matched no expected argument list for this method こちらも無事エラーとなり、エラーメッセージには引数がマッチしない旨の記述があります。\noverloadは対象クラスがすでにnewされている場合には使えないなど、少し制約はありますがモックが難しい場面ではとても頼りになるのでぜひ活用してみてくださいね。\n","date":"2023-08-26T04:03:53+09:00","permalink":"https://www.larajapan.com/2023/08/26/mockery-overload-%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%97%E3%81%A6%E3%83%A6%E3%83%8B%E3%83%83%E3%83%88%E3%83%86%E3%82%B9%E3%83%88/","title":"Mockery overload を使用してユニットテスト"},{"content":"webアプリにおいても時折、シェルスクリプトやLinuxコマンドなどを実行したい場合があります。そんな時、従来はexec()やsystem()を使用して実行していましたが、Laravel10からはProcessファサードが導入されLaravel的なインタフェースが用意された事でより直感的且つ読み易いコードが書けるようになりました。今回はProcessファサードの基本的な使い方を解説したいと思います。\n従来の方法 Processの解説に入る前に、従来の手法についておさらいしておきましょう。冒頭で述べたようにexec()やsystem()を使用するか、あるいはProcessファサードの核であるSymphonyのProcessコンポーネントを使用するなど、色々やり方があります。私の場合は主にexec()を使用していました。\nexec()は以下のように使います。\nexec(\u0026#39;実行するコマンド\u0026#39;, $output, $resultCode); 第二引数の$outputと第三引数の$resultCodeは参照渡しです。従って関数の実行後に$outputには実行したコマンドの出力、$resultCodeには終了ステータスが格納されます。試しにディレクトリ内のファイル一覧を取得するlsコマンドをtinkerで実行してみましょう。\nexec(\u0026#39;ls -1\u0026#39;, $output, $resultCode); // オプション-1はファイル名を１行ずつ表示するオプション $output \u0026gt;\u0026gt; [ \u0026#34;README.md\u0026#34;, \u0026#34;app\u0026#34;, \u0026#34;artisan\u0026#34;, \u0026#34;bootstrap\u0026#34;, \u0026#34;composer.json\u0026#34;, \u0026#34;composer.lock\u0026#34;, \u0026#34;config\u0026#34;, \u0026#34;database\u0026#34;, \u0026#34;lang\u0026#34;, \u0026#34;package.json\u0026#34;, \u0026#34;phpunit.xml\u0026#34;, \u0026#34;public\u0026#34;, \u0026#34;resources\u0026#34;, \u0026#34;routes\u0026#34;, \u0026#34;storage\u0026#34;, \u0026#34;tests\u0026#34;, \u0026#34;vendor\u0026#34;, \u0026#34;vite.config.js\u0026#34;, ] $resultCode \u0026gt;\u0026gt; 0 $outputは配列で各出力行が格納されます。$resultCodeには整数が格納されます、一般的なLinuxコマンドなら0で正常終了、0以外なら異常終了です。\n次にProcessファサードを使って実行する場合はどうなるのか？見てみましょう。\nProcessでコマンドを実行する Processでコマンドを実行するにはrun()かstart()を使用します。前者のrun()は同期実行、つまり、実行したコマンドが終了してから次の処理に移ります。一方、後者のstart()は非同期実行で実行したコマンドの完了を待たずにバックグラウンドで実行させたまますぐ次の処理に進むことができます。今回は前者のrun()についてのみ取り上げます。早速、Process::run()を使用して前項と同じコマンドを実行してみましょう。\n// tinkerでの実行 $result = Process::run(\u0026#39;ls -1\u0026#39;) Process::run()を実行するとProcessResultクラスのインスタンスが返却され、そのインスタンスメソッドを通してコマンドの実行結果について色々確認できます。\n// 実行したコマンドを確認 $result-\u0026gt;command(); \u0026gt;\u0026gt; \u0026#34;ls -1\u0026#34; // コマンド実行時の標準出力を取得 $result-\u0026gt;output(); \u0026gt;\u0026gt; \u0026#34;\u0026#34;\u0026#34; README.md\\n app\\n artisan\\n bootstrap\\n composer.json\\n composer.lock\\n ... \u0026#34;\u0026#34;\u0026#34; // 出力に特定の文字列が含まれているかチェック $result-\u0026gt;seeInOutput(\u0026#39;app\u0026#39;); \u0026gt;\u0026gt; true // コマンドの終了ステータス取得 $result-\u0026gt;exitCode(); \u0026gt;\u0026gt; 0 // コマンド実行成功？ ※終了ステータスが0ならtrue $result-\u0026gt;successful(); \u0026gt;\u0026gt; true // コマンド失敗？ ※終了ステータスが0以外ならtrue $result-\u0026gt;failed(); \u0026gt;\u0026gt; false exec()の場合は$outputで取得した出力が配列でしたが、Processではoutput()で取得した出力は文字列なので注意です。\nエラー出力 コマンドが異常終了したか否かは終了ステータスを確認すれば分かりますが、その原因を把握する為にはエラー出力を確認する必要があります。エラー出力を取得する場合、exec()では実行するコマンドの末尾に2\u0026gt;\u0026amp;1を追加してエラー出力を標準出力にリダイレクトさせる必要がありました。\n// lsコマンドに存在しないディレクトリaaaを引数で指定 exec(\u0026#39;ls aaa\u0026#39;, $output, $resultCode); // 2\u0026gt;\u0026amp;1無しだとエラー出力が$outputに格納されない $output \u0026gt;\u0026gt; [] // 2\u0026gt;\u0026amp;1付で実行 exec(\u0026#39;ls aaa 2\u0026gt;\u0026amp;1\u0026#39;, $output, $resultCode); // 今度は$outputに格納された $output \u0026gt;\u0026gt; [ \u0026#34;ls: aaa: No such file or directory\u0026#34;, ] Processの場合はコマンドに変更を加えなくてもerrorOutput()でエラー出力を取得できます。\nProcess::run(\u0026#39;ls aaa\u0026#39;)-\u0026gt;errorOutput(); \u0026gt;\u0026#34;ls: aaa: No such file or directory\\n\u0026#34; カレントディレクトリを指定して実行 特定のディレクトリにてコマンドを実行したい場合、exec()では先頭にcd {指定のディレクトリ};を付与して実行します。\n// 例. 親階層に移動してからpwdを実行 exec(\u0026#39;cd ../;pwd\u0026#39;, $output, $resultCode); Processの場合はpath()でコマンドを実行したいディレクトリを指定できます。\nProcess::path(\u0026#39;../\u0026#39;)-\u0026gt;run(\u0026#39;pwd\u0026#39;); パイプ処理 Linuxコマンドの場合、パイプ(|)でコマンド同士を繋げて実行すると、前のコマンドの出力を次のコマンドの入力として実行できます。例えば、アクセスログに特定のIPが記載されている行が何行あるか調べたい時、以下の様に実行します。\ngrep \u0026#39;192.168.101.1\u0026#39; access.log | wc -l \u0026gt;\u0026gt; 5 上記の実行ではgrepコマンドの出力をwcコマンドの入力として受け渡しています。上記の実行では５行ヒットしました。\nこちらをProcessで実行する場合はpipe()というメソッドを使います。\n$result = Process::pipe([ \u0026#34;grep \u0026#39;192.168.101.1\u0026#39; access.log\u0026#34;, \u0026#39;wc -l\u0026#39;, ]); // wc -l の実行結果には余白が含まれている $result-\u0026gt;output() \u0026gt;\u0026gt; \u0026#34; 5\\n\u0026#34; // trimすれば数値のみ取得できる（またはsedコマンドをpipeして整形しても良いかも） trim($result-\u0026gt;output()) \u0026gt;\u0026gt; \u0026#34;5\u0026#34; もちろん、run()の引数にパイプで結合したコマンドを指定しても同じ結果が得られます。\n$result = Process::run(\u0026#34;grep \u0026#39;192.168.101.1\u0026#39; access.log | wc -l\u0026#34;); trim($result-\u0026gt;output()); \u0026gt;\u0026gt; \u0026#34;5\u0026#34; しかし、プログラムから実行するコマンドは長文になりがち、かつ、要所要所で変数が埋め込まれがち（ファイルのパスなど）なので、pipe()を使った方がスッキリ書けると思います。\nまとめ Processの基本的な操作について従来のexec()を使った方法と比較しながら解説しました。まとめていて気が付いたのは、exec()を使用していた時はコマンドの末尾にエラー出力用に2\u0026gt;\u0026amp;1を付与したり、カレントディレクトリを指定する為に先頭にcdを付与したり、パイプ処理を実行する際はコマンドの途中にパイプ（|）を織り交ぜたり、とコマンド文がゴチャゴチャになりがちでした。一方でProcessではコマンドを汚さずに素の状態のまま保つ事ができるのでコードが読みやすく理解しやすいと思いました。\n","date":"2023-08-25T04:13:45+09:00","permalink":"https://www.larajapan.com/2023/08/25/process%E3%83%95%E3%82%A1%E3%82%B5%E3%83%BC%E3%83%89%E3%81%A7linux%E3%82%B3%E3%83%9E%E3%83%B3%E3%83%89%E3%82%92%E5%AE%9F%E8%A1%8C%E3%81%99%E3%82%8B/","title":"ProcessファサードでLinuxコマンドを実行する"},{"content":"コレクションの空をチェックする関数は、isEmpty()、しかし現実では「～が空でないとき」という条件文も良く使います。 もちろん、! で if (! $collection-\u0026gt;isEmpty()) とすることでも十分なのですが、なぜか if ($collection-\u0026gt;isNotEmpty()) の方が私にはわかりやすい。\n４年前に、「正しいのはisEmptyでした」という記事を書いたのですが、不思議にもこの記事はとても人気がある記事となっています。過去のトップ１０に入っています。今回はこの反対の isNotEmpty() の紹介です。\nまず、コレクションでは、\n\u0026gt; $users = User::all(); = Illuminate\\Database\\Eloquent\\Collection {#7175 all: [ App\\Models\\User {#7578 id: 1, name: \u0026#34;name\u0026#34;, email: \u0026#34;test@example.com\u0026#34;, email_verified_at: null, #password: \u0026#34;$2y$10$wIaPHmNNWNtd.MoaR.SQFeiUCk5gAbHylHM4i6TkIh17rbv8US046\u0026#34;, #remember_token: null, created_at: \u0026#34;2023-06-26 21:29:04\u0026#34;, updated_at: \u0026#34;2023-06-26 21:29:04\u0026#34;, }, ], } \u0026gt; if ($users-\u0026gt;isNotEmpty()) echo \u0026#39;ユーザーレコードが存在する\u0026#39;; ユーザーレコードが存在する 文字列のStrオブジェクトでは、\n\u0026gt; $str = Str::of(\u0026#39;空ではない\u0026#39;) = Illuminate\\Support\\Stringable {#7575 value: \u0026#34;空ではない\u0026#34;, } \u0026gt; $str-\u0026gt;isNotEmpty() = true ここで紹介した、HtmlStringのオブジェクトでも\n\u0026gt; use Illuminate\\Support\\HtmlString; \u0026gt; $html = new HtmlString(\u0026#39;\u0026lt;h1\u0026gt;空ではない\u0026lt;/h1\u0026gt;\u0026#39;); = Illuminate\\Support\\HtmlString {#7581 html: \u0026#34;\u0026lt;h1\u0026gt;空ではない\u0026lt;/h1\u0026gt;\u0026#34;, } \u0026gt; $html-\u0026gt;isNotEmpty() = true 最後に、エラーなど使用されるMessageBagでも、\n\u0026gt; use Illuminate\\Support\\MessageBag; \u0026gt; $errors = new MessageBag([\u0026#39;quantity\u0026#39;, \u0026#39;数字でありません\u0026#39;]); = Illuminate\\Support\\MessageBag {#6215} \u0026gt; $errors-\u0026gt;all(); = [ \u0026#34;quantity\u0026#34;, \u0026#34;数字でありません\u0026#34;, ] \u0026gt; $errors-\u0026gt;isNotEmpty() = true ","date":"2023-08-13T03:18:09+09:00","permalink":"https://www.larajapan.com/2023/08/13/isnotempty-%E7%A9%BA%E3%81%A7%E3%81%AF%E3%81%AA%E3%81%84/","title":"isNotEmpty 空ではない"},{"content":"2011年6月に最初のVersionがリリースされてから１２年。ついにLaravelも大台のVersion 10 を迎える事となりましたね。（パチパチパチ！！）という事でいつも通りinstallしてキャッチアップです。\nサポート サポートの予定については上の表の通りです。L8.xのリリース以降１年毎のバージョンアップとなっている為、次のL11.xのリリースが２０２４年初旬に控えていますね。明確な時期は未定の様ですが。前回のL9.xのインストール解説時にも触れましたが、L10.xからはPHP8.1以上が必須です、それ以前のバージョンを使用している方はアップグレードが必要となります。macOS環境の方は以前紹介したphpbrewを使えば複数のバージョンをinstallして切り替えて使えるので便利ですよ。\n新規プロジェクト作成 それではいつも通りプロジェクトを作成してみましょう。\ncomposer create-project laravel/laravel L10.x プロジェクトディレクトリへ移動し、インストールされたLaravelのバージョンを確認してみます。2023年8月1日現在、10.16.1がインストールされました。\ncd L10.x php artisan --version \u0026gt;\u0026gt; Laravel Framework 10.16.1 続いて、ビルドインサーバを立ち上げて見ます。\nphp artisan serv ブラウザで http://127.0.0.1:8000/にアクセスし、お馴染みの画面が表示されました。\nここまではL9.xから特に変わり無いようです。\nlangディレクトリ インストールしてみて気づいたのですが、プロジェクトのルートディレクトリからlangが無くなっています。langはL9.xでresources配下からルートディレクトリに移動したばかりです、今度はどこへ行ってしまったのでしょう？と、探していたらアップグレードガイドに記載がありました。\nhttps://laravel.com/docs/10.x/upgrade#language-directory\n新規プロジェクトを作成した初期状態ではlangディレクトリは作成されなくなったようです。従ってバリデーションエラーなどを日本語に変えたい場合などは\nphp artisan lang:publish \u0026gt;\u0026gt; INFO Language files published successfully. を実行してプロジェクトにlangディレクトリに追加する必要があります。\ntestの--profileオプション リリースノートを眺めていて使えるかも？と、思った新機能です。テスト実行時に\u0026ndash;profileオプションを付けると遅いテストを表示してくれるようです。試しに、SlowTestというテストクラスにて、sleep()を使用して時間が掛かるテストを追加して実行してみました。\n以下が追加したテスト\ntests/Unit/SlowTest.php \u0026lt;?php namespace Tests\\Unit; use PHPUnit\\Framework\\TestCase; class SlowTest extends TestCase { public function test_that_takes_3_seconds(): void { sleep(3); $this-\u0026gt;assertTrue(true); } /** * @dataProvider provider_test_that_takes_x_seconds */ public function test_that_takes_x_seconds($seconds): void { sleep($seconds); $this-\u0026gt;assertTrue(true); } public function provider_test_that_takes_x_seconds(): array { return [ \u0026#39;1sec\u0026#39; =\u0026gt; [1], \u0026#39;2sec\u0026#39; =\u0026gt; [2], \u0026#39;5sec\u0026#39; =\u0026gt; [5], ]; } } 以下が\u0026ndash;profileを付けて実行した結果です。\nphp artisan test --profile \u0026gt;\u0026gt; PASS Tests\\Unit\\ExampleTest ✓ that true is true PASS Tests\\Unit\\SlowTest ✓ that takes 3 seconds 3.01s ✓ that takes x seconds with data set \u0026#34;1sec\u0026#34; 1.00s ✓ that takes x seconds with data set \u0026#34;2sec\u0026#34; 2.01s ✓ that takes x seconds with data set \u0026#34;5sec\u0026#34; 5.00s PASS Tests\\Feature\\ExampleTest ✓ the application returns a successful response 0.15s Tests: 6 passed (6 assertions) Duration: 11.28s Top 10 slowest tests: Tests\\Unit\\SlowTest \u0026gt; that takes x seconds with data set \u0026#34;5sec\u0026#34; 5.00s Tests\\Unit\\SlowTest \u0026gt; that takes 3 seconds 3.01s Tests\\Unit\\SlowTest \u0026gt; that takes x seconds with data set \u0026#34;2sec\u0026#34; 2.01s Tests\\Unit\\SlowTest \u0026gt; that takes x seconds with data set \u0026#34;1sec\u0026#34; 1.00s Tests\\Feature\\ExampleTest \u0026gt; the application returns a successful response 0.15s Tests\\Unit\\ExampleTest \u0026gt; that true is true 0.00s ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── (99.17% of 11.28s) 11.19s なるほど、テスト実行結果の後に遅いテストランキングを表示してくれる訳ですね。dataProviderでデータセットを指定している場合は、使用したデータ毎に実行時間を計測しランキングに反映されます。止むを得ず時間が掛かってしまうテストはどうしようも無いですが、単純に色々詰め込みすぎて遅くなっているテストが見つかったなら、分けることが出来ないか検討する良い機会だと思います。肥大化したテストは管理が大変ですから。\nその他 リリースノートを眺めていて他に気になった新機能としてLaravel PennantとProcessファサードがあります。現時点では、Laravel Pennantはフラグ管理機能、Processファサードはshellコマンドなどが実行できるFacadeという認識です。いずれも従来は自作のヘルパ関数を作ったり、cmd()を使ったりと、Laravelishではない方法で対応してきました。新機能を使う事でどう変わるのか気になる処です。これらについては別の記事でコードを交えながら噛み砕いていきたいと思います。\n","date":"2023-08-04T19:22:15+09:00","permalink":"https://www.larajapan.com/2023/08/04/laravel-10-x%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB/","title":"Laravel 10.xのインストール"},{"content":"テスト対象が外部APIにアクセスしている場合、テストの度にAPIにアクセスするためテストに時間がかかる・APIが落ちているとテストも落ちる、といった不便さがあります。そこでモックを使用すると、APIを介すことなく対象のテストを行うことができて便利です。本記事では、Mockeryを使ったモックの作成方法・テストの書き方をご紹介します。\nテスト対象クラス 以下のTestApiがテスト対象です。外部APIを介してデータを取得し、リストを返却。\nなんらかのエラーでデータが取得できなかった場合にはfalseを返すというシンプルなものです。\nclass TestApi { private $connection; public function __construct(TestOAuth $testOAuth) // ←　TestAuthがモックの対象 { $this-\u0026gt;connection = $testOAuth; } public function getList(string $param) { $list = $this-\u0026gt;connection-\u0026gt;get($param); //データ取得できなかった場合 if (property_exists($list, \u0026#39;errors\u0026#39;)) { return false; } return $list-\u0026gt;statuses; } } また、モック対象は、TestOAuthです。TestApiのコンストラクタでインジェクションされていますね。\nMockeryでモックしてテスト：正常ケース テストコード全体は以下のようになります。\npublic function getListTest() { //モックを作成 $mock = \\Mockery::mock(TestOAuth::class); $mock-\u0026gt;shouldReceive(\u0026#39;get\u0026#39;) -\u0026gt;once() -\u0026gt;with(\u0026#39;正常test\u0026#39;) -\u0026gt;andReturn((object) [ \u0026#39;statuses\u0026#39; =\u0026gt; [ (object)[\u0026#39;data1\u0026#39;], (object)[\u0026#39;data2\u0026#39;] ] ]); //テストここから $testApi = new TestApi($mock); $actual = $testApi-\u0026gt;getList(\u0026#39;正常test\u0026#39;); $this-\u0026gt;assertCount(2, $actual); } モック部分を解説します。\n$mock = \\Mockery::mock(TestOAuth::class); まず最初に、mockメソッドでモックインスタンスを作成します。引数にはモック対象のTestOAuthを渡します。\n$mock-\u0026gt;shouldReceive(\u0026#39;get\u0026#39;) -\u0026gt;once() -\u0026gt;with(\u0026#39;正常test\u0026#39;) -\u0026gt;andReturn([ \u0026#39;statuses\u0026#39; =\u0026gt; [ (object)[\u0026#39;data1\u0026#39;], (object)[\u0026#39;data2\u0026#39;] ] ]); 次に、作成したモックインスタンスに対してgetメソッドが呼ばれた際の振る舞いを指定しています。\nonceは、メソッドが１度だけ呼ばれること。\nwithは、getメソッドに渡される引数が「正常test」であること。\nそして、最後のandReturnではAPIからのレスポンスにあたる部分で、このモックでは2つのオブジェクトを含む配列を持つstatusesプロパティが返るようにしています。\nまた、モックインスタンスは作成しただけではテストに反映されません。以下のようにコンストラクタに渡すことでTestApiのnew時にDIされ、実際のTestOAuthではなくモックが使用されるようになります。\n$testApi = new TestApi($mock); テストは以下のように、コマンドラインで実行します。\n$ vendor/bin/phpunit --filter getListTest PHPUnit 9.5.16 by Sebastian Bergmann and contributors. . 1 / 1 (100%) Time: 00:00.131, Memory: 22.00 MB 無事テストが通ったので、モックに設定したふるまい（getが１度呼ばれる・引数の中身）が正常であったことが確認できました。\nMockeryでモックしてテスト：エラーケース 次は、エラーケースのテストになります。テスト対象メソッドの以下にあたる箇所ですね。\n//データ取得できなかった場合 if (property_exists($list, \u0026#39;errors\u0026#39;)) { return false; } APIからのレスポンスにerrorsというプロパティが含まれている場合に、正しくfalseが返るかをテストします。\nそのため、モック作成部分では以下のように指定します。\n$mock = \\Mockery::mock(TestOAuth::class); $mock-\u0026gt;shouldReceive(\u0026#39;get\u0026#39;) -\u0026gt;once() -\u0026gt;with(\u0026#39;エラーtest\u0026#39;) -\u0026gt;andReturn([\u0026#39;errors\u0026#39; =\u0026gt; []]); モックオブジェクトの作成やgetが１度呼ばれるという箇所は同じですが、andReturnで返り値にerrorsプロパティを含むように記述しています。\nモック部分を含めたテストの全文は以下のようになります。\npublic function getListTest_error() { $mock = \\Mockery::mock(TestOAuth::class); $mock-\u0026gt;shouldReceive(\u0026#39;get\u0026#39;) -\u0026gt;once() -\u0026gt;with(\u0026#39;エラーtest\u0026#39;) -\u0026gt;andReturn([\u0026#39;errors\u0026#39; =\u0026gt; []]); $testApi = new TestApi($mock); $actual = $testApi-\u0026gt;getList(\u0026#39;エラーtest\u0026#39;); $this-\u0026gt;assertEquals(false, $actual); } これでテストを実行しOKであれば、呼ばれたモックオブジェクトの振る舞いが期待通りかに加え、正しくfalseが返っていることが確認できます。\n","date":"2023-08-01T06:11:58+09:00","permalink":"https://www.larajapan.com/2023/08/01/mockery%E3%81%A7%E5%A4%96%E9%83%A8api%E3%82%92%E3%83%A2%E3%83%83%E3%82%AF%E3%81%97%E3%81%A6%E3%83%A6%E3%83%8B%E3%83%83%E3%83%88%E3%83%86%E3%82%B9%E3%83%88/","title":"Mockeryで外部APIをモックしてユニットテスト"},{"content":"前回の記事にてタイムアウトとなった場合に２種類の例外がスローされる事を説明しました。クエリ実行中にタイムオーバーとなった場合は、QueryException。クエリ実行外でタイムオーバーとなり、その後クエリを実行してエラーとなる場合は、自作したTimeoutExceptionです。今回はこれらのエラーをどうキャッチしてハンドリングするのか解説します。\n検証用のページ 説明の為にテスト用のコントローラを用意しました。\napp/Http/Controllers/TimeoutTestController.php \u0026lt;?php namespace App\\Http\\Controllers; use App\\Services\\DbQueryTimeout; use Illuminate\\Support\\Facades\\DB; class TimeoutTestController extends Controller { public function index() { $timeout = (int) request(\u0026#39;timeout\u0026#39;); try { // 制限時間の指定があればセット if ($timeout) { DbQueryTimeout::set($timeout); } // 重いクエリ１ DB::statement(\u0026#39;select sleep(3) union select 1\u0026#39;); // 重いクエリ２ DB::statement(\u0026#39;select sleep(5) union select 1\u0026#39;); } catch (\\Exception $e) { return \u0026#39;error\u0026#39;; } return \u0026#39;success\u0026#39;; } } ルーティングは以下の通りです。\nroutes/web.php \u0026lt;?php use Illuminate\\Support\\Facades\\Route; use App\\Http\\Controllers\\TimeoutTestController; Route::get(\u0026#39;/timeout_test\u0026#39;, [TimeoutTestController::class, \u0026#39;index\u0026#39;]); indexアクションでは重いクエリが２つ実行されます。それらが正常に処理できた場合はsuccess、何かしらのエラーが発生した場合はerrorを返します。尚、GETパラメタでtimeoutの指定がある場合は、DbQueryTimeoutを使用して制限時間が設定されます。ビルドインサーバーを起動して挙動を確認してみましょう。\nphp artisan serv まず、パラメタ無しで /timeout_test へアクセスしてみてください。約8秒後にsuccessと表示されるはずです。次に、timeoutパラメタを指定して制限時間を設定してみましょう。/timeout_test?timeout=5 として制限時間を5秒にしてアクセスしてみて下さい。すると、約５秒後に処理が中断されerrorと表示されるはずです。\nTips: 重いクエリをシミュレートする方法 重いクエリ = 時間が掛かるクエリ、という事でDB側でsleep()を実行してシミュレートできます。mysqlではsleep()は正常にスリープできた場合は0、途中で中断された場合は1が返却されます。しかし、実際の環境ではmax_execution_timeで指定した時間をオーバーして処理が中断された場合は１ではなくエラーが発生します。sleep()においてもタイムオーバーした場合にエラーを返させるには、unionで追加の処理を実行すればOKです。追加の処理はなんでも良いので今回は適当にselect 1としました。実際にmysql clientにて以下を実行して試してみると理解しやすいと思います。\n# 制限時間を5秒に設定 mysql\u0026gt; set max_execution_time = 5000; Query OK, 0 rows affected (0.01 sec) # 制限時間内に終わるsleepなら0が返却される mysql\u0026gt; select sleep(4); +----------+ | sleep(4) | +----------+ | 0 | +----------+ 1 row in set (4.05 sec) # 制限時間内に終わらずsleepが中断されたら1が返却される mysql\u0026gt; select sleep(5); +----------+ | sleep(5) | +----------+ | 1 | +----------+ 1 row in set (5.06 sec) # unionで追加の処理を含めるとエラーが返却される mysql\u0026gt; select sleep(6) union select 1; ERROR 3024 (HY000): Query execution was interrupted, maximum statement execution time exceeded エラーのハンドリング 冒頭で述べた通り制限時間をオーバーした場合は２種類のタイムアウトエラーが発生します。しかし、アプリにおいては当然それ以外のエラーも発生し得るので、タイムアウトエラーとそうでないエラーの場合で判別してエラーを出し分ける必要があります。それぞれどの様に判別すれば良いでしょうか。\nTimeoutExceptionをキャッチ 先に簡単な方から、TimeoutExceptionを判別する場合です。こちらはシンプルにスローされたExceptionのインスタンスをチェックすれば判別できます。TimeoutTestController.php のindexアクションを以下の様に書き換えてみましょう。\napp/Http/Controllers/TimeoutTestController.php ... public function index() { $timeout = (int) request(\u0026#39;timeout\u0026#39;); try { // 制限時間の指定があればセット if ($timeout) { DbQueryTimeout::set($timeout); } // 重いクエリ１ DB::statement(\u0026#39;select sleep(3) union select 1\u0026#39;); // クエリ以外の処理で3秒掛かった sleep(3); // 重いクエリ２ DB::statement(\u0026#39;select sleep(5) union select 1\u0026#39;); } catch (\\Exception $e) { // TimeoutExceptionの場合のエラー文言 if ($e instanceof TimeoutException) { return \u0026#39;timeout error\u0026#39;; } return \u0026#39;error\u0026#39;; } return \u0026#39;success\u0026#39;; } TimeoutExceptionは、クエリ実行外でタイムオーバーし、次のクエリが実行されるタイミングでスローされます。そちらを再現する為に、クエリ１とクエリ２の間にPHP側のスリープを3秒挟みました。これにより制限時間が5秒の場合はTimeoutExceptionがスローされるはずです。再度、ビルドインサーバにて/timeout_test?timeout=5へアクセスしてみて下さい。今度はtimeout errorと表示されたはずです。\nクエリ実行中にタイムアウトした場合のエラー クエリ実行中にタイムアウトした場合はQueryExceptionがスローされます。QueryExceptionはタイムアウト以外でもスローされる例外ですので、前項のTimeoutExceptionの様にインスタンスだけではタイムアウトエラーを判別できません。判別には一歩踏み込んで、$e-\u0026gt;errorInfoのチェックが必要です。errorInfoはQueryExceptionの親クラスであるPDOExceptionにセットされていた値で、PHPマニュアルによると、データベースハンドラにおける直近の操作に関連する拡張エラー情報が配列で格納されています。タイムアウトエラーとしてQueryExceptionがスローされた際の$e-\u0026gt;errorInfoは以下の様になります。\n[ 0 =\u0026gt; \u0026#34;HY000\u0026#34; 1 =\u0026gt; 3024 2 =\u0026gt; \u0026#34;Query execution was interrupted, maximum statement execution time exceeded\u0026#34; ] 0はSQLSTATEエラーコード、1はドライバ固有のエラーコード、2はそのエラーメッセージです。判別には1のドライバ固有のエラーコードを使用します。こちらの記事によると、max_execution_timeの設定をオーバーした場合のエラーコードは MySQL 8.0.19 以前は3024か1028で、以降は3024に統一されたとの事です。私の環境はまだMySQL5.7なので、1028のエラーコードも補足する必要があります。これらを考慮して、TimeoutTestControllerの例外処理部分を次の様に修正しました。\napp/Http/Controllers/TimeoutTestController.php ... } catch (\\Exception $e) { $timeoutError = false; switch (get_class($e)) { case TimeoutException::class: $timeoutError = true; break; case QueryException::class: if (in_array($e-\u0026gt;errorInfo[1], [3024, 1028])) { $timeoutError = true; } break; } if ($timeoutError) { return \u0026#39;timeout error\u0026#39;; } return \u0026#39;error\u0026#39;; } return \u0026#39;success\u0026#39;; } } インスタンスのチェックはswitch文でget_class($e)でクラス名を取得して分岐する様に変更しました。QueryExceptionの場合はerrorInfoでエラーコードもチェックします。クエリ実行中にタイムオーバーとなるように/timeout_test?timeout=7へアクセスしてみて下さい。再度、timeout errorが表示されるはずです。\nリファクタリング これまで説明の為にコントローラ側にタイムアウトエラーのエラー処理のコードを記述してきましたが、DbQueryTimeoutと一緒に色々な箇所で使い回すのでまとめておいた方が良さそうです。という事で、DbQueryTimeout::isTimeoutError()にコードを移しました。\napp/Services/DbQueryTimeout.php ... // タイムアウトエラー発生？ public static function isTimeoutError(\\Exception $e): bool { $timeoutError = false; switch (get_class($e)) { case TimeoutException::class: $timeoutError = true; break; case \\Illuminate\\Database\\QueryException::class: if (in_array($e-\u0026gt;errorInfo[1], [3024, 1028])) { $timeoutError = true; } break; } return $timeoutError; } ... isTimeoutError()はシンプルにタイムアウトエラーか否かを判別するメソッドです。このメソッドを使う事でTimeoutTestController側は次の様にシンプルになりました。\napp/Http/Controllers/TimeoutTestController.php \u0026lt;?php namespace App\\Http\\Controllers; use App\\Services\\DbQueryTimeout; use Illuminate\\Support\\Facades\\DB; class TimeoutTestController extends Controller { public function index() { $timeout = (int) request(\u0026#39;timeout\u0026#39;); try { // 制限時間の指定があればセット if ($timeout) { DbQueryTimeout::set($timeout); } // 重いクエリ１ DB::statement(\u0026#39;select sleep(3) union select 1\u0026#39;); // クエリ以外の処理で3秒掛かった sleep(3); // 重いクエリ２ DB::statement(\u0026#39;select sleep(5) union select 1\u0026#39;); } catch (\\Exception $e) { if (DbQueryTimeout::isTimeoutError($e)) { return \u0026#39;timeout error\u0026#39;; } return \u0026#39;error\u0026#39;; } return \u0026#39;success\u0026#39;; } } ","date":"2023-07-27T11:10:26+09:00","permalink":"https://www.larajapan.com/2023/07/27/%E3%82%AF%E3%82%A8%E3%83%AA%E3%81%AE%E7%B4%AF%E8%A8%88%E6%99%82%E9%96%93%E3%81%AB%E3%82%BF%E3%82%A4%E3%83%A0%E3%82%A2%E3%82%A6%E3%83%88%E3%82%92%E8%A8%AD%E5%AE%9A%E3%81%99%E3%82%8B%EF%BC%88%EF%BC%92/","title":"クエリの累計時間にタイムアウトを設定する（２）"},{"content":"先日、管理サイトにてアラートが発生しました。調査すると、ある検索画面で利用者が重いクエリを連続で発行した事が原因でした。待ち時間が長かった為、不安になり検索ボタンを連打してしまったようです。サーバに負荷を掛ける操作については注意喚起するとして、待ち時間に制限が無いのはよくありません。そこで、検索処理に制限時間を設ける事にしました。今回はそちらの実装にあたって色々学んだことがあるので紹介します。\nクエリの実行時間を制限する 今回実装した内容を簡単に説明すると、検索処理に対して実行可能時間を設定し、時間切れとなった場合にエラーを返すというものです、要はタイムアウトの設定です。検索処理においてボトルネックとなっていたのはDBへのクエリ部分で、それ以外の処理時間は無視して良いレベルでした。従って、クエリの実行時間に制限を設ける事にしました。\nクエリにタイムアウトを設定する方法については過去の記事の「クエリーの実行所有時間の制限」の項目で紹介されており、今回もこちらを使います。簡単におさらいすると、DB::statement()を使用して、MySQLのmax_execution_timeを設定する方法です。例えば、以下は5秒に制限する場合のコードです。\nDB::statement(\u0026#39;SET SESSION MAX_EXECUTION_TIME = 5000\u0026#39;); こちらを実行すると、以降クエリで5秒以上掛かった場合にQueryExceptionがスローされます。\nクエリの累計時間を制限する ボトルネックとなるクエリが１つの場合は前項の方法で制限時間を設ければOKですが、今回の実装先には重いクエリが複数実行されており、それらの累計時間を制限する必要がありました。例えば、以下のように制限時間を５秒に設定した場合、クエリ１とクエリ２で合わせて５秒を超えたらタイムアウトエラーとします。\nクエリの累計時間に制限を設けるには、クエリを実行する毎に都度残り時間を計算し、それを前項で紹介した方法で設定していけば良さそうです。つまり、以下の様に\nクエリ１の時点では残り時間を５秒として制限時間を設定しますが、クエリ２の時点ではクエリ１で３秒掛かったので、残りの実行可能時間２秒を制限時間として再設定します。\nDbQueryTimeout.php ロジックについて理解が深まったところで、実際のコードを紹介します。クエリの制限時間を管理するクラスとして app/Services 配下に DbQueryTimeout.php を追加しました。\napp/Services/DbQueryTimeout.php \u0026lt;?php namespace App\\Services; use App\\Exceptions\\TimeoutException; use Illuminate\\Support\\Facades\\DB; use Illuminate\\Support\\Str; class DbQueryTimeout { const TIMEOUT_QUERY = \u0026#39;SET SESSION MAX_EXECUTION_TIME\u0026#39;; public static $maxExecutionTime; // 最大実行可能時間（ミリ秒） public static $processStartTime; // プロセス開始時刻（マイクロ秒） // タイムアウト設定初期化 public static function set(int $seconds): void { self::$maxExecutionTime = $seconds * 1000; self::$processStartTime = microtime(true); DB::beforeExecuting(function ($query) { // タイムアウト設定のクエリなら何もしない if (Str::startsWith($query, self::TIMEOUT_QUERY)) { return; } // 残りの実行可能時間を計算 $timeRemaining = self::timeRemaining(); // 実行可能時間が残っていないなら例外スロー if ($timeRemaining \u0026lt;= 0) { throw new TimeoutException; } self::setTimeout($timeRemaining); }); } // タイムアウト設定 public static function setTimeout(int $time): void { DB::statement(self::TIMEOUT_QUERY.\u0026#39; = \u0026#39;.$time.\u0026#39;;\u0026#39;); } // 残りの実行可能時間をミリ秒で返す public static function timeRemaining(): int { return self::$maxExecutionTime - self::elapsedTime(); } // 経過時間をミリ秒で返す public static function elapsedTime(): int { return (int) ((microtime(true) - self::$processStartTime) * 1000); } } 後で説明しますが、このクラス内で使用されている TimeoutException は自作の例外クラスになります。app/Exceptions 配下に TimeoutException.php を追加してください。\napp/Exceptions/TimeoutException.php \u0026lt;?php namespace App\\Exceptions; use Exception; class TimeoutException extends Exception { } DbQueryTimeout の使い方はとてもシンプルで、実行時間を制限したい箇所の最初にset()を実行するだけです。set()の引数には制限時間を秒で指定します。例えば、制限時間を５秒としたいなら以下の様に。\nDbQueryTimeout::set(5); すると、set()が実行された時点からカウントダウンが始まり、タイムオーバーとなった場合は以下の２パターンでエラーが発生します。\nクエリ実行中 クエリ実行外で、それ以後にクエリを実行しようとした 前者の場合はQueryExceptionがスローされ、後者の場合はTimeoutExceptionがスローされます。\n参考までに実装例を掲載します。以下は注文データを検索するページです。\napp/Http/Controllers/InvoiceController.php \u0026lt;?php namespace App\\Http\\Controllers; use App\\Models\\Invoice; use App\\Models\\InvoiceShip; use Illuminate\\Http\\Request; use App\\Services\\DbQueryTimeout; use Illuminate\\Contracts\\View\\View; class InvoiceSearchController extends Controller { /** * 検索画面 */ public function index(Request $request): View { $input = $request-\u0026gt;all(); // 制限時間を5秒に設定 DbQueryTimeout::set(5); // 注文データ $invoices = Invoice::query() -\u0026gt;whereBetween(\u0026#39;invoice_date\u0026#39;, [$input[\u0026#39;date_start\u0026#39;], $input[\u0026#39;date_end\u0026#39;]]) -\u0026gt;where(\u0026#39;invoice_status\u0026#39;, \u0026#39;=\u0026#39;, $input[\u0026#39;invoice_status\u0026#39;]) -\u0026gt;orderBy(\u0026#39;invoice_date\u0026#39;, \u0026#39;desc\u0026#39;) -\u0026gt;paginate(config(\u0026#39;local.items_per_page\u0026#39;)) -\u0026gt;withQueryString(); // 注文配送データ $invoice_ships = InvoiceShip::query() -\u0026gt;whereIn(\u0026#39;invoice_id\u0026#39;, $invoices-\u0026gt;pluck(\u0026#39;invoice_id\u0026#39;)) -\u0026gt;where(\u0026#39;invoice_ship_status\u0026#39;, \u0026#39;=\u0026#39;, $input[\u0026#39;invoice_ship_status\u0026#39;]) -\u0026gt;get(); return view(\u0026#39;admin.invoice_index\u0026#39;) -\u0026gt;with(compact(\u0026#39;input\u0026#39;, \u0026#39;invoices\u0026#39;, \u0026#39;invoice_ships\u0026#39;)); } } 上記のコントローラのindexアクションでは以下の３つのクエリが実行されています。\n１ページに表示する注文データ 注文データの件数 注文データに紐づく注文配送データ これら３つのクエリの累計実行時間が５秒を超える場合はエラーが発生します。エラーが発生した場合の処理については次の投稿で紹介します。\nコードのポイント コア部分であるset()で行われている処理について補足です。set()ではクエリ毎に実行されるbefore処理を登録しています。before処理では以下の処理が実行されます。\n実行されるクエリが制限時間の設定クエリなら何もせず、そのまま実行 それ以外のクエリなら残りの実行可能時間を計算 残り時間なし（既にタイムオーバー）ならTimeoutExceptionをスロー 残り時間があるならそれを制限時間として設定 before処理の登録にDB::beforeExecuting()を使用している点がポイントです。こちらはドキュメントには載っていませんが、イベントリスナーの様にEloquentやクエリビルダにてクエリが実行される毎に直前に呼び出すコールバックを登録できます。余談ですが、after処理の登録にはDB::listen()というメソッドがあります。（こちらはドキュメントにも載っている。）\nDB::beforeExecuting()のクロージャの引数$queryはSQLが文字列で渡されます。1. 実行されるクエリが制限時間の設定クエリなら何もせず、そのまま実行ではそちらが、SET SESSION MAX_EXECUTION_TIMEから始まるクエリかチェックして、そうならbefore処理を終了しそのままクエリの実行に移ります。そうしなければ、この制限時間を設定するクエリに対しても更にbefore処理をして、、、と無限ループとなってしまうからです。\n長くなってしまったので、一旦ここで切ります。次回はエラー処理について解説します。\n","date":"2023-07-27T11:06:11+09:00","permalink":"https://www.larajapan.com/2023/07/27/%E3%82%AF%E3%82%A8%E3%83%AA%E3%81%AE%E7%B4%AF%E8%A8%88%E6%99%82%E9%96%93%E3%81%AB%E3%82%BF%E3%82%A4%E3%83%A0%E3%82%A2%E3%82%A6%E3%83%88%E3%82%92%E8%A8%AD%E5%AE%9A%E3%81%99%E3%82%8B%EF%BC%88%EF%BC%91/","title":"クエリの累計時間にタイムアウトを設定する（１）"},{"content":"factoryを使ったダミーデータ作成方法の3回目です。今回は、複数のデータ作成に便利なsequenceメソッドと、そのユースケースをご紹介します。\n以前の投稿はこちらから閲覧できます。（１）・（２）\n前回、同じく複数データを作成するcreateManyメソッドを使った記述をご紹介しました。\n少し振り返りになりますが、name属性がそれぞれ「高橋・佐藤・田中」のデータをcreateManyで作成する場合、以下のように記述します。\nUser::factory()-\u0026gt;createMany([ [\u0026#39;name\u0026#39; =\u0026gt; \u0026#39;高橋\u0026#39;], [\u0026#39;name\u0026#39; =\u0026gt; \u0026#39;佐藤\u0026#39;], [\u0026#39;name\u0026#39; =\u0026gt; \u0026#39;田中\u0026#39;], ]); 作成したいデータを配列でcreateManyに渡すことで、簡潔に記述することができます。\nでは、sequenceで同じデータを作る場合はどのようにするのでしょうか。\nsequenceで複数データを作成 先ほどと同じ、name属性が「高橋・佐藤・田中」のデータを作成してみましょう。以下のように記述します。\nUser::factory() -\u0026gt;count(3) -\u0026gt;sequence( [\u0026#39;name\u0026#39; =\u0026gt; \u0026#39;高橋\u0026#39;], [\u0026#39;name\u0026#39; =\u0026gt; \u0026#39;佐藤\u0026#39;], [\u0026#39;name\u0026#39; =\u0026gt; \u0026#39;田中\u0026#39;] ) -\u0026gt;create(); countメソッドに作成件数を指定し、sequenceには作成したいデータを渡します。またcreateメソッドも繋ぐ必要があります。\nこのケースではcreateManyと比較して、sequenceの方がやや冗長に見えますよね。ですが、以下のようなケースならどうでしょう。\nsequenceが適しているケース（１） sex属性が「女性」と「解答なし」のデータをそれぞれ3件、全6件分作成したい。このようなケースでは、sequenceを使って以下のように記述できます。\nUser::factory() -\u0026gt;count(6) -\u0026gt;sequence( [\u0026#39;sex\u0026#39; =\u0026gt; \u0026#39;女性\u0026#39;], [\u0026#39;sex\u0026#39; =\u0026gt; \u0026#39;解答なし\u0026#39;], ) -\u0026gt;create(); tinkerで実行すると、「女性・解答なし・女性・解答なし\u0026hellip;.」と繰り返すデータがちゃんと６件作成されていることがわかります。\n= Illuminate\\Database\\Eloquent\\Collection {#29800 all: [ App\\Models\\User {#29813 sex: \u0026#34;女性\u0026#34;, name: \u0026#34;三宅 涼平\u0026#34;, id: 142, }, App\\Models\\User {#29793 sex: \u0026#34;解答なし\u0026#34;, name: \u0026#34;渡辺 充\u0026#34;, id: 143, }, App\\Models\\User {#29777 sex: \u0026#34;女性\u0026#34;, name: \u0026#34;近藤 裕樹\u0026#34;, id: 144, }, App\\Models\\User {#29845 sex: \u0026#34;解答なし\u0026#34;, name: \u0026#34;伊藤 加奈\u0026#34;, id: 145, }, App\\Models\\User {#29771 sex: \u0026#34;女性\u0026#34;, name: \u0026#34;三宅 加奈\u0026#34;, id: 146, }, App\\Models\\User {#29781 sex: \u0026#34;解答なし\u0026#34;, name: \u0026#34;青田 くみ子\u0026#34;, id: 147, }, sequenceを使わなくても、以下のように「女性」のレコード・「解答なし」のレコードをそれぞれで作成しても良いかもしれません。\nUser::factory(3)-\u0026gt;create([\u0026#39;sex\u0026#39; =\u0026gt; \u0026#39;女性\u0026#39;]); User::factory(3)-\u0026gt;create([\u0026#39;sex\u0026#39; =\u0026gt; \u0026#39;解答なし\u0026#39;]); ただ、その場合作成されたレコード群は「女性・女性・女性・解答なし・解答なし・解答なし」と不自然にデータが固まってしまうので、テストケースによってどちらにするかは使い分けるとよさそうです。\nちなみに、今のようなデータをcreateManyを使って作成しようとすると、\nUser::factory()-\u0026gt;createMany([ [\u0026#39;sex\u0026#39; =\u0026gt; \u0026#39;女性\u0026#39;], [\u0026#39;sex\u0026#39; =\u0026gt; \u0026#39;解答なし\u0026#39;], [\u0026#39;sex\u0026#39; =\u0026gt; \u0026#39;女性\u0026#39;], [\u0026#39;sex\u0026#39; =\u0026gt; \u0026#39;解答なし\u0026#39;], [\u0026#39;sex\u0026#39; =\u0026gt; \u0026#39;女性\u0026#39;], [\u0026#39;sex\u0026#39; =\u0026gt; \u0026#39;解答なし\u0026#39;], ]); このように6件分のデータを１つ１つ配列に渡さないといけないので、より冗長になってしまいます。\nsequenceが適しているケース（２） もう１つのsequenceが適しているケースは、「データに一意性を持たせたい場合」です。\n以下は、私が関わらせていただいているプロジェクトで使われているコードを本記事用に少し変更したものですが、sequenceの使用例としてまさにぴったりです。\nテスト用に47都道府県のレコードを作成したい。県名は、連続した一意性のあるデータとしたい。そういう場合、以下のようなシンプルな記述で作成できます。\nPrefecture::factory()-\u0026gt;count(47) -\u0026gt;sequence(function ($sequence) { return [\u0026#39;name\u0026#39; =\u0026gt; \u0026#34;都道府県{$sequence-\u0026gt;index}\u0026#34;]; })-\u0026gt;create(); 上記のコードではクロージャーで引数として$sequenceを受け取っています。$sequenceオブジェクトが持つindexプロパティを使用し、県名に連番を割り当てています。\n以下はtinkerの実行結果です。「都道府県0、都道府県1、都道府県2、都道府県3、都道府県4、、、」と連続した番号が割り当てられることで、それぞれのデータに一意性を持たせることができます。\n= Illuminate\\Database\\Eloquent\\Collection {#29831 all: [ App\\Models\\Prefecture {#29822 id: 20, name: \u0026#34;都道府県0\u0026#34;, }, App\\Models\\Prefecture {#29825 id: 21, name: \u0026#34;都道府県1\u0026#34;, }, App\\Models\\Prefecture {#29821 id: 22, name: \u0026#34;都道府県2\u0026#34;, }, App\\Models\\Prefecture {#29820 id: 23, name: \u0026#34;都道府県3\u0026#34;, }, App\\Models\\Prefecture {#29819 id: 24, name: \u0026#34;都道府県4\u0026#34;, }, App\\Models\\Prefecture {#29818 id: 25, name: \u0026#34;都道府県5\u0026#34;, }, App\\Models\\Prefecture {#29817 id: 26, name: \u0026#34;都道府県6\u0026#34;, }, App\\Models\\Prefecture {#29816 id: 27, name: \u0026#34;都道府県7\u0026#34;, }, . . . $sequenceのindexプロパティは「0」始まりなので、連番の始まりを「１」にしたい場合は、コールバック関数の中で +1 としてください。\n-\u0026gt;sequence(function ($sequence) { return [\u0026#39;name\u0026#39; =\u0026gt; \u0026#34;都道府県\u0026#34; . ($sequence-\u0026gt;index + 1)]; }) sequenceはそれほど出番が多いメソッドではないかもしれませんが、適した箇所に使うととっても便利です。ぜひご活用ください。\n","date":"2023-07-10T01:51:16+09:00","permalink":"https://www.larajapan.com/2023/07/10/laravel-factory%E3%81%A7%E3%83%86%E3%82%B9%E3%83%88%E7%94%A8%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92%E4%BD%9C%E6%88%90%EF%BC%88%EF%BC%93%EF%BC%89sequence/","title":"Laravel factoryでテスト用データを作成（３）sequence"},{"content":"個人で色々試したいものがある時に最新のPHPが必要となる事が多々あります。そんな時、今まではHomebrewで両方のversionをインストールして環境変数のパスを手作業で切り替えていましたが、正直面倒です。今回はPHPBrewを使ってそんな面倒が解消されるか試してみます。今回の記事は私の作業環境であるmacOSでの話となります、ご承知ください。\nPHPBrew 前々からPHPBrew自体は知っていましたが、ローカルには既にHomeBrewで複数のversionがインストールされていいた為、試してはいませんでした。今回、Laravel10.xのインストールを試すのにPHP8.1以上が必須となった為、そちらのインストールに使用してみます。\nREADMEに書いてありますが、PHPBrewは異なるPHPのversionをホームディレクトリ配下（macだと~/.phpbrew/php配下）にビルドしインストールするツールです。PHPBrewはバージョン毎にディレクトリを分けてインストールする為、それぞれが独立しており、管理や使用するバージョンの切り替えが容易となる様です。恐らくバージョン切り替え時は単に参照のパスを書き換えているのでしょうね。これは期待大です。\nOSのサポート PHPBrewはUnix系のOSでサポートされているようです。今回紹介するのはmacOSでの手順ですが、他にもLinuxのディストリビューション（Ubuntu, Debian, RHEL, Fedoraなど）でも使えるようです。\ninstall 早速、試してみましょう。まず、install前にRequirementから各OSごとに事前にinstallが必要なパッケージ等が書いてあるのでチェックします。macOSの場合はHomebrewが必要との事でした。私の環境には既にinstallされているので飛ばします。\n次にcurlでphpbrewをダウンロードし、/usr/local/bin配下に配置します。\ncurl -L -O https://github.com/phpbrew/phpbrew/releases/latest/download/phpbrew.phar # chmodで実行権限を付ける chmod +x phpbrew.phar # /usr/local/bin 配下にphpbrewとして設置 mv phpbrew.phar /usr/local/bin/phpbrew /usr/local/bin配下はパスが通っているので、以下のようにどこからでも呼び出せるようになったはずです。\nwhich phpbrew \u0026gt;\u0026gt;\u0026gt; /usr/local/bin/phpbrew 次に、phpbrewを初期化します。\nphpbrew init 初期化が完了すると $HOME/.phpbrew 配下に必要なファイルが生成されます。\nls ~/.phpbrew \u0026gt;\u0026gt;\u0026gt; bashrc build php phpbrew.fish Terminal起動時に必要な機能を読み込ませる為に、.bashrc または .zshrc に以下の行を追記します。（私の環境では.zshrcに追記した）\n~/.zshrc [[ -e ~/.phpbrew/bashrc ]] \u0026amp;\u0026amp; source ~/.phpbrew/bashrc 追記後にsourceコマンドで.zshrcから設定を読み直しましょう。\nsource ~/.zshrc 最後にライブラリ探索用のプレフィックスをセットしてinstall完了です。これをセットしないとライブラリが見つからない旨のエラーが沢山出てしまいます。\nphpbrew lookup-prefix homebrew PHPbrewを使ってPHPをインストール phpbrewの準備ができたところで任意のVersionのPHPをインストールしてみましょう。今回はPHP8.1をインストールします。まず、インストール可能なPHPのバージョンを確認します。\nphpbrew known \u0026gt;\u0026gt;\u0026gt;===\u0026gt; Fetching release list... Downloading https://www.php.net/releases/index.php?json=1\u0026amp;version=8\u0026amp;max=100 via curl extension Downloading https://www.php.net/releases/index.php?json=1\u0026amp;version=7\u0026amp;max=100 via curl extension 8.2: 8.2.7, 8.2.6, 8.2.5, 8.2.4, 8.2.3, 8.2.2, 8.2.1, 8.2.0 ... 8.1: 8.1.20, 8.1.19, 8.1.18, 8.1.17, 8.1.16, 8.1.15, 8.1.14, 8.1.13 ... 8.0: 8.0.29, 8.0.28, 8.0.27, 8.0.26, 8.0.25, 8.0.24, 8.0.23, 8.0.22 ... 7.4: 7.4.33, 7.4.32, 7.4.30, 7.4.29, 7.4.28, 7.4.27, 7.4.26, 7.4.25 ... 7.3: 7.3.33, 7.3.32, 7.3.31, 7.3.30, 7.3.29, 7.3.28, 7.3.27, 7.3.26 ... 7.2: 7.2.34, 7.2.33, 7.2.32, 7.2.31, 7.2.30, 7.2.29, 7.2.28, 7.2.27 ... 7.1: 7.1.33, 7.1.32, 7.1.31, 7.1.30, 7.1.29, 7.1.28, 7.1.27, 7.1.26 ... 7.0: 7.0.33, 7.0.32 ... 最新は8.2までありますね、古いバージョンは7.0まで指定できる様です。インストール可能なリストに8.1も含まれているので、次のコマンドでインストールします。ビルドに時間が掛かるようで、私の環境では15分ほど掛かりました。\nphpbrew install 8.1 +default +dbs +apxs2 8.1の最新のバージョンをインストールしたいので、マイナバージョン（8.1.XのX部分）は指定しませんでした。 Versionの後に指定している+default +dbs +apxs2というのはバリアントと呼ばれるものです。\nバリアント バリアントとは、通常のPHPをビルド・インストール際に指定するオプションを抽象化したものです。DBのドライバであったり、opensslなどのライブラリであったり、PHPで有効にしたい機能をオプションとして指定できます。バリアント無しだと最小構成でインストールされます。+defaultだとよく使われるバリアントがまとめてインストールされます。また、今回のインストールではDB（mysql）を使用する為の+dbs、Apache2を使用する為の+apxs2を追加で指定しました。以下のコマンドで、指定可能なバリアントが確認できます。 phpbrew variants \u0026gt;\u0026gt;\u0026gt; Variants: all, apxs2, bcmath, bz2, calendar, cgi, cli, ctype, curl, dba, debug, dom, dtrace, editline, embed, exif, fileinfo, filter, fpm, ftp, gcov, gd, gettext, gmp, hash, iconv, imap, inifile, inline, intl, ipc, ipv6, json, kerberos, ldap, libgcc, mbregex, mbstring, mcrypt, mhash, mysql, opcache, openssl, pcntl, pcre, pdo, pear, pgsql, phar, phpdbg, posix, readline, session, soap, sockets, sodium, sqlite, static, tidy, tokenizer, wddx, xml, xmlrpc, zip, zlib, zts Virtual variants: dbs: sqlite, mysql, pgsql, pdo mb: mbstring, mbregex neutral: small: bz2, cli, dom, filter, ipc, json, mbregex, mbstring, pcre, phar, posix, readline, xml, curl, openssl default: bcmath, bz2, calendar, cli, ctype, dom, fileinfo, filter, ipc, json, mbregex, mbstring, mhash, pcntl, pcre, pdo, pear, phar, posix, readline, sockets, tokenizer, xml, curl, openssl, zip everything: dba, ipv6, dom, calendar, wddx, static, inifile, inline, cli, ftp, filter, gcov, zts, json, hash, exif, mbstring, mbregex, libgcc, pdo, posix, embed, sockets, debug, phpdbg, zip, bcmath, fileinfo, ctype, cgi, soap, pcntl, phar, session, tokenizer, opcache, imap, ldap, tidy, kerberos, xmlrpc, fpm, dtrace, pcre, mhash, mcrypt, zlib, curl, readline, editline, gd, intl, sodium, openssl, mysql, sqlite, pgsql, xml, gettext, iconv, bz2, ipc, gmp, pear +defaultには以下が含まれている様です、\ndefault: bcmath, bz2, calendar, cli, ctype, dom, fileinfo, filter, ipc, json, mbregex, mbstring, mhash, pcntl, pcre, pdo, pear, phar, posix, readline, sockets, tokenizer, xml, curl, openssl, zip 話をインストールに戻します。installコマンドが完了したらlistコマンドでインストール済みのバージョン一覧に表示されるか確認しましょう。\nphpbrew list \u0026gt;\u0026gt;\u0026gt; * (system) php-8.1.20 アスタリスク（*）が付いているのが現在使用中のバージョンです。(system)はHomebrewでインストールしたシステムデフォルトのバージョン（8.0.28がインストールされています）の事です。PHPBrewによって php-8.1.20 がインストールされた様ですね。このバージョンに切り替えるにはuseコマンドで指定します。\nphpbrew use php-8.1.20 バージョンが切り替わったか、確認してみましょう。\nphp -v \u0026gt;\u0026gt;\u0026gt; PHP 8.1.20 (cli) (built: Jun 20 2023 21:18:43) (NTS) Copyright (c) The PHP Group Zend Engine v4.1.20, Copyright (c) Zend Technologies 8.1.20 になりましたね。尚、このままでは一度Terminalを再起動するとまたシステムデフォルトの8.0.28に戻ってしまいます。PHPBrewでインストールしたPHPをデフォルトで使用するにはswitchコマンドで指定します。\nphpbrew switch php-8.1.20 逆にPHPBrewの使用をやめてシステムデフォルトに戻したい場合は\nphpbrew switch-off それまでに使用していた環境も残しつつ、別バージョンをインストールして試せるのは安心感があって良いですね。\nまとめ 以上、PHPBrewのインストールから基本的な操作の解説でした。Homebrewでも複数のバージョンをインストールして切り替える事は可能でしたが、冒頭で書いたように手動で切り替えが必要な部分があります。最近L10.xの検証などでバージョンを切り替える頻度が多いため、コマンド一発で切り替えられるPHPBrewは重宝しそうです。","date":"2023-06-27T03:30:18+09:00","permalink":"https://www.larajapan.com/2023/06/27/%E3%80%90mac%E3%80%91phpbrew%E3%81%A7%E8%A4%87%E6%95%B0%E3%81%AE%E3%83%90%E3%83%BC%E3%82%B8%E3%83%A7%E3%83%B3%E3%82%92%E5%88%87%E3%82%8A%E6%9B%BF%E3%81%88%E3%81%A6%E4%BD%BF%E3%81%86/","title":"【mac】phpbrewで複数のバージョンを切り替えて使う"},{"content":"factoryを使ったダミーデータ作成方法の２回目です。前回は単一モデルを扱った内容でしたが、今回は「１対多」のリレーションデータを作成します。\n前回はこちらから閲覧できます。\n１対多のリレーション 店舗（Shop）と、店舗に複数の商品（Product）が属している１対多のリレーションです。商品テーブルは、店舗のプライマリキーであるshop_idを外部キーとして格納しています。\nfactoryの定義 factoryでダミーデータを作るにあたり、factoryにモデルの属性値を定義する必要があります。\nまず、Shopに対して以下のように定義します。\nShopFactory.php class ShopFactory extends Factory { public function definition() { return [ \u0026#39;name\u0026#39; =\u0026gt; $this-\u0026gt;faker-\u0026gt;name(), ]; } } Product側も同様です。子は関連する親のid情報を持つ必要があるので、shop_idカラムも定義します。\nProductFactory.php class ProductFactory extends Factory { public function definition() { return [ \u0026#39;name\u0026#39; =\u0026gt; $this-\u0026gt;faker-\u0026gt;name(), \u0026#39;shop_id\u0026#39; =\u0026gt; $this-\u0026gt;faker-\u0026gt;randomNumber(), ]; } } 作成したfactoryの対象Modelへの紐付けも忘れずに行いましょう。HasFactoryトレイトを追加します。\nShop.php use Illuminate\\Database\\Eloquent\\Factories\\HasFactory; class Shop extends Model { use HasFactory; //★ .... } Product.php use Illuminate\\Database\\Eloquent\\Factories\\HasFactory; class Product extends Model { use HasFactory; //★ .... } ここまででで、factoryを使用する準備ができました。\nfactoryでリレーションデータを作成 では早速テスト用のダミーデータを作ってみましょう。前回の単一Modelデータの作成と同じ書き方で、簡単にリレーションデータが作成できます。\n$shop = Shop::factory()-\u0026gt;create(); Product::factory()-\u0026gt;create([ \u0026#39;shop_id\u0026#39; =\u0026gt; $shop-\u0026gt;shop_id ]); まず親を作成し、子のデータを作成する際shop_idへ親のデータを渡すことでそれぞれのインスタンスを紐づけています。\nこれは見た目にも分かりやすくとっつきやすい書き方ですが、以下のようにforメソッドを使用するともっとコンパクトに記述することができます。\n$product = Product::factory() -\u0026gt;for(Shop::factory()-\u0026gt;create()) -\u0026gt;create(); している事は先ほどと同じで、Productと、それに関連するShopデータの作成です。\ntinkerで実行してみると、以下のような結果となりました。\n= App\\Models\\Product {#29792 product_id: 36958, shop_id: 4763, name: \u0026#34;笹田 あすか\u0026#34;, } name属性に何も指定していないので、factoryで定義した$this-\u0026gt;faker-\u0026gt;name()で生成されたデータが入っています。 product_idはModelの主キーであるため、factoryで定義していなくてもカラムが生成されます。\nまた、shop_idには「4763」という値が入っています。これが親のshop_idと同じであればリレーションデータが正しく作成されている、ということになりますので、Shop情報をtinkerで確認してみましょう。\n\u0026gt; $product-\u0026gt;shop = App\\Models\\Shop {#29755 shop_id: 4763, name: \u0026#34;中島 康弘\u0026#34;, ... } ProductModelインスタンスが持っているshop_idと合致するShopModelインスタンスが取得できました！ちゃんとリレーションデータが作成されていますね。\n属性をオーバーライドして任意の値に では次は、テスト用に都合のよいデータを作成するため属性値をオーバーライドしてみます。createメソッドに上書きしたいデータを配列を渡してあげます。\n$product = Product::factory() -\u0026gt;for(Shop::factory()-\u0026gt;create([ \u0026#39;shop_id\u0026#39; =\u0026gt; 1, \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;テスト店\u0026#39;, ])) -\u0026gt;create([ \u0026#39;product_id\u0026#39; =\u0026gt; 100, \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;テスト商品\u0026#39;, ]); 上記をtinkerで実行すると、以下のProductModelインスタンスが作成されました。ちゃんとproduct_id、name、そしてshop_idが反映されています。\n= App\\Models\\Product {#29799 product_id: 100, name: \u0026#34;テスト商品\u0026#34;, shop_id: 1, ... } リレーション先のShopModelインスタンスも確認してみます。\n\u0026gt; $product-\u0026gt;shop = App\\Models\\Shop {#29774 shop_id: 1, name: \u0026#34;テスト店\u0026#34;, ... } shop_id、nameもセットした値になっていますね。\ncreateManyで複数データ作成 また複数のProductを一気に作る場合、createManyを使うと簡潔に記述できます。\n$products = Product::factory() -\u0026gt;for(Shop::factory()-\u0026gt;create( [ \u0026#39;shop_id\u0026#39; =\u0026gt; 1, \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;テスト店\u0026#39;, ] )) -\u0026gt;createMany([ [ \u0026#39;product_id\u0026#39; =\u0026gt; 50, \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;テスト商品01\u0026#39; ], [ \u0026#39;product_id\u0026#39; =\u0026gt; 51, \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;テスト商品02\u0026#39;, ], [ \u0026#39;product_id\u0026#39; =\u0026gt; 52, \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;テスト商品03\u0026#39;, ] ]); tinkerの実行結果は以下のようになりました。\n= Illuminate\\Database\\Eloquent\\Collection {#29727 all: [ App\\Models\\Product {#29730 shop_id: 1, product_id: 50, name: \u0026#34;テスト商品01\u0026#34;, ... }, App\\Models\\Product {#29723 shop_id: 1, product_id: 51, name: \u0026#34;テスト商品02\u0026#34;, ... }, App\\Models\\Product {#29814 shop_id: 1, product_id: 52, name: \u0026#34;テスト商品03\u0026#34;, ... }, ], ModelではなくCollectionが返ってきています。中身にはちゃんとProductModelインスタンスが３つ入っており、shop_idも指定した通りです。\n簡単にダミーデータを作成でき、テスト作成にとても便利なので是非使ってみてくださいね。\n","date":"2023-06-21T02:26:30+09:00","permalink":"https://www.larajapan.com/2023/06/21/laravel-factory%E3%81%A7%E3%83%86%E3%82%B9%E3%83%88%E7%94%A8%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92%E4%BD%9C%E6%88%90%EF%BC%88%EF%BC%92%EF%BC%89/","title":"Laravel factoryでテスト用データを作成（２）"},{"content":"プログラムの中のどこからでもカスタムログを作成できるようにしたのは前回の話。今回はそのログを例えば３ヶ月ごとにローテーションしてみます。\n前回のカスタムログの関数はこちらです。\nローテーションの目的 ログのファイルをローテーションする目的は、いくつかあります。１つのファイルにログを保存していると、ファイルが大きくなるのでオープンするのに時間がかかる、また、ファイルが大きくなるとディスクスペースも取られ、最悪はディスク使用率が100%となるかもしれません。ということで、ファイル名に年月を入れて必要な月数だけのファイルをキープするのがログローテーションです。\n具体的な例として、現在ディレクトリに以下のようなログファイルがあるとします。\ncustom-2023-04.log custom-2023-05.log custom-2023-06.log 7/1になりログが保存されると、\ncustom-2023-05.log custom-2023-06.log custom-2023-07.log ４月のファイルが削除されて７月のファイルが作成されます。つまりいつも３ヶ月分のログファイルが存在します。\nローテーションするには Laravelはログの管理には、phpではとても有名なMonologのパッケージを使っています。Monologはとてもフレキシブルに出来ていていろいろなカスタム化が可能です。今回はMonologのログローテーションのハンドラーを使い、月次のローテーションを設定します。\nまず、RotatingFileHandlerのクラスを継承して、ファイルフォーマットの設定を、デフォルトの日次FILE_PER_DAYから、月次のFILE_PER_MONTHに設定します。\napp/logging/MonthlyRotatingHandler.php namespace App\\Logging; use Monolog\\Handler\\RotatingFileHandler; use Monolog\\Logger; class MonthlyRotatingHandler extends RotatingFileHandler { /** * @param string $filename　ログファイル名 * @param int $maxFiles 最高のローテーション数（0ならローテーションなし） * @param string|int $level これがコールされる最低のログレベル（デフォルトはDEBUG） * @param bool $bubble stackチャンネルが可能か（デフォルトはtrue） * @param int|null $filePermission ファイルの許可（デフォルトは0644） * @param bool $useLocking 書き込み中にログファイルをロックするか（デフォルトはfalse） */ public function __construct(string $filename, int $maxFiles = 0, $level = Logger::DEBUG, bool $bubble = true, ?int $filePermission = null, bool $useLocking = false) { parent::__construct($filename, $maxFiles, $level, $bubble, $filePermission, $useLocking); $this-\u0026gt;setFilenameFormat(\u0026#39;{filename}-{date}\u0026#39;, static::FILE_PER_MONTH); // デフォルトの\u0026#39;Y-m-d\u0026#39;から\u0026#39;Y-m\u0026#39;に変える } } 簡単ですね。ちなみに、年次としたいなら、FILE_PER_YEARとしてください。\n次に、このハンドラーを使い、前回作成したヘルパーを編集します。\napp/helpers.php use Illuminate\\Support\\Facades\\Log; if (! function_exists(\u0026#39;custom_log\u0026#39;)) { function custom_log($prefix, $message) { Log::build([ \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;monolog\u0026#39;, \u0026#39;handler\u0026#39; =\u0026gt; App\\Logging\\MonthlyRotatingHandler::class, \u0026#39;with\u0026#39; =\u0026gt; [ \u0026#39;filename\u0026#39; =\u0026gt; storage_path(\u0026#39;logs/\u0026#39;.$prefix.\u0026#39;.log\u0026#39;), \u0026#39;filePermission\u0026#39; =\u0026gt; 0666, //すべてで読み書き可能とする \u0026#39;maxFiles\u0026#39; =\u0026gt; 3, // ３ヶ月でローテーション ], ])-\u0026gt;info($message); } } これだけです。早速テストしてみると、storage/logsには以下のようにファイルが作成されました。\n$ ls -l -rw-rw-rw- 1 kenji kenji 114 Jun 14 14:20 custom-2023-06.log -rw-rw-rw- 1 kenji kenji 0 Jun 14 14:19 laravel.log また、上のfilePermissionで指定したように、ファイルのパーミッションは0666となっています。\n","date":"2023-06-17T04:28:42+09:00","permalink":"https://www.larajapan.com/2023/06/17/%E8%A8%AD%E5%AE%9A%E3%81%AA%E3%81%97%E3%81%A7%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%A0%E3%83%AD%E3%82%B0%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%92%E4%BD%9C%E6%88%90%EF%BC%88%EF%BC%92%EF%BC%89/","title":"設定なしでカスタムログファイルを作成（２）"},{"content":"プログラムの中で必要なときにちょろっとある情報をログに残したい、しかもlaravel.logではなくカスタム（ここが大事）のログファイルを作成して残したい、というのが私の願いです。さて、どうプログラムするか？\n通常は 以前にも紹介したように、カスタムのログファイルを使うには、config/logging.phpに以下のように追加して、\nconfig/logging.php ... \u0026#39;channels\u0026#39; =\u0026gt; [ \u0026#39;single\u0026#39; =\u0026gt; [ \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;single\u0026#39;, \u0026#39;path\u0026#39; =\u0026gt; storage_path(\u0026#39;logs/laravel.log\u0026#39;), \u0026#39;level\u0026#39; =\u0026gt; \u0026#39;debug\u0026#39;, ], ... \u0026#39;custom\u0026#39; =\u0026gt; [ \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;single\u0026#39;, \u0026#39;path\u0026#39; =\u0026gt; storage_path(\u0026#39;logs/custom.log\u0026#39;), \u0026#39;level\u0026#39; =\u0026gt; \u0026#39;info\u0026#39;, \u0026#39;permission\u0026#39; =\u0026gt; 0666, ], ], ]; と設定して、\nuse Illuminate\\Support\\Facades\\Log; ... Log::channel(\u0026#39;custom\u0026#39;)-\u0026gt;info(\u0026#39;テストだよ\u0026#39;); のようにコールすると、storage/logs/custom.logが作成されて渡した文字列が日時とともに記録されます。\nしかし、ログファイル名を変えたいなら、いちいちconfig/logging.phpで設定しなればならないのが難点です。\nオンデマンドログ そこで登場するのがオンデマンドのログです。以下のように使います。 use Illuminate\\Support\\Facades\\Log; ... Log::build([ \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;single\u0026#39;, \u0026#39;path\u0026#39; =\u0026gt; storage_path(\u0026#39;logs/custom.log\u0026#39;), ])-\u0026gt;info(\u0026#39;記録したいことをここに書く\u0026#39;); ... Logをコールするときに、どのファイルに書き込むかなどの設定もその場で指定してしまいます。 しかし、今度はタイプする文字数が多いのが難点。\nヘルパーの作成 先の実行文をヘルパーにして、以下のようにコールするようにしたらシンプルになりますね。 custom_log(\u0026#39;custom\u0026#39;, \u0026#39;記録したいことをここに書く\u0026#39;); // storage/logs/customer.logに保存される custom_log(\u0026#39;invalid_email\u0026#39;, \u0026#39;不正なメールアドレス：\u0026#39;.$request-\u0026gt;email); // storage/logs/invalid_email.logに保存される このヘルパーの関数は、以下のように定義してapp/helpers.phpへ入れます。\napp/helpers.php \u0026lt;?php use Illuminate\\Support\\Facades\\Log; if (! function_exists(\u0026#39;custom_log\u0026#39;)) { function custom_log($prefix, $message) { Log::build([ \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;single\u0026#39;, \u0026#39;path\u0026#39; =\u0026gt; storage_path(\u0026#39;logs/\u0026#39;.$prefix.\u0026#39;.log\u0026#39;), ])-\u0026gt;info($message); } } そして、composer.jsonファイルを編集して、以下のようにautoloadにfilesとして含みます。\ncomposer.json ... \u0026#34;autoload\u0026#34;: { \u0026#34;files\u0026#34;: [ \u0026#34;app/helpers.php\u0026#34; ], \u0026#34;psr-4\u0026#34;: { \u0026#34;App\\\\\u0026#34;: \u0026#34;app/\u0026#34;, \u0026#34;Database\\\\Factories\\\\\u0026#34;: \u0026#34;database/factories/\u0026#34;, \u0026#34;Database\\\\Seeders\\\\\u0026#34;: \u0026#34;database/seeders/\u0026#34; } }, ... 最後に以下を実行してローダーを更新します。\ncomposer dump-autoload これでLaravelのプログラムのどこでも、customer_log()を使用することができます。\n","date":"2023-06-07T09:20:09+09:00","permalink":"https://www.larajapan.com/2023/06/07/laravel%E3%81%A7%E8%A8%AD%E5%AE%9A%E3%81%AA%E3%81%97%E3%81%A7%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%A0%E3%83%AD%E3%82%B0%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%92%E4%BD%9C%E6%88%90/","title":"設定なしでカスタムログファイルを作成（１）"},{"content":"普段、エラーバッグからエラーを取得する際、$errors-\u0026gt;first(\u0026rsquo;name\u0026rsquo;)などとしますが、この時裏ではどんな処理が行われているかご存知でしょうか？私自身、全然意識せずに使っていましたが、調べてみると実はdefaultという名前付きエラーバッグからエラーを取得している事が分かりました。\n前回の記事、名前付きエラーバッグによるエラー管理の執筆時にエラーバッグの構造について調べたのでまとめます。久々にUnder the hoodシリーズ（？）です。前回の記事の内容が多く含まれているのでまだの方は先にそちらを読むことをオススメします。\n$errorsの構造 どう言うことか？$errorsの構造を紐解きながら説明します。エラー発生時にblade側でdd($errors)とすると以下の様な出力が確認できます。\nIlluminate\\Support\\ViewErrorBag {#1285 ▼ #bags: array:1 [▼ \u0026#34;default\u0026#34; =\u0026gt; Illuminate\\Support\\MessageBag {#1286 ▼ // ← key が default #messages: array:1 [▼ \u0026#34;name\u0026#34; =\u0026gt; array:1 [▼ 0 =\u0026gt; \u0026#34;名前を入力してください。\u0026#34; ] ] #format: \u0026#34;:message\u0026#34; } ] } これを見て分かることは\n$errorsの実態はViewErrorBagのインスタンスである ViewErrorBagの$bagsプロパティにMessageBagがdefaultというキーで格納されている エラーメッセージはMessageBagの$messagesプロパティに格納されている つまり、$errors-\u0026gt;first(\u0026rsquo;name\u0026rsquo;) とした時は、bagsプロパティの、defaultキーの、（MessageBagの）messagesプロパティの、nameキーの最初のエラーを取得している訳ですね。想像してたより深い構造をしています。\n$errors-\u003efirst(...)の処理を追う 構造からデータへアクセスする際のイメージが掴めたので実際のコードを追ってみましょう。$errors-\u0026gt;first(\u0026hellip;)とした時、$errorsはViewErrorBagのインスタンスなのだから、ViewErrorBagにfirst()メソッドが定義されているのだろう、と普通は思いますよね。ところが、ViewErrorBagにはそんなメソッドがありません。代わりに以下のマジックメソッドがcallされています。\nvendor/laravel/framework/src/Illuminate/Support/ViewErrorBag.php ... /** * Dynamically call methods on the default bag. * * @param string $method * @param array $parameters * @return mixed */ public function __call($method, $parameters) { return $this-\u0026gt;getBag(\u0026#39;default\u0026#39;)-\u0026gt;$method(...$parameters); } ... マジックメソッド __call() についての詳しい解説はPHPのドキュメントを確認して頂きたいのですが、 簡潔に説明すると、\nクラスに存在しないメソッドがcallされた場合に実行されるメソッド 第一引数（$method）は実行しようとしたメソッド名 第二引数（$parameters）は実行しようとしたメソッドに渡す予定だった引数 なので例えば、$errors-\u0026gt;first(\u0026rsquo;name\u0026rsquo;) 実行時は$method が first、　$parameters が [\u0026rsquo;name\u0026rsquo;]となります。getBag()は指定のkeyのMessageBagを$bagsから取得するメソッドです。従って、$errors-\u0026gt;first(\u0026hellip;) はdefaultというキーに紐づいた MessageBagクラスのfirst()をcallしているという事ですね。\n名前付きエラーバッグの場合 今度は名前付きエラーバッグの場合の処理を追ってみましょう。前回の記事のおさらいになりますが、名前付きエラーバッグからエラーを取得する際は$errors-\u0026gt;エラーバッグ名-\u0026gt;first(\u0026hellip;) などとします。例えば、エラーバッグ名がuserなら、\n$errors-\u0026gt;user-\u0026gt;first(\u0026#39;name\u0026#39;); です。\nこのコードを見ると、userプロパティがあるのかな？と思いがちですが、（Again！）そんなプロパティはViewErrorBagに存在しません。今度は__get()というマジックメソッドがcallされます。\nvendor/laravel/framework/src/Illuminate/Support/ViewErrorBag.php ... /** * Dynamically access a view error bag. * * @param string $key * @return \\Illuminate\\Contracts\\Support\\MessageBag */ public function __get($key) { return $this-\u0026gt;getBag($key); } ... getBag() は前項でも出てきましたね。ここでは紐づいたMessageBagを返しているだけの様です。\nまとめ 通常のエラーバッグと名前付きエラーバッグ、どちらもエラー参照時にはgetBag()にてMessageBagを取得していることが分かりました。違いは、通常のエラーバッグ時はハードコードされたdefaultというキーを使用しているのに対し、名前付きエラーバッグは任意のキーを使用しているという点のみで、処理自体に違いはありません。冒頭で、通常のエラーバッグがdefaultという名前付きエラーバッグである、と言ったのはそういう意味です。なので実は以下の様にエラーを参照することもできます（意味は全く無いですが）\n$errors-\u0026gt;default-\u0026gt;first(\u0026#39;name\u0026#39;); ","date":"2023-06-05T01:50:26+09:00","permalink":"https://www.larajapan.com/2023/06/05/%E3%80%90under-the-hood%E3%80%91%E3%82%A8%E3%83%A9%E3%83%BC%E3%83%90%E3%83%83%E3%82%B0%E3%81%AE%E6%A7%8B%E9%80%A0/","title":"【Under the hood】エラーバッグの構造"},{"content":"１つのページに複数のフォームが存在する場合、エラーの管理が煩雑になりがちです。 そんな時は名前付きエラーバッグを使ってフォーム毎にエラーを分けると管理しやすいです。\n煩雑になってしまう例 複数のフォームがある場合にエラー管理が煩雑になる、とはどんな時でしょうか？ 例を用いて解説します。\n以下の様な会員情報の編集ページがあったとします。 フォームが２つあり、それぞれ別のルートにPOSTされます。\nここでプランナーさんから\n「バリデーションエラー発生時に警告文をフォーム毎に文言を変えて表示したい！」\nとリクエストが来たとします。つまり、 ユーザ情報フォームに関する何かしらのエラーがあれば以下を表示 そして、会社情報フォームに関する何かしらのエラーがあれば以下を表示 何かしらのエラーがあれば、、、と言う条件はフォームが１つであれば簡単で\n@if ($errors-\u0026gt;any()) ... 警告文 @endif とできますが、フォームが複数ある場合はどうすれば良いでしょうか？ sessionに判別用の値をセットする？それともリダイレクト元のルートで判別する？ やり方は色々あると思いますが、名前付きエラーバッグを使うとスマートにできます。\n名前付きエラーバッグとは？ その名の通り、エラーバッグに任意の名前を付けたものです。 （ここで言うエラーバッグとは、blade側でエラーを参照する際に使う $errors です）\nフォーム毎にエラーバッグの名前を分けて付けることで、 エラーを分けて管理できるようになります。\nエラーバッグに名前をつける エラーバッグに名前を付けるにはバリデーション時にvalidateWithBag()を使用します。 例えば、前項のフォームのバリデーションで以下のようにしています。\nユーザ情報フォーム側では、エラーバッグ名をuserとして第一引数に指定しています。\n$validated = $request-\u0026gt;validateWithBag( \u0026#39;user\u0026#39;, // ←エラーバッグ名 [\u0026#39;name\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;], [\u0026#39;name.required\u0026#39; =\u0026gt; \u0026#39;名前を入力してください。\u0026#39;] ); 会社情報フォーム側では、エラーバッグ名をcompanyとして指定しています。\n$validated = $request-\u0026gt;validateWithBag( \u0026#39;company\u0026#39;, // ←エラーバッグ名 [\u0026#39;company_name\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;], [\u0026#39;company_name.required\u0026#39; =\u0026gt; \u0026#39;会社名を入力してください。\u0026#39;] ); また、withErrors()を使って リダイレクトにエラーをセットする際は第二引数にエラーバッグ名を指定します。\nreturn back()-\u0026gt;withErrors($validator, \u0026#39;user\u0026#39;)-\u0026gt;withInput(); 名前付きエラーバッグからエラーを参照する エラーバッグ名の指定ができたので、今度はblade側でエラーを参照してみましょう。 任意の名前のエラーバッグからエラーを参照する際は、 $errors-\u0026gt;エラーバッグ名-\u0026gt;first(\u0026hellip;)などとします。従って、\n// ユーザ情報フォームのエラーを取得する場合、 {{ $errors-\u0026gt;user-\u0026gt;first(\u0026#39;name\u0026#39;) }} // 会社情報フォームのエラーを取得する場合、 {{ $errors-\u0026gt;company-\u0026gt;first(\u0026#39;name\u0026#39;) }} // errorディレクティブを使う場合は第二引数にエラーバッグ名を指定 @error(\u0026#39;name\u0026#39;, \u0026#39;user\u0026#39;) {{ $message }} @enderror また、前項の警告文の様に、ユーザ情報フォームに何かしらのエラーがあるか判別するには\n@if ($errors-\u0026gt;user-\u0026gt;any()) ... @endif が使えます。\n","date":"2023-05-24T08:56:03+09:00","permalink":"https://www.larajapan.com/2023/05/24/%E5%90%8D%E5%89%8D%E4%BB%98%E3%81%8D%E3%82%A8%E3%83%A9%E3%83%BC%E3%83%90%E3%83%83%E3%82%B0%E3%81%AB%E3%82%88%E3%82%8B%E3%82%A8%E3%83%A9%E3%83%BC%E7%AE%A1%E7%90%86/","title":"名前付きエラーバッグによるエラー管理"},{"content":"テスト用のダミーデータを作成する際、factoryをよく使います。factoryを使ったダミーデータの作成方法は色々ありますが、今回は単一Modelのデータ作成でよく使う記述をご紹介します。\nfactoryの定義 factoryを使用するには、事前にfactoryにモデルの属性値を定義する必要があります。 まず、Userモデルに対してのfactoryの定義です。 UserFactory.php class UserFactory extends Factory { public function definition() { return [ \u0026#39;name\u0026#39; =\u0026gt; $this-\u0026gt;faker-\u0026gt;name(), \u0026#39;email\u0026#39; =\u0026gt; $this-\u0026gt;faker-\u0026gt;unique()-\u0026gt;safeEmail(), \u0026#39;active_flag\u0026#39; =\u0026gt; \u0026#39;Y\u0026#39;, ]; } } 作成したfactoryを対象のUserモデルへHasFactoryのトレイトで紐付すると、\nUser.php use Illuminate\\Database\\Eloquent\\Factories\\HasFactory; class User extends Model { use HasFactory; //★ .... } 事前準備は完了です。次にテストデータを作成してみます。\nテストデータの作成とDB保存 makeメソッドを使うと、インスタンスの作成のみでDBへは保存されません。\n$user = User::factory()-\u0026gt;make(); 上記をtinkerで実行してみると、以下のようなインスタンスが作成されました。factoryで定義したname・email・active_flagのプロパティは、ちゃんとダミーデータになっていますね。\n= App\\Models\\User {#29806 name: \u0026#34;木村 直子\u0026#34;, email: \u0026#34;jun.yoshida@example.com\u0026#34;, active_flag: \u0026#34;Y\u0026#34;, } 次は、インスタンスを作成しDBにも保存する場合です。createメソッドを使用します。\nUser::factory()-\u0026gt;create(); tinkerでの実行結果は、以下となります。DBに保存されているので、生成されたデータにはcreated_atやupdated_atも割り当てられています。\n= App\\Models\\User {#29843 name: \u0026#34;工藤 里佳\u0026#34;, email: \u0026#34;watanabe.rika@example.net\u0026#34;, active_flag: \u0026#34;Y\u0026#34;, updated_at: \u0026#34;2023-05-21 16:41:50\u0026#34;, created_at: \u0026#34;2023-05-21 16:41:50\u0026#34;, id: 21, } factoryで定義していないidが割り振られているのは、Userテーブルにidカラムが主キーとして設定されているためです。\nでは次は、複数のデータを作成してみましょう。以下のようにfactoryに生成したい数を指定します。今回は10としました。\n$users = User::factory(10)-\u0026gt;create(); tinkerで実行した結果がこちらになります。先ほどまでと違い、返り値はCollectionとなっていますね。\n= Illuminate\\Database\\Eloquent\\Collection {#29785 all: [ App\\Models\\User {#29795 name: \u0026#34;喜嶋 治\u0026#34;, email: \u0026#34;miki66@example.org\u0026#34;, active_flag: \u0026#34;Y\u0026#34;, updated_at: \u0026#34;2023-05-21 17:12:34\u0026#34;, created_at: \u0026#34;2023-05-21 17:12:34\u0026#34;, id: 101, }, App\\Models\\User {#29799 name: \u0026#34;江古田 陽子\u0026#34;, email: \u0026#34;jtsuda@example.org\u0026#34;, active_flag: \u0026#34;Y\u0026#34;, updated_at: \u0026#34;2023-05-21 17:12:34\u0026#34;, created_at: \u0026#34;2023-05-21 17:12:34\u0026#34;, id: 102, }, ... 以下省略 ２つ目以降の実行結果は省略しましたが、Collectionの中にUserモデルインスタンスが10生成されています。tinkerでカウントしてみると、以下のように10あるのが分かります。\n\u0026gt; $users-\u0026gt;count(); = 10 また、テストではプロパティを任意の値に指定したい場合が多いと思います。以下のようにcreateメソッドに配列を指定すると指定したプロパティのオーバーライドができます。\nUser::factory()-\u0026gt;create([ \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;テストユーザー\u0026#39;, \u0026#39;active_flag\u0026#39; =\u0026gt; \u0026#39;N\u0026#39;, ]); nameとactive_flagの値を指定しました。tinkerの実行結果を見ると、ちゃんと指定した値で生成されているのがわかります。\n= App\\Models\\User {#29789 name: \u0026#34;テストユーザー\u0026#34;, email: \u0026#34;gyamaguchi@example.net\u0026#34;, active_flag: \u0026#34;N\u0026#34;, updated_at: \u0026#34;2023-05-21 17:24:30\u0026#34;, created_at: \u0026#34;2023-05-21 17:24:30\u0026#34;, id: 112, } いずれも最後にcreateメソッドを付けないと、データはDBに保存されないのでご注意ください。\n","date":"2023-05-19T00:04:19+09:00","permalink":"https://www.larajapan.com/2023/05/19/laravel-factory%E3%81%A7%E3%83%86%E3%82%B9%E3%83%88%E7%94%A8%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92%E4%BD%9C%E6%88%90/","title":"Laravel factoryでテスト用データを作成"},{"content":"前回の記事ではRectorを使ってプロジェクト内の古い記法を書き換える手順を紹介しました。書き換えたい記法が１つの場合は前回の解説で十分だと思いますが、例えば、PHPのアップグレードに合わせて新たに導入された記法をまとめて適用したい、という場合は Set Lists を使うのがオススメです。今回はそちらの手順を紹介します。\nSet Lists セットリストはいくつかのルールをトピック別にグループ化したものです。 例えば設定ファイルを以下の様に書き換えると、PHP8.0で導入された記法をまとめて適用することができます。\nrector.php \u0026lt;?php declare(strict_types=1); use Rector\\Config\\RectorConfig; use Rector\\Set\\ValueObject\\SetList; return static function (RectorConfig $rectorConfig): void { // app 配下のみを対象にする $rectorConfig-\u0026gt;paths([ __DIR__ . \u0026#39;/app\u0026#39;, ]); // PHP8.0で導入された変更を適用する $rectorConfig-\u0026gt;sets([SetList::PHP_80]); }; 指定可能なセットリストは以下です。\nhttps://github.com/rectorphp/rector/blob/main/packages/Set/ValueObject/SetList.php\nPHPバージョン別に導入された記法であったり、コーディングスタイルであったり、デッドコードであったり、 色々ありますね。\n各セットリストで適用される変更はRules OverviewのCategoriesから確認できます。\nLevel Sets PHPのバージョンアップ用にLevel Set Listというセットリストも用意されています。 これは、指定したバージョン以前のruleもまとめて適用したい場合に使えます。 例えば、前項のサンプルコードの様に、\n$rectorConfig-\u0026gt;sets([SetList::PHP_80]); とした場合はPHP8.0のruleのみ適用されますが、PHP7.2, 7.3, 7.4 のruleもまとめて適用したい場合は\n$rectorConfig-\u0026gt;sets([LevelSetList::UP_TO_PHP_80]); とすれば変更に含まれます。\n一部のruleを無視する ruleをまとめて適用できるセットリストは便利ですが、 プロジェクトによってはセットリストに含まれる１部のruleを除外したいケースがあるかもしれません。 その場合は以下の様にskip()を使って除外したいruleを指定します。\n例えば、以下のように変更するとswitch文をmatch文に変更するルールが無視されます。\nrector.php \u0026lt;?php declare(strict_types=1); use Rector\\Config\\RectorConfig; use Rector\\Set\\ValueObject\\LevelSetList; use Rector\\Php80\\Rector\\Switch_\\ChangeSwitchToMatchRector; return static function (RectorConfig $rectorConfig): void { // app 配下のみを対象にする $rectorConfig-\u0026gt;paths([ __DIR__ . \u0026#39;/app\u0026#39;, ]); // PHP8.0で導入された変更を適用する $rectorConfig-\u0026gt;sets([LevelSetList::UP_TO_PHP_80]); // switch -\u0026gt; match への変更を無視する $rectorConfig-\u0026gt;skip([ ChangeSwitchToMatchRector::class, ]); }; まとめ いかがでしたでしょうか。\n普段PHPのバージョンを上げる際は廃止される記法の書き換えはmustで行いますが、 新しい記法の導入には慎重になりがちです。\nその理由の１つに、書き換えた結果どれ位コードが良くなるか分からないから、というのがあります。\nしかし、Rectorを活用することで取り敢えず書き換えてみる、 その上で取り入れるかどうか検討してみる、といった事もできそうです。\n参照 Rectorでリファクタリングを自動化　その１ Rectorでリファクタリングを自動化　その３","date":"2023-05-12T00:50:55+09:00","permalink":"https://www.larajapan.com/2023/05/12/rector%E3%81%A7%E3%83%AA%E3%83%95%E3%82%A1%E3%82%AF%E3%82%BF%E3%83%AA%E3%83%B3%E3%82%B0%E3%82%92%E8%87%AA%E5%8B%95%E5%8C%96%E3%80%80%E3%81%9D%E3%81%AE%EF%BC%92/","title":"Rectorでリファクタリングを自動化　その２"},{"content":"タイトルを見てv2とは古いんじゃないと言われそうです。しかし「私はロボットでありません」というのが好きなお客さんも要るのです。今回はGoogle reCaptchaを簡単に取り付けできるパッケージを紹介します。もちろん、v3あるいはさらにエンタープライズバージョンも将来に紹介する予定です。\nサイトを登録してキーを取得 Googleのアカウントをすでに持っている、そしてログインしていることを仮定して以下のURLへ行きます。 https://www.google.com/recaptcha/admin 日本語版の画面というのはないみたいですね（間違っていたらお知らせを）。 画面の赤箱部分を入力します。\nLabelは設定の名前なので代表する名前を入力してください。保存後に編集可能です。 reCAPTCHA Typeは、ここではv2の「私はロボットでありません」（I'm not a robot）のチェックボックスを選択しますが、チェックボックスなし（invisible）も選択可能です。画面の右下に表示されているのが多分にそのreCAPTCHAタイプでしょう。それから、こちらの設定は保存後に変更できません。 DomainsはreCaptchaを使用するサイトのドメイン名です。複数のドメインの入力が可能で自分が持っていないドメイン（例えばお客さんの）でもOKです。保存後に編集も可能です。 上の画面で「Submit」ボタンを押すと以下の画面へ遷移します。\nサイトキーとシークレットキーが生成されています。ここでそれらをコピーして控える必要はないです。Settingの画面に行くといつでも以下のように両方とも閲覧可能です。\nこの画面では、Ownersのところで私以外に他のユーザーがオーナーとなることも可能です。後に話しますが、不正アクセス数を含むアクセスの統計を表示する画面があるのでそれらは関係者と共有した方がいいですね。\n上の画面では、Send Alert To Ownersは、何か問題が起きた時に先に登録したオーナーにメールするのでチェックボックスをオンとした方が良いです。\nv2のパッケージ 今回の目的のためにLaravelで使用できるcomposerのパッケージはいくつかあります。 まず、本家のGoogleから、 https://github.com/google/recaptcha もちろん、これがベストなのですがバリデーションルールやフロントエンドなどの設定は自作する必要あります。自作してもそうは難しくないですけれど。 https://github.com/biscolab/laravel-recaptcha こちら、パッケージ名にLaravelがあるくらいだから、Laravelのために作られています。Laravelのバージョン5から10まで対応しています。オプションが多く細かく指定できるようですが、私としてはちょっと多すぎでそこまで頭を使いたくありません。\nということで、落ち着いたのが以下、 https://github.com/thinhbuzz/laravel-google-captcha こちらも、Laravel特定です。バリデーションもあるし、ブレードにある関数を埋め込むだけでgoogleのapi urlも入れてくれるのでフロンエンドの作成も簡単です。\nlaravel-google-captchaのインストールと設定 早速インストールしてみましょう。\ncomposer require buzz/laravel-google-captcha \u0026ndash;devがないことに注意を。\nconfig/captcha.phpを生成するために以下も実行します。\nphp artisan vendor:publish --provider=\u0026#34;Buzz\\LaravelGoogleCaptcha\\CaptchaServiceProvider\u0026#34; そして、.envにおいて以下を追加します。先ほどの画面からキーは取得します。\n.env CAPTCHA_SITEKEY=6Lc18solAAAAABF13H_BrF0t0pQ237n-KmXBFWWC CAPTCHA_SECRET=6Lc18solAAAAADWvyxFaAT_XA29eV6SHi-JMb-NJ さて、ブレードには以下のように入れます。表示のときには、\n{!! Captcha::display() !!} @error(\u0026#39;g-recaptcha-response\u0026#39;) \u0026lt;div class=\u0026#34;alert alert-danger\u0026#34;\u0026gt;{{ $message }}\u0026lt;/div\u0026gt; @enderror 画面が表示されるときには、\n\u0026lt;script src=\u0026#34;https://www.google.com/recaptcha/api.js?hl=ja\u0026#34; async defer\u0026gt;\u0026lt;/script\u0026gt;\u0026lt;div class=\u0026#34;g-recaptcha\u0026#34; theme=\u0026#34;light\u0026#34; id=\u0026#34;buzzNoCaptchaId_5cfd27964f5cbd34338165f72c4675ab\u0026#34; data-sitekey=\u0026#34;6Lc18solAAAAABF13H_BrF0t0pQ237n-KmXBFWWC\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; ように、google apiへのurlも入れてくれます。\nもちろん、その画面には以下のように「私はロボットでありません」のチェックボックスが表示されます。\nサーバー側でのバリデーションでは、以下のようにcaptchaを指定するだけです。\n$request-\u0026gt;validate([ \u0026#39;email\u0026#39; =\u0026gt; [\u0026#39;required\u0026#39;, \u0026#39;email\u0026#39;, \u0026#39;unique:customer,email\u0026#39;], \u0026#39;password\u0026#39; =\u0026gt; [\u0026#39;required\u0026#39;, new CustomerPasswordRule, \u0026#39;confirmed\u0026#39;], \u0026#39;name\u0026#39; =\u0026gt; [\u0026#39;required\u0026#39;, \u0026#39;string\u0026#39;], \u0026#39;g-recaptcha-response\u0026#39; =\u0026gt; [\u0026#39;required\u0026#39;, \u0026#39;captcha\u0026#39;], ]; Googleの管理画面 最後にですが、Googleの管理画面で以下のような統計情報を見せてくれます。\n青の棒グラフが「私はロボットでありません」をクリックしても、テストするための画像が表示されなくパスしたもので、緑がテストの画像が表示されましたがユーザーが問題なくパス。そして赤がテストの画像が表示されて失敗したものとなります。\n","date":"2023-05-02T05:08:18+09:00","permalink":"https://www.larajapan.com/2023/05/02/google%E3%83%AA%E3%82%AD%E3%83%A3%E3%83%97%E3%83%81%E3%83%A3v2%E3%81%AE%E5%B0%8E%E5%85%A5/","title":"Googleリキャプチャv2の導入"},{"content":"前回紹介したmatch式 や Constructor Property Promotion のように新しい記法が導入されることでボイラープレートが駆逐されコードがより簡潔に読みやすくなります。しかし、長年運用されているプロジェクトなどでは書き換え必要な箇所が多く、導入するのに及び腰になってしまいがちです。Rectorを使うことでそんな億劫な作業が一瞬で完了するかもしれませんよ。\nRectorとは？ About Rectorより\n「Rector」とは、どんなPHPプロジェクトでも実行可能なPHPツールであり、瞬時にアップグレードや自動リファクタリングを実行できます。 という事で、まさにPHP8から導入されたsyntaxで書き換えたい、という私の要求にマッチしています。それらを自動で行なってくれるなら大助かりです。\n現時点（2023/04/22）で ver 0.15.23 が最新で実行にはPHP7.2以上が必要となっています。\nどんなツールか早速使って体感してみましょう。\nインストール 新しく作成したLaravelのprojectにてインストールしました。\ncomposer require rector/rector --dev すると vendor/bin/rector が追加されます。\nvendor/bin/rector --version \u0026gt;\u0026gt; Rector 0.15.25 設定ファイルを追加 次に設定ファイルを（rector.php）プロジェクトに追加します。\nvendor/bin/rector init // rector.php を作成するか問われるので yes とタイプ No \u0026#34;rector.php\u0026#34; config found. Should we generate it for you? [yes]: \u0026gt; yes [OK] The config is added now. Re-run command to make Rector do the work! プロジェクトのルートディレクトリに rector.php が作成されました。 デフォルトでは以下の状態です。\nrector.php \u0026lt;?php declare(strict_types=1); use Rector\\CodeQuality\\Rector\\Class_\\InlineConstructorDefaultToPropertyRector; use Rector\\Config\\RectorConfig; use Rector\\Set\\ValueObject\\LevelSetList; return static function (RectorConfig $rectorConfig): void { $rectorConfig-\u0026gt;paths([ __DIR__ . \u0026#39;/app\u0026#39;, __DIR__ . \u0026#39;/bootstrap\u0026#39;, __DIR__ . \u0026#39;/config\u0026#39;, __DIR__ . \u0026#39;/lang\u0026#39;, __DIR__ . \u0026#39;/public\u0026#39;, __DIR__ . \u0026#39;/resources\u0026#39;, __DIR__ . \u0026#39;/routes\u0026#39;, __DIR__ . \u0026#39;/tests\u0026#39;, ]); // register a single rule $rectorConfig-\u0026gt;rule(InlineConstructorDefaultToPropertyRector::class); // define sets of rules // $rectorConfig-\u0026gt;sets([ // LevelSetList::UP_TO_PHP_80 // ]); }; リファクタリングしたいパスは\n$rectorConfig-\u0026gt;paths([...]); にて指定できます。\nまた、適用したい変更をruleとして指定します。\n$rectorConfig-\u0026gt;rule(InlineConstructorDefaultToPropertyRector::class); // 複数のruleをまとめて指定するならrules $rectorConfig-\u0026gt;rules([ InlineConstructorDefaultToPropertyRector::class, InlineIfToExplicitIfRector::class ]); Rectorで扱うリファクタリングは主に記法の統一なので、 「こういう時はこう書きましょう」というルールを定めるイメージですね。\nRules Overview にて指定可能なルールがまとめられていますので、ご確認を。\n実践 前回紹介した Constructor Property Promotion について リファクタリング可能か試してみましょう。\nまず、以前の記事で使用した例と同じ Person.php を app/Models/ 配下に作成します。\napp/Models/Person.php \u0026lt;?php namespace App\\Models; class Person { public string $name; public int $age; public string $sex; public function __construct(string $name, int $age, string $sex) { $this-\u0026gt;name = $name; $this-\u0026gt;age = $age; $this-\u0026gt;sex = $sex; } } 次に、rector.php を以下のように書き換え設定します。\nrector.php \u0026lt;?php declare(strict_types=1); use Rector\\Config\\RectorConfig; use Rector\\Php80\\Rector\\Class_\\ClassPropertyAssignToConstructorPromotionRector; return static function (RectorConfig $rectorConfig): void { // app 配下のみを対象にする $rectorConfig-\u0026gt;paths([ __DIR__ . \u0026#39;/app\u0026#39;, ]); // Constructor Property Promotion を適用するruleを追加 $rectorConfig-\u0026gt;rule(ClassPropertyAssignToConstructorPromotionRector::class); }; Constructor Property Promotion に書き換えるruleはこちらにありました。\n準備が整ったので書き換えてみましょう。 まずは\u0026ndash;dry-runを付けて実行します。\nvendor/bin/rector process --dry-run \u0026ndash;dry-runではコード自体は変更されません。 変更前後の差分が表示されるので、それを見て意図通りの変更が適用されるか事前確認します。\n1 file with changes =================== 1) app/Models/Person.php:3 ---------- begin diff ---------- @@ @@ class Person { - public string $name; - public int $age; - public string $sex; - - public function __construct(string $name, int $age, string $sex) + public function __construct(public string $name, public int $age, public string $sex) { - $this-\u0026gt;name = $name; - $this-\u0026gt;age = $age; - $this-\u0026gt;sex = $sex; } } ----------- end diff ----------- Applied rules: * ClassPropertyAssignToConstructorPromotionRector (https://wiki.php.net/rfc/constructor_promotion https://github.com/php/php-src/pull/5291) [OK] 1 file would have changed (dry-run) by Rector 意図通りの変更が施されていますね。 問題無かったので今度は\u0026ndash;dry-runオプションを省いて実行します。\nvendor/bin/rector process Person.php がリファクタリングされ以下となりました。\napp/Models/Person.php \u0026lt;?php namespace App\\Models; class Person { public function __construct(public string $name, public int $age, public string $sex) { } } まとめ 長くなってしまったのでRector導入編、ということで一旦ここで切ります。 今回の記事では掘り下げられなかったSet Listsについては次回以降の記事で紹介できればと思います。\n今回は解説用なので１ファイルのみのリファクタリングとなりましたが、 大きいプロジェクトであれば数十数百のファイルにコマンド一発で変更が適用されるので 記法を置換する際にはとても便利なツールだと思います。\nまた、今回は古い記法を置換する目的で触ってみましたが、 例えばチームで開発している際に以前に紹介されたpintなどと同様に コードスタイルを統一する目的でも活用できるのでは、と思います。\n参照 Rectorでリファクタリングを自動化　その２ Rectorでリファクタリングを自動化　その３","date":"2023-05-01T01:30:18+09:00","permalink":"https://www.larajapan.com/2023/05/01/rector%E3%81%A7%E3%83%AA%E3%83%95%E3%82%A1%E3%82%AF%E3%82%BF%E3%83%AA%E3%83%B3%E3%82%B0%E3%82%92%E8%87%AA%E5%8B%95%E5%8C%96/","title":"Rectorでリファクタリングを自動化　その１"},{"content":"前回のmatch式の紹介に続き、PHP8.0で導入されたsyntaxの紹介です。 今回は Constructor Property Promotion について。\nConstructor Property Promotion Classにおいてコンストラクタの引数に指定した値をそのままプロパティに割り当てる事がよくあります。 例えば、以下のように。\nclass Person { public string $name; public int $age; public string $sex; public function __construct(string $name, int $age, string $sex) { $this-\u0026gt;name = $name; $this-\u0026gt;age = $age; $this-\u0026gt;sex = $sex; } } 上記ではインスタンスプロパティを宣言し、コンストラクタ内の処理で値をセットしています。 引数に指定した変数を何度も書かなければならないので冗長ですね。\nPHP8.0からは次のように、 引数にアクセス修飾子(public, protected, private)を付与することでプロパティの宣言と値のセットを同時に行うことができます。\nclass Person { public function __construct( public string $name, public int $age, public string $sex ) {} 簡潔になりスッキリしました。\nプロパティへの値の割当はコンストラクタの処理が走る前に行われるので、 コンストラクタ内で参照する事もできます。\nclass Person { public $initial; public function __construct(public string $name) { // イニシャルを取得 $this-\u0026gt;initial = strtoupper(substr($this-\u0026gt;name, 0, 1)); } } // インスタンス生成時にイニシャルが割り当てられている $person = new Person(\u0026#39;Steve\u0026#39;); echo $person-\u0026gt;initial; \u0026gt;\u0026gt;\u0026gt; \u0026#39;S\u0026#39; 導入する事でシンプルに行数が減るので良いのですが、 既存のプロジェクトのクラスを全て書き換えるのは骨が折れそうです。\n何か一括で書き換える方法は無いものかと調べた処、 Rectorというツールでできることが分かりました。 次回の記事でその手順を紹介させていただきます。\n","date":"2023-04-23T00:36:42+09:00","permalink":"https://www.larajapan.com/2023/04/23/php8-0-%E3%81%AE%E3%82%AD%E3%83%A3%E3%83%83%E3%83%81%E3%82%A2%E3%83%83%E3%83%97-constructor-property-promotion/","title":"php8.0 のキャッチアップ - コンストラクタにおける簡略記法"},{"content":"自営業の私は、いつも勉強姿勢。自分でAWSのアカウントを持ち、EC2マシンを立ち上げ、自分でDBサーバーやウェブサーバーを設定して、さらにWordPressを使いこなしてブログ書く、という面倒なことをしています。みな自分の勉強のためと思って（実際は非常にケチ）。しかし、このWordPress、とにかく攻撃がひどい！ついこの間までは１週間に必ず１度はすべてのサービスを落として再スタートしなければ、CPUがほとんど100%近い状態。このサイトに来てくれる人に申し訳ない。ということでWordPressから脱却して静的ページに移行しました。さて、どう移行したか？　ヒント：実はまだ裏ではWordPressを使っています。\nヘッドレスWordPress なんかマジックのようなこと書きましたが、なんのことはない同じWordPressを違うサイトに移動して、それを作業サイトとしてそこで今までのようにブログを書いて、プラグインを使って、そこから本サイトのために静的ページを生成します。静的ページはプログラムでないから、攻撃されてもなにも影響なく、しかもPHPとかDBとかのお世話にならないので非常に軽いのです。おかげさまでここ何週間とCPUは50%を超えることなく快適です。 しかし、記事のタグとかカテゴリとかのページもすべて静的ページとなるから、生成後の総ファイル数は1600以上にもなりました。生成にかかる時間は11分くらいです。ちなみにEC2のt3.smallのマシンを使用しています。\nもちろん、すべての人向けというわけではないですが、興味のあるお方のためにこれを可能にした素晴らしいプラグインをSimple Staticを紹介します。でもそのための準備やらなんやで結構いろいろ過程があります。もちろん、これらはこのブログの読者であるあなただからできることです（忠告）。\n静的ページへの道 プラグインのことをどこかで読み、試行錯誤で始めた私はつっかえたところたくさんありました。その経験を生かして、まず移行作業のプランをします。動的WordPressサイトから静的ページサイトへの変換のステップとして以下あります。 作業WordPressサイトを構築 Simple Staticのプラグインを設定 Simple Staticで静的ページを生成 使えなくなった機能を補う 作業WordPressサイトを構築 作業WordPressサイトは、基本的に現WordPressサイトのコピーサイトです。しかし、WordPressは同じドメインでも記事のリンクや画像のリンクをドメイン入りURLでDBにばんばんと収納するので、単にサイトのファイルとデータをコピーで終わりとはなりません。それらを作業サイトのホスト名への変更も必要です。ということで、以下のような処理が必要となります。 WordPressの作業サイトを用意する。作業サイトのホスト名は、例えば、現在サイトがwww.example.comなら、icecream.example.comとかわからない名前にすること勧めます。作業サイトが攻撃されたら意味ないです。もちろん、Lets Encryptでセキュア認証も取得して、ウェブサーバーの設定も必要です。ここではすべて説明しません。 WordPressが使用しているDBのデータをmysqldumpでエクスポートしてファイルに落とす。一応これがバックアップにもなります。 作業サイトが使用する新規のDBを作成して、先のデータをインポートする。 DBのデータを作業サイトのドメインに変換する。以下mysqlのコマンドラインで実行します。 UPDATE wp_options SET option_value = replace(option_value, \u0026#39;www.example.com\u0026#39;, \u0026#39;icecream.example.com\u0026#39;) WHERE option_name = \u0026#39;home\u0026#39; OR option_name = \u0026#39;siteurl\u0026#39;; UPDATE wp_posts SET guid = replace(guid, \u0026#39;www.example.com\u0026#39;,\u0026#39;icecream.example.com\u0026#39;); UPDATE wp_posts SET post_content = replace(post_content, \u0026#39;www.example.com\u0026#39;, \u0026#39;icecream.example.com\u0026#39;); 既存のWordPressのファイルをすべて作業サイトの新規のディレクトリにコピーする。rsyncあるいはtarを使うかな。 作業サイトにアクセスしてみる。ここで問題なくアクセスできればハッピーですが、使用しているプラグインで問題になるかもしれません。私の場合はGoogle Site KitやW3 Total Cacheを使っていてそれらを無効化にする必要ありました。ここの対応はケースバイケースです。 Simple Staticのプラグインを設定 作業サイトが構築されたところで、次は本サイトの静的ページ生成のためのプラグインのインストールです。作業サイトのWordPressの管理画面で、以下のように、プラグインを検索してインストールします。 上の画面でのインストール後には、「有効化」ボタンが出るのでそれもクリックします。\n次は、以下のようにプラグインで静的ページの生成かの診断を実行します。\nここで１つでも赤字のエラーがあるなら、解決する必要あります。同画面の一番下に診断のデバッグログの生成のチェックボックスあるので活用してみてください。\n次は設定です。ここではデフォルトの配送方法はzipとなっています。新作業サイト現サイトと同じマシンに物理的に存在するので、以下のようにローカルディレクトリを選択し、そのパスを入力します。そして「変更を保存」します。もちろん、現サイトのディレクトリにはWordPressのファイルがあるので、最初は違うディレクトリとして生成後にディレクトリをスワップした方がよいです。\nSimple Staticで静的ページを生成 さて、いよいよ静的ページの生成です。以下の画面で「静的ファイルを生成する」ボタンを押します。ボタンの下の「アクティビティログ」にメッセージが出てきてページ生成の開始をします。私のケースではなぜかいつもボタンを２回クリックしないとスタートしませんでした。\n生成が完了したら、「ログのエクスポート」としていろいろなメッセージがリストされます。これが結構重要です。特にエラーとなる、 3xxや4xxや5xxの部分です。\n私の場合は、3xxのリダイレクトのエラーに問題があり、生成されたホームページがおかしくなり表示できない問題ありました。上のログには問題のページのメモが記されているのでそれを手がかりに解決する必要あります。もちろん解決したら再度「静的ファイルを生成する」必要あります。\n使えなくなった機能を補う さて、上の作業をすべて完了して成功して問題なければハッピーですが、そうは問屋です。静的ページに変換することにより使えなくなった機能があります。私のケースでは、以下の解決すべき問題ありました。まだ解決できてないものもあります。\nプラグインのGoogle Site Kitは現サイトでは、Google Analyticsのスクリプトを自動的に埋め込んでくれます。しかし作業サイトは本サイトと違うドメインであり、Google Site Kitでは違うドメインを対応することできません。それゆえに手動で「外観」のテーマに埋め込みました。 以前は、お問い合わせの画面では、Contact Form 7を使用していました。しかしそれはPHPのプログラムのゆえに静的ページでは使えなく、mailtoを使用することにしました。Simple Static Proを買えば、どうかしてContact Form 7が使えるようですが、有料なゆえに足踏み。 以前はテーマの機能として検索機能がありました。DB内のデータ検索です。しかし、静的ページゆえにそれを使うことはできません。現在は本サイトには検索機能はありません。不便です。Algoliaを使うことを考えているので、完成したら説明のブログ書く予定です。 ","date":"2023-03-25T08:37:18+09:00","permalink":"https://www.larajapan.com/2023/03/25/%E3%83%AF%E3%83%BC%E3%83%89%E3%83%97%E3%83%AC%E3%82%B9%E3%81%8B%E3%82%89%E9%9D%99%E7%9A%84%E3%83%9A%E3%83%BC%E3%82%B8%EF%BC%88%E6%94%BB%E6%92%83%E3%83%95%E3%83%AA%E3%83%BC%EF%BC%89%E3%81%AB%E7%A7%BB/","title":"WordPress（ワードプレス）から静的ページに移行！"},{"content":"バリデーションの話は尽きないです。過去にもいくつか取り上げました。今回もちょっと頭をひねった話です。\n入力例 まずは入力例から、 ユーザーの登録の入力画面で、メルマガの購読をするならドロップダウンで１週間に受信したい回数を指定します。上のキャプチャでは見えないけれど、ドロップダウンで１回、２回、３回の選択が出てきます。メルマガの購読のチェックボックスをオフにしたら、javascriptで回数のドロップダウンの選択を無効としてもいいですね。\nこの画面の入力値のバリデーションとして、以下のようなコントローラのコードとします。\nclass DemoController extends Controller { public function create(Request $request) { $options = [ 0 =\u0026gt; \u0026#39;-- 選択してください --\u0026#39;, 1 =\u0026gt; \u0026#39;１回\u0026#39;, 2 =\u0026gt; \u0026#39;２回\u0026#39;, 3 =\u0026gt; \u0026#39;３回\u0026#39;, ]; return view(\u0026#39;demo2\u0026#39;)-\u0026gt;with(compact(\u0026#39;options\u0026#39;)); } public function store(Request $request) { $validated = $request-\u0026gt;mergeIfMissing([ \u0026#39;subscribe\u0026#39; =\u0026gt; \u0026#39;N\u0026#39;, // メルマガ購読がオフだと値が返ってこないので、ここで値を設定 ])-\u0026gt;validate([ \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;required|email\u0026#39;, \u0026#39;subscribe\u0026#39; =\u0026gt; \u0026#39;required|in:Y,N\u0026#39;, // メルマガ購読？ \u0026#39;frequency\u0026#39; =\u0026gt; \u0026#39;required_if:subscribe,Y|integer|in:1,2,3\u0026#39;, //購読するなら受信回数をチェック ], [\u0026#39;frequency.in\u0026#39; =\u0026gt; \u0026#39;選択してください\u0026#39;]); dd($validated); } } 登録ボタンを押して実行してみましょう！\nメルマガ購読のチェックボックスをオン（subscribe = \u0026lsquo;Y\u0026rsquo;）にしたら、必ず受信回数の選択が必要となり期待通りのエラーが表示されます。 上のfrequencyのルールでは、\nrequired_if:subscribe,Yは、「メルマガを購読する」（subscribe = 'Y'）がオンとなり、requiredとなりfrequencyの値（０）があるので正 integerでは、値（０）は整数ゆえに正 in:1,2,3では、値（０）は１でも２でも３でもないので誤 となりエラーです。 さて、今度はメルマガ購読をオフ（subscribe = \u0026lsquo;N\u0026rsquo;）にして、投稿してみます。\n「メルマガを購読する」はオフなのに、どうしてこれもエラーとなるのでしょう？ 先と違ってrequired_if:subscribe,Yではrequiredとなりませんが、その結果は誤でもありません。しかし最後のルールin:1,2,3では前回と同様に誤となりエラーなります。\nその証拠に、今度はメルマガ購読オフで、１回を選択するとエラーとはなりません。\n以前習ったbailを使っても同じです。required_if:subscribe,Yのルールの結果が誤となるわけでないので。\n\u0026#39;frequency\u0026#39; =\u0026gt; \u0026#39;bail|required_if:subscribe,Y|integer|in:1,2,3\u0026#39;, //購読するなら受信回数をチェック exclude_if 一体どうすればよいのでしょうか。そこで登場するのが、exclude_ifのルールです。\n\u0026#39;frequency\u0026#39; =\u0026gt; \u0026#39;exclude_if:subscribe,N|required_if:subscribe,Y|integer|in:1,2,3\u0026#39;, //購読するなら受信回数をチェック メルマガ購読がオフ（subscribe = \u0026lsquo;N\u0026rsquo;）なら、exclude_if:subscribe,Nで他のルールは適用されなく、以下のようにエラーとなりません。\nnullable exclude_ifがややこしいと思うなら、これはどうでしょう？\n「\u0026ndash; 選択してください \u0026ndash;」の選択値を０でなくnullとします。 また、frequencyのルールにおいて、exclude_ifの代わりにnullableと追加します。\nclass DemoController extends Controller { public function create(Request $request) { $options = [ null =\u0026gt; \u0026#39;-- 選択してください --\u0026#39;,　// 0でなくnullです。 1 =\u0026gt; \u0026#39;１回\u0026#39;, 2 =\u0026gt; \u0026#39;２回\u0026#39;, 3 =\u0026gt; \u0026#39;３回\u0026#39;, ]; return view(\u0026#39;demo2\u0026#39;)-\u0026gt;with(compact(\u0026#39;options\u0026#39;)); } public function store(Request $request) { $validated = $request-\u0026gt;mergeIfMissing([ \u0026#39;subscribe\u0026#39; =\u0026gt; \u0026#39;N\u0026#39;, // メルマガ購読がオフだと値が返ってこないので、ここで値を設定 ])-\u0026gt;validate([ \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;required|email\u0026#39;, \u0026#39;subscribe\u0026#39; =\u0026gt; \u0026#39;required|in:Y,N\u0026#39;, // メルマガ購読？ \u0026#39;frequency\u0026#39; =\u0026gt; \u0026#39;nullable|required_if:subscribe,Y|integer|in:1,2,3\u0026#39;, // nullableが入っています。 ], [\u0026#39;frequency.in\u0026#39; =\u0026gt; \u0026#39;選択してください\u0026#39;]); if ($validated[\u0026#39;subscribe\u0026#39;] === \u0026#39;N\u0026#39;) { $validated[\u0026#39;frequency\u0026#39;] = 0; // nullでなく0とする } dd($validated); } } 匿名関数 まだややこしいと思うならこれはどうでしょう？ ルールの匿名関数を使用して「メルマガを購読する」がオンのときだけ値の数字のチェックとしました。 class DemoController extends Controller { public function create(Request $request) { $options = [ 0 =\u0026gt; \u0026#39;-- 選択してください --\u0026#39;, 1 =\u0026gt; \u0026#39;１回\u0026#39;, 2 =\u0026gt; \u0026#39;２回\u0026#39;, 3 =\u0026gt; \u0026#39;３回\u0026#39;, ]; return view(\u0026#39;demo2\u0026#39;)-\u0026gt;with(compact(\u0026#39;options\u0026#39;)); } public function store(Request $request) { $validated = $request-\u0026gt;mergeIfMissing([ \u0026#39;subscribe\u0026#39; =\u0026gt; \u0026#39;N\u0026#39;, ])-\u0026gt;validate([ \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;required|email\u0026#39;, \u0026#39;subscribe\u0026#39; =\u0026gt; \u0026#39;required|in:Y,N\u0026#39;, \u0026#39;frequency\u0026#39; =\u0026gt; [ \u0026#39;required_if:subscribe,Y\u0026#39;, \u0026#39;integer\u0026#39;, function($attribute, $value, $fail) { if (request(\u0026#39;subscribe\u0026#39;) === \u0026#39;Y\u0026#39;) { if (! in_array($value, [1,2,3])) { $fail(\u0026#39;選択してください\u0026#39;); } } } ] ]); dd($validated); } } ","date":"2023-03-15T06:05:35+09:00","permalink":"https://www.larajapan.com/2023/03/15/%E4%BB%96%E3%81%AE%E9%A0%85%E7%9B%AE%E3%81%AB%E4%BE%9D%E5%AD%98%E3%81%99%E3%82%8B%E9%A0%85%E7%9B%AE%E3%81%AE%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3/","title":"他の項目に依存する項目のバリデーション"},{"content":"巷ではLaravel 10がリリースされ賑わっていますが、最近私が担当した案件はL9へのUpgradeです。 いつもの様にLaravel Shiftが出してくれるPull Requestを眺めていると、見慣れない記法が出てきました。 調べてみるとPHP8.0から導入されてた記法でした。\nPHP8.0がリリースされて間も無くキャッチアップしたはずなのですが、全く記憶にありません。 ということで、再度PHP8.0から導入された機能を復習しました。 いくつか取り入れたい記法を見つけたのでご紹介します。 今回はmatch式です。\nmatch式 公式マニュアルより抜粋。\n$return_value = match (制約式) { 単一の条件式 =\u0026gt; 返却式, 条件式1, 条件式2 =\u0026gt; 返却式, }; match式はswitch文と似ています。条件とそれにmatchした場合に返却する値を指定します。\nswitch文と異なり条件にmatchしているか比較する際に\u0026quot;==\u0026quot;ではなく\u0026quot;===\u0026quot;が用いられます。\nつまり、\n// switch文 $num = \u0026#39;0\u0026#39;; switch ($num) { case 0: echo \u0026#34;integer zero\u0026#34;; break; case \u0026#39;0\u0026#39;: echo \u0026#34;string zero\u0026#34;; } \u0026gt;\u0026gt; integer zero // match式 $matched = match ($num) { 0 =\u0026gt; \u0026#34;integer zero\u0026#34;, \u0026#39;0\u0026#39; =\u0026gt; \u0026#34;string zero\u0026#34;, }; echo $matched; \u0026gt;\u0026gt; string zero 因みにmatchは式なので末尾のセミコロンを忘れないように注意です！\n複数の条件で同じ値を返す場合やいずれの条件にも当てはまらなかった場合（デフォルト）の書き方はswitch文と似ているので理解しやすいです。\n$matched = match ($trafficSignal) { \u0026#39;blue\u0026#39;, \u0026#39;green\u0026#39; =\u0026gt; \u0026#39;go\u0026#39;, // blue, green のいずれかなら \u0026#39;yellow\u0026#39; =\u0026gt; \u0026#39;caution\u0026#39;, \u0026#39;red\u0026#39; =\u0026gt; \u0026#39;stop\u0026#39;, default =\u0026gt; \u0026#39;nothing to do\u0026#39; // いずれの条件にもmatchしないなら }; Laravel9から学ぶ活用方法 Laravel9からはPHP8.0以上が要件となった事で、ソースコード内でも色々な箇所で活用されている様です。 どの様な使い方をしているのでしょうか？\n例えば、Handler.php にて例外がスローされた際のレスポンスをレンダリングしている箇所では\nvendor/laravel/framework/src/Illuminate/Foundation/Exceptions/Handler.php public function render($request, Throwable $e) { 〜〜省略〜〜 return match (true) { $e instanceof HttpResponseException =\u0026gt; $e-\u0026gt;getResponse(), $e instanceof AuthenticationException =\u0026gt; $this-\u0026gt;unauthenticated($request, $e), $e instanceof ValidationException =\u0026gt; $this-\u0026gt;convertValidationExceptionToResponse($e, $request), default =\u0026gt; $this-\u0026gt;renderExceptionResponse($request, $e), }; } 前の段落で扱ったサンプルコードではいずれも制約式に変数を渡していましたが、こちらはtrueを指定しています。 そして、上から順番にtrueを返す条件式が無いかチェックしていきます。\nもし、$eがHttpResponseExceptionのインスタンスなら、$e-\u0026gt;getResponse()を実行して結果を返却。 それ以外でもし、$eがAuthenticationExceptionのインスタンスなら、$this-\u0026gt;unauthenticated($request, $e)を実行して結果を返却。 それ以外でもし、、、（以下省略）\nこれはつまり、従来は if \u0026hellip; elseif \u0026hellip; で書いていたパターンですね。\n実際、Laravel 8まではif文で書かれていた様です。\nvendor/laravel/framework/src/Illuminate/Foundation/Exceptions/Handler.php public function render($request, Throwable $e) { 〜〜省略〜〜 if ($e instanceof HttpResponseException) { return $e-\u0026gt;getResponse(); } elseif ($e instanceof AuthenticationException) { return $this-\u0026gt;unauthenticated($request, $e); } elseif ($e instanceof ValidationException) { return $this-\u0026gt;convertValidationExceptionToResponse($e, $request); } return $this-\u0026gt;shouldReturnJson($request, $e) ? $this-\u0026gt;prepareJsonResponse($request, $e) : $this-\u0026gt;prepareResponse($request, $e); } if \u0026hellip; elseif \u0026hellip; よりもmatch式を使った書き方の方が行数も少ないし、随分シンプルな印象を受けます。 今後の開発時に活用していきたいと思いました。\n","date":"2023-02-24T07:02:58+09:00","permalink":"https://www.larajapan.com/2023/02/24/php8-0-%E3%81%AE%E3%82%AD%E3%83%A3%E3%83%83%E3%83%81%E3%82%A2%E3%83%83%E3%83%97-match%E5%BC%8F/","title":"php8.0 のキャッチアップ - match式"},{"content":"１年と言っても必ずしも365日とはならないです。４年に１回のうるう年は366日となります。さて、この１年というあいまいな定義をデータベースのSQLあるいはプログラムで計算するとどういう結果になるかという話です。\nSQLで計算 こういうときは、もちろんtinkerを使います。 一番最近のうるう年の2020-02-29を起点として、まずDBでの１年後の計算。 \u0026gt; DB::select(DB::raw(\u0026#39;select DATE_ADD(\u0026#34;2020-02-29\u0026#34;, INTERVAL 1 YEAR)\u0026#39;)); = [ {#30074 +\u0026#34;DATE_ADD(\u0026#34;2020-02-29\u0026#34;, INTERVAL 1 YEAR)\u0026#34;: \u0026#34;2021-02-28\u0026#34;, }, ] １年の代わりに365日を足しても同じです。\n\u0026gt; DB::select(DB::raw(\u0026#39;select DATE_ADD(\u0026#34;2020-02-29\u0026#34;, INTERVAL 365 DAY)\u0026#39;)); = [ {#30059 +\u0026#34;DATE_ADD(\u0026#34;2020-02-29\u0026#34;, INTERVAL 365 DAY)\u0026#34;: \u0026#34;2021-02-28\u0026#34;, }, ] 今度は引き算で１年前の計算。\n\u0026gt; DB::select(DB::raw(\u0026#39;select DATE_SUB(\u0026#34;2020-02-29\u0026#34;, INTERVAL 1 YEAR)\u0026#39;)); = [ {#30062 +\u0026#34;DATE_SUB(\u0026#34;2020-02-29\u0026#34;, INTERVAL 1 YEAR)\u0026#34;: \u0026#34;2019-02-28\u0026#34;, }, ] １年の代わりに、365日を引くと、\n\u0026gt; DB::select(DB::raw(\u0026#39;select DATE_SUB(\u0026#34;2020-02-29\u0026#34;, INTERVAL 365 DAY)\u0026#39;)); = [ {#30090 +\u0026#34;DATE_SUB(\u0026#34;2020-02-29\u0026#34;, INTERVAL 365 DAY)\u0026#34;: \u0026#34;2019-03-01\u0026#34;, }, ] １日分の差が出ました。最初のは２月の最後から１年前の２月の最後という計算ですね。\n※ちなみにこれはmysqlの結果です。\nphpの関数で計算 今度は、Carbonを使用して計算です。\n\u0026gt; use Carbon\\Carbon; \u0026gt; Carbon::parse(\u0026#39;2020-02-29\u0026#39;)-\u0026gt;addYear(1)-\u0026gt;format(\u0026#39;Y-m-d\u0026#39;); = \u0026#34;2021-03-01\u0026#34; あれ、365日計算でないですね。\n\u0026gt; Carbon::parse(\u0026#39;2020-02-29\u0026#39;)-\u0026gt;addDays(366)-\u0026gt;format(\u0026#39;Y-m-d\u0026#39;); = \u0026#34;2021-03-01\u0026#34; 366日の計算です。\n今度は引き算で１年前の計算。\n\u0026gt; Carbon::parse(\u0026#39;2020-02-29\u0026#39;)-\u0026gt;subYear(1)-\u0026gt;format(\u0026#39;Y-m-d\u0026#39;); = \u0026#34;2019-03-01\u0026#34; むむ。今度は365日の計算となりました。\n\u0026gt; Carbon::parse(\u0026#39;2020-02-29\u0026#39;)-\u0026gt;subDays(365)-\u0026gt;format(\u0026#39;Y-m-d\u0026#39;); = \u0026#34;2019-03-01\u0026#34; ","date":"2023-02-11T05:17:54+09:00","permalink":"https://www.larajapan.com/2023/02/11/%E3%81%86%E3%82%8B%E3%81%86%E5%B9%B4%E3%81%AE%E6%97%A5%E3%81%AE%EF%BC%91%E5%B9%B4%E5%BE%8C%E3%81%82%E3%82%8B%E3%81%84%E3%81%AF%EF%BC%91%E5%B9%B4%E5%89%8D%E3%81%AE%E8%A8%88%E7%AE%97%E3%81%84%E3%82%8D/","title":"うるう年の日の１年後あるいは１年前の計算いろいろ"},{"content":"のHTMLタグにおける文字列の入力（type=\u0026ldquo;text,emailなど\u0026rdquo;）と違ってチェックボックス（type=\u0026ldquo;checkbox\u0026rdquo;）は、値をそのまま表示しません。オン・オフのチェックマークとしてUIで表示する必要あります。また、フォームの投稿の際には、チェックボックスがオフの状態つまりチェックされていない状態だと値もフォームのリクエストに入ってきません。今回は、チェックボックスのコンポーネントの作成の話です。\n入力画面 以下のような画面でチェックボックスのコンポーネントx-checkboxを使います。 この画面のブレードはこんな感じです。\n\u0026lt;x-layout\u0026gt; \u0026lt;div class=\u0026#34;container\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;row justify-content-center\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;col-md-8\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;card\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;card-header\u0026#34;\u0026gt;登録\u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;card-body\u0026#34;\u0026gt; \u0026lt;x-form action=\u0026#34;{{ route(\u0026#39;checkbox\u0026#39;) }}\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;row mb-3\u0026#34;\u0026gt; \u0026lt;x-label name=\u0026#34;email\u0026#34;\u0026gt;メールアドレス\u0026lt;/x-label\u0026gt; \u0026lt;div class=\u0026#34;col-md-6\u0026#34;\u0026gt; \u0026lt;x-input type=\u0026#34;email\u0026#34; name=\u0026#34;email\u0026#34; required autocomplete=\u0026#34;email\u0026#34; /\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;row mb-3\u0026#34;\u0026gt; \u0026lt;x-label name=\u0026#34;dummy\u0026#34;\u0026gt;\u0026lt;/x-label\u0026gt; \u0026lt;div class=\u0026#34;col-md-6\u0026#34;\u0026gt; \u0026lt;x-checkbox name=\u0026#34;subscribe\u0026#34; value=\u0026#34;Y\u0026#34;\u0026gt;メルマガを購読する\u0026lt;/x-checkbox\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;row mb-0\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;col-md-6 offset-md-4\u0026#34;\u0026gt; \u0026lt;x-button type=\u0026#34;submit\u0026#34; class=\u0026#34;m-2\u0026#34; level=\u0026#34;primary\u0026#34;\u0026gt;登録\u0026lt;/x-button\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/x-form\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/x-layout\u0026gt; フォームでバリデーションエラーが起こったとき 先の画面でバリデーションのエラーが起こった時は、以下のように、ユーザーが選択したチェックの状態を保つ必要あります。\nそのためには、inputにおけるcheckedの属性の表示を、以下のようにold()と$valueの値で比較すればいいですね。\ncheckbox.blade.php props([ \u0026#39;name\u0026#39;, \u0026#39;value\u0026#39;, \u0026#39;checked\u0026#39; =\u0026gt; \u0026#39;false\u0026#39;, ]) \u0026lt;div class=\u0026#34;form-check\u0026#34;\u0026gt; \u0026lt;input type=\u0026#34;checkbox\u0026#34; id = \u0026#34;{{ $name }}\u0026#34; name=\u0026#34;{{ $name }}\u0026#34; value=\u0026#34;{{ $value }}\u0026#34; {{ old($name) == $value ? \u0026#39;checked\u0026#39; : \u0026#39;\u0026#39; }} {{ $attributes-\u0026gt;merge([\u0026#39;class\u0026#39; =\u0026gt; \u0026#39;form-check-input\u0026#39;]) }} \u0026gt; \u0026lt;label class=\u0026#34;form-check-label\u0026#34; for=\u0026#34;{{ $name }}\u0026#34;\u0026gt;{{ $slot }}\u0026lt;/label\u0026gt; \u0026lt;/div\u0026gt; 投稿時のチェックがオンなら、値のYが返されるので、old(\u0026lsquo;subscribe\u0026rsquo;) = \u0026lsquo;Y\u0026rsquo;となり、$valueの\u0026rsquo;Y\u0026rsquo;と同じで、checkedが属性となりエラー表示の画面ではチェックはオンになります。反対に、チェックがオフなら、何も値がないので、old(\u0026lsquo;subscribe\u0026rsquo;) = nullとなり、checkedの属性は表示されなく、エラー表示の画面ではチェックはオフになります。\n初期画面のデフォルト さて、先の画面では、できればメルマガを受け取ってもらいたいので、チェックボックスをわざわざオンにしてもらうのではなく、最初からチェックをオンにして表示したいです。\nデフォルトとしてチェックボックスをオンとするには、ブレードで以下のようにcheckedを指定します。\n\u0026lt;x-checkbox name=\u0026#34;subscribe\u0026#34; value=\u0026#34;Y\u0026#34; :checked=\u0026#34;true\u0026#34;\u0026gt;メルマガを購読する\u0026lt;/x-checkbox\u0026gt; checkedはコンポーネントのプロパティなので、コンポーネントの属性としては、:checkedにphpの式を文字列で指定します。指定している\u0026quot;true\u0026quot;も文字列ではなくブーリアンのtrueとしてコンポーネントに渡されます。ちなみに、以下も同じ結果となります。\n\u0026lt;x-checkbox name=\u0026#34;subscribe\u0026#34; value=\u0026#34;Y\u0026#34; :checked=\u0026#34;1=1\u0026#34;\u0026gt;メルマガを購読する\u0026lt;/x-checkbox\u0026gt; デフォルトをオフとしたいなら、最初のように:checkedを指定しません。コンポーネントにおいて、\u0026lsquo;checked\u0026rsquo; =\u0026gt; falseとしているので、自動的にfalseとなります。もちろん、以下のように、\u0026ldquo;false\u0026quot;を渡してもOKです。\n\u0026lt;x-checkbox name=\u0026#34;subscribe\u0026#34; value=\u0026#34;Y\u0026#34; :checked=\u0026#34;false\u0026#34;\u0026gt;メルマガを購読する\u0026lt;/x-checkbox\u0026gt; 同様に、これが新規画面でなく編集画面なら、以下のようにDBからの値を使用し反映できます。\n\u0026lt;x-checkbox name=\u0026#34;subscribe\u0026#34; value=\u0026#34;Y\u0026#34; :checked=\u0026#34;$member-\u0026gt;subscribe == \u0026#39;Y\u0026#39;\u0026#34;\u0026gt;メルマガを購読する\u0026lt;/x-checkbox\u0026gt; ","date":"2022-12-26T08:53:50+09:00","permalink":"https://www.larajapan.com/2022/12/26/%E3%83%96%E3%83%AC%E3%83%BC%E3%83%89%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%EF%BC%88%EF%BC%93%EF%BC%89%E3%83%81%E3%82%A7%E3%83%83%E3%82%AF%E3%83%9C%E3%83%83%E3%82%AF%E3%82%B9/","title":"ブレードコンポーネント（３）チェックボックス"},{"content":"前回の例はメールアドレス１つの入力でしたが、今度はちょっと複雑にして複数のメールアドレスを入力する配列入力の話です。\n入力画面 画面はこんな感じです。 そして、その画面のブレードはこんなです。前回で定義したx-inputのコンポーネントを使用しています。\n\u0026lt;x-layout\u0026gt; \u0026lt;div class=\u0026#34;container\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;row justify-content-center\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;col-md-8\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;card\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;card-header\u0026#34;\u0026gt;登録\u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;card-body\u0026#34;\u0026gt; \u0026lt;x-form action=\u0026#34;{{ route(\u0026#39;register\u0026#39;) }}\u0026#34;\u0026gt; @for($i=1;$i \u0026lt;=3;$i++) \u0026lt;div class=\u0026#34;row mb-3\u0026#34;\u0026gt; \u0026lt;x-label name=\u0026#34;email{{ $i }}\u0026#34;\u0026gt;メールアドレス {{ $i }}\u0026lt;/x-label\u0026gt; \u0026lt;div class=\u0026#34;col-md-6\u0026#34;\u0026gt; \u0026lt;x-input id=\u0026#34;email{{ $i }}\u0026#34; type=\u0026#34;email\u0026#34; name=\u0026#34;emails[{{ $i }}]\u0026#34; /\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; @endfor \u0026lt;div class=\u0026#34;row mb-0\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;col-md-6 offset-md-4\u0026#34;\u0026gt; \u0026lt;x-button type=\u0026#34;submit\u0026#34; class=\u0026#34;m-2\u0026#34; level=\u0026#34;primary\u0026#34;\u0026gt;登録\u0026lt;/x-button\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/x-form\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/x-layout\u0026gt; 前回と違って、name=\u0026ldquo;emails[1]\u0026quot;のように配列の文字列となっていることに注意を。\nなにかがおかしい！ 上の画面で以下のようにEメールでない値を入れて「登録」ボタンを押すと、\nもちろん、画面ではエラーが表示されることを期待しますが、何もでてきません。しかも、入力した値は空になってしまいます。まさに、最初の空の入力画面と同じ状態です。\nちなみに、バリデーションのルールは以下です。\n\u0026#39;emails\u0026#39; =\u0026gt; [\u0026#39;required\u0026#39;, \u0026#39;array\u0026#39;], \u0026#39;emails.*\u0026#39; =\u0026gt; [\u0026#39;string\u0026#39;, \u0026#39;email\u0026#39;, \u0026#39;max:255\u0026#39;], 何がおかしいのでしょう？\nここで、前回のx-inputのコンポーネントをここで再度掲載します。\ninput.blade.php @props([ \u0026#39;type\u0026#39; =\u0026gt; \u0026#39;text\u0026#39;, \u0026#39;name\u0026#39;, \u0026#39;value\u0026#39; =\u0026gt; null, ]) \u0026lt;input type=\u0026#34;{{ $type }}\u0026#34; name=\u0026#34;{{ $name }}\u0026#34; value=\u0026#34;{{ old($name, $value) }}\u0026#34; @error($name) {{ $attributes-\u0026gt;class(\u0026#39;form-control is-invalid\u0026#39;) }} @else {{ $attributes-\u0026gt;class(\u0026#39;form-control\u0026#39;) }} @enderror /\u0026gt; @error($name) \u0026lt;span class=\u0026#34;invalid-feedback\u0026#34; role=\u0026#34;alert\u0026#34;\u0026gt; \u0026lt;strong\u0026gt;{{ $message }}\u0026lt;/strong\u0026gt; \u0026lt;/span\u0026gt; @enderror 調査すると、問題はコンポーネントにおける、old($name, $value)と@error($name)に渡している$nameの値です。\n配列なので、この$nameには、emails[1]、emails[2]、emails[3]の文字列が入ってきます。 しかし、old()はこれらの文字列を理解しません。@errorに関しては、debugbarでセッションを見ると、以下のように、emails[1]ではなく、emails.1という文字列になっています。\n対応 ということは、old()や@error()に渡す指標が配列の文字列なら、ドットの文字列に変換すればよいですね。 以下は、改良したコンポーネントです。\n@props([ \u0026#39;type\u0026#39; =\u0026gt; \u0026#39;text\u0026#39;, \u0026#39;name\u0026#39;, \u0026#39;value\u0026#39; =\u0026gt; null, ]) @php $index = str_replace([\u0026#39;.\u0026#39;, \u0026#39;[]\u0026#39;, \u0026#39;[\u0026#39;, \u0026#39;]\u0026#39;], [\u0026#39;_\u0026#39;, \u0026#39;\u0026#39;, \u0026#39;.\u0026#39;, \u0026#39;\u0026#39;], $name); // \u0026#39;emails[1]\u0026#39;が\u0026#39;emails.1\u0026#39;となる @endphp \u0026lt;input type=\u0026#34;{{ $type }}\u0026#34; name=\u0026#34;{{ $name }}\u0026#34; value=\u0026#34;{{ old($index, $value) }}\u0026#34; @error($index) {{ $attributes-\u0026gt;class(\u0026#39;form-control is-invalid\u0026#39;) }} @else {{ $attributes-\u0026gt;class(\u0026#39;form-control\u0026#39;) }} @enderror /\u0026gt; @error($index) \u0026lt;span class=\u0026#34;invalid-feedback\u0026#34; role=\u0026#34;alert\u0026#34;\u0026gt; \u0026lt;strong\u0026gt;{{ $message }}\u0026lt;/strong\u0026gt; \u0026lt;/span\u0026gt; @enderror これで再度「登録」ボタンを押すと、期待通りにエラーが表示されます。\n","date":"2022-12-18T04:56:15+09:00","permalink":"https://www.larajapan.com/2022/12/18/%E3%83%96%E3%83%AC%E3%83%BC%E3%83%89%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%EF%BC%88%EF%BC%92%EF%BC%89%E9%85%8D%E5%88%97%E3%81%AE%E5%85%A5%E5%8A%9B%E3%81%AE%E5%AF%BE%E5%BF%9C/","title":"ブレードコンポーネント（２）配列の入力の対応"},{"content":"今回は、VueJSのコンポーネントの話ではありません。Inertiaの話でもありません。Laravelのブレードコンポーネントの話です。使い始めると結構いいものです。\nデモ こんな入力画面あるとします。\nもともとのブレードは、こんな感じです。\n... \u0026lt;div class=\u0026#34;card-body\u0026#34;\u0026gt; \u0026lt;form method=\u0026#34;POST\u0026#34; action=\u0026#34;{{ route(\u0026#39;register\u0026#39;) }}\u0026#34;\u0026gt; @csrf \u0026lt;div class=\u0026#34;row mb-3\u0026#34;\u0026gt; \u0026lt;label for=\u0026#34;email\u0026#34; class=\u0026#34;col-md-4 col-form-label text-md-end\u0026#34;\u0026gt;メールアドレス\u0026lt;/label\u0026gt; \u0026lt;div class=\u0026#34;col-md-6\u0026#34;\u0026gt; \u0026lt;input id=\u0026#34;email\u0026#34; type=\u0026#34;email\u0026#34; class=\u0026#34;form-control @error(\u0026#39;email\u0026#39;) is-invalid @enderror\u0026#34; name=\u0026#34;email\u0026#34; value=\u0026#34;{{ old(\u0026#39;email\u0026#39;) }}\u0026#34; required autocomplete=\u0026#34;email\u0026#34; autofocus\u0026gt; @error(\u0026#39;email\u0026#39;) \u0026lt;span class=\u0026#34;invalid-feedback\u0026#34; role=\u0026#34;alert\u0026#34;\u0026gt; \u0026lt;strong\u0026gt;{{ $message }}\u0026lt;/strong\u0026gt; \u0026lt;/span\u0026gt; @enderror \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;row mb-0\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;col-md-6 offset-md-4\u0026#34;\u0026gt; \u0026lt;button type=\u0026#34;submit\u0026#34; class=\u0026#34;btn btn-primary\u0026#34;\u0026gt; 登録 \u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/form\u0026gt; \u0026lt;/div\u0026gt; ... 今度は、コンポーネントを使用した、ブレードファイルの中身を見てみましょう。\n... \u0026lt;div class=\u0026#34;card-body\u0026#34;\u0026gt; \u0026lt;x-form action=\u0026#34;{{ route(\u0026#39;register\u0026#39;) }}\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;row mb-3\u0026#34;\u0026gt; \u0026lt;x-label name=\u0026#34;email\u0026#34;\u0026gt;メールアドレス\u0026lt;/x-label\u0026gt; \u0026lt;div class=\u0026#34;col-md-6\u0026#34;\u0026gt; \u0026lt;x-input type=\u0026#34;email\u0026#34; name=\u0026#34;email\u0026#34; required autocomplete=\u0026#34;email\u0026#34; /\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;row mb-0\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;col-md-6 offset-md-4\u0026#34;\u0026gt; \u0026lt;x-button type=\u0026#34;submit\u0026#34; level=\u0026#34;primary\u0026#34;\u0026gt;登録\u0026lt;/x-button\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/x-form\u0026gt; \u0026lt;/div\u0026gt; ... 見た目、とてもシンプルになりました。しかし、見慣れないHTMLタグがx-form, x-label, x-input, x-buttonと４つありますね。 もちろんそれらがコンポーネントなのですが、どうやってそれらのタグが利用できるようになるのでしょうか？　コンポーネントの作成 コンポーネントの作成には、２つ方法があり、app/View/Componentsのディレクトリ下にクラスファイルを作成する方法と、resources/views/componentsに匿名コンポーネントを作成する方法があります。ここでは使いやすい後者を紹介します。\n先のデモの４つのコンポーネントは、以下のようなファイルに定義されています。\nresources/views/components ├── button.blade.php ├── form.blade.php ├── input.blade.php └── label.blade.php button.blade.phpというコンポーネントブレードファイルを作成すれば、それだけで、他のブレードファイルで、のタグとして使用可能になります。\nコンポーネントの定義 まずは、一番外側のx-formの定義です。\nform.blade.php @props([ \u0026#39;method\u0026#39; =\u0026gt; \u0026#39;POST\u0026#39;, \u0026#39;action\u0026#39;, ]) @php $method = Str::upper($method); @endphp \u0026lt;form method=\u0026#34;{{ $method === \u0026#39;GET\u0026#39; ? \u0026#39;GET\u0026#39; : \u0026#39;POST\u0026#39; }}\u0026#34; action=\u0026#34;{{ $action }}\u0026#34; {{ $attributes }}\u0026gt; @csrf @if (! in_array($method, [\u0026#39;GET\u0026#39;, \u0026#39;POST\u0026#39;])) @method($method) @endif {{ $slot }} \u0026lt;/form\u0026gt; 御覧のように、通常のブレードファイルと同じように、@ifや@phpが使えます。しかし、@propsは見たことありませんね。\n@propsでは、コンポーネントを使用するブレードから、コンポーネントにデータを渡すための変数の宣言と初期化をします。\nここでは、methodとactionの変数がありますが\n\u0026lt;x-form action=\u0026#34;{{ route(\u0026#39;register\u0026#39;) }}\u0026#34;\u0026gt; では、actionにしかデータを渡していません。しかし、@propsにおいてmethodのデフォルトはPOSTと指定されているので、実際に出力されるHTMLは、以下のように初期化されます。\n\u0026lt;form method=\u0026#34;POST\u0026#34; action=\u0026#34;http://localhost/register\u0026#34;\u0026gt; \u0026lt;input type=\u0026#34;hidden\u0026#34; name=\u0026#34;_token\u0026#34; value=\u0026#34;8GdTncryczY6s2gkHCMFYJyRlvab2tkLbVp9iRUE\u0026#34;\u0026gt; .. \u0026lt;/form\u0026gt; もう１つ重要なのは、{{ $slot }}です。 そこの部分には、とのタグの間の値がすべて渡されます。\nx-labelでも同様に{{ $slot }}を使います。\nlabel.blade.php @props([ \u0026#39;name\u0026#39;, ]) \u0026lt;label for=\u0026#34;{{ $name }}\u0026#34; class=\u0026#34;col-md-4 col-form-label text-md-end\u0026#34;\u0026gt;{{ $slot }}\u0026lt;/label\u0026gt; x-buttonも同様です。\nbutton.blade.php @props([ \u0026#39;type\u0026#39; =\u0026gt; \u0026#39;button\u0026#39;, \u0026#39;level\u0026#39; =\u0026gt; \u0026#39;default\u0026#39;, ]) \u0026lt;button type=\u0026#34;{{ $type }}\u0026#34; {{ $attributes-\u0026gt;class(\u0026#34;btn btn-$level\u0026#34;)}}\u0026gt;{{ $slot }}\u0026lt;/button\u0026gt; @attributes-\u0026gt;classは、既存のclassの属性に引数のclassをマージします。 例えば、\n\u0026lt;x-button type=\u0026#34;submit\u0026#34; class=\u0026#34;m-2\u0026#34; level=\u0026#34;primary\u0026#34;\u0026gt;登録\u0026lt;/x-button\u0026gt; なら、\n\u0026lt;button type=\u0026#34;submit\u0026#34; class=\u0026#34;btn btn-primary m-2\u0026#34;\u0026gt;登録\u0026lt;/button\u0026gt; となります。\n最後に、x-inputの定義です。\ninput.blade.php @props([ \u0026#39;type\u0026#39; =\u0026gt; \u0026#39;text\u0026#39;, \u0026#39;name\u0026#39;, \u0026#39;value\u0026#39; =\u0026gt; null, ]) \u0026lt;input type=\u0026#34;{{ $type }}\u0026#34; name=\u0026#34;{{ $name }}\u0026#34; value=\u0026#34;{{ old($name, $value) }}\u0026#34; @error($name) {{ $attributes-\u0026gt;class(\u0026#39;form-control is-invalid\u0026#39;) }} @else {{ $attributes-\u0026gt;class(\u0026#39;form-control\u0026#39;) }} @enderror /\u0026gt; @error($name) \u0026lt;span class=\u0026#34;invalid-feedback\u0026#34; role=\u0026#34;alert\u0026#34;\u0026gt; \u0026lt;strong\u0026gt;{{ $message }}\u0026lt;/strong\u0026gt; \u0026lt;/span\u0026gt; @enderror 以下のようにコンポーネントで定義していない属性（requiredやautocomplete）も渡していますが、\n\u0026lt;x-input type=\u0026#34;email\u0026#34; name=\u0026#34;email\u0026#34; required autocomplete=\u0026#34;email\u0026#34; /\u0026gt; これらは、そのまま最終のHTMLに渡されます。\n\u0026lt;input type=\u0026#34;email\u0026#34; name=\u0026#34;email\u0026#34; value=\u0026#34;\u0026#34; class=\u0026#34;form-control\u0026#34; required autocomplete=\u0026#34;email\u0026#34;\u0026gt; ","date":"2022-12-11T12:05:35+09:00","permalink":"https://www.larajapan.com/2022/12/11/%E3%83%96%E3%83%AC%E3%83%BC%E3%83%89%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88/","title":"ブレードコンポーネント"},{"content":"ここ1年位で良く聞くようになったVite。Vue.jsの生みの親であるEvan Youさん開発、との事なので、 Vue.jsとの親和性が高いのだろうな、と。Laravel + Vue.js のスタックで開発する機会があれば使ってみたいな、と。 私の中ではその程度の位置付けでした。\nしかし、今年の６月、Laravel 9.2.0のリリース時のTaylerさんの以下のツイートを見て、認識を改めました。\nToday we're pumped to announce that new Laravel projects use Vite to bundle frontend assets. Breeze and Jetstream have been updated as well. 🔥\nExperience lightning fast Hot Module Replacement when using the new Breeze / Vite stack with Inertia Vue or React. ⚡\n— taylor otwell 🪐 (@taylorotwell) June 28, 2022 光速のHMR。。。是非使ってみたい！\nViteが採用された理由 従来のLaravelはアセットバンドラにwebpackをラップしたLaravel-Mixを使うのが一般的でした。 この決定を知った時、私はViteについて冒頭に述べた程度の認識だったので、\n「なぜViteに変えるの？Laravel-Mixの何がダメなの?」と、思いました。 因みに、同様の質問がTwitterで投げられており、Taylerさんがシンプルに\n\u0026ldquo;much faster(断然速いから)\u0026quot;と答えていました。\nこの業界においてスピードは正義ですから、より速いツールが開発されたら導入しない理由はないですよね。 でも何がどう速いのでしょうか？もっと知りたくなり調べてみました。\n調べていく内に、Laravel-MixとViteは全く別物、ということが分かってきました。\nLaravel-Mixの問題点 Viteが速いということは、Laravel-Mixが遅いという事ですが、具体的にどの部分が遅かったのでしょうか？\n従来のLaravelの開発ではnpm run devを実行すると、resources配下から必要なアセットファイル（cssやjsなど）をかき集め、依存関係を解決しつつ１ファイルにまとめるという処理が行われていました。 アセットバンドルについてはlaravel-mixでフロントエンドを開発にてkhino氏が詳しく解説しているので是非ご一読下さい。\n開発者のアクションとしては、\njsファイルを変更する npm run dev を実行しアセットをバンドル（ビルドともいう）する ビルドした成果物をブラウザで確認する といった感じです。 プロジェクト規模が大きくなりファイル数が多くなるとバンドル処理に時間が掛かり、開発効率が落ちてしまいます。 私のようにjsをふりかけ程度にしか使っていない場合は大したこと無いですが、VueでSPAを開発している場合などは、 上記のフローを高頻度で繰り返し開発するため、変更〜確認までにラグがある事は大きなストレスになります。\nViteはノーバンドル ではViteはどうなのか？\n公式ページに詳しく書いてありますが、Viteは開発サーバにおいて基本バンドルしません。パッケージやライブラリなど開発者が基本変更しないようなファイルは事前バンドルされるのですが、開発者が頻繁に変更するソースコードについてはバンドルせずブラウザに提供するのです。\n最近のブラウザはES6で導入されたモジュール機能(import, export)に対応しており、ブラウザが表示するページに必要なモジュールを把握しそれらをimportしてくれます。そして、開発者がファイルを編集した際はViteがその変更を把握し、変更されたモジュールのみ再読み込みしてくれるのです。\nアセットのバンドルも無ければ、リロード範囲も最小限な為、前項で問題とした変更〜確認までのラグというのが最小限に抑えられるという訳です。\nViteを体感してみる 冒頭のTaylerさんのTweetにある通り、Laravel 9.2.0 以降ではViteがフロントエンドのデフォルトビルダとして採用されており、npm run devを実行するとViteがHMRモードで起動します。\nHMRとはHot Module Replacementの略でコードを変更し保存するとページのリロード無しでブラウザに変更が反映されます。\n驚いたのは、保存してからブラウザに反映されるまでの速さです。文字通り一瞬で全くラグを感じませんでした。Vite恐るべし。\n本番用にはバンドルする 開発時においてはバンドルせずにアセットを提供する事で開発者をハッピーにしてくれるViteですが、本番においてはバンドルした方がハッピーな様です。\nなぜなら、ノーバンドルでアセットを提供した場合、ブラウザが逐次必要なモジュールをネットワーク経由で取得する必要があり非効率だからです。通常ネットワーク経由でデータを取得する際、細切れになったデータをロードするよりも、それらを１つにまとめてロードした方が効率的なのです。\n開発時においてそれが問題にならないのは取得するデータが全てローカルに存在し、ネットワーク経由で取得する必要が無い為です。\nまとめ 長く（と言っても数年）Laravel-Mixを使って開発していると、ついアセットはバンドルするもの、という固定観念に縛られがちですが。 開発時は敢えてバンドルせず、ブラウザ側で出来ることはブラウザに任せよう、という思い切った試みがパズルのピースのように完璧にバチッとはまっており、改めて素晴らしい発明だなぁ、と思いました。Evan Youさん、あっぱれです。\n","date":"2022-10-30T07:08:54+09:00","permalink":"https://www.larajapan.com/2022/10/30/%E5%85%89%E9%80%9F%E3%81%AEvite/","title":"光速のVite"},{"content":"過去の記事でも紹介されていますが、親子（or 1対多）関係にあるModelにおいて、「１つ以上子を持つ親」などの条件で絞り込む際にhas()は便利です。\nしかし、has()を使わずともjoinSub()でサブクエリを指定して同じ条件で絞り込む事もできそうです。 どちらを使うのがベターなのか？気になったので調べてみました。\nhas()とjoinSub()の違い 例えば、店(Shop)と商品(Product)というModelが有った場合、「１つ以上商品を持つ店」はそれぞれ以下で絞り込むことができます。\n// has()で絞り込み $shops = Shop::has(\u0026#39;product\u0026#39;)-\u0026gt;get(); // joinSub()で絞り込み $sub = Product::distinct()-\u0026gt;select(\u0026#39;shop_id\u0026#39;); $shops = Shop::joinSub($sub, \u0026#39;sub\u0026#39;, function ($join) { $join-\u0026gt;on(\u0026#39;sub.shop_id\u0026#39;, \u0026#39;=\u0026#39;, \u0026#39;shop.shop_id\u0026#39;); })-\u0026gt;get(); 見ての通り、joinSub()の方が冗長ですね。\n発行されたsqlはそれぞれ以下になります。\n# has()のsql select * from `shop` where exists (select * from `product` where `shop`.`shop_id` = `product`.`shop_id`); # joinSub()のsql select * from `shop` inner join (select distinct `shop_id` from `product`) as `sub` on `sub`.`shop_id` = `shop`.`shop_id`; has()ではEXISTS句で相関サブクエリを使用して絞り込んでいます。\n相関サブクエリとは mysqlの公式ドキュメントからの引用です。\n相関サブクエリは、外部クエリにも現れるテーブルへの参照を含むサブクエリです。 前述のhas()のsqlでは、外部クエリのFROMに指定しているshopをサブクエリ内で参照している為、相関サブクエリと言えます。\n相関サブクエリの注意点 レコード数が多いテーブルに紐づいたModelにてhas()使う際は注意が必要です。\n相関サブクエリでは外部クエリの結果行数分、比較処理が行われる可能性があり、結果行数が多ければ遅くなる為です。\n例えば前述のhas()のsqlでは、shopのレコード数 x productのレコード数分の比較処理が走ることになります。 （EXISTS句では条件にヒットする行が見つかればその時点でtrueを返し、次のレコードの比較処理に移るため、あくまでも全件ヒットしなかった場合）\nとはいえ、それをjoinSub()に置き換えた場合もGROUP BYやDISTINCTで重複を削除する処理のコストが有るため、一概にどちらが速いと言い切れません。\n実際に実装する環境でテストしてみるのが良いでしょう。\n今回の実装環境ではhas()でもjoinSub()でも速度に大きな違いはありませんでした。 であればよりシンプルに記述できるhas()がベターと考えてそちらを採用することにしました。\n","date":"2022-10-24T12:54:19+09:00","permalink":"https://www.larajapan.com/2022/10/24/%E3%80%90eloquent%E3%80%91has-vs-joinsub/","title":"【Eloquent】has() vs joinSub()"},{"content":"これまた、データベースの話ですが、長期でアクティブに管理しているプロジェクトでは必ず登場してくる作業です。\nどういうときに必要？ 例えば、以下のようなデータの会員テーブル(users)があります。 id name rank 1 高橋 美加子 A 2 浜田 太一 B 3 中島 充 A 4 渚 あすか B 5 渚 和也 A 最後の項目は、会員ランクで過去２年間に会員が購入した商品の送金額により会員ランクが決まります。会員ランクには、ＡとＢの２つあり、会員ランクＡの会員は、購入の際に5%の割引があるなどの恩恵があります。Ｂランクの会員は残念ながら恩恵なしです。\nさて、会員数も増えてきて、Ａランク会員が多すぎて購入額の総額も同じランクでばらつきがあることから、ランクを１つ増やすことにします。つまり、今度はＡ，Ｂ，Ｃと３つのランクとなります。\nもちろん、Artisanコマンドを作成して新たなランク計算式を使い、usersのそれぞれのレコードを更新することも可能ですね。こんな感じで。\nUser::all()-\u0026gt;each(function($user) { $user-\u0026gt;rank = $user-\u0026gt;calcNewRank(); $user-\u0026gt;save(); }) しかし、これではもとのランクの値がなんであったか残りません。更新前に、usersのテーブルをdumpしてファイルとして残してもよいですが、もしかしたら、移行後にも旧のランク値はクレームときのために必要かもしれません。\n値の移行のためのテーブルを新規に作成 このようなときは、まず、一時的に使用するテーブルを作成して、そこでusersのそれぞれのレコードに対応する旧と新のランクをあらかじめレコードを作成します。テーブル名は、change_rankとして以下のようなデータとなります。\nuser_id name old_rank new_rank 1 高橋 美加子 A A 2 浜田 太一 B C 3 中島 充 A B 4 渚 あすか B C 5 渚 和也 A B change_rankのテーブルとusersのテーブルは、両者のプライマリーキー(users.idとchange_rank.user_id)を介してまったく１対１の関係となり、後の一括更新に使用されます。さらに、この移行のテーブルを残しておけば、旧と新のランク値のスナップショットとなり、何かのときにとても有用なものとなります。\n一括更新 通常更新は、１つのテーブルにおいて、例えば以下のように更新します。\nUPDATE users SET rank = \u0026#39;A\u0026#39;; しかし、今回のケースは２つのテーブルが関わる、つまり１つのテーブルの値をもとにもう１つのテーブルの値の更新となるので、joinが必要となります。SQL文では以下のようになります。\nUPDATE users INNER JOIN change_rank ON users.id = change_rank.user_id SET user.rank = change_rank.new_rank; さて、これをSQL文でなく、クエリビルダーで行うなら、以下のようなコードとなります。\nDB::table(\u0026#39;users\u0026#39;) -\u0026gt;join(\u0026#39;change_rank\u0026#39;, \u0026#39;users.id\u0026#39;, \u0026#39;=\u0026#39;, \u0026#39;change_rank.user_id\u0026#39;) -\u0026gt;update([\u0026#39;users.rank\u0026#39; =\u0026gt; DB::raw(\u0026#39;change_rank.new_rank\u0026#39;)]); ここ以下のように書いてしまいそうですが、DB::raw()がないとエラーとなるので注意を。\nDB::table(\u0026#39;users\u0026#39;) -\u0026gt;join(\u0026#39;change_rank\u0026#39;, \u0026#39;users.id\u0026#39;, \u0026#39;=\u0026#39;, \u0026#39;change_rank.user_id\u0026#39;) -\u0026gt;update([\u0026#39;users.rank\u0026#39; =\u0026gt; change_rank.new_rank]); ","date":"2022-10-16T11:57:03+09:00","permalink":"https://www.larajapan.com/2022/10/16/db%E3%83%86%E3%83%BC%E3%83%96%E3%83%AB%E3%82%92join%E3%81%97%E3%81%A6%E5%80%A4%E3%82%92%E6%9B%B4%E6%96%B0/","title":"DBテーブルをjoinして値を更新"},{"content":"私らの開発環境が最近やっとphp 8.0となりましたので、Laravel Pint（ピント）なるものを使ってみました。正式にLaravelの開発チームから登場したコードスタイル整形のツールですが、なんのことはない、Larajapanでも以前から数回紹介したphp-cs-fixerを土台にしたLaravel版です。しかし、より簡単になりました。\n以前に紹介したのは、「コードスタイルを統一するツール」です。ここでも、同様な醜い(Ugly Class）のコードを用意して、Laravel Pintを紹介しましょう。\nインストール Laravelのプロジェクトのルートのディレクトリにおいて、以下を実行します。\n$ composer require laravel/pint --dev 開発環境でしか必要ないので、最後の引数\u0026ndash;devは忘れないように。\n実行 実行もいたって簡単です。php-cs-fixerと違って、コード整形のためのルールの設定はまったく要りません。完全にLaravel用にデフォルト設定になっています。\n$ ./vendor/bin/pint ...................................................... ───────────────────────────────────────────────────────────────── Laravel PASS ....................................................... 54 files もちろん、何も変えてないのでエラーはゼロでPASSです。\n今度は、前回作成したUglyクラスのファイルを追加して、\nnamespace App\\Models; use Log; use Illuminate\\Database\\Eloquent\\Model; use DB; class Ugly extends Model { public function test ($x, $y) { if ($x == 1) { // } else if ($y == 2) { /* */ } } } 再度実行すると、\n$ ./vendor/bin/pint .....................................................✓. ───────────────────────────────────────────────────────────────── Laravel FIXED ................................. 55 files, 1 style issue fixed ✓ app/Models/Ugly.php indentation_type, elseif, braces, function_declara… しっかり、修正（FIXED）されて、\nnamespace App\\Models; use Illuminate\\Database\\Eloquent\\Model; class Ugly extends Model { public function test($x, $y) { if ($x == 1) { // } elseif ($y == 2) { /* */ } } } 綺麗に整形されました。\nカスタム設定 Laravel Pintは、デフォルトで完全にLaravelのコードスタイルとしてくれます。しかし、ちょっとだけ私がカスタマイズしたい部分があります。\nコードにおけるオブジェクトの表示です。私は以下のように、=\u0026gt;で項目を揃えたいのです。\nprotected $rules = [ \u0026#39;cost\u0026#39; =\u0026gt; \u0026#39;required|integer|min:0\u0026#39;, \u0026#39;cost_ship\u0026#39; =\u0026gt; \u0026#39;required|integer|min:0\u0026#39;, \u0026#39;cost_other\u0026#39; =\u0026gt; \u0026#39;required|integer|min:0\u0026#39;, \u0026#39;commission\u0026#39; =\u0026gt; \u0026#39;required|numeric|min:0\u0026#39;, ]; しかし、これをpintで実行すると、\nprotected $rules = [ \u0026#39;cost\u0026#39; =\u0026gt; \u0026#39;required|integer|min:0\u0026#39;, \u0026#39;cost_ship\u0026#39; =\u0026gt; \u0026#39;required|integer|min:0\u0026#39;, \u0026#39;cost_other\u0026#39; =\u0026gt; \u0026#39;required|integer|min:0\u0026#39;, \u0026#39;commission\u0026#39; =\u0026gt; \u0026#39;required|numeric|min:0\u0026#39;, ]; となってしまいます。もちろんそれがLaravelのデフォルトのスタイルなのですが。\nこれを強制しないようにするには、設定ファイルが必要となります。以下のファイル(pint.json）をプロジェクトのルートディレクトリに置きます。\npint.json { \u0026#34;preset\u0026#34;: \u0026#34;laravel\u0026#34;, \u0026#34;rules\u0026#34;: { \u0026#34;binary_operator_spaces\u0026#34;:{\u0026#34;default\u0026#34;:\u0026#34;single_space\u0026#34;,\u0026#34;operators\u0026#34;:{\u0026#34;=\u0026gt;\u0026#34;:null}} }, \u0026#34;exclude\u0026#34;: [ \u0026#34;bootstrap\u0026#34;, \u0026#34;src\u0026#34; ] } 上のrulesのルールは、以下のphp-cs-fixerで使用するものと同じです。 https://mlocati.github.io/php-cs-fixer-configurator/#version:3.11|fixer:binary_operator_spaces\nしかし、そこでの設定はphpの配列となります。以下のように。\n\u0026#39;binary_operator_spaces\u0026#39; =\u0026gt; [ \u0026#39;default\u0026#39; =\u0026gt; \u0026#39;single_space\u0026#39;, \u0026#39;operators\u0026#39; =\u0026gt; [\u0026#39;=\u0026gt;\u0026#39; =\u0026gt; null], ], pintツールは、phpなのになぜか設定ファイルはjsonフォーマットです。ということで、tinkerで以下のように変換して、pint.jsonに入れる必要あります。\n\u0026gt;\u0026gt;\u0026gt; json_encode([\u0026#39;binary_operator_spaces\u0026#39; =\u0026gt; [ \u0026#39;default\u0026#39; =\u0026gt; \u0026#39;single_space\u0026#39;, \u0026#39;operators\u0026#39; =\u0026gt; [\u0026#39;=\u0026gt;\u0026#39; =\u0026gt; null], ]]); =\u0026gt; \u0026#34;{\u0026#34;binary_operator_spaces\u0026#34;:{\u0026#34;default\u0026#34;:\u0026#34;single_space\u0026#34;,\u0026#34;operators\u0026#34;:{\u0026#34;=\u0026gt;\u0026#34;:null}}}\u0026#34; ","date":"2022-09-21T11:35:58+09:00","permalink":"https://www.larajapan.com/2022/09/21/laravel-pint%E3%82%92%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%A0%E8%A8%AD%E5%AE%9A/","title":"Laravel Pintをカスタム設定"},{"content":"前回に複数のSQL文の結果をうまく１つのSQL文にまとめる話をしました。今回もその続きです。\n有効と無効のそれぞれの商品数 実行して知りたい結果は、有効な商品と無効な商品のそれぞれの商品数です。\nSQL文のwhereで条件を指定すれば、\nmysql\u0026gt; select count(*) from product where active_flag = \u0026#39;Y\u0026#39;; +----------+ | count(*) | +----------+ | 9492 | +----------+ 1 row in set (0.01 sec) mysql\u0026gt; select count(*) from product where active_flag = \u0026#39;N\u0026#39;; +----------+ | count(*) | +----------+ | 4065 | +----------+ 1 row in set (0.01 sec) と２つのSQL文を実行します。\nしかし、これを１つのSQL文にすると、\nmysql\u0026gt; select count(active_flag = \u0026#39;Y\u0026#39;) as 有効な商品数, count(active_flag = \u0026#39;N\u0026#39;) as 無効な商品数 from product; +--------------------+--------------------+ | 有効な商品数 | 無効な商品数 | +--------------------+--------------------+ | 13557 | 13557 | +--------------------+--------------------+ 1 row in set (0.02 sec) 結果おかしいですね。有効も無効も同じ商品数となってしまいました。しかも両方ともすべての商品数です。\nCOUNT()関数の引数 SQL文のCOUNT()関数では、その引数の値がNULLでないときにカウントします。\n例えば、Laravelのデフォルトのプロジェクトで作成されるDBテーブルのusersに対して、以下のようにクエリーすると、\nselect count(remember_token) from users ログインで「次回から自動ログインする」(Remember Me?)のチェックボックスをオンにしてログインしたユーザーのみのレコード数を得られます。なぜなら、remember_tokenはレコード作成時にはNULLの値であり、先のようにログインしたときのみにremember_tokenにランダムの文字列が入ります。\n先の、有効も無効も結果が同じ商品数となったケースは、以下のように、引数に指定するExpressionは０あるいは１の値となり、NULLの値とはなりません。それゆえに、それらの引数でCOUNT()すると全レコードが対象となります。\nmysql\u0026gt; select (active_flag = \u0026#39;Y\u0026#39;), (active_flag = \u0026#39;N\u0026#39;) from product where active_flag = \u0026#39;Y\u0026#39; limit 1; +---------------------+---------------------+ | (active_flag = \u0026#39;Y\u0026#39;) | (active_flag = \u0026#39;N\u0026#39;) | +---------------------+---------------------+ | 1 | 0 | +---------------------+---------------------+ 1 row in set (0.00 sec) ちなみに、引数にを指定するとき、つまりの馴染みのCOUNT()では、レコードにNULLの値が含まれても全レコードを返します。\n条件付きでレコードをカウントするには さて、それではどう条件付きをカウントしたらよいでしょうか？\n１つは先のExpressionが返す値を利用して、合計を計算するSUM()の関数を利用できます。\nmysql\u0026gt; select sum(active_flag = \u0026#39;Y\u0026#39;) as 有効な商品数, sum(active_flag = \u0026#39;N\u0026#39;) as 無効な商品数 from product; +--------------------+--------------------+ | 有効な商品数 | 無効な商品数 | +--------------------+--------------------+ | 9492 | 4065 | +--------------------+--------------------+ 1 row in set (0.02 sec) あるいは、IF()の関数で返す値を非NULLあるいはNULLとしてからカウントします。\nmysql\u0026gt; select count(if(active_flag = \u0026#39;Y\u0026#39;, 1, null)) as 有効な商品数, count(if(active_flag = \u0026#39;N\u0026#39;, 1, null)) as 無効な商品数 from product; +--------------------+--------------------+ | 有効な商品数 | 無効な商品数 | +--------------------+--------------------+ | 9492 | 4065 | +--------------------+--------------------+ 1 row in set (0.01 sec) ごちゃごちゃしているなら、以下のようにシンプルにもできます。\nmysql\u0026gt; select count(active_flag = \u0026#39;Y\u0026#39; or null) as 有効な商品数, count(active_flag = \u0026#39;N\u0026#39; or null) as 無効な商品数 from product; +--------------------+--------------------+ | 有効な商品数 | 無効な商品数 | +--------------------+--------------------+ | 9492 | 4065 | +--------------------+--------------------+ 1 row in set (0.01 sec) ","date":"2022-08-22T05:40:59+09:00","permalink":"https://www.larajapan.com/2022/08/22/sql%E3%81%AEcount%E9%96%A2%E6%95%B0%E3%81%A8%E6%9D%A1%E4%BB%B6/","title":"SQLのCOUNT()関数と条件"},{"content":"以前に「調査クエリー」と称して、ウェブ画面でSQL文を入力して実行できるツールを作成しました。それ以来いろいろなSQL文を作成することになったのですが、これはできないだろうというのが最近可能と判って、大喜びです。\nDBテーブルのレコード数取得のSQL文は、例えば、\nmysql\u0026gt; select count(*) as \u0026#39;商品数\u0026#39; from product; +-----------+ | 商品数 | +-----------+ | 31228 | +-----------+ 1 row in set (0.01 sec) というように、fromでテーブルを指定して、count(*)です。\n１つ以上のテーブルのレコード数を取得するには、もう１つのSQL文を連続して実行します。\nmysql\u0026gt; select count(*) as \u0026#39;商品数\u0026#39; from product;select count(*) as \u0026#39;店舗数\u0026#39; from shop; +-----------+ | 商品数 | +-----------+ | 31228 | +-----------+ 1 row in set (0.01 sec) +-----------+ | 店舗数 | +-----------+ | 947 | +-----------+ 1 row in set (0.00 sec) しかし、先の私の「調査クエリー」ツールでは、１つのSQL文しか１回に実行できないので、使えませんね。 私が欲しいのは、１つのSQL文で両方取得です。\nなんと、これができるんだなあ。\nmysql\u0026gt; select (select count(*) from product) as 商品数, (select count(*) from shop) as 店舗数; +-----------+-----------+ | 商品数 | 店舗数 | +-----------+-----------+ | 31228 | 947 | +-----------+-----------+ 1 row in set (0.01 sec) SQL文を見やすくすると、\nselect ( select count(*) from product ) as 商品数, ( select count(*) from shop ) as 店舗数; SELECT文の項目がそれぞれExpressionなので、項目がSELECT文でもいいのですね。\n","date":"2022-08-14T03:26:09+09:00","permalink":"https://www.larajapan.com/2022/08/14/%E8%A4%87%E6%95%B0%E3%81%AE%E3%83%86%E3%83%BC%E3%83%96%E3%83%AB%E3%81%AE%E3%83%AC%E3%82%B3%E3%83%BC%E3%83%89%E6%95%B0%E3%82%92%EF%BC%91%E3%81%A4%E3%81%AEsql%E6%96%87%E3%81%A7%E5%AE%9F%E8%A1%8C/","title":"複数のテーブルのレコード数を１つのSQL文で実行"},{"content":"Eloquentでクエリする際も内部的にはクエリビルダのメソッドをcallしている事が多いので、何も考えずにクエリビルダで出来ていた事をそのままEloquentで実行していたらハマってしまいました、落とし穴に。おかげで「なるほど、そういう事もあるのだな」位の心構えができるようになりました。今回はそんな気づきを与えてくれたvalue()についての解説です。\nvalue()とは？ value()はクエリの最初の結果から特定の項目のみ取得して返してくれる便利なメソッドです。DBからfetchするレコードが１つだと分かっていて、且つ、特定の１項目のみが必要な場合などに重宝します。\n例えば、ユーザの名前と生年月日の組み合わせがユニークであり、そのemailを取得したい場合は\n$email = DB::table(\u0026#39;user\u0026#39;) -\u0026gt;where(\u0026#39;name\u0026#39;, \u0026#39;=\u0026#39;, \u0026#39;山田太郎\u0026#39;) -\u0026gt;where(\u0026#39;birthday\u0026#39;, \u0026#39;=\u0026#39;, \u0026#39;1970-01-01\u0026#39;) -\u0026gt;value(\u0026#39;email\u0026#39;); \u0026gt;\u0026gt;\u0026gt; \u0026#34;y.taro@sample.com\u0026#34; と書けば一発で取得できます。 上記はクエリビルダでの取得ですが、Eloquentでも同様にvalue()を使うことが出来ます。\n$email = User::query() -\u0026gt;where(\u0026#39;name\u0026#39;, \u0026#39;=\u0026#39;, \u0026#39;山田太郎\u0026#39;) -\u0026gt;where(\u0026#39;birthday\u0026#39;, \u0026#39;=\u0026#39;, \u0026#39;1970-01-01\u0026#39;) -\u0026gt;value(\u0026#39;email\u0026#39;); \u0026gt;\u0026gt;\u0026gt; \u0026#34;y.taro@sample.com\u0026#34; いずれも実行されるsqlは以下の通り、\nselect `email` from `member` where `name` = \u0026#39;山田太郎\u0026#39; and `birthday` = \u0026#39;1970-01-01\u0026#39; limit 1; 特定の項目のみselectしつつ、limitで取得する結果を１つに限定している点がポイントです。\nこうしてみると、クエリビルダとEloquentのvalue()は一見同じ処理を行っているように見えますね。 しかし、次の例では異なる結果を返します。\n私がハマった落とし穴 以下ではとあるブログの投稿(post)に紐付いたタグ名(tag.name)を取得しGROUP_CONCATで連結して取得しました。\n$tags = DB::table(\u0026#39;post\u0026#39;) -\u0026gt;join(\u0026#39;tag\u0026#39;, \u0026#39;post.post_id\u0026#39;, \u0026#39;=\u0026#39;, \u0026#39;tag.post_id\u0026#39;) -\u0026gt;where(\u0026#39;post.post_id\u0026#39;, \u0026#39;=\u0026#39;, 1) -\u0026gt;groupBy(\u0026#39;post.post_id\u0026#39;) -\u0026gt;value(DB::raw(\u0026#39;GROUP_CONCAT(tag.name)\u0026#39;)); \u0026gt;\u0026gt; \u0026#34;Laravel,PHP,Eloquent\u0026#34; クエリビルダではカンマで連結されたタグ名が取得できました。 Eloquentではどうでしょうか？\n$tags = Post::query() -\u0026gt;join(\u0026#39;tag\u0026#39;, \u0026#39;post.post_id\u0026#39;, \u0026#39;=\u0026#39;, \u0026#39;tag.post_id\u0026#39;) -\u0026gt;where(\u0026#39;post.post_id\u0026#39;, \u0026#39;=\u0026#39;, 1) -\u0026gt;groupBy(\u0026#39;post.post_id\u0026#39;) -\u0026gt;value(DB::raw(\u0026#39;GROUP_CONCAT(tag.name)\u0026#39;)); \u0026gt;\u0026gt; null あれれ、どういう訳かnullが返されました。\nソースを解析 以下はEloquentのvalue()のソースコードです。\nvendor/laravel/framework/src/Illuminate/Database/Eloquent/Builder.php /** * Get a single column\u0026#39;s value from the first result of a query. * * @param string|\\Illuminate\\Database\\Query\\Expression $column * @return mixed */ public function value($column) { if ($result = $this-\u0026gt;first([$column])) { return $result-\u0026gt;{Str::afterLast($column, \u0026#39;.\u0026#39;)}; } } なるほど、first([$column])でクエリ結果を取得し、$columnで指定した値のコロン（\u0026rsquo;.\u0026rsquo;）以降をプロパティ名としてアクセスしています。\nつまり先ほどの例だと、$columnはGROUP_CONCAT(tag.name)なので、コロン以降はname)となります。従って、Undefined propertyとして処理され、nullとなった訳です。\n一方、結果が正しく取得できたクエリビルダはというと、\nvendor/laravel/framework/src/Illuminate/Database/Query/Builder.php /** * Get a single column\u0026#39;s value from the first result of a query. * * @param string $column * @return mixed */ public function value($column) { $result = (array) $this-\u0026gt;first([$column]); return count($result) \u0026gt; 0 ? reset($result) : null; } 配列に変換した上で、reset()を使って１つ目の結果を取得していました。\n回避策 回避策は色々ありそうですが、私の中では生クエリ（DB::raw）とvalue()を併用したい場合は一度selectRaw()などで別名を付けるルールを採用する事にしました。\n$tags = Post::query() -\u0026gt;join(\u0026#39;tag\u0026#39;, \u0026#39;post.post_id\u0026#39;, \u0026#39;=\u0026#39;, \u0026#39;tag.post_id\u0026#39;) -\u0026gt;where(\u0026#39;post.post_id\u0026#39;, \u0026#39;=\u0026#39;, 1) -\u0026gt;groupBy(\u0026#39;post.post_id\u0026#39;) -\u0026gt;selectRaw(\u0026#39;GROUP_CONCAT(tag.name) AS tag_names\u0026#39;) -\u0026gt;value(\u0026#39;tag_names\u0026#39;); \u0026gt;\u0026gt; \u0026#34;Laravel,PHP,Eloquent\u0026#34; 現場からは以上です。\n","date":"2022-07-26T23:31:57+09:00","permalink":"https://www.larajapan.com/2022/07/26/eloquent%E3%81%A8query-builder%E3%81%AEvalue%E3%81%AE%E9%81%95%E3%81%84/","title":"EloquentとQuery Builderのvalue()の違い"},{"content":"以前の投稿「FormRequestで入力値を補正」でチェックボックスがオフのときの値の補正に関して説明しました。今度はFormRequestを使わないときの補正の仕方です。\nこのような入力画面があるとします。\nそれを受けるコントローラのメソッドを以下とすると、\n... public function update(Request $request, Customer $customer) { $validated = $request-\u0026gt;validate([ \u0026#39;active_flag\u0026#39; =\u0026gt; [\u0026#39;nullable\u0026#39;], \u0026#39;email\u0026#39; =\u0026gt; [\u0026#39;required\u0026#39;, \u0026#39;email\u0026#39;, \u0026#39;unique:customer,email\u0026#39;], \u0026#39;name\u0026#39; =\u0026gt; [\u0026#39;required\u0026#39;], \u0026#39;name_kana\u0026#39; =\u0026gt; [\u0026#39;required\u0026#39;], ]); dd($validated); } ... このような配列が返ってきます。\n[ \u0026#39;active_flag\u0026#39; =\u0026gt; \u0026#39;Y\u0026#39;, \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;test@example.com\u0026#39;, \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;テスト太郎\u0026#39;, \u0026#39;name_kana\u0026#39; =\u0026gt; \u0026#39;てすとたろう\u0026#39;, ] しかし、先のチェックボックス「有効」がオフだとすると、\n[ \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;test@example.com\u0026#39;, \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;テスト太郎\u0026#39;, \u0026#39;name_kana\u0026#39; =\u0026gt; \u0026#39;てすとたろう\u0026#39;, ] と、active_flagの項目はリクエストには存在しません。チェックボックスがオフだとブラウザはサーバーに何も返さないからです。\nしかし、それをコントローラで判断してDBに無効（この場合N）を保存したいです。\nそのような場合は、RequestのメソッドのmergeIfMissing()を使います。オフのときの値を引数として与えます。複数の項目の指定も可能です。\n... public function update(Request $request, Customer $customer) { $request-\u0026gt;mergeIfMissing([\u0026#39;active_flag\u0026#39; =\u0026gt; \u0026#39;N\u0026#39;]); // デフォルトの値を設定！ $validated = $request-\u0026gt;validate([ \u0026#39;active_flag\u0026#39; =\u0026gt; [\u0026#39;required\u0026#39;, \u0026#39;In:Y,N\u0026#39;], // 必須と値を確認！ \u0026#39;email\u0026#39; =\u0026gt; [\u0026#39;required\u0026#39;, \u0026#39;email\u0026#39;, \u0026#39;unique:customer,email\u0026#39;], \u0026#39;name\u0026#39; =\u0026gt; [\u0026#39;required\u0026#39;], \u0026#39;name_kana\u0026#39; =\u0026gt; [\u0026#39;required\u0026#39;], ]); dd($validated); } ... 値は補正されて、期待通りにNが入ってきます。上のバリデーションルールで見られるように、値が必須であること、値がYあるいはNであることもチェックできます。\n[ \u0026#39;active_flag\u0026#39; =\u0026gt; \u0026#39;N\u0026#39;, \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;test@example.com\u0026#39;, \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;テスト太郎\u0026#39;, \u0026#39;name_kana\u0026#39; =\u0026gt; \u0026#39;てすとたろう\u0026#39;, ] ","date":"2022-07-09T00:21:25+09:00","permalink":"https://www.larajapan.com/2022/07/09/%E3%83%81%E3%82%A7%E3%83%83%E3%82%AF%E3%83%9C%E3%83%83%E3%82%AF%E3%82%B9%E3%81%AE%E5%85%A5%E5%8A%9B%E5%80%A4-mergeifmissing/","title":"チェックボックスの入力値 - mergeIfMissing()"},{"content":"Eloquentモデルを配列に変換して渡す必要がある際にたまに使うtoArray()ですが。先日何気なく使用していて思わぬエラーに遭遇、原因を調べてみると実はLaravel 7.xから変更されていた仕様でした。Upgrade Guideには目を通していたつもりでしたが、どうしてなかなか行き当たりばったりなキャッチアップになってしまう、Lazy Loadingな私です。\n$user-\u003etoArray() まず、toArray()を使用していて遭遇したエラーについて説明します。 Laravelインストール後にデフォルトで存在するUserモデルをtoArray()で変換してみましょう。 // factory でダミーレコード生成 \u0026gt;\u0026gt;\u0026gt; $user = User::factory()-\u0026gt;create(); =\u0026gt; App\\Models\\User {#3575 name: \u0026#34;Mr. Buck Schinner MD\u0026#34;, email: \u0026#34;waters.montana@example.com\u0026#34;, email_verified_at: \u0026#34;2022-06-05 04:34:30\u0026#34;, #password: \u0026#34;$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi\u0026#34;, #remember_token: \u0026#34;wp5fKxXY7s\u0026#34;, updated_at: \u0026#34;2022-06-05 04:34:30\u0026#34;, created_at: \u0026#34;2022-06-05 04:34:30\u0026#34;, id: 1, } \u0026gt;\u0026gt;\u0026gt; $user-\u0026gt;toArray(); =\u0026gt; [ \u0026#34;name\u0026#34; =\u0026gt; \u0026#34;Mr. Buck Schinner MD\u0026#34;, \u0026#34;email\u0026#34; =\u0026gt; \u0026#34;waters.montana@example.com\u0026#34;, \u0026#34;email_verified_at\u0026#34; =\u0026gt; \u0026#34;2022-06-05T04:34:30.000000Z\u0026#34;, \u0026#34;updated_at\u0026#34; =\u0026gt; \u0026#34;2022-06-05T04:34:30.000000Z\u0026#34;, \u0026#34;created_at\u0026#34; =\u0026gt; \u0026#34;2022-06-05T04:34:30.000000Z\u0026#34;, \u0026#34;id\u0026#34; =\u0026gt; 1, ] お気づきでしょうか？ email_verified_at, updated_at, created_at の日付形式がY-m-d H:i:sからISO-8601に変換されています。\nなぜでしょうか？\n日付のフォーマット これはLaravel 7から内部的にCarbonのtoJson()を使用するようになったためのようです。\nhttps://laravel.com/docs/7.x/upgrade#date-serialization\n確かに、CarbonでtoJson()を使用するとISO-8601形式で日付が出力されますね。\nCarbon::today()-\u0026gt;toJson(); =\u0026gt; \u0026#34;2022-06-05T00:00:00.000000Z\u0026#34; とは言え、全てのdatetimeやtimestamp型の項目がフォーマットされるわけではなく、デフォルトではcreated_atとupdated_atのみです。 その他の項目についてはemail_verified_atの様に、$castsにてdatetimeを指定すると同様にISO-8601形式でフォーマットされます。\nデフォルトのフォーマットを変更する 今回私が遭遇したケースではcreated_atとupdated_atをY-m-d H:i:sの形式のまま配列に変換する必要がありました。 toArray()で変換する際に日付のフォーマットを指定するにはどうすれば良いでしょう？\n$casts １つはemail_verified_atの様に$castsにて個別に指定する方法です。以下の様に、datetime:に続いて出力したいフォーマットを指定することができます。\napp/Models/User.php ... /** * The attributes that should be cast to native types. * * @var array */ protected $casts = [ \u0026#39;email_verified_at\u0026#39; =\u0026gt; \u0026#39;datetime\u0026#39;, \u0026#39;created_at\u0026#39; =\u0026gt; \u0026#39;datetime:Y-m-d H:i:s\u0026#39;, // ←日付の形式を指定 \u0026#39;updated_at\u0026#39; =\u0026gt; \u0026#39;datetime:Y-m-d H:i:s\u0026#39;, // ←日付の形式を指定 ]; ... tinkerで出力を確認してみましょう。\n\u0026gt;\u0026gt;\u0026gt; $user-\u0026gt;toArray(); =\u0026gt; [ \u0026#34;id\u0026#34; =\u0026gt; 1, \u0026#34;name\u0026#34; =\u0026gt; \u0026#34;Dr. Wanda Gottlieb\u0026#34;, \u0026#34;email\u0026#34; =\u0026gt; \u0026#34;brenda39@example.com\u0026#34;, \u0026#34;email_verified_at\u0026#34; =\u0026gt; \u0026#34;2022-03-09T13:55:28.000000Z\u0026#34;, \u0026#34;created_at\u0026#34; =\u0026gt; \u0026#34;2022-03-09 13:55:28\u0026#34;, \u0026#34;updated_at\u0026#34; =\u0026gt; \u0026#34;2022-03-09 13:55:28\u0026#34;, ] 正しく反映されていますね。\nserializeDate もう１つはserializeDate()をオーバーライドすることです。このメソッドはModelクラスがuseしているtraitのHasAttributesに定義されており、toArray()実行時に内部的に呼び出されています。このメソッドをオーバーライドすることでcreated_atとupdated_atを含めdatetimeにキャストしている項目のtoArray(), toJson()時のデフォルトのフォーマットを指定できます。フォーマットは以下の様にformat()で指定します。\napp/Models/User.php ... use DateTimeInterface; ... /** * Prepare a date for array / JSON serialization. * * @param \\DateTimeInterface $date * @return string */ protected function serializeDate(DateTimeInterface $date) { return $date-\u0026gt;format(\u0026#39;Y-m-d H:i:s\u0026#39;); } ... 因みに、serializeDate()で指定しているのはデフォルトのフォーマットなので、$castsでのフォーマット指定と併用した場合は$casts側の指定が優先されます。以下は併用した例です。\napp/Models/User.php ... /** * The attributes that should be cast to native types. * * @var array */ protected $casts = [ \u0026#39;email_verified_at\u0026#39; =\u0026gt; \u0026#39;datetime:Y-m-d\u0026#39;, // 日付のみに指定してみる ]; ... toArray()すると、created_at, updated_atはY-m-d H:i:sですが、email_verified_atはY-m-dのみの出力となります。\n\u0026gt;\u0026gt;\u0026gt; $user-\u0026gt;toArray(); =\u0026gt; [ \u0026#34;id\u0026#34; =\u0026gt; 1, \u0026#34;name\u0026#34; =\u0026gt; \u0026#34;Dr. Wanda Gottlieb\u0026#34;, \u0026#34;email\u0026#34; =\u0026gt; \u0026#34;brenda39@example.com\u0026#34;, \u0026#34;email_verified_at\u0026#34; =\u0026gt; \u0026#34;2022-03-09\u0026#34;, // Y-m-d のみ \u0026#34;created_at\u0026#34; =\u0026gt; \u0026#34;2022-03-09 13:55:28\u0026#34;, \u0026#34;updated_at\u0026#34; =\u0026gt; \u0026#34;2022-03-09 13:55:28\u0026#34;, ] ","date":"2022-06-28T09:46:39+09:00","permalink":"https://www.larajapan.com/2022/06/28/toarray%E3%81%A7created_atupdated_at%E3%81%AE%E3%83%95%E3%82%A9%E3%83%BC%E3%83%9E%E3%83%83%E3%83%88%E3%81%8C%E5%A4%89%E3%82%8F%E3%82%8B/","title":"toArray()でcreated_at,updated_atのフォーマットが変わる"},{"content":"FormRequestのバリデーションを問題なくパスしたら、コントローラで$request-\u0026gt;validatedで配列としてフォームの入力値を取得できます。取得後の値は安全なのでそのままDBに保存することも可能です。しかし、その配列から必要のない値を抜いたり、足りない値を追加したりとかの処理はどうするかという説明です。\n会員登録のFormRequest 例として会員登録の画面を使います。画面はこんな感じです。 その画面の入力をチェックするFormRequestは以下と定義します。\nnamespace App\\Http\\Requests; use App\\Rules\\CaptchaRule; use App\\Rules\\CustomerPasswordRule; use Illuminate\\Foundation\\Http\\FormRequest; class CustomerRequest extends FormRequest { public function rules() { $rules = [ \u0026#39;email\u0026#39; =\u0026gt; [\u0026#39;required\u0026#39;, \u0026#39;email\u0026#39;, \u0026#39;unique:customer,email\u0026#39;], \u0026#39;password\u0026#39; =\u0026gt; [\u0026#39;required\u0026#39;, new CustomerPasswordRule, \u0026#39;confirmed\u0026#39;], \u0026#39;name\u0026#39; =\u0026gt; [\u0026#39;required\u0026#39;], \u0026#39;g-recaptcha-response\u0026#39; =\u0026gt; [\u0026#39;required\u0026#39;, new CaptchaRule], ]; return $rules; } } CustomerPasswordRuleやCaptchaRuleは事前に用意したカスタムのバリデーションのクラスです。\nCustomerRequestのバリデーションを問題なく通過したら、以下のようにコントローラでDBに保存します。\nnamespace App\\Http\\Controllers\\User; use App\\Http\\Controllers\\Controller; use App\\Http\\Requests\\CustomerRequest; use App\\Models\\Customer; use Illuminate\\Foundation\\Auth\\RegistersUsers; class RegisterController extends Controller { use RegistersUsers; ... /** * 会員の登録 * * @param \\App\\Http\\Requests\\CustomerRequest $request * @return \\Illuminate\\Http\\RedirectResponse */ public function register(CustomerRequest $request) { $validated = $request-\u0026gt;validated(); Customer::create($validated); ... } } ここでのregister()メソッドの$validatedの値の中身を見てみましょう。\n$validated = [ \u0026#34;email\u0026#34; =\u0026gt; \u0026#34;test@example.com\u0026#34;, \u0026#34;password\u0026#34; =\u0026gt; \u0026#34;Test1234567890!\u0026#34;, \u0026#34;name\u0026#34; =\u0026gt; \u0026#34;テスト太郎\u0026#34;, \u0026#34;g-recaptcha-response\u0026#34; =\u0026gt; \u0026#34;03AGdBq26f1ycQWiqz2OeBFz....\u0026#34;, ] DBに保存するのに必要なのは、email, password（暗号化する）、nameの３つだけで、キャプチャのg-recaptach-responseは必要でありませんので、先の配列値から抜く必要あります。また、会員が有効かどうかを指定するactive_flagが必要なので、その値を配列に加える必要あります。\nこのような処理を行うには、Laravelのバージョン8.x以降では、以下のようにvalidated()を使わずに、safe(), merge(), except()を使って可能です。\n... $input = $request-\u0026gt;safe() -\u0026gt;merge([\u0026#39;active_flag\u0026#39; =\u0026gt; \u0026#39;Y\u0026#39;]) -\u0026gt;except([\u0026#39;g-recaptcha-response\u0026#39;]); Customer::create($input); ... safeは、バリデーションをパスした値を抽出し、mergeは値を追加、exceptは値を削除です。しかし、mergeとexceptの使用の順番には注意してください。これらを逆にするとエラーとなってしまいます。なぜなら、exceptは配列を返すのでその後にメソッドの連結ができません。\n処理の結果の配列は、以下のようになります。\n$input = [ \u0026#34;email\u0026#34; =\u0026gt; \u0026#34;test@example.com\u0026#34;, \u0026#34;password\u0026#34; =\u0026gt; \u0026#34;Test1234567890!\u0026#34;, \u0026#34;name\u0026#34; =\u0026gt; \u0026#34;テスト太郎\u0026#34;, \u0026#34;active_flag\u0026#34; =\u0026gt; \u0026#34;Y\u0026#34;, ] よりフレキシブルな処理を望むなら、以下のようにcollect()を通してCollectionのオブジェクトにすることです。\n... $input = $request-\u0026gt;safe() -\u0026gt;collect() -\u0026gt;except([\u0026#39;g-recaptcha-response\u0026#39;]) -\u0026gt;merge([\u0026#39;active_flag\u0026#39; =\u0026gt; \u0026#39;Y\u0026#39;]) -\u0026gt;all(); Customer::create($input); ... さて、safe()の対応がないLaravel 7.xではどう対応しましょう。 こちらも、配列をCollectionにしてしまえば、問題ありません。\n... $input = collect($request-\u0026gt;validated()) -\u0026gt;except([\u0026#39;g-recaptcha-response\u0026#39;]) -\u0026gt;merge([\u0026#39;active_flag\u0026#39; =\u0026gt; \u0026#39;Y\u0026#39;] -\u0026gt;all(); Customer::create($input); ... 私としては、Collectionを使用した方が好きです。 以下のように、ケースにより配列の中身を変えることも簡単です。\n$input = collect($request-\u0026gt;validated()) -\u0026gt;when(! $request-\u0026gt;active_flag, function ($collection) { return $collection-\u0026gt;put(\u0026#39;active_flag\u0026#39;, \u0026#39;N\u0026#39;); // 値がないときはNとする }) -\u0026gt;when(! $request-\u0026gt;paymethod_id, function ($collection) { $collection-\u0026gt;forget([ // 値がないときは、以下の設定は必要ではない \u0026#39;paymethod_id\u0026#39;, \u0026#39;card_number\u0026#39;, \u0026#39;card_exp\u0026#39;, \u0026#39;card_holder\u0026#39;, ]); return $collection; }) -\u0026gt;all(); ","date":"2022-06-20T06:07:34+09:00","permalink":"https://www.larajapan.com/2022/06/20/%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E5%BE%8C%E3%81%AErequest%E3%81%AE%E5%87%A6%E7%90%86/","title":"バリデーション後のRequestの処理"},{"content":"３年前に書いた投稿の更新です（その投稿自体その４年前の投稿の更新です）。factoryからEloquentのインスタンスの作成も変わり、また新たな発見がありました。\nまず、tinkerを使用して、isDirty()の復習です。 factory()を使って、１つレコードを作成します。\n\u0026gt;\u0026gt;\u0026gt; User::factory()-\u0026gt;create(); =\u0026gt; App\\Models\\User {#3632 name: \u0026#34;村山 涼平\u0026#34;, email: \u0026#34;ekoda.asuka@example.com\u0026#34;, email_verified_at: \u0026#34;2022-06-12 20:18:11\u0026#34;, #password: \u0026#34;$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi\u0026#34;, #remember_token: \u0026#34;h7d19XhYd4\u0026#34;, updated_at: \u0026#34;2022-06-12 20:18:11\u0026#34;, created_at: \u0026#34;2022-06-12 20:18:11\u0026#34;, id: 1, } 作成したレコードの名前を更新しますが、DBでなくオブジェクトのみの更新のために、fill()を使用します。\n\u0026gt;\u0026gt;\u0026gt; $user = User::find(1); =\u0026gt; App\\Models\\User {#3949 id: 1, name: \u0026#34;村山 涼平\u0026#34;, email: \u0026#34;ekoda.asuka@example.com\u0026#34;, email_verified_at: \u0026#34;2022-06-12 20:18:11\u0026#34;, #password: \u0026#34;$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi\u0026#34;, #remember_token: \u0026#34;h7d19XhYd4\u0026#34;, created_at: \u0026#34;2022-06-12 20:18:11\u0026#34;, updated_at: \u0026#34;2022-06-12 20:18:11\u0026#34;, } \u0026gt;\u0026gt;\u0026gt; $user-\u0026gt;fill([\u0026#39;name\u0026#39; =\u0026gt; \u0026#39;山田太郎\u0026#39;]); =\u0026gt; App\\Models\\User {#3949 id: 1, name: \u0026#34;山田太郎\u0026#34;, email: \u0026#34;ekoda.asuka@example.com\u0026#34;, email_verified_at: \u0026#34;2022-06-12 20:18:11\u0026#34;, #password: \u0026#34;$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi\u0026#34;, #remember_token: \u0026#34;h7d19XhYd4\u0026#34;, created_at: \u0026#34;2022-06-12 20:18:11\u0026#34;, updated_at: \u0026#34;2022-06-12 20:18:11\u0026#34;, } nameが変わりましたね。 実際、オブジェクトが更新したかどうかは、isDirty()を使います。\n\u0026gt;\u0026gt;\u0026gt; $user-\u0026gt;isDirty(); =\u0026gt; true 変更していますね。 項目ごとはどうでしょう？\n\u0026gt;\u0026gt;\u0026gt; $user-\u0026gt;isDirty(\u0026#39;name\u0026#39;); =\u0026gt; true \u0026gt;\u0026gt;\u0026gt; $user-\u0026gt;isDirty(\u0026#39;email\u0026#39;); =\u0026gt; false nameの変更がありますが、emailの変更はなしです。正しいです。\nここで、変更したオブジェクトをもとに対応するレコードをDBに保存します。\n\u0026gt;\u0026gt;\u0026gt; $user-\u0026gt;save(); =\u0026gt; true さて、この時点でのisDirty()の返り値は？\n\u0026gt;\u0026gt;\u0026gt; $user-\u0026gt;isDirty(); =\u0026gt; false \u0026gt;\u0026gt;\u0026gt; $user-\u0026gt;isDirty(\u0026#39;name\u0026#39;); =\u0026gt; false もう、isDirty()では、変更があったかどうかの判断はできません、何もDirtyではありません。\nこの時点、つまりオブジェクトの更新をDBに反映した時点でレコードが更新したかどうかを判断するには、isDirty()ではなく、wasChanged()を使用します。\n\u0026gt;\u0026gt;\u0026gt; $user-\u0026gt;wasChanged(); =\u0026gt; true \u0026gt;\u0026gt;\u0026gt; $user-\u0026gt;wasChanged(\u0026#39;name\u0026#39;); =\u0026gt; true \u0026gt;\u0026gt;\u0026gt; $user-\u0026gt;wasChanged(\u0026#39;email\u0026#39;); =\u0026gt; false save()前のisDirty()の結果とまったく同じです。\n実際には、こんなコードでwasChanged()は使われます。\n... $user-\u0026gt;fill([\u0026#39;name\u0026#39; =\u0026gt; \u0026#39;山田太郎\u0026#39;]); if ($user-\u0026gt;isDirty()) { $user-\u0026gt;save(); if ($user-\u0026gt;wasChagned(\u0026#39;name\u0026#39;)) { //nameが更新されたときのみ、以下のコードを実行 ... } } ... さてさて、上の話は既存にあるレコードを更新したときの話です。新規のレコード作成のときもチェックしてみます。\n\u0026gt;\u0026gt;\u0026gt; $user = User::factory()-\u0026gt;make(); =\u0026gt; App\\Models\\User {#4588 name: \u0026#34;浜田 京助\u0026#34;, email: \u0026#34;tsubasa.saito@example.org\u0026#34;, email_verified_at: \u0026#34;2022-06-12 20:21:59\u0026#34;, #password: \u0026#34;$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi\u0026#34;, #remember_token: \u0026#34;ZzZh0fHqJA\u0026#34;, } 先ほどと違うのは、create()の代わりにmake()が使用されているところです。create()と違ってまだDBレコードは作成されていません。 この時点では、\n\u0026gt;\u0026gt;\u0026gt; $user-\u0026gt;isDirty(); =\u0026gt; true 更新と同じですね。 DBにレコードを保存してみます。\n\u0026gt;\u0026gt;\u0026gt; $user-\u0026gt;save(); =\u0026gt; true さて、この時点でのisDirty()の返り値は？\n\u0026gt;\u0026gt;\u0026gt; $user-\u0026gt;isDirty(); =\u0026gt; false 期待通りです。 しかし、\n\u0026gt;\u0026gt;\u0026gt; $user-\u0026gt;wasChanged(); =\u0026gt; false こちらは、更新時と違ってtrueとはなりません。wasChangedは訳せば更新したかという意味であり、作成されたかではないとすればfalseになるのは正しいのでしょう。\nしかし、$userのようなインスタンスが既存のレコードを更新したのか、新規に作成されたのかが前もってわからないときの判断としてwasChanged()を使用できません。とかいってwasCreated()というメソッドもないのです。滅多に直面するケースは少ないですが、注意しましょう。\n","date":"2022-06-13T05:46:58+09:00","permalink":"https://www.larajapan.com/2022/06/13/isdirty-vs-waschanged%EF%BC%88%E6%9B%B4%E6%96%B0%EF%BC%89/","title":"isDirty() vs wasChanged()（更新）"},{"content":"あるプロジェクトで使用されているブレードファイル内のHTML文の置換が必要となりました。aritisanコマンドを作成して、resources/viewsのファイル１つずつオープンして上書きが必要です。さて、問題はサブフォルダーやサブサブフォルダーがあるフォルダーからどうやってファイル名を取得するか。\nresources/viewsのフォルダー まずは、treeコマンドで目的のフォルダー内を見てみます。\nresources/views ├── auth │ ├── confirm-password.blade.php │ ├── forgot-password.blade.php │ ├── login.blade.php │ ├── passwords │ │ ├── confirm.blade.php │ │ ├── email.blade.php │ │ └── reset.blade.php │ ├── register.blade.php │ ├── reset-password.blade.php │ ├── verify.blade.php │ └── verify-email.blade.php ├── dashboard.blade.php ├── home.blade.php ├── layouts │ ├── app.blade.php │ ├── guest.blade.php │ └── navigation.blade.php ├── test.blade.php ├── user │ └── search.blade.php ├── vendor │ └── pagination │ ├── bootstrap-4.blade.php │ ├── default.blade.php │ ├── semantic-ui.blade.php │ ├── simple-bootstrap-4.blade.php │ ├── simple-default.blade.php │ ├── simple-tailwind.blade.php │ └── tailwind.blade.php └── welcome.blade.php 結構ネストされていますね。\nここのblade.phpファイルをすべて取得するのが今回のゴールです。\nWebmozart Glob 調査すると、もうすでに素晴らしいパッケージがこのためにありました。\n$ composer require webmozart/glob とパッケージをインストールして、tinkerで実行します。\n\u0026gt;\u0026gt;\u0026gt; Glob::glob(base_path(\u0026#39;resources/views/**/*.blade.php\u0026#39;)); =\u0026gt; [ \u0026#34;/var/www/repos/l8x/resources/views/auth/confirm-password.blade.php\u0026#34;, \u0026#34;/var/www/repos/l8x/resources/views/auth/forgot-password.blade.php\u0026#34;, \u0026#34;/var/www/repos/l8x/resources/views/auth/login.blade.php\u0026#34;, \u0026#34;/var/www/repos/l8x/resources/views/auth/passwords/confirm.blade.php\u0026#34;, \u0026#34;/var/www/repos/l8x/resources/views/auth/passwords/email.blade.php\u0026#34;, \u0026#34;/var/www/repos/l8x/resources/views/auth/passwords/reset.blade.php\u0026#34;, \u0026#34;/var/www/repos/l8x/resources/views/auth/register.blade.php\u0026#34;, \u0026#34;/var/www/repos/l8x/resources/views/auth/reset-password.blade.php\u0026#34;, \u0026#34;/var/www/repos/l8x/resources/views/auth/verify-email.blade.php\u0026#34;, \u0026#34;/var/www/repos/l8x/resources/views/auth/verify.blade.php\u0026#34;, \u0026#34;/var/www/repos/l8x/resources/views/dashboard.blade.php\u0026#34;, \u0026#34;/var/www/repos/l8x/resources/views/home.blade.php\u0026#34;, \u0026#34;/var/www/repos/l8x/resources/views/layouts/app.blade.php\u0026#34;, \u0026#34;/var/www/repos/l8x/resources/views/layouts/guest.blade.php\u0026#34;, \u0026#34;/var/www/repos/l8x/resources/views/layouts/navigation.blade.php\u0026#34;, \u0026#34;/var/www/repos/l8x/resources/views/test.blade.php\u0026#34;, \u0026#34;/var/www/repos/l8x/resources/views/user/search.blade.php\u0026#34;, \u0026#34;/var/www/repos/l8x/resources/views/vendor/pagination/bootstrap-4.blade.php\u0026#34;, \u0026#34;/var/www/repos/l8x/resources/views/vendor/pagination/default.blade.php\u0026#34;, \u0026#34;/var/www/repos/l8x/resources/views/vendor/pagination/semantic-ui.blade.php\u0026#34;, \u0026#34;/var/www/repos/l8x/resources/views/vendor/pagination/simple-bootstrap-4.blade.php\u0026#34;, \u0026#34;/var/www/repos/l8x/resources/views/vendor/pagination/simple-default.blade.php\u0026#34;, \u0026#34;/var/www/repos/l8x/resources/views/vendor/pagination/simple-tailwind.blade.php\u0026#34;, \u0026#34;/var/www/repos/l8x/resources/views/vendor/pagination/tailwind.blade.php\u0026#34;, \u0026#34;/var/www/repos/l8x/resources/views/welcome.blade.php\u0026#34;, ] 素晴らしい！\nフィルターをかけている部分の /**/ は、resources/views/以下のフォルダーとサブフォルダー（階層OK）にマッチします。そして、 *.blade.php は、拡張子が.blade.phpのファイルとマッチします。\nphpのglob関数との違い フォルダー内のファイル名を取得する関数はすでにphpの標準関数としてglob()がありますが、先のパッケージのglobと違って１レベルのフォルダーしか対応しません。\nつまり、\n\u0026gt;\u0026gt;\u0026gt; glob(\u0026#39;resources/views/*.blade.php\u0026#39;) =\u0026gt; [ \u0026#34;resources/views/dashboard.blade.php\u0026#34;, \u0026#34;resources/views/home.blade.php\u0026#34;, \u0026#34;resources/views/test.blade.php\u0026#34;, \u0026#34;resources/views/welcome.blade.php\u0026#34;, ] \u0026gt;\u0026gt;\u0026gt; glob(\u0026#39;resources/views/*/*.blade.php\u0026#39;) =\u0026gt; [ \u0026#34;resources/views/auth/confirm-password.blade.php\u0026#34;, \u0026#34;resources/views/auth/forgot-password.blade.php\u0026#34;, \u0026#34;resources/views/auth/login.blade.php\u0026#34;, \u0026#34;resources/views/auth/register.blade.php\u0026#34;, \u0026#34;resources/views/auth/reset-password.blade.php\u0026#34;, \u0026#34;resources/views/auth/verify-email.blade.php\u0026#34;, \u0026#34;resources/views/auth/verify.blade.php\u0026#34;, \u0026#34;resources/views/layouts/app.blade.php\u0026#34;, \u0026#34;resources/views/layouts/guest.blade.php\u0026#34;, \u0026#34;resources/views/layouts/navigation.blade.php\u0026#34;, \u0026#34;resources/views/user/search.blade.php\u0026#34;, ] \u0026gt;\u0026gt;\u0026gt; glob(\u0026#39;resources/views/*/*/*.blade.php\u0026#39;) =\u0026gt; [ \u0026#34;resources/views/auth/passwords/confirm.blade.php\u0026#34;, \u0026#34;resources/views/auth/passwords/email.blade.php\u0026#34;, \u0026#34;resources/views/auth/passwords/reset.blade.php\u0026#34;, \u0026#34;resources/views/vendor/pagination/bootstrap-4.blade.php\u0026#34;, \u0026#34;resources/views/vendor/pagination/default.blade.php\u0026#34;, \u0026#34;resources/views/vendor/pagination/semantic-ui.blade.php\u0026#34;, \u0026#34;resources/views/vendor/pagination/simple-bootstrap-4.blade.php\u0026#34;, \u0026#34;resources/views/vendor/pagination/simple-default.blade.php\u0026#34;, \u0026#34;resources/views/vendor/pagination/simple-tailwind.blade.php\u0026#34;, \u0026#34;resources/views/vendor/pagination/tailwind.blade.php\u0026#34;, ] ということは、同じレベルに存在するサブフォルダーごとに再帰処理が必要となり、不可能でないものの少々ややこしいプログラムとなりますね。\n","date":"2022-05-31T06:28:03+09:00","permalink":"https://www.larajapan.com/2022/05/31/%E3%83%95%E3%82%A9%E3%83%AB%E3%83%80%E3%83%BC%E3%82%84%E3%82%B5%E3%83%96%E3%83%95%E3%82%A9%E3%83%AB%E3%83%80%E5%86%85%E3%81%AE%E3%81%99%E3%81%B9%E3%81%A6%E3%81%AE%E3%83%96%E3%83%AC%E3%83%BC%E3%83%89/","title":"フォルダーやサブフォルダ内のすべてのブレードファイル名の取得"},{"content":"whereIn()はLaravelではEloquentやQuery Builderで良く使われます。特にwith()メソッドでは自動的に。今までこのクエリが返すレコードの順番はたいして気にしていなかったのですが、これはどうしたものか、という状況にぶち当たりました。\nSQLというのは何も指定しない、つまりorderByを指定しないとSQLが返すレコードの順番は保証がありません。以下見てください。\n\u0026gt;\u0026gt;\u0026gt; User::whereIn(\u0026#39;id\u0026#39;, [10,1,3])-\u0026gt;pluck(\u0026#39;id\u0026#39;); =\u0026gt; Illuminate\\Support\\Collection {#4581 all: [ 1, 3, 10, ], } 直感的には、10,1,3を返すと思うところ、なぜかその順番にソートされていません。自動的にid順となっています。 多分、idがDBテーブルusersのプライマリーキーだから、その順で返している？\nとにかく、orderByを明確に指定していないので返す順を頼りにすることはできません。\nしかし、状況によっては指定した順番で返す必要あります。以下のように、お客さんが閲覧した商品の順番、最新閲覧した商品を一番左に持ってきます。\n例えば、この商品のidがクッキーにその表示順でidを保存した場合は、その表示順で商品名や価格をDBから取得してくる必要あります。\nこの場合は、一般的はDBの項目の値に基づくorderByは使えません（もちろん、閲覧日時とかを別のDBテーブルに保存していないという仮定で）。\nさて、どうしましょう？とリサーチすると便利なSQL関数がありました。 こんな感じで使います。\n\u0026gt;\u0026gt;\u0026gt; User::whereIn(\u0026#39;id\u0026#39;, [10,1,3])-\u0026gt;orderByRaw(\u0026#34;FIELD(id,10,1,3)\u0026#34;)-\u0026gt;pluck(\u0026#39;id\u0026#39;); =\u0026gt; Illuminate\\Support\\Collection {#4579 all: [ 10, 1, 3, ], } そして、これを先の商品の例に適応すると、こんな感じです。$productIdsには配列として、例えば[10,1,3]のような値があると仮定します。\nProduct::query() -\u0026gt;whereIn(\u0026#39;product_id\u0026#39;, $productIds) -\u0026gt;orderByRaw(\u0026#39;FIELD(product_id, \u0026#39;.implode(\u0026#39;,\u0026#39;, $productIds).\u0026#39;)\u0026#39;) -\u0026gt;pluck(\u0026#39;name\u0026#39;, \u0026#39;product_id\u0026#39;); ","date":"2022-05-23T04:08:20+09:00","permalink":"https://www.larajapan.com/2022/05/23/wherein%E3%81%8C%E8%BF%94%E3%81%99%E9%A0%86%E7%95%AA/","title":"whereInが返す順番"},{"content":"今のプロジェクトでは独自に作成したコンソールコマンドが40~50個ほどあります。バッチ処理であったり、クロンで定期実行している処理であったりと様々です。システムが大きくなるとそれだけ必要な処理が増えるので致し方ありません。しかし時折、\nこのコマンドは何をやっているんだっけ？と見返した時にすぐ思い出せるようにコード量を少なく、シンプルに保っておきたいものです。\nそう思いながら今まで作成したコマンドらを眺めていると、毎回決り文句のように記述している処理がいくつかある事に気づきました。気づいてしまったが故にDRYアレルギーが発症してしまい、どこか適切な場所に共通処理として切り出せないだろうか？と調べてみると、CommandにはEventが用意されていました。\nCommandのEvent 公式ドキュメントの\u0026quot;Artisan Console\u0026quot;ページの最後に少しだけCommandのEventに関する説明があります。それによると、Commandが実行される際は以下の３つのイベントが発行されるそうです。\nArtisanStarting: artisan実行開始時 CommandStarting: Commandが実行される直前、つまりbefore処理 CommandFinished: Commandが実行された後、つまりafter処理 デフォルトでは上記のEventに対するListenerは用意されていないので、新たに作成し紐付ける必要があります。\nbefore/after処理を追加 ここでは例として、CommandStartingイベントとCommandFinishedイベントを使いコマンド実行前と後に処理を追加してみましょう。まずはbefore処理から。\nCommandStartingListener作成 before処理を実装する為のListenerクラスを作成します。以下のコマンドを実行。\nphp artisan make:listener CommandStartingListener CommandStartingListenerクラスのhandle()にbefore処理を追加します。以下では開始ログを出力してみました。\napp/Listeners/CommandStartingListener.php \u0026lt;?php namespace App\\Listeners; use Illuminate\\Support\\Facades\\Log; class CommandStartingListener { /** * Handle the event. * * @param object $event * @return void */ public function handle($event) { // 開始ログを出力 Log::info($event-\u0026gt;command.\u0026#34;を開始しました。\u0026#34;); } } EventとListenerを紐付け 作成したCommandStartingListenerはそのままでは発火されません。EventServiceProviderにてCommandStartingイベントに紐付けましょう。\napp/Providers/EventServiceProvider.php \u0026lt;?php namespace App\\Providers; use Illuminate\\Auth\\Events\\Registered; use Illuminate\\Auth\\Listeners\\SendEmailVerificationNotification; use Illuminate\\Foundation\\Support\\Providers\\EventServiceProvider as ServiceProvider; class EventServiceProvider extends ServiceProvider { /** * The event listener mappings for the application. * * @var array */ protected $listen = [ Registered::class =\u0026gt; [ SendEmailVerificationNotification::class, ], // CommandStartingイベントにリスナを紐付け \\Illuminate\\Console\\Events\\CommandStarting::class =\u0026gt; [ \\App\\Listeners\\CommandStartingListener::class, ], ]; /** * Register any events for your application. * * @return void */ public function boot() { // } } 同様の手順でCommandFinishedListenerを作成し、CommandFinishedイベントに紐付け、終了ログを出力してみましょう。EventServiceProviderは最終的に以下のようになります。\napp/Providers/EventServiceProvider.php ... /** * The event listener mappings for the application. * * @var array */ protected $listen = [ Registered::class =\u0026gt; [ SendEmailVerificationNotification::class, ], // CommandStartingイベントにリスナを紐付け \\Illuminate\\Console\\Events\\CommandStarting::class =\u0026gt; [ \\App\\Listeners\\CommandStartingListener::class, ], // CommandFinishedイベントにリスなを紐付け \\Illuminate\\Console\\Events\\CommandFinished::class =\u0026gt; [ \\App\\Listeners\\CommandFinishedListener::class, ], ]; ... 最後に 適当なコマンドを実行しログ出力されているか試してみましょう。以下ではルートのリストを出力するコマンドを実行しています。\nphp artisan route:list laravel.logに開始と終了のログが出力されましたね。\nstorage/logs/laravel.log [2022-05-08 08:21:18] local.INFO: route:listを開始しました。 [2022-05-08 08:21:18] local.INFO: route:listを終了しました。 ","date":"2022-05-16T07:56:30+09:00","permalink":"https://www.larajapan.com/2022/05/16/command%E3%81%ABbefore-after%E5%87%A6%E7%90%86%E3%82%92%E8%BF%BD%E5%8A%A0%E3%81%99%E3%82%8B/","title":"Commandにbefore/after処理を追加する"},{"content":"Laravelの.envは、ご存知のように実行する環境により中身が違ってきます。その環境の種類を設定するのがAPP_ENVの環境変数です。今回はそのお話です。\n設定値 Laravel以下のように.envファイル内でいろいろな環境変数の設定ができます。\n.env APP_NAME=Laravel APP_ENV=local　# これが今回の主人公 APP_KEY= APP_DEBUG=true .. その中で、APP_ENVの値はどの環境であるかを設定します。既存値としては、local あるいは production の２つあります。ローカルの開発環境あるいはライブの環境です。しかし、必要ならば自分で好きな値で設定しています。私らのステージングサーバでは、stagingの値を使用しています。\nまた、.envでは指定しないですが、ユニットテスト実行のときに自動的に testing という値が設定されます。これは、phpunit.xmlの以下で設定しています。\nphpunit.xml ... \u0026lt;php\u0026gt; \u0026lt;env name=\u0026#34;APP_ENV\u0026#34; value=\u0026#34;testing\u0026#34;/\u0026gt; \u0026lt;env name=\u0026#34;APP_URL\u0026#34; value=\u0026#34;https://localhost\u0026#34;/\u0026gt; ... どの環境か判断 さて、プログラムの中でどの環境で現在実行されているかを、どのように判断するかというと、これ意外とバリエーションあります。\n最近まで私が使用していたのは、\nif (config(\u0026#39;app.env\u0026#39;) === \u0026#39;production\u0026#39;) { // } config()のヘルバーは他の環境変数でも使えるので重宝します。\nしかし、マニュアルを見ると、以下のようにAppのファサードのメソッドを使っています。\nif (App::environment(\u0026#39;production\u0026#39;)) { // } これ知らなかったなあ。しかも、複数の環境のテストも可能です。stagingあるいはproductionなら、\nif (App::environment([\u0026#39;staging\u0026#39;, \u0026#39;production\u0026#39;])) { // } さらに、こんなのもありです。\nif (App::isProduction()) { // } このバリエーションには、ローカル環境のチェックもあります。\nif (App::isLocal()) { // } さて、テスト環境はなんだと思いますか？\nif (App::runningUnitTests()) { // } 残念、App::isTesting()ではなかったですね。\nそれから、Appのファサードを使用しなくても、app()のヘルパーを使用しても同様です。例えば、\nif (app()-\u0026gt;isProduction()) { // } 最後に 環境変数と関係ないですが、コマンドラインでLaravelを実行しているときは、以下でチェックできます。\nif (App::runningInConsole()) { // } tinkerで実行したら、\n\u0026gt;\u0026gt;\u0026gt; App::runningInConsole() =\u0026gt; true ","date":"2022-05-09T02:45:59+09:00","permalink":"https://www.larajapan.com/2022/05/09/laravel%E3%81%AF%E3%81%A9%E3%81%AE%E7%92%B0%E5%A2%83%E3%81%A7%E5%AE%9F%E8%A1%8C%E3%81%95%E3%82%8C%E3%81%A6%E3%81%84%E3%82%8B%EF%BC%9F/","title":"Laravelはどの環境で実行されている？"},{"content":"前回の記事、ログイン後のリダイレクト先、その２では未ログインの状態でログインが必要なページにアクセスした場合、ログインページへ飛ばされ、ログインすると元々アクセスしたかったページへリダイレクトされる挙動について確認し、sessionのurl.intendedにセットされた値によってログイン後のリダイレクト先が決定する事が理解できました。しかし、url.intendedは一体いつどこでsessionにセットされたのでしょうか？気になったので処理を追ってみました。\n注意 今回の記事に掲載しているソースコードはLaravel8から引用しました。Laravel9ではPHP8.0の記法が導入され一部異なりますが行っている処理は同じなのでそちらを使う場合でも参考になるかと思います。\nIlluminate\\Auth\\Middleware\\Authenticate.php ルートのmiddlewareにauthを指定すると、そのページにリクエストが投げられた際にこのクラスのauthenticate()が実行されます。まずはここから見ていきましょう。このメソッドでは$this-\u0026gt;auth-\u0026gt;guard($guard)-\u0026gt;check()にてユーザがログイン済みか否かを確認し、未ログインであればその後の$this-\u0026gt;unauthenticated($request, $guards);が実行されます。unauthenticatedはAuthenticationExceptionをスローします。\nvendor/laravel/framework/src/Illuminate/Auth/Middleware/Authenticate.php /** * Determine if the user is logged in to any of the given guards. * * @param \\Illuminate\\Http\\Request $request * @param array $guards * @return void * * @throws \\Illuminate\\Auth\\AuthenticationException */ protected function authenticate($request, array $guards) { if (empty($guards)) { $guards = [null]; } foreach ($guards as $guard) { if ($this-\u0026gt;auth-\u0026gt;guard($guard)-\u0026gt;check()) { // ログイン済みかチェック return $this-\u0026gt;auth-\u0026gt;shouldUse($guard); } } $this-\u0026gt;unauthenticated($request, $guards); // 未ログインならこちらを実行 } /** * Handle an unauthenticated user. * * @param \\Illuminate\\Http\\Request $request * @param array $guards * @return void * * @throws \\Illuminate\\Auth\\AuthenticationException */ protected function unauthenticated($request, array $guards) { throw new AuthenticationException( // 専用の例外をスロー \u0026#39;Unauthenticated.\u0026#39;, $guards, $this-\u0026gt;redirectTo($request) ); } Illuminate/Foundation/Exceptions/Handler.php Laravelの全ての例外はApp\\Exceptions\\Handlerクラスにて処理されます。実際に実行される処理は親クラスである、Illuminate\\Foundation\\Exceptions\\Handlerクラスに定義されています。例外をHTTPレスポンスに変換するrender()を見てみると、AuthenticationExceptionが投げられた場合の処理が記述されています。\nvendor/laravel/framework/src/Illuminate/Foundation/Exceptions/Handler.php /** * Render an exception into an HTTP response. * * @param \\Illuminate\\Http\\Request $request * @param \\Throwable $e * @return \\Symfony\\Component\\HttpFoundation\\Response * * @throws \\Throwable */ public function render($request, Throwable $e) { ... if ($e instanceof HttpResponseException) { return $e-\u0026gt;getResponse(); } elseif ($e instanceof AuthenticationException) { return $this-\u0026gt;unauthenticated($request, $e); // ←AuthenticationExceptionが投げられた場合の処理 } elseif ($e instanceof ValidationException) { return $this-\u0026gt;convertValidationExceptionToResponse($e, $request); } ... } そして、同じクラス内のunauthenticated()を呼び出しています。\nvendor/laravel/framework/src/Illuminate/Foundation/Exceptions/Handler.php ... /** * Convert an authentication exception into a response. * * @param \\Illuminate\\Http\\Request $request * @param \\Illuminate\\Auth\\AuthenticationException $exception * @return \\Symfony\\Component\\HttpFoundation\\Response */ protected function unauthenticated($request, AuthenticationException $exception) { return $this-\u0026gt;shouldReturnJson($request, $exception) ? response()-\u0026gt;json([\u0026#39;message\u0026#39; =\u0026gt; $exception-\u0026gt;getMessage()], 401) : redirect()-\u0026gt;guest($exception-\u0026gt;redirectTo() ?? route(\u0026#39;login\u0026#39;)); } ... ajaxリクエストの場合はjsonレスポンスを返し、そうでない場合は、redirect()-\u0026gt;guest(\u0026hellip;)が実行されます。\nIlluminate\\Routing\\Redirector guest()はコメントにある通り、カレントのURLをセッションにセットしリダイレクトレスポンスを作成します。このカレントのURLというのがアクセスしようとしていたURLの事であり、setIntendedUrl()によってsessionのurl.intendedにセットされるのです。\nvendor/laravel/framework/src/Illuminate/Routing/Redirector.php /** * Create a new redirect response, while putting the current URL in the session. * * @param string $path * @param int $status * @param array $headers * @param bool|null $secure * @return \\Illuminate\\Http\\RedirectResponse */ public function guest($path, $status = 302, $headers = [], $secure = null) { $request = $this-\u0026gt;generator-\u0026gt;getRequest(); $intended = $request-\u0026gt;method() === \u0026#39;GET\u0026#39; \u0026amp;\u0026amp; $request-\u0026gt;route() \u0026amp;\u0026amp; ! $request-\u0026gt;expectsJson() // ←カレントURLを取得 ? $this-\u0026gt;generator-\u0026gt;full() : $this-\u0026gt;generator-\u0026gt;previous(); if ($intended) { $this-\u0026gt;setIntendedUrl($intended); } return $this-\u0026gt;to($path, $status, $headers, $secure); } ... /** * Set the intended url. * * @param string $url * @return void */ public function setIntendedUrl($url) { $this-\u0026gt;session-\u0026gt;put(\u0026#39;url.intended\u0026#39;, $url); // sessionのurl.intendedにセット } url.intendedは色々なクラスの色々なメソッドを介して最終的にセットされているため、少し複雑でしたが処理の流れを把握できてスッキリしました。お疲れさまでした。\n","date":"2022-05-04T07:59:30+09:00","permalink":"https://www.larajapan.com/2022/05/04/%E3%80%90under-the-hood%E3%80%91url-intended%E3%81%AF%E3%81%84%E3%81%A4%E3%81%A9%E3%81%93%E3%81%A7%E3%82%BB%E3%83%83%E3%83%88%E3%81%95%E3%82%8C%E3%81%9F%E3%81%AE%E3%81%8B%EF%BC%9F/","title":"【Under the hood】url.intendedはいつどこでセットされたのか？"},{"content":"前回の記事ではログインボタンからログインした場合について解説しました。そちらのケースでは、RouteServiceProvider::HOMEを任意の値に変更する事でリダイレクト先を指定できました。今回はもう少し掘り下げ、ログインが必要なページにアクセスし強制的にログインページに飛ばされた場合について見てみましょう。\nログインページへ強制リダイレクト、からのログイン 未ログインの状態でログインが必要なページに遷移するとアクセスがガードされログインフォームに飛ばされますが、そこからログインした場合はどこにリダイレクトするでしょうか？前回の記事でRouteServiceProvider::HOMEに設定したトップページ（\u0026rsquo;/\u0026rsquo;）でしょうか？確認してみましょう。\n以下のようにweb.phpを編集して\u0026rsquo;sayhi\u0026rsquo;というrouteを追加します。\nroutes/web.php \u0026lt;?php use Illuminate\\Support\\Facades\\Route; /* |-------------------------------------------------------------------------- | Web Routes |-------------------------------------------------------------------------- | | Here is where you can register web routes for your application. These | routes are loaded by the RouteServiceProvider within a group which | contains the \u0026#34;web\u0026#34; middleware group. Now create something great! | */ Route::get(\u0026#39;/\u0026#39;, function () { return view(\u0026#39;welcome\u0026#39;); }); Route::get(\u0026#39;/dashboard\u0026#39;, function () { return view(\u0026#39;dashboard\u0026#39;); })-\u0026gt;middleware([\u0026#39;auth\u0026#39;])-\u0026gt;name(\u0026#39;dashboard\u0026#39;); // ルート sayhi を追加 Route::get(\u0026#39;/sayhi\u0026#39;, function () { return sprintf(\u0026#39;\u0026lt;h1\u0026gt;Hi, %s\u0026lt;/h1\u0026gt;\u0026#39;, auth()-\u0026gt;user()-\u0026gt;name); })-\u0026gt;middleware([\u0026#39;auth\u0026#39;])-\u0026gt;name(\u0026#39;sayhi\u0026#39;); require __DIR__.\u0026#39;/auth.php\u0026#39;; 未ログインの状態でsayhiページを開いてみましょう。php artisan serveでwebサーバを起動しているならhttp://localhost:8000/sayhiから開けます。アクセスがガードされログインページに飛ばされるはずです。そしてログインすると。。。\nsayhiページが表示されました。\nredirect()-\u003eintended(...)、再び 認証が必要なページにアクセスしログインページに飛ばされた場合、ログイン後は元々アクセスしようとしていたページにリダイレクトされるようです。 これはどういうプロセスでそうなっているのでしょうか？\n以下、AuthenticatedSessionControllerのstore()を再掲します。\napp/Http/Controllers/AuthenticatedSessionController.php /** * Handle an incoming authentication request. * * @param \\App\\Http\\Requests\\Auth\\LoginRequest $request * @return \\Illuminate\\Http\\RedirectResponse */ public function store(LoginRequest $request) { $request-\u0026gt;authenticate(); $request-\u0026gt;session()-\u0026gt;regenerate(); return redirect()-\u0026gt;intended(RouteServiceProvider::HOME); } 前回は掘り下げなかった、redirect()-\u0026gt;intended(\u0026hellip;) の処理を追ってみましょう。 intended()は以下のように定義されています。\nvendor/laravel/framework/src/Illuminate/Routing/Redirector.php /** * Create a new redirect response to the previously intended location. * * @param string $default * @param int $status * @param array $headers * @param bool|null $secure * @return \\Illuminate\\Http\\RedirectResponse */ public function intended($default = \u0026#39;/\u0026#39;, $status = 302, $headers = [], $secure = null) { $path = $this-\u0026gt;session-\u0026gt;pull(\u0026#39;url.intended\u0026#39;, $default); return $this-\u0026gt;to($path, $status, $headers, $secure); } ここでの処理のポイントは３つです。\nsessionにurl.intendedがセットされていればその値をリダイレクト先とする url.intendedがセットされていなければ、引数で渡されたパスをリダイレクト先とする（ここではRouteServiceProvider::HOME） url.intendedも引数もセットされていない場合、引数のデフォルト値である'/'をリダイレクト先とする ログインページに飛ばされた際、debugbarのsessionタブを開くとurl.intendedに元々アクセスしようとしていたURLがセットされているのが確認できます。\n尚、sessionのpull()はセッションから値を取得し削除する為、ログイン後url.intendedはセッションに残っていません。\n","date":"2022-05-03T07:55:47+09:00","permalink":"https://www.larajapan.com/2022/05/03/%E3%83%AD%E3%82%B0%E3%82%A4%E3%83%B3%E5%BE%8C%E3%81%AE%E3%83%AA%E3%83%80%E3%82%A4%E3%83%AC%E3%82%AF%E3%83%88%E5%85%88%E3%80%81%E3%81%9D%E3%81%AE%EF%BC%92/","title":"ログイン後のリダイレクト先、その２"},{"content":"ECサイトなど会員登録機能を備えたサイトは大きくなると利便性やマーケティング的な観点からページ遷移にとても気を使うようになります。ログイン後のリダイレクト先も重視される導線の一つですね。先の案件でそれらを見直す機会があったので挙動や設定方法について備忘録を兼ねてまとめます。\n事前準備 以降の解説はLaravel Breezeがインストールされている状態から進めます。一緒に手を動かしながら進める方は過去の記事、Laravel 8.xのインストールを参考にインストールして下さい。\n途中、debugbarでsessionの状態を確認します。必要であればそちらもインストールを、\ncomposer require barryvdh/laravel-debugbar --dev ログインボタンからログインした際のリダイレクト先 まずは通常のログイン遷移を確認してみましょう。 新規のプロジェクトにLaravel Breezeをインストールした状態だと、トップページ右上のLog inからログインからログインできますね。 そして、ログイン後はdashboardページへ遷移しました。\nこれを任意のURL、例えば、トップページ（\u0026rsquo;/\u0026rsquo;）に変更したい場合はどうすれば良いでしょうか？\nログイン時に実行されているのはAuthenticatedSessionControllerのstore()ですので、そちらを確認してみましょう。\napp/Http/Controllers/AuthenticatedSessionController.php /** * Handle an incoming authentication request. * * @param \\App\\Http\\Requests\\Auth\\LoginRequest $request * @return \\Illuminate\\Http\\RedirectResponse */ public function store(LoginRequest $request) { $request-\u0026gt;authenticate(); $request-\u0026gt;session()-\u0026gt;regenerate(); return redirect()-\u0026gt;intended(RouteServiceProvider::HOME); } リダイレクト先は、redirect()-\u0026gt;intended(RouteServiceProvider::HOME);によって決まっているようです。 こちらのメソッドの詳しい解析は後に回して、引数のRouteserviceProvider::HOMEという定数に注目です。\nRouteServiceProviderを確認するとHOMEは現在のリダイレクト先である\u0026rsquo;/dashboard\u0026rsquo;となっていました。 この値をトップページ（\u0026rsquo;/\u0026rsquo;）に変更してみましょう。\napp/Providers/RouteServiceProvider.php class RouteServiceProvider extends ServiceProvider { /** * The path to the \u0026#34;home\u0026#34; route for your application. * * This is used by Laravel authentication to redirect users after login. * * @var string */ public const HOME = \u0026#39;/\u0026#39;; // \u0026#39;/dashboard\u0026#39; から変更 ... 一旦ログアウトしてから再ログインすると、今度はトップページ（\u0026rsquo;/\u0026rsquo;）にリダイレクトされましたね。 続きます。\n","date":"2022-05-02T07:55:30+09:00","permalink":"https://www.larajapan.com/2022/05/02/%E3%83%AD%E3%82%B0%E3%82%A4%E3%83%B3%E5%BE%8C%E3%81%AE%E3%83%AA%E3%83%80%E3%82%A4%E3%83%AC%E3%82%AF%E3%83%88%E5%85%88%E3%80%81%E3%81%9D%E3%81%AE%EF%BC%91/","title":"ログイン後のリダイレクト先、その１"},{"content":"再びSPA（シングルページアプリケーション）への挑戦です！　右のTAGSで「SPA」をクリックしてもらえばわかりますが過去にいくつか私のキャパ内でSPAに挑戦しております。しかし適用した技術がLaravelとの関連がまったくなかったり、完全にjQueryを脱却できなかったりと、どうも私の中ではしっくりこない。でも今度は違います。Inertia（イナーシャ）を使うからです。\nInertia SPAと言えば、フロントエンドのReacdtJSやVueJSのようなjavascriptのフレームとバックエンドのapiで開発するものと思っていたのですが、Inertiaを使うとなんとLaravelのRouteやコントローラをそのまま使いSPAが可能となります。しかも、コントローラに使用するテンプレートはBladeの代わりにVueJSのコンポーネントとなります。HTMLの要素を含めてです。それゆえに、今まではBladeにおいてjQueryを使いボタンのクリックイベントとかでフロントエンドのプログラムをしていたところ、jQueryをまったく使わずにVueJSだけでプログラムできるのです。 ちなみに、Inertia、日本語直訳では「怠惰」。レイジーな私にぴったりで、これほどわくわくするものはありません。\nスターターキット このInertiaを簡単に体験してもらうために、hikaru氏の協力を得て以下の組み合わせのスターターキットを作成してみました。\nLaravel Breeze 以前はLaravel UIというユーザー認証に必要な機能を含むスターターパッケージがありましたが、現在はその変わりにLaravel Breezeとなっています。ユーザーの登録、ログイン、ログアウト、パスワードリセットの対応の機能が含まれています。 Inertia SPA対応のパッケージで、composerを通してのバックエンドのためのphpコード、npmを通してのフロントエンドのためのjavascriptが提供されています。 Vue ３ Inertiaのフロントエンドは先にも述べたようにReactJSやVueJSなどを採用しています。VueJSにおいてはバージョン２と去年にリリースされたバージョン３に対応しています。ここでは、Composition APIを使用してより理解しやすくなったバージョン３をコンポーネントに採用しています。 Bootstrap ５ 超有名なCSSのフレームワークですが、バージョン５では従来のバージョンと異なり、jQueryに頼らず全て純のJavascriptで書かれています。ちなみに、Laravel Breezeでのインストールの選択においてInertiaを選択することが可能ですが、そこでインストールされるCSSフレームワークはTailwind CSSです。しかし、それはまた違うパラダイムゆえにここではお馴染みのBootstrapに書き換えています。 日本語版 最後に、日本語の開発者のために、Vueのコンポーネント、バリデーションなどにおいて日本語訳を提供しています。 インストール 以下、スターターキットのインストールの手順です。\nまず以下のレポからクローンします。\n$ git clone git@github.com:lotsofbytes/breeze-bootstrap.git breeze-bootstrapというサブディレクトリーが作成されるます。この時点のmainブランチは英語版なので、dev-japanese-verにブランチに以下で変更します。\n$ cd breeze-bootstrap $ git checkout dev-japanesse-ver 以下はお馴染みのLaravelの設定です。\n$ cp .env.example .env # .envを必要に応じて編集してください。 $ composer install $ php artisan key:gen $ npm install $ php artisan migrate --seed そしてウェブサーバーを立ち上げます。\n$ php artisan serve 最初の画面です。\nユーザーを登録してみましょう。\nエラーも表示されますね。 登録が成功すると、ログイン後のダッシュボード画面になります。\nRouteはどう変わった？ ブレードを使用するrouteの設定とInertiaを比較してみましょう。\nこれがお馴染みのブレード版です。\nroutes/web.php ... Route::get(\u0026#39;/\u0026#39;, function () { return view(\u0026#39;welcome\u0026#39;); }); Route::get(\u0026#39;/dashboard\u0026#39;, function () { return view(\u0026#39;dashboard\u0026#39;); })-\u0026gt;middleware([\u0026#39;auth\u0026#39;, \u0026#39;verified\u0026#39;])-\u0026gt;name(\u0026#39;dashboard\u0026#39;); Inertia版は、\nroutes/web.php ... Route::get(\u0026#39;/\u0026#39;, function () { return Inertia::render(\u0026#39;Welcome\u0026#39;, [ \u0026#39;canLogin\u0026#39; =\u0026gt; Route::has(\u0026#39;login\u0026#39;), \u0026#39;canRegister\u0026#39; =\u0026gt; Route::has(\u0026#39;register\u0026#39;), \u0026#39;laravelVersion\u0026#39; =\u0026gt; Application::VERSION, \u0026#39;phpVersion\u0026#39; =\u0026gt; PHP_VERSION, ]); }); Route::get(\u0026#39;/dashboard\u0026#39;, function () { return Inertia::render(\u0026#39;Dashboard\u0026#39;); })-\u0026gt;middleware([\u0026#39;auth\u0026#39;, \u0026#39;verified\u0026#39;])-\u0026gt;name(\u0026#39;dashboard\u0026#39;); ... 基本的にview()がInertia::render()に代わっただけです。関数の引数は、ブレードからVueのコンポーネントとなっていて、ブレード同様に変数を渡すことが可能です。ちなみに、inertia()というヘルパーがあるので以下のようにも書けるようです。\nRoute::get(\u0026#39;/dashboard\u0026#39;, function () { return inertia(\u0026#39;Dashboard\u0026#39;); })-\u0026gt;middleware([\u0026#39;auth\u0026#39;, \u0026#39;verified\u0026#39;])-\u0026gt;name(\u0026#39;dashboard\u0026#39;); コントローラでの変更は？ 今度はコントローラの部分、こちらはどう変わったのか、見てみましょう。\napp/Http/Controllers/Auth/RegisteredUserController.php ... class RegisteredUserController extends { /** * Display the registration view. * * @return \\Inertia\\Response */ public function create() { return Inertia::render(\u0026#39;Auth/Register\u0026#39;); } /** * Handle an incoming registration request. * * @param \\Illuminate\\Http\\Request $request * @return \\Illuminate\\Http\\RedirectResponse * * @throws \\Illuminate\\Validation\\ValidationException */ public function store(Request $request) { $request-\u0026gt;validate([ \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;required|string|max:255\u0026#39;, \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;required|string|email|max:255|unique:users\u0026#39;, \u0026#39;password\u0026#39; =\u0026gt; [\u0026#39;required\u0026#39;, \u0026#39;confirmed\u0026#39;, Rules\\Password::defaults()], ]); $user = User::create([ \u0026#39;name\u0026#39; =\u0026gt; $request-\u0026gt;name, \u0026#39;email\u0026#39; =\u0026gt; $request-\u0026gt;email, \u0026#39;password\u0026#39; =\u0026gt; Hash::make($request-\u0026gt;password), ]); event(new Registered($user)); Auth::login($user); return redirect(RouteServiceProvider::HOME); } } 上はユーザー登録画面のコードですが、見ての通り、変わったのは入力画面表示においてInertia::render()となった部分だけです。情報保存のためのstore()メソッドは変更なしです。\nフロントエンドのファイルはどこへ？ フロントエンドに関わるファイルは、ブレード版ではみなresources/viewsのディレクトリ下に収められていましたが、InertiaのVueコンポーネントのファイルはみなresouces/js下となります。こんな感じです。\nresurces/js . ├── app.js ├── bootstrap.js ├── Components │ ├── ApplicationLogo.vue │ └── ValidationErrors.vue ├── Layouts │ ├── Authenticated.vue │ └── Guest.vue └── Pages ├── Auth │ ├── ConfirmPassword.vue │ ├── ForgotPassword.vue │ ├── Login.vue │ ├── Register.vue │ ├── ResetPassword.vue │ └── VerifyEmail.vue ├── Dashboard.vue └── Welcome.vue ブレードがすべてなくなったわけではありません。大元として１つだけブレードファイルがあります。\nresources/views/app.blade.php \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;{{ str_replace(\u0026#39;_\u0026#39;, \u0026#39;-\u0026#39;, app()-\u0026gt;getLocale()) }}\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;utf-8\u0026#34;\u0026gt; \u0026lt;meta name=\u0026#34;viewport\u0026#34; content=\u0026#34;width=device-width, initial-scale=1\u0026#34;\u0026gt; \u0026lt;meta name=\u0026#34;csrf-token\u0026#34; content=\u0026#34;{{ csrf_token() }}\u0026#34;\u0026gt; \u0026lt;title inertia\u0026gt;{{ config(\u0026#39;app.name\u0026#39;, \u0026#39;Laravel\u0026#39;) }}\u0026lt;/title\u0026gt; \u0026lt;!-- Fonts --\u0026gt; \u0026lt;link rel=\u0026#34;stylesheet\u0026#34; href=\u0026#34;https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700\u0026amp;display=swap\u0026#34;\u0026gt; \u0026lt;!-- Styles --\u0026gt; \u0026lt;link rel=\u0026#34;stylesheet\u0026#34; href=\u0026#34;{{ url(mix(\u0026#39;css/bootstrap.css\u0026#39;)) }}\u0026#34;\u0026gt; \u0026lt;link rel=\u0026#34;stylesheet\u0026#34; href=\u0026#34;{{ url(mix(\u0026#39;css/app.css\u0026#39;)) }}\u0026#34;\u0026gt; \u0026lt;!-- Scripts --\u0026gt; @routes \u0026lt;script src=\u0026#34;{{ url(mix(\u0026#39;js/app.js\u0026#39;)) }}\u0026#34; defer\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body class=\u0026#34;font-sans antialiased\u0026#34;\u0026gt; @inertia \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; @inertiaの部分に先のVueのコンポーネントから作成されたHTMLが入り込み、アクセスしたページにより内容が変わります。\nVueのファイルを変更したら このスターターキットをインストールしたら、まずはVueのコンポーネントをいじってみたいはず。ブレードでは変更して保存したら、画面を更新すれば変更がすぐに反映されます。同様なことをしたいなら、以下のように、\n$ npm run watch を実行しておきます。この実行は通常のコマンドのようにすぐに終了するのではなく常に走り続けます。それゆえに、関わるファイルが変更されると自動的に、Laravel-mixを通してcssやjsファイルをコンパイルしてくれます。\nそこで表示されたファイルは、publicのディレクトリ下のファイルとして保存されます。コンパイルが完了したら、ブレードと同様に画面を更新してcssやjsファイルを読み直すと変更が反映されます。\n最終的には、以下の実行で最適化、最小化されたProduction Readyのファイルの作成が可能です。\n$ npm run prod 最後に Inertiaを使っての開発がより楽しくなるように、いろいろ将来の投稿を考えています。お楽しみに。\n","date":"2022-04-25T05:55:31+09:00","permalink":"https://www.larajapan.com/2022/04/25/spa%E3%81%AE%E9%96%8B%E7%99%BA%E3%82%92%E6%A5%BD%E3%81%97%E3%81%8F%E3%81%95%E3%81%9B%E3%82%8Binertia-laravel-breeze-inertia-vue3-bootstrap5-%E6%97%A5%E6%9C%AC%E8%AA%9E%E8%A8%B3/","title":"SPAの開発を楽しくさせるInertia：Laravel Breeze + Inertia + Vue3 + Bootstrap5 + 日本語訳"},{"content":"Collectionでのmap()とtransform()のメソッドは似ているようで大きな違いがあります。\nまず、テストデータのレコードをtinkerで３つ作成します。\n\u0026gt;\u0026gt;\u0026gt; User::factory(3)-\u0026gt;create(); =\u0026gt; Illuminate\\Database\\Eloquent\\Collection {#4463 all: [ App\\Models\\User {#4464 id: 1, name: \u0026#34;坂本 知実\u0026#34;, email: \u0026#34;tfujimoto@example.net\u0026#34;, email_verified_at: \u0026#34;2022-04-03 02:36:03\u0026#34;, #password: \u0026#34;$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi\u0026#34;, #remember_token: \u0026#34;mBZjRiwtp2\u0026#34;, created_at: \u0026#34;2022-04-03 02:36:03\u0026#34;, updated_at: \u0026#34;2022-04-03 02:36:03\u0026#34;, }, App\\Models\\User {#4465 id: 2, name: \u0026#34;高橋 知実\u0026#34;, email: \u0026#34;kazuya.miyazawa@example.net\u0026#34;, email_verified_at: \u0026#34;2022-04-03 02:36:03\u0026#34;, #password: \u0026#34;$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi\u0026#34;, #remember_token: \u0026#34;2ERHaySLEo\u0026#34;, created_at: \u0026#34;2022-04-03 02:36:03\u0026#34;, updated_at: \u0026#34;2022-04-03 02:36:03\u0026#34;, }, App\\Models\\User {#4466 id: 3, name: \u0026#34;藤本 修平\u0026#34;, email: \u0026#34;mkiriyama@example.net\u0026#34;, email_verified_at: \u0026#34;2022-04-03 02:36:03\u0026#34;, #password: \u0026#34;$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi\u0026#34;, #remember_token: \u0026#34;JpPxv9yr0d\u0026#34;, created_at: \u0026#34;2022-04-03 02:36:03\u0026#34;, updated_at: \u0026#34;2022-04-03 02:36:03\u0026#34;, }, ], } 作成されたレコードを$usersの変数に入れます。\n\u0026gt;\u0026gt;\u0026gt; $users = User::all(); map()もtransform()もCollectionの中のそれぞれのアイテムを処理してCollectionの値を返すのは同じです。\nまず、mapから、\n\u0026gt;\u0026gt;\u0026gt; $users-\u0026gt;map(function ($user) { return $user-\u0026gt;only([\u0026#39;id\u0026#39;, \u0026#39;name\u0026#39;]); }) =\u0026gt; Illuminate\\Support\\Collection {#4473 all: [ [ \u0026#34;id\u0026#34; =\u0026gt; 1, \u0026#34;name\u0026#34; =\u0026gt; \u0026#34;坂本 知実\u0026#34;, ], [ \u0026#34;id\u0026#34; =\u0026gt; 2, \u0026#34;name\u0026#34; =\u0026gt; \u0026#34;高橋 知実\u0026#34;, ], [ \u0026#34;id\u0026#34; =\u0026gt; 3, \u0026#34;name\u0026#34; =\u0026gt; \u0026#34;藤本 修平\u0026#34;, ], ], } この後に$usersの中身を見てみましょう。\n\u0026gt;\u0026gt;\u0026gt; $users =\u0026gt; Illuminate\\Database\\Eloquent\\Collection {#4463 all: [ App\\Models\\User {#4464 id: 1, name: \u0026#34;坂本 知実\u0026#34;, email: \u0026#34;tfujimoto@example.net\u0026#34;, email_verified_at: \u0026#34;2022-04-03 02:36:03\u0026#34;, #password: \u0026#34;$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi\u0026#34;, #remember_token: \u0026#34;mBZjRiwtp2\u0026#34;, birth_date: \u0026#34;1988-03-20\u0026#34;, created_at: \u0026#34;2022-04-03 02:36:03\u0026#34;, updated_at: \u0026#34;2022-04-03 02:36:03\u0026#34;, }, App\\Models\\User {#4465 id: 2, name: \u0026#34;高橋 知実\u0026#34;, email: \u0026#34;kazuya.miyazawa@example.net\u0026#34;, email_verified_at: \u0026#34;2022-04-03 02:36:03\u0026#34;, #password: \u0026#34;$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi\u0026#34;, #remember_token: \u0026#34;2ERHaySLEo\u0026#34;, birth_date: \u0026#34;1975-06-25\u0026#34;, created_at: \u0026#34;2022-04-03 02:36:03\u0026#34;, updated_at: \u0026#34;2022-04-03 02:36:03\u0026#34;, }, App\\Models\\User {#4466 id: 3, name: \u0026#34;藤本 修平\u0026#34;, email: \u0026#34;mkiriyama@example.net\u0026#34;, email_verified_at: \u0026#34;2022-04-03 02:36:03\u0026#34;, #password: \u0026#34;$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi\u0026#34;, #remember_token: \u0026#34;JpPxv9yr0d\u0026#34;, birth_date: \u0026#34;2011-06-17\u0026#34;, created_at: \u0026#34;2022-04-03 02:36:03\u0026#34;, updated_at: \u0026#34;2022-04-03 02:36:03\u0026#34;, }, ], } 何も変わっていませんね。 今度は、transformです。\n\u0026gt;\u0026gt;\u0026gt; $users-\u0026gt;transform(function($user) { return $user-\u0026gt;only([\u0026#39;id\u0026#39;, \u0026#39;name\u0026#39;]); }) =\u0026gt; Illuminate\\Database\\Eloquent\\Collection {#4463 all: [ [ \u0026#34;id\u0026#34; =\u0026gt; 1, \u0026#34;name\u0026#34; =\u0026gt; \u0026#34;坂本 知実\u0026#34;, ], [ \u0026#34;id\u0026#34; =\u0026gt; 2, \u0026#34;name\u0026#34; =\u0026gt; \u0026#34;高橋 知実\u0026#34;, ], [ \u0026#34;id\u0026#34; =\u0026gt; 3, \u0026#34;name\u0026#34; =\u0026gt; \u0026#34;藤本 修平\u0026#34;, ], ], } ここまではmapの結果とまったく同じですね。 しかし、もとの変数の中身を見ると、\n\u0026gt;\u0026gt;\u0026gt; $users =\u0026gt; Illuminate\\Database\\Eloquent\\Collection {#4463 all: [ [ \u0026#34;id\u0026#34; =\u0026gt; 1, \u0026#34;name\u0026#34; =\u0026gt; \u0026#34;坂本 知実\u0026#34;, ], [ \u0026#34;id\u0026#34; =\u0026gt; 2, \u0026#34;name\u0026#34; =\u0026gt; \u0026#34;高橋 知実\u0026#34;, ], [ \u0026#34;id\u0026#34; =\u0026gt; 3, \u0026#34;name\u0026#34; =\u0026gt; \u0026#34;藤本 修平\u0026#34;, ], ], } 中身が返り値と同じ値で上書きとなっているのです。transformという名前はそれゆえにです。\n","date":"2022-04-04T03:09:52+09:00","permalink":"https://www.larajapan.com/2022/04/04/laravel-collection%EF%BC%88%EF%BC%91%EF%BC%90%EF%BC%89map%E3%81%A8transform%E3%81%AE%E9%81%95%E3%81%84/","title":"Laravel Collection（１０）mapとtransformの違い"},{"content":"古くなってもう要らないDBレコードを削除することを、プルーン(prune)あるいはパージ(purge)などと言います。カスタムのArtisanのコマンドを書いて処理してもいいのだけれど、LaravelではこれがModelの中であるトレイトを使うだけで、組織的できてしまいます。\nプルーンの対象は特定のDBモデルのレコードです。ここでは、会員ログインのレコードを貯めているuser_logsのテーブルのモデルUserLogを例とします。以下のようにPrunableのトレイトを追加して、削除したい対象のクエリーの条件をprunable()で定義するだけです。\napp/Models/UserLog.php namespace App\\Models; use Illuminate\\Database\\Eloquent\\Model; use Illuminate\\Database\\Eloquent\\Prunable; class UserLog extends Model { use HasFactory; use Prunable; const UPDATED_AT = null; // updated_atの項目はない /** * Get the prunable model query. * * @return \\Illuminate\\Database\\Eloquent\\Builder */ public function prunable() { return static::where(\u0026#39;created_at\u0026#39;, \u0026#39;\u0026lt;=\u0026#39;, now()-\u0026gt;subYear()); // １年以上昔のレコードが対象 } } そして、以下のコマンドの実行をクロンに設定にします。\n$ php artisan model::prune 簡単でしょう。\n削除する前に以下のように実際の削除なしの予行も可能です。\n$ php artisan model:prune --pretend 8 [App\\Models\\UserLog] records will be pruned. ８個のレコードが削除されるよ、と知らせてくれます\ntinkerで実際にプルーンを実行してみましょう。どのようなSQL文が実行されるか興味あります。\n\u0026gt;\u0026gt;\u0026gt; Artisan::call(\u0026#39;model:prune\u0026#39;); =\u0026gt; 0 \u0026gt;\u0026gt;\u0026gt; sql(); =\u0026gt; [ [ \u0026#34;query\u0026#34; =\u0026gt; \u0026#34;select count(*) as aggregate from `user_logs` where `created_at` \u0026lt;= ?\u0026#34;, \u0026#34;bindings\u0026#34; =\u0026gt; [ Illuminate\\Support\\Carbon @1616039564 {#3572 date: 2021-03-18 03:52:44.536379 UTC (+00:00), }, ], \u0026#34;time\u0026#34; =\u0026gt; 4.94, ], [ \u0026#34;query\u0026#34; =\u0026gt; \u0026#34;select * from `user_logs` where `created_at` \u0026lt;= ? order by `id` asc limit 1000\u0026#34;, \u0026#34;bindings\u0026#34; =\u0026gt; [ Illuminate\\Support\\Carbon @1616039586 {#3546 date: 2021-03-18 03:53:06.837232 UTC (+00:00), }, ], \u0026#34;time\u0026#34; =\u0026gt; 1.36, ], [ \u0026#34;query\u0026#34; =\u0026gt; \u0026#34;delete from `user_logs` where `id` = ?\u0026#34;, \u0026#34;bindings\u0026#34; =\u0026gt; [ 1, ], \u0026#34;time\u0026#34; =\u0026gt; 7.25, ], [ \u0026#34;query\u0026#34; =\u0026gt; \u0026#34;delete from `user_logs` where `id` = ?\u0026#34;, \u0026#34;bindings\u0026#34; =\u0026gt; [ 2, ], \u0026#34;time\u0026#34; =\u0026gt; 3.11, ], ... なるほど、１個１個レコードは削除されるのですね。これは、モデルのdeletingやdeletedのイベント処理を可能にするためです。\nそのイベント処理が必要でないなら、一括実行も可能です。トレイトをPrunableからMassPrunableにするだけです。\napp/Models/UserLog.php namespace App\\Models; use Illuminate\\Database\\Eloquent\\Model; use Illuminate\\Database\\Eloquent\\MassPrunable; class UserLog extends Model { use HasFactory; use MassPrunable; const UPDATED_AT = null; /** * Get the prunable model query. * * @return \\Illuminate\\Database\\Eloquent\\Builder */ public function prunable() { return static::where(\u0026#39;created_at\u0026#39;, \u0026#39;\u0026lt;=\u0026#39;, now()-\u0026gt;subYear()); // １年以上昔のレコードが対象 } } 再度、tinkerでSQL文をチェックすると、\n\u0026gt;\u0026gt;\u0026gt; Artisan::call(\u0026#39;model:prune\u0026#39;); =\u0026gt; 0 \u0026gt;\u0026gt;\u0026gt; sql(); =\u0026gt; [ [ \u0026#34;query\u0026#34; =\u0026gt; \u0026#34;delete from `user_logs` where `created_at` \u0026lt;= ? limit 1000\u0026#34;, \u0026#34;bindings\u0026#34; =\u0026gt; [ Illuminate\\Support\\Carbon @1616039978 {#3567 date: 2021-03-18 03:59:38.916448 UTC (+00:00), }, ], \u0026#34;time\u0026#34; =\u0026gt; 5.61, ], ] \u0026gt;\u0026gt;\u0026gt; 削除のSQL文１つだけになっていました。いたれりつくせりです。\n","date":"2022-03-19T12:14:36+09:00","permalink":"https://www.larajapan.com/2022/03/19/%E5%8F%A4%E3%81%84%E3%83%AC%E3%82%B3%E3%83%BC%E3%83%89%E3%82%92%E3%83%97%E3%83%AB%E3%83%BC%E3%83%B3%E3%81%97%E3%81%BE%E3%81%99/","title":"古いレコードをプルーンします"},{"content":"以前に「Laravelへの移行」と称してLaravelのスターターパッケージを２つ紹介しました。スターターパッケージとはすでにある程度デザインや機能が揃っていて、時間がかかる最初のプロジェクトの立ち上げをかなり減少してくれるものです。そからまだ２年も経過していないのですが、その間Bootstrapは4から5になるし、Tailwindが知られてくるようになりつつあり、フロントエンドのフレームワークの更新の時期となりました。今回は、それらを簡単にインストールできるLaraスターターの紹介です。\nLaraスターターのインストール 早速インストールしてみます。\nLaravelのプロジェクトをインストールします。以下でLaravel 8.x、Laravel 9.xのインストールの説明あります。認証のパッケージのインストールはLaraスターターに含まれているのでインストールの必要はないです。 Laravel 9.xのインストール Laravel 8.xのインストール\nLaraスターターのパッケージをインストールします。 $ composer require laraveldaily/larastarters --dev Laraスターターのインストールを実行します。 $ php artisan larastarters:install この実行では以下のような選択が問われます。 最初の質問は、「どのスターターキットを使用しますか？」 今回は手堅くLaravel UI (Bootstrap)を選択します。 次の質問は、「どのテーマーを使用しますか？」 Bootstrap 5使用のvoltを選びます。\nnpmのパッケージをインストールします。 $ npm install \u0026amp;\u0026amp; npm run dev Volt インストールが完了したらブラウザで見てみます。\nなかなかいいデザインですね。\nLaraスターターで使用されているのは以下の無料版です。スターターのためにこれをLaravelのブレードとしてくれました。感謝です。\nhttps://github.com/themesberg/volt-bootstrap-5-dashboard\n有料版は以下で69ドルで販売されています。\nhttps://demo.themesberg.com/volt-pro/index.html\n無料版と有料版は何が違うというと、\nコンポーネントの数が1000でなく100ですね。しかしたいていはそれくらい事足ります。\n最後に Laraスターターを作成したお方は、Laravel業界では誰でも御存知のLaravel DailyのYouTuberです。\nhttps://www.youtube.com/c/LaravelDaily\n昔はLaravelのブログを毎日書くことから始め、現在は毎日You Tubeのビデオを作成しています。１週間に１回ブログを書けるかどうかの私にはとても真似ができません。ちなみに、彼（Povilas Korop）はリトアニア (Lithuania)の人です。昔はドイツやロシアに占領されたもののソビエトの崩壊後民主主義のEU加盟国です。\n","date":"2022-03-14T04:10:32+09:00","permalink":"https://www.larajapan.com/2022/03/14/lara%E3%82%B9%E3%82%BF%E3%83%BC%E3%82%BF%E3%83%BC-bootstrap-5/","title":"Laraスターター - Bootstrap 5"},{"content":"「ページネーションのデータを変える」の続きです。検索画面を用意して検索結果をページネーションします。\n検索画面のページネーション まず、こんな検索画面を作りたいです。\n検索項目に値を入れると、値により絞り込まれた検索結果が表示されます。ごく普通の検索画面です。画面の右下にはページネーションも表示されます。ここでは１画面に２レコードだけ表示です。\nrouteの定義は、GETだけで十分です。そう、検索条件はすべてURLに入れます。\nroutes/web.php ... Route::get(\u0026#39;/search\u0026#39;, [SearchController::class, \u0026#39;index\u0026#39;])-\u0026gt;name(\u0026#39;search\u0026#39;); .. コントローラの作成も簡単です。あまりにも短いのでカットせずにすべて見せてしまいます。\napp/Controllers/SearchController.php namespace App\\Http\\Controllers; use App\\Models\\User; use Illuminate\\Http\\Request; class SearchController extends Controller { public function index(Request $request) { $query = User::query(); if ($value = $request-\u0026gt;name) { $query-\u0026gt;where(\u0026#39;name\u0026#39;, \u0026#39;like\u0026#39;, \u0026#34;%{$value}%\u0026#34;); } if ($value = $request-\u0026gt;email) { $query-\u0026gt;where(\u0026#39;email\u0026#39;, \u0026#39;like\u0026#39;, \u0026#34;%{$value}%\u0026#34;); } $users = $query-\u0026gt;paginate(2); return view(\u0026#39;user.search\u0026#39;, compact(\u0026#39;users\u0026#39;)); } } しかし、このコード大いに問題ありです。先の画面でページネーションの２ページをクリックすると、\n検索結果３件だったのに１０件になっているし、ページネーションも２ページから５ページに。２ページ目なら表示するのは１件のみでは。 おかしいところだらけです。一番の問題はGETだから、ブラウザのURLには入力した値が追加されるのに、?page=2だけです。\nこの問題の解決はとても簡単です。\n上のコントローラのコードで、withQueryString()を以下のように追加するだけなのです。\napp/Controllers/SearchController.php ... public function index(Request $request) { $query = User::query(); if ($value = $request-\u0026gt;name) { $query-\u0026gt;where(\u0026#39;name\u0026#39;, \u0026#39;like\u0026#39;, \u0026#34;%{$value}%\u0026#34;); } if ($value = $request-\u0026gt;email) { $query-\u0026gt;where(\u0026#39;email\u0026#39;, \u0026#39;like\u0026#39;, \u0026#34;%{$value}%\u0026#34;); } $users = $query-\u0026gt;paginate(2)-\u0026gt;withQueryString(); //ここに追加 return view(\u0026#39;user.search\u0026#39;, compact(\u0026#39;users\u0026#39;)); } ... 修正した２ページ目の画面です。\nURLに?email=example.com\u0026amp;page=2とクエリーの文字列が入っていますね。\nクエリーをリファクター 上のコントローラのコードでは条件文のifを使い検索のクエリーを作成しています。 それ自体は問題は何もありません。すでに十分わかりやすいです。 しかし、Laravel風にすると以下のようにも書くことができます。\napp/Controllers/SearchController.php ... class SearchController extends Controller { public function index(Request $request) { $query = User::query() -\u0026gt;when($request-\u0026gt;name, function($query, $value) { return $query-\u0026gt;where(\u0026#39;name\u0026#39;, \u0026#39;like\u0026#39;, \u0026#34;%{$value}%\u0026#34;); }) -\u0026gt;when($request-\u0026gt;email, function($query, $value) { return $query-\u0026gt;where(\u0026#39;email\u0026#39;, \u0026#39;like\u0026#39;, \u0026#34;%{$value}%\u0026#34;); }); $users = $query-\u0026gt;paginate(2)-\u0026gt;withQueryString(); return view(\u0026#39;user.search\u0026#39;, compact(\u0026#39;users\u0026#39;, \u0026#39;request\u0026#39;)); } } ... whereでなくwhenの関数となっているところに注意してください。最初の引数の値が存在するなら次で定義する匿名関数を実行です。\n","date":"2022-03-07T07:04:03+09:00","permalink":"https://www.larajapan.com/2022/03/07/%E6%A4%9C%E7%B4%A2%E7%B5%90%E6%9E%9C%E3%82%92%E3%83%9A%E3%83%BC%E3%82%B8%E3%83%8D%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3/","title":"検索結果をページネーション"},{"content":"L6以来のLTSという事で待望の(?)Laravel9がリリースされました。という事で早速installを試してみます。\nバージョン まずはサポートの予定をチェックしてみましょう。\nおや？Laravel9のLTS表記が消えていますね。なぜでしょう？ Taylor OtwellさんがTwitterで説明していました。\nL9はPHP8.0以上をシステム要件としていましたが、依存パッケージである Symfony6.1 がPHP8.1を要件とする可能性があり（2022/02/16現在 github上で協議中）、L9のサポート期限はその決定次第で決まるとの事です。 表の期限は最悪のシナリオを想定したもので、symfonyの最低要件がPHP8.1になった場合でも２年間はサポートされるよ、との事です。\nこちらの情報はモニターしておく必要がありそうです。\n※追記@2022-02-28 Symfony6.1はPHP8.1以上が必須となりました、という事でL9のサポートは２年間となりました。 また、これに合わせて次のL10ではPHP8.1以上が要件となりした。 新規プロジェクト 前項でも触れましたがL9はPHP8.0以上が必要です、まだの方は先にPHPをバージョンアップして下さい。以下のコマンドでL9xというプロジェクトを作成してみましょう。\ncomposer create-project --prefer-dist laravel/laravel:^9.0 l9x プロジェクトディレクトリに移動し、早速立ち上げてみましょう。\ncd L9x php artisan serve ブラウザでhttp://127.0.0.1:8000にアクセスするとお馴染みのホーム画面が表示されました。\nroute:list 今回のアップデートで変わった内容の１つにroute:listが刷新された、とありました。早速試してみましょう。\nphp artisan route:list 以下のようなカラフルな出力になりました。 L8では以下のようなテーブル出力でした。 刷新されたエラーページ エラーページの出力も刷新され、パッケージがfacade/ignitionからspatie/laravel-ignitionに変更されました。\n試しに表示してみましょう。ルートページにアクセスした際にエラーが発生するように変更します。\nroutes/web.php ... Route::get(\u0026#39;/\u0026#39;, function () { throw new \\Exception(\u0026#39;Error on purpose\u0026#39;); // 追加 return view(\u0026#39;welcome\u0026#39;); }); ブラウザでhttp://127.0.0.1:8000にアクセスするとエラーページが表示されました。 ダークモードですね。\nリクエストの詳細情報などはページ下部にあります。メニューバーのCONTEXTからも飛べます。 見やすいし、コピペしやすそうですね。\n右上の歯車マークから、使用しているEDITORを指定する事で、エラー画面からエディタを開き該当行にジャンプできます。 facade/ignitionにも同様の機能がありましたが、デフォルトのエディタにPHPStormが指定されており、私が使用しているVS Codeに変更する場合は.envに設定を書かなければならなかったので大変便利です。\n該当行のペンマークのアイコンをクリックしてエディタ上でコードの該当部分にジャンプ。\nエラー調査が捗りそうです。\nddd()復活？ 過去の記事で紹介したddd()ですが、L9で使うとCall to undefined function ddd()エラーが発生してしまいます。\nその件について、こちらのGitHubのDiscussionでDiscussされています。\nspatie/laravel-ignitionの開発メンバー側はddd()を使ってないのでパッケージをシンプルにする為に削除したとの事。\nしかし、戻して欲しい、という意見が多かったので近い内に追加されるそうです。 コミュニティの意見をしっかりと聞いて反映する良いパッケージだな、という印象を受けました。\n","date":"2022-02-17T08:44:12+09:00","permalink":"https://www.larajapan.com/2022/02/17/laravel-9-x%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB/","title":"Laravel 9.xのインストール"},{"content":"例えば、User::paginate(5)で返されるデータは、@foreachでループできるゆえにコレクションと思いきや、そうでありません。\nページネーション ページネーションは、以下の画面のように複数のレコードをページごと閲覧できるために使われます。\n上の表示に使われるコントローラは、以下のように簡単なコードです。\nnamespace App\\Http\\Controllers; use App\\Models\\User; class UserController extends Controller { public function index() { $users = User::paginate(2); //１ページ２個表示の設定 return view(\u0026#39;users.index\u0026#39;, compact(\u0026#39;users\u0026#39;)); } } ブレードも簡単です。\n@extends(\u0026#39;layouts.app\u0026#39;) @section(\u0026#39;content\u0026#39;) \u0026lt;div class=\u0026#34;main py-4\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;card card-body border-0 shadow table-wrapper table-responsive\u0026#34;\u0026gt; \u0026lt;h2 class=\u0026#34;mb-4 h5\u0026#34;\u0026gt;会員名\u0026lt;/h2\u0026gt; \u0026lt;table class=\u0026#34;table table-hover\u0026#34;\u0026gt; \u0026lt;thead\u0026gt; \u0026lt;tr\u0026gt; \u0026lt;th class=\u0026#34;border-gray-200\u0026#34;\u0026gt;名前\u0026lt;/th\u0026gt; \u0026lt;th class=\u0026#34;border-gray-200\u0026#34;\u0026gt;メールアドレス\u0026lt;/th\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;/thead\u0026gt; \u0026lt;tbody\u0026gt; @foreach ($users as $user) \u0026lt;tr\u0026gt; \u0026lt;td\u0026gt;\u0026lt;span class=\u0026#34;fw-normal\u0026#34;\u0026gt;{{ $user-\u0026gt;name }}\u0026lt;/span\u0026gt;\u0026lt;/td\u0026gt; \u0026lt;td\u0026gt;\u0026lt;span class=\u0026#34;fw-normal\u0026#34;\u0026gt;{{ $user-\u0026gt;email }}\u0026lt;/span\u0026gt;\u0026lt;/td\u0026gt; \u0026lt;/tr\u0026gt; @endforeach \u0026lt;/tbody\u0026gt; \u0026lt;/table\u0026gt; \u0026lt;div class=\u0026#34;card-footer px-3 border-0 d-flex flex-column flex-lg-row align-items-center justify-content-between\u0026#34;\u0026gt; {{ $users-\u0026gt;links() }} \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; @endsection 表示するレコードを変える さて、画面１で表示されるコードに会員IDを追加して、以下のように表示したいです。\nブレードを以下のようにすれば簡単に表示を変更できます。\n... \u0026lt;td\u0026gt;\u0026lt;span class=\u0026#34;fw-normal\u0026#34;\u0026gt;{{ $user-\u0026gt;id }} : {{ $user-\u0026gt;name }}\u0026lt;/span\u0026gt;\u0026lt;/td\u0026gt; ... しかし、ブレードを変更せずに、$user-\u0026gt;nameに会員ID + 名前の値としたいなら、どうしましょうか？ 先のコントローラの定義において、\n... $users = User::paginate(2); $users-\u0026gt;each(function($user) { $user-\u0026gt;name = $user-\u0026gt;id.\u0026#39; : \u0026#39;.$user-\u0026gt;name; }); ... と変更すればよいです。transform()を使用してもよいです。\n... $users = User::paginate(2); $users-\u0026gt;transform(function($user) { $user-\u0026gt;name = $user-\u0026gt;id.\u0026#39; : \u0026#39;.$user-\u0026gt;name; return $user; }); ... され、これらをみて、誰しも思うのは、以下のようにメソッドをチェーンすることです。わかりやすいし、Laravelらしい。\n... $users = User::paginate(2)-\u0026gt;each(function($user) { $user-\u0026gt;name = $user-\u0026gt;id.\u0026#39; : \u0026#39;.$user-\u0026gt;name; }); ... しかし、そうするとエラーとなってしまいます。\nそこで気が付くのは、User::paginate(2)が返すのはCollectionではない、ということです。\n通常は、以下のようにCollectionを返します。\n\u0026gt;\u0026gt;\u0026gt; get_class(User::all()); =\u0026gt; \u0026#34;Illuminate\\Database\\Eloquent\\Collection\u0026#34; しかし、ページネーションだと、\n\u0026gt;\u0026gt;\u0026gt; get_class(User::paginate(2)); =\u0026gt; \u0026#34;Illuminate\\Pagination\\LengthAwarePaginator\u0026#34; 返すのは、Collectionではなく、LengthAwarePaginatorです。これには、ページに表示するレコードだけでなく、何番目のページとか、他のページに移動するためのリンクとか、など他にもたくさんデータが詰まっています。\n\u0026gt;\u0026gt;\u0026gt; User::paginate(2)-\u0026gt;toArray(); [!] Aliasing \u0026#39;User\u0026#39; to \u0026#39;App\\Models\\User\u0026#39; for this Tinker session. =\u0026gt; [ \u0026#34;current_page\u0026#34; =\u0026gt; 1, \u0026#34;data\u0026#34; =\u0026gt; [ [ \u0026#34;id\u0026#34; =\u0026gt; 1, \u0026#34;name\u0026#34; =\u0026gt; \u0026#34;村山 涼平\u0026#34;, \u0026#34;email\u0026#34; =\u0026gt; \u0026#34;kenichi.yoshida@example.org\u0026#34;, \u0026#34;email_verified_at\u0026#34; =\u0026gt; \u0026#34;2022-02-11T02:11:12.000000Z\u0026#34;, \u0026#34;created_at\u0026#34; =\u0026gt; \u0026#34;2022-02-11T02:11:12.000000Z\u0026#34;, \u0026#34;updated_at\u0026#34; =\u0026gt; \u0026#34;2022-02-11T02:11:12.000000Z\u0026#34;, ], [ \u0026#34;id\u0026#34; =\u0026gt; 2, \u0026#34;name\u0026#34; =\u0026gt; \u0026#34;笹田 裕樹\u0026#34;, \u0026#34;email\u0026#34; =\u0026gt; \u0026#34;shuhei99@example.com\u0026#34;, \u0026#34;email_verified_at\u0026#34; =\u0026gt; \u0026#34;2022-02-11T02:11:12.000000Z\u0026#34;, \u0026#34;created_at\u0026#34; =\u0026gt; \u0026#34;2022-02-11T02:11:12.000000Z\u0026#34;, \u0026#34;updated_at\u0026#34; =\u0026gt; \u0026#34;2022-02-11T02:11:12.000000Z\u0026#34;, ], ], \u0026#34;first_page_url\u0026#34; =\u0026gt; \u0026#34;http://localhost?page=1\u0026#34;, \u0026#34;from\u0026#34; =\u0026gt; 1, \u0026#34;last_page\u0026#34; =\u0026gt; 2, \u0026#34;last_page_url\u0026#34; =\u0026gt; \u0026#34;http://localhost?page=2\u0026#34;, \u0026#34;links\u0026#34; =\u0026gt; [ [ \u0026#34;url\u0026#34; =\u0026gt; null, \u0026#34;label\u0026#34; =\u0026gt; \u0026#34;\u0026amp;laquo; Previous\u0026#34;, \u0026#34;active\u0026#34; =\u0026gt; false, ], [ \u0026#34;url\u0026#34; =\u0026gt; \u0026#34;http://localhost?page=1\u0026#34;, \u0026#34;label\u0026#34; =\u0026gt; \u0026#34;1\u0026#34;, \u0026#34;active\u0026#34; =\u0026gt; true, ], [ \u0026#34;url\u0026#34; =\u0026gt; \u0026#34;http://localhost?page=2\u0026#34;, \u0026#34;label\u0026#34; =\u0026gt; \u0026#34;2\u0026#34;, \u0026#34;active\u0026#34; =\u0026gt; false, ], [ \u0026#34;url\u0026#34; =\u0026gt; \u0026#34;http://localhost?page=2\u0026#34;, \u0026#34;label\u0026#34; =\u0026gt; \u0026#34;Next \u0026amp;raquo;\u0026#34;, \u0026#34;active\u0026#34; =\u0026gt; false, ], ], \u0026#34;next_page_url\u0026#34; =\u0026gt; \u0026#34;http://localhost?page=2\u0026#34;, \u0026#34;path\u0026#34; =\u0026gt; \u0026#34;http://localhost\u0026#34;, \u0026#34;per_page\u0026#34; =\u0026gt; 2, \u0026#34;prev_page_url\u0026#34; =\u0026gt; null, \u0026#34;to\u0026#34; =\u0026gt; 2, \u0026#34;total\u0026#34; =\u0026gt; 3, ] それゆえに、先ほどのようにメソッドのチェーンをしてデータの変更をすると、これらのデータが失われエラーとなってしまうのです。それでは、どのようにメソッドをチェーンできるのでしょう？\npaginate()にチェーン そのためのメソッドがあるのです。\n... $users = User::paginate(2)-\u0026gt;through(function($user) { $user-\u0026gt;name = $user-\u0026gt;id.\u0026#39; : \u0026#39;.$user-\u0026gt;name; return $user; }); ... through()は、Collectionのtransform()のようなもので、ページネーションのオブジェクトのdataの部分だけの編集が可能です。\nちなみに、以下のようにtap()を使用しても同様なことができます。 （参照：https://stackoverflow.com/questions/37102841/laravel-change-pagination-data）\n... $users = tap(User::paginate(2), function($page) { return $page-\u0026gt;each( function($user) { $user-\u0026gt;name = $user-\u0026gt;id.\u0026#39; : \u0026#39;.$user-\u0026gt;name; }); }); ... なるほど、tap()はこのようなときに使うのですね。paginate()が返すLengthAwarePaginatorのオブジェクトはキープして、その中身を操作することが可能なのです。\n","date":"2022-02-12T12:41:41+09:00","permalink":"https://www.larajapan.com/2022/02/12/%E3%83%9A%E3%83%BC%E3%82%B8%E3%83%8D%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92%E5%A4%89%E3%81%88%E3%82%8B/","title":"ページネーションのデータを変える"},{"content":"先日に作成した生SQL文の実行クエリの機能を使用しています。重宝していますが、SQL文が複雑になってくると括弧が多くなってきたりして、わかりづらくなってきます。PHPのコードやHTMLソースと同じようなもので、SQL文の整形の機能が必要です。探したらまさにそのパッケージありました。\nまず、私が使用している機能とはこちらのことです。 SQL文を画面で入力して安全に実行して結果を返す\nそこで掲載されているSQL文の例はごく簡単なのですが、例えばこんなのは、どうでしょう。\nサブクエリーがあるし少々複雑。このSQL文にあるパッケージを適用すると、このようになります。\nとても見やすい！\nそのパッケージとは、以下のSQLフォーマッターです。\nhttps://github.com/doctrine/sql-formatter\nインストールはとても簡単。\n$ composer require doctrine/sql-formatter 使用もとても簡単です。\n... use Doctrine\\SqlFormatter\\SqlFormatter; ... class QueryController extends Controller { /** * 表示 * * @param \\App\\Models\\Query $query * @return \\Illuminate\\Contracts\\View\\View */ public function show(Query $query) { $query-\u0026gt;sql = (new SqlFormatter)-\u0026gt;format($query-\u0026gt;sql); // ここで整形 return view(\u0026#39;admin.query_show\u0026#39;) -\u0026gt;with(compact(\u0026#39;page\u0026#39;, \u0026#39;query\u0026#39;)); } ... } 表示では、先に掲載したようにSQL文の整形だけでなく、selectやfromなどのSQLのリザーブワードを太文字にするなどのシンタックスハイライトもしてくれます。また、SQL文が間違っているなら、赤文字で表示もしてくれます。以下は最後の閉める括弧が足りないときです。\nHTMLタグを伴う出力が要らずに、SQL文の整形のみが必要なら（保存のときに便利）、\n... use Doctrine\\SqlFormatter\\NullHighlighter; use Doctrine\\SqlFormatter\\SqlFormatter; ... echo (new SqlFormatter(new NullHighlighter()))-\u0026gt;format($query-\u0026gt;sql); ... とコードします。その出力は以下のようになります。\nselect invoice_id, invoice_status, invoice_date from invoice where invoice_status = 5 and invoice_date between \u0026#39;2019-01-01\u0026#39; and \u0026#39;2022-02-01\u0026#39; and invoice_id not in ( select invoice.invoice_id from invoice_member inner join invoice on invoice_member.invoice_id = invoice.invoice_id where invoice_status = 5 and invoice_date between \u0026#39;2019-01-01\u0026#39; and \u0026#39;2022-02-01\u0026#39; ) ","date":"2022-02-07T03:20:25+09:00","permalink":"https://www.larajapan.com/2022/02/07/sql%E6%96%87%E3%82%92%E3%83%95%E3%82%A9%E3%83%BC%E3%83%9E%E3%83%83%E3%83%88/","title":"SQL文をフォーマット"},{"content":"以前にEloquentの出力のチャンキングを紹介しました。処理中のメモリーの使用量を減らすためのチャンキングですが、実行速度が遅いということでそれを改善する対策もこしらえました。３年前以上のことです。しかし、そのためのメソッドがすでにEloquentのメソッドに存在することに最近気づきました。\nまず、以前のポストのリンクを再掲載（読んでね）。 チャンキングで使用メモリを抑える チャンキングは遅い\n後者の「チャンキングは遅い」での対策は、対象のテーブルのプライマリIDを指定してクエリーを実行して、チャンキングの速度の改善を図りました。ここで再掲載します。\n... $max = User::max(\u0026#39;id\u0026#39;); $unit = 1000; for ($i = 0; $i \u0026lt; intval(ceil($max/$unit)); $i++) { $from = $i * $unit + 1; $to = ($i + 1) * $unit; $rows = User::where(\u0026#39;id\u0026#39;, \u0026#39;\u0026gt;=\u0026#39;, $from) -\u0026gt;where(\u0026#39;id\u0026#39;, \u0026#39;\u0026lt;=\u0026#39;, $to) -\u0026gt;get(); foreach ($rows as $row) { $values = [ $row-\u0026gt;id, $row-\u0026gt;created_at, $row-\u0026gt;name, $row-\u0026gt;email ]; fputcsv($fh, $values); } } ... 単にループしているだけで、毎回毎回これをコードするのはちょっと面倒ですね。\nところが、最近、新人のプログラマーが書いたコードをレビューしていたら、\n... User::chunkById(1000, function($rows) use($fh) { foreach ($rows as $row) { $values = [ $row-\u0026gt;id, $row-\u0026gt;created_at, $row-\u0026gt;name, $row-\u0026gt;email ]; fputcsv($fh, $values); } }); ... chunkById()というメソッドを使いすっきりしたコードになっているではないですか。\nそんなメソッドあったかなあと思いつつ、tinkerを使って実行されたsql文を見てみると、\n\u0026gt;\u0026gt;\u0026gt; User::chunkbyId(1000, function($rows) {}) =\u0026gt; true \u0026gt;\u0026gt;\u0026gt; sql() =\u0026gt; [ [ \u0026#34;query\u0026#34; =\u0026gt; \u0026#34;select * from `users` order by `id` asc limit 1000\u0026#34;, \u0026#34;bindings\u0026#34; =\u0026gt; [], \u0026#34;time\u0026#34; =\u0026gt; 3.86, ], [ \u0026#34;query\u0026#34; =\u0026gt; \u0026#34;select * from `users` where `id` \u0026gt; ? order by `id` asc limit 1000\u0026#34;, \u0026#34;bindings\u0026#34; =\u0026gt; [ 1000, ], \u0026#34;time\u0026#34; =\u0026gt; 2.03, ], [ \u0026#34;query\u0026#34; =\u0026gt; \u0026#34;select * from `users` where `id` \u0026gt; ? order by `id` asc limit 1000\u0026#34;, \u0026#34;bindings\u0026#34; =\u0026gt; [ 2000, ], \u0026#34;time\u0026#34; =\u0026gt; 1.85, ], ... 私の解決策では上限と下限のidを指定してレコードを取得しているところ、chunkId()では、idで並べ替えして、開始のidを指定してそこからチャンクの数（ここでは、1000）を指定しています。なるほど。実行速度も測定したところ私の解決策と同様にchunk()に比べて確実に速い。\nさて、この便利もののchunkById()ですが、ひとつ注意が必要です。\n例えば、以下のように２つのテーブルをjoinしてchunkById()を適用するとき、\n\u0026gt;\u0026gt;\u0026gt; Shop::join(\u0026#39;product\u0026#39;, \u0026#39;shop.shop_id\u0026#39;, \u0026#39;=\u0026#39;, \u0026#39;product.shop_id\u0026#39;) -\u0026gt;chunkbyId(1000, function($row) { /* nothing */ }); Illuminate\\Database\\QueryException with message \u0026#39;SQLSTATE[23000]: Integrity constraint violation: 1052 Column \u0026#39;shop_id\u0026#39; in where clause is ambiguous (SQL: select `shop`.`shop_id`, `product`.`product_id` from `shop` inner join `product` on `shop`.`shop_id` = `product`.`shop_id` where `shop_id` \u0026gt; 1913 order by `shop_id` asc limit 1000)\u0026#39; shop_idが曖昧というエラーになってしまいます。\n使用されている、sql文を見ると、\n\u0026gt;\u0026gt;\u0026gt; sql() =\u0026gt; [ [ \u0026#34;query\u0026#34; =\u0026gt; \u0026#34;select `shop`.`shop_id`, `product`.`product_id` from `shop` inner join `product` on `shop`.`shop_id` = `product`.`shop_id` order by `shop_id` asc limit 1000\u0026#34;, \u0026#34;bindings\u0026#34; =\u0026gt; [], \u0026#34;time\u0026#34; =\u0026gt; 5.26, ], ] order By shop_idのshop_idがshopのテーブルの項目か、productのテーブルの項目かわらかないのです。\nこれを解決するには、\n\u0026gt;\u0026gt;\u0026gt; Shop::join(\u0026#39;product\u0026#39;, \u0026#39;shop.shop_id\u0026#39;, \u0026#39;=\u0026#39;, \u0026#39;product.shop_id\u0026#39;) -\u0026gt;chunkbyId(1000, function($row) { /* nothing */ }, \u0026#39;shop.shop_id\u0026#39;, \u0026#39;shop_id\u0026#39;); chunkById()の第３番目の引数に、明確に使用する項目名(shop.shop_id)を、第４番目にそれに取り替わる元の項目名(shop_id)を指定する必要があります。\n実行するsql文は以下のようになり、エラーがなくなります。\n\u0026gt;\u0026gt;\u0026gt; sql() =\u0026gt; [ [ \u0026#34;query\u0026#34; =\u0026gt; \u0026#34;select * from `shop` inner join `product` on `shop`.`shop_id` = `product`.`shop_id` order by `shop`.`shop_id` asc limit 1000\u0026#34;, \u0026#34;bindings\u0026#34; =\u0026gt; [], \u0026#34;time\u0026#34; =\u0026gt; 27.81, ], [ \u0026#34;query\u0026#34; =\u0026gt; \u0026#34;select * from `shop` inner join `product` on `shop`.`shop_id` = `product`.`shop_id` where `shop`.`shop_id` \u0026gt; ? order by `shop`.`shop_id` asc limit 1000\u0026#34;, \u0026#34;bindings\u0026#34; =\u0026gt; [ 1913, ], \u0026#34;time\u0026#34; =\u0026gt; 22.15, ], ... ","date":"2022-01-30T12:45:32+09:00","permalink":"https://www.larajapan.com/2022/01/30/%E3%81%84%E3%81%A4%E3%81%AE%E3%81%BE%E3%81%AB%E3%81%8B%E7%99%BB%E5%A0%B4%E3%81%97%E3%81%9Fchunkbyid/","title":"存在を知らなかったchunkById()"},{"content":"phpでは配列のデータをループするならforeachを使います。LaravelのCollectionでもforeachを使ってループできます。しかし、Collectionならeach()を使いましょう。\nforeachからeachへ まず、tinkerでCollectionのデータを作成します。\n\u0026gt;\u0026gt;\u0026gt; use Illuminate\\Support\\Collection; \u0026gt;\u0026gt;\u0026gt; $c = Collection::times(5); =\u0026gt; Illuminate\\Support\\Collection {#3500 all: [ 1, 2, 3, 4, 5, ], } foreachを使ってループします。\n\u0026gt;\u0026gt;\u0026gt; foreach($c as $item) { echo $item.\u0026#34; \u0026#34;; } 1 2 3 4 5 これと同じことが、Collectionのメソッドのeach()で可能です。\n\u0026gt;\u0026gt;\u0026gt; $c-\u0026gt;each(function($item) { echo $item.\u0026#34; \u0026#34;; }) 1 2 3 4 5 continue, break foreachでは、ループの途中である条件でスキップ(continue)、あるいはループから抜け出す(break)ことができます。each()でも同様なことができますが、ちょっと違うやり方です。\ncontinueの代わりに、return true（あるいは returnだけ）とします。\n\u0026gt;\u0026gt;\u0026gt; foreach($c as $item) { if ($item \u0026gt; 2) continue; echo $item.\u0026#34; \u0026#34;; } 1 2 \u0026gt;\u0026gt;\u0026gt; $c-\u0026gt;each(function($item) { if ($item \u0026gt; 2) return true; echo $item.\u0026#34; \u0026#34;; }) 1 2 breakの代わりに、return falseとします。\n\u0026gt;\u0026gt;\u0026gt; foreach($c as $item) { if ($item \u0026gt; 2) break; echo $item.\u0026#34; \u0026#34;; } 1 2 \u0026gt;\u0026gt;\u0026gt; $c-\u0026gt;each(function($item) { if ($item \u0026gt; 2) return false; echo $item.\u0026#34; \u0026#34;; }) 1 2 変数のスコープ foreachでは以下のようなことは簡単です。 \u0026gt;\u0026gt;\u0026gt; $sum = 0 \u0026gt;\u0026gt;\u0026gt; foreach($c as $item) { $sum += $item; } \u0026gt;\u0026gt;\u0026gt; echo $sum 15 しかし、eachでは、\n\u0026gt;\u0026gt;\u0026gt; $sum = 0 \u0026gt;\u0026gt;\u0026gt; $c-\u0026gt;each(function($item) { $sum += $item; }) \u0026lt;warning\u0026gt;PHP Notice: Undefined variable: sum in Psy Shell code on line 2\u0026lt;/warning\u0026gt; \u0026lt;warning\u0026gt;PHP Notice: Undefined variable: sum in Psy Shell code on line 2\u0026lt;/warning\u0026gt; \u0026lt;warning\u0026gt;PHP Notice: Undefined variable: sum in Psy Shell code on line 2\u0026lt;/warning\u0026gt; \u0026lt;warning\u0026gt;PHP Notice: Undefined variable: sum in Psy Shell code on line 2\u0026lt;/warning\u0026gt; \u0026lt;warning\u0026gt;PHP Notice: Undefined variable: sum in Psy Shell code on line 2\u0026lt;/warning\u0026gt; \u0026gt;\u0026gt;\u0026gt; echo $sum 0 foreachではOkなのに、each()のループの中では、$sumは未定義となりループの外では値が初期値のままです。警告もでていますね。\nこれを正しく行うには、each()のコールバックの関数に$sumの変数をポインターで渡す必要あります。この手のuse()の使用はよくあります。\n\u0026gt;\u0026gt;\u0026gt; $c-\u0026gt;each(function($item) use(\u0026amp;$sum) { $sum += $item; }) \u0026gt;\u0026gt;\u0026gt; echo $sum; 15 まあ、この例ではそんなことしなくても、sum()を使えば、\n\u0026gt;\u0026gt;\u0026gt; $c-\u0026gt;sum() 15 で済みますけれど。\nしかし、each()を使えば、こんなことも可能です。\n\u0026gt;\u0026gt;\u0026gt; $sum = $c-\u0026gt;each(function($item) { echo $item.\u0026#34; \u0026#34;; })-\u0026gt;sum() 1 2 3 4 5 \u0026gt;\u0026gt;\u0026gt; echo $sum 15 これは、each()がCollectionを返すので、それをsum()にチェーンできるからです。\n","date":"2022-01-24T03:42:16+09:00","permalink":"https://www.larajapan.com/2022/01/24/laravel-collection%EF%BC%88%EF%BC%99%EF%BC%89foreach%E3%81%AE%E4%BB%A3%E3%82%8F%E3%82%8A%E3%81%ABeach/","title":"Laravel Collection（９）foreachの代わりにeach"},{"content":"サイトのアクセスが増えてくると、ロードバランスを導入して複数の仮想マシンでウェブサーバーを立ち上げる必要性が出てきます。となると、必要になるのは例えば認証したユーザーのセッション情報を複数のマシンで共有すること。そこでredisの登場です。\nセッションのデータはどこにある Laravelのプログラムにアクセスすると、すぐに以下のようなセッションのためのクッキーがブラウザに作成されます。クッキーの名前は、.envのAPP_NAMEの値＋'_session'です。 そしてサーバー側でも、storage/framework/sessionsのディレクトリに以下のようにファイルが作成されます。\n$ ls -l ... -rw-r--r-- 1 www-data www-data 179 Jan 15 11:07 JursZgi5bCURHOze0BmxxKne8EEn0EObDAVT5dBn セッションのファイルの中身は、serialize()されたデータです。\n$ cat JursZgi5bCURHOze0BmxxKne8EEn0EObDAVT5dBn a:3:{s:6:\u0026#34;_token\u0026#34;;s:40:\u0026#34;0s8g4C7pDtn1HMgoR5KHzeuBUBEYeo8kOSq4YdpH\u0026#34;;s:9:\u0026#34;_previous\u0026#34;;a:1:{s:3:\u0026#34;url\u0026#34;;s:21:\u0026#34;http://repos.test/l8x\u0026#34;;}s:6:\u0026#34;_flash\u0026#34;;a:2:{s:3:\u0026#34;old\u0026#34;;a:0:{}s:3:\u0026#34;new\u0026#34;;a:0:{}}}% 以上がデフォルトのセッションの設定です。\nさて、ロードバランスを導入して、１つのウェブサイトのアクセスを複数のマシンで処理するとなると、アクセスするユーザーが実際にどのマシンにアクセスするかは、毎回のアクセスごとに変わります。最初のアクセスはマシン１のウェブサーバーだけれど、次はマシン２のウェブサーバーとか。セッションがファイルに保管されるなら、セッションがそれぞれのマシンのファイルで作成されることになり、もしマシン１でログインしたなら、その認証情報を含むセッションはマシン１のみに存在し、マシン２にアクセスすると認証されていないという問題が起こります。\nこの問題を解決するには、セッションの情報を複数のマシンで共有する要があります。セッションのデータを違うサーバーで一元管理して、どのマシンのLaravelも同じセッションにアクセスすることができるようにするのです。それを実現するのが、redisサーバーです。\nローカル環境でredisを立ち上げる ライブの環境では、awsのelasticcacheなどのサービスを使用しますが、ここではローカル環境でredisサーバーを立ち上げます。\n以下は私のWindowsのWSLのUbuntuの環境でのredisサーバーのインストールです。\n$ sudo apt install redis-server /etc/redis/redis.confの以下の部分の設定が、\n/etc/redis/redis.conf ... # If you run Redis from upstart or systemd, Redis can interact with your # supervision tree. Options: # supervised no - no supervision interaction # supervised upstart - signal upstart by putting Redis into SIGSTOP mode # supervised systemd - signal systemd by writing READY=1 to $NOTIFY_SOCKET # supervised auto - detect upstart or systemd method based on # UPSTART_JOB or NOTIFY_SOCKET environment variables # Note: these supervision methods only signal \u0026#34;process is ready.\u0026#34; # They do not enable continuous liveness pings back to your supervisor. supervised systemd ... supervised systemd となるように確認あるいは編集してから、以下を実行してredisサーバーをスタートさせます。\n$ sudo service redis-server start Laravelでredisの設定 Laravelでredisを使用するには、まず、以下のパッケージのインストールが必要です。\n$ composer require predis/predis また、以下でRedisのファサードのエイリアスが定義されていることを確認してください。コメントされているかもしれません。\nconfig/app.php ... \u0026#39;aliases\u0026#39; =\u0026gt; [ ... \u0026#39;Redis\u0026#39; =\u0026gt; Illuminate\\Support\\Facades\\Redis::class, ... 次に、.envで以下の環境変数を設定します。\n.env ... SESSION_DRIVER=redis #　デフォルトではこれはfile REDIS_CLIENT=predis REDIS_URL=\u0026#34;tcp://127.0.0.1:6379?database=2\u0026#34; # ローカル環境のredisサーバー用 REDIS_PASSWORD= # aws elastic cacheを使用すると以下のような設定 # REDIS_URL=\u0026#34;tls://master.xxxx.cache.amazonaws.com:6379?database=2\u0026#34; # REDIS_PASSWORD=\u0026#34;xxxxx\u0026#34; ... REDIS関連の環境変数は、データベースの設定ファイルで定義されています。\nconfig/database.php ... \u0026#39;redis\u0026#39; =\u0026gt; [ \u0026#39;client\u0026#39; =\u0026gt; env(\u0026#39;REDIS_CLIENT\u0026#39;, \u0026#39;phpredis\u0026#39;), \u0026#39;options\u0026#39; =\u0026gt; [ \u0026#39;cluster\u0026#39; =\u0026gt; env(\u0026#39;REDIS_CLUSTER\u0026#39;, \u0026#39;redis\u0026#39;), \u0026#39;prefix\u0026#39; =\u0026gt; env(\u0026#39;REDIS_PREFIX\u0026#39;, Str::slug(env(\u0026#39;APP_NAME\u0026#39;, \u0026#39;laravel\u0026#39;), \u0026#39;_\u0026#39;).\u0026#39;_database_\u0026#39;), ], \u0026#39;default\u0026#39; =\u0026gt; [ \u0026#39;url\u0026#39; =\u0026gt; env(\u0026#39;REDIS_URL\u0026#39;), \u0026#39;host\u0026#39; =\u0026gt; env(\u0026#39;REDIS_HOST\u0026#39;, \u0026#39;127.0.0.1\u0026#39;), \u0026#39;password\u0026#39; =\u0026gt; env(\u0026#39;REDIS_PASSWORD\u0026#39;, null), \u0026#39;port\u0026#39; =\u0026gt; env(\u0026#39;REDIS_PORT\u0026#39;, \u0026#39;6379\u0026#39;), \u0026#39;database\u0026#39; =\u0026gt; env(\u0026#39;REDIS_DB\u0026#39;, \u0026#39;0\u0026#39;), ], ... redisのセッションにアクセスする さて、設定が完了したところで、ブラウザでサイトにアクセスしてみます。 その後、tinkerでセッションの値が作成されるか見てみましょう。 \u0026gt;\u0026gt;\u0026gt; Redis::keys(\u0026#39;*\u0026#39;) =\u0026gt; [ \u0026#34;laravel_database_laravel_cache:Z6UYF4Af706mB1WGJJwQZRXLvPF7b8ffTy70HTe6\u0026#34;, ] ローカル環境では、私一人しかアクセスしていないので、セッションのキーは１つしかありません。 そのキーに対応する値を見るには、REDIS_PREFIXで指定されている、laravel_database_以降の文字列をReids::get()の引数に指定する必要あります。\n\u0026gt;\u0026gt;\u0026gt; Redis::get(\u0026#39;laravel_cache:Z6UYF4Af706mB1WGJJwQZRXLvPF7b8ffTy70HTe6\u0026#39;) =\u0026gt; \u0026#34;s:179:\u0026#34;a:3:{s:6:\u0026#34;_token\u0026#34;;s:40:\u0026#34;h3xxEGjtBqajmdLG8LjcP1UKBTiCtfqmGSGlyiDL\u0026#34;;s:9:\u0026#34;_previous\u0026#34;;a:1:{s:3:\u0026#34;url\u0026#34;;s:21:\u0026#34;http://repos.test/l8x\u0026#34;;}s:6:\u0026#34;_flash\u0026#34;;a:2:{s:3:\u0026#34;old\u0026#34;;a:0:{}s:3:\u0026#34;new\u0026#34;;a:0:{}}}\u0026#34;;\u0026#34; \u0026gt;\u0026gt;\u0026gt; セッションの値が見えましたね。\n","date":"2022-01-17T12:20:40+09:00","permalink":"https://www.larajapan.com/2022/01/17/%E3%82%BB%E3%83%83%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E3%82%B9%E3%83%88%E3%83%AC%E3%83%BC%E3%82%B8%E3%81%ABredis%E3%82%92%E4%BD%BF%E3%81%86/","title":"セッションのストレージにredisを使う"},{"content":"「お客さんにメールが届かないのでどうかして？」というＣＳからの質問。見てみるとexample@yahoo.comであるところ、example@yahpo.comになっていたりします。たった１文字違いですが、会員登録してもこのためにメールが届かないばかりか、次回からは多分ログインもできません。もちろんお客さんの間違いですが、登録時にこれが通ってしまうというのは問題です。これをどうかしたいです。\n緩いメールアドレスのバリデーション Laravelのデフォルトのメールアドレスのバリデーションemailは、結構緩いです。どれだけ緩いが実例見てみましょう。以下は、tinkerを使って、test@gmailという不正なメールアドレスチェックします。 \u0026gt;\u0026gt;\u0026gt; $v = validator([\u0026#39;email\u0026#39; =\u0026gt; \u0026#39;test@gmail\u0026#39;], [\u0026#39;email\u0026#39; =\u0026gt; \u0026#39;required|email\u0026#39;]) =\u0026gt; Illuminate\\Validation\\Validator {#3534 +customMessages: [], +fallbackMessages: [], +customAttributes: [], +customValues: [], +excludeUnvalidatedArrayKeys: null, +extensions: [], +replacers: [], } \u0026gt;\u0026gt;\u0026gt; $v-\u0026gt;passes() =\u0026gt; true あっさり、通ってしまいます。\nまた、example.comとかのドメイン名は実在しないのに、これも通ります。\n\u0026gt;\u0026gt;\u0026gt; $v = validator([\u0026#39;email\u0026#39; =\u0026gt; \u0026#39;test@example.com\u0026#39;], [\u0026#39;email\u0026#39; =\u0026gt; \u0026#39;required|email\u0026#39;]) ... \u0026gt;\u0026gt;\u0026gt; $v-\u0026gt;passes() =\u0026gt; true email:dnsの登場 メールアドレスの＠マークの後ろの部分、例えば、test@example.comなら、example.comの部分、はドメイン名と言ってインターネットでは非常に重要なデータなのですが、このドメインが実在するかどうか、そしてメールを受け取るかどうかがわかれば、少なくともメールアドレスのタイポは登録時に警告できますね。 それを利用したのが、email:dnsのバリデーションルールです。早速、tinkerで試してみましょう。\n\u0026gt;\u0026gt;\u0026gt; $v = validator([\u0026#39;email\u0026#39; =\u0026gt; \u0026#39;test@gmail\u0026#39;], [\u0026#39;email\u0026#39; =\u0026gt; \u0026#39;required|email:dns\u0026#39;]) ... \u0026gt;\u0026gt;\u0026gt; $v-\u0026gt;passes() =\u0026gt; false \u0026gt;\u0026gt;\u0026gt; $v = validator([\u0026#39;email\u0026#39; =\u0026gt; \u0026#39;test@example.com\u0026#39;], [\u0026#39;email\u0026#39; =\u0026gt; \u0026#39;required|email:dns\u0026#39;]) ... \u0026gt;\u0026gt;\u0026gt; $v-\u0026gt;passes() =\u0026gt; false \u0026gt;\u0026gt;\u0026gt; $v = validator([\u0026#39;email\u0026#39; =\u0026gt; \u0026#39;test@yahpo.com\u0026#39;], [\u0026#39;email\u0026#39; =\u0026gt; \u0026#39;required|email:dns\u0026#39;]) ... \u0026gt;\u0026gt;\u0026gt; $v-\u0026gt;passes() =\u0026gt; false 素晴らしいです、みな通りません！\nemail:dnsの使用において注意が必要なのは、それを利用するには、以下の国際化関数、intlのライブラリが必要です。 https://www.php.net/manual/ja/book.intl.php\ndns_get_record() ちょっとemail:dnsの仕組みを探ってみましょう。Laravelのマニュアルによると、egulias/email-validatorのパッケージを使用しているそうな。そこから関心のコードの一部を掲載します。 https://github.com/egulias/EmailValidator/blob/3.x/src/Validation/DNSCheckValidation.php ... class DNSCheckValidation implements EmailValidation { /** * @var int */ protected const DNS_RECORD_TYPES_TO_CHECK = DNS_MX + DNS_A + DNS_AAAA; /** * Reserved Top Level DNS Names (https://tools.ietf.org/html/rfc2606#section-2), * mDNS and private DNS Namespaces (https://tools.ietf.org/html/rfc6762#appendix-G) */ const RESERVED_DNS_TOP_LEVEL_NAMES = [ //これらのドメイン名はみなエラーとなります // Reserved Top Level DNS Names \u0026#39;test\u0026#39;, \u0026#39;example\u0026#39;, \u0026#39;invalid\u0026#39;, \u0026#39;localhost\u0026#39;, // mDNS \u0026#39;local\u0026#39;, // Private DNS Namespaces \u0026#39;intranet\u0026#39;, \u0026#39;internal\u0026#39;, \u0026#39;private\u0026#39;, \u0026#39;corp\u0026#39;, \u0026#39;home\u0026#39;, \u0026#39;lan\u0026#39;, ]; ... private function validateDnsRecords($host) : bool { // A workaround to fix https://bugs.php.net/bug.php?id=73149 /** @psalm-suppress InvalidArgument */ set_error_handler( static function (int $errorLevel, string $errorMessage): ?bool { throw new \\RuntimeException(\u0026#34;Unable to get DNS record for the host: $errorMessage\u0026#34;); } ); try { // Get all MX, A and AAAA DNS records for host $dnsRecords = dns_get_record($host, static::DNS_RECORD_TYPES_TO_CHECK); } catch (\\RuntimeException $exception) { $this-\u0026gt;error = new InvalidEmail(new UnableToGetDNSRecord(), \u0026#39;\u0026#39;); return false; } finally { restore_error_handler(); } ... 上の４５行目見てください。dns_get_record()なるphp関数があったのですね。 https://www.php.net/manual/ja/function.dns-get-record.php\n上の説明によると、こんなしてドメイン情報取得できるようです。\n\u0026gt;\u0026gt;\u0026gt; dns_get_record(\u0026#34;yahpo.com\u0026#34;, DNS_MX); =\u0026gt; [ [ \u0026#34;host\u0026#34; =\u0026gt; \u0026#34;yahpo.com\u0026#34;, \u0026#34;class\u0026#34; =\u0026gt; \u0026#34;IN\u0026#34;, \u0026#34;ttl\u0026#34; =\u0026gt; 0, \u0026#34;type\u0026#34; =\u0026gt; \u0026#34;MX\u0026#34;, \u0026#34;pri\u0026#34; =\u0026gt; 0, \u0026#34;target\u0026#34; =\u0026gt; \u0026#34;\u0026#34;, ], ] \u0026gt;\u0026gt;\u0026gt; dns_get_record(\u0026#34;yahoo.com\u0026#34;, DNS_MX); =\u0026gt; [ [ \u0026#34;host\u0026#34; =\u0026gt; \u0026#34;yahoo.com\u0026#34;, \u0026#34;class\u0026#34; =\u0026gt; \u0026#34;IN\u0026#34;, \u0026#34;ttl\u0026#34; =\u0026gt; 0, \u0026#34;type\u0026#34; =\u0026gt; \u0026#34;MX\u0026#34;, \u0026#34;pri\u0026#34; =\u0026gt; 1, \u0026#34;target\u0026#34; =\u0026gt; \u0026#34;mta5.am0.yahoodns.net\u0026#34;, ], [ \u0026#34;host\u0026#34; =\u0026gt; \u0026#34;yahoo.com\u0026#34;, \u0026#34;class\u0026#34; =\u0026gt; \u0026#34;IN\u0026#34;, \u0026#34;ttl\u0026#34; =\u0026gt; 0, \u0026#34;type\u0026#34; =\u0026gt; \u0026#34;MX\u0026#34;, \u0026#34;pri\u0026#34; =\u0026gt; 1, \u0026#34;target\u0026#34; =\u0026gt; \u0026#34;mta6.am0.yahoodns.net\u0026#34;, ], [ \u0026#34;host\u0026#34; =\u0026gt; \u0026#34;yahoo.com\u0026#34;, \u0026#34;class\u0026#34; =\u0026gt; \u0026#34;IN\u0026#34;, \u0026#34;ttl\u0026#34; =\u0026gt; 0, \u0026#34;type\u0026#34; =\u0026gt; \u0026#34;MX\u0026#34;, \u0026#34;pri\u0026#34; =\u0026gt; 1, \u0026#34;target\u0026#34; =\u0026gt; \u0026#34;mta7.am0.yahoodns.net\u0026#34;, ], ] 最初のタイポのドメイン名のtargetは存在しないけれど、正式なドメインは３つtargetを返しています。Laravelのemail:dnsはこれを利用しているのですね。\n最後に メールのバリデーションには、email:dnsの他にもrfcとかstrictとかspoofとかあります。 emailとemail:rfcは同じです。また、email:rfc,dnsとように合わせての使用も可能です。 あと大事なのは、バリデーションでは必ず弾かれてしまうが、test.@docomo.ne.jpのようにドットが不正な場所にあるにもかかわらず実在するメールアドレスの対応です。docomo.ne.jpやezweb.ne.jpではその不正を許す必要あります。まだ使っている人いるんですよ。\n","date":"2022-01-09T01:43:32+09:00","permalink":"https://www.larajapan.com/2022/01/09/dns%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E3%83%A1%E3%83%BC%E3%83%AB%E3%82%A2%E3%83%89%E3%83%AC%E3%82%B9%E3%82%92%E3%83%81%E3%82%A7%E3%83%83%E3%82%AF/","title":"DNSを使ってメールアドレスをチェック"},{"content":"Laravelのようなフレームワークが登場する以前は、誰しもSQL文を作成してmysqli_query()のような関数に引数として渡して実行していたものです。QueryBuilderやEloquentのORMなぞ聞いたこともない時代でした。その過去に戻るわけではないですが、それと同じこと、つまりSQL文を管理画面に直接入力して実行できないかと考えた次第です。\nこんなものが欲しい 以下の画面のように、SQL文を入力してその実行ボタンを押すと結果を画面で表示してくれるものが欲しいのです。この画面では、テスト１とテスト２の２つサイトがあり、それぞれのサイトで使用されているDBに対してSQL文を実行しています。\nなぜこのような機能が欲しいかというと、お客さんのビジネスでは今月の売り上げなどの定型のレポートだけでなく、例えば、ある時期にこの商品を購入したお客さんの中で何人が違う時期（最初の時期以降）で同じ商品を買ったのか、とかアドホックのリクエストが結構あるからです。\nまた、お客さんだけでなくシステム管理者は特定のDBテーブルのidの最大値が使用DBタイプの最大値に近づいていないか、など定期的に実行するクエリーも必要です。\nしかし、これらのためにいちいち画面を伴うコントローラを作成するのは面倒であるし、そのときだけに必要で後には要らなくなることも多々です。それらのときに、上のようにカスタムのSQLを入力するだけで実行でき結果を見れる、さらにクエリと結果を保存し後に必要なときにも実行できる、としたら素晴らしいと思いませんか！\n管理画面でSQL文を直接入力して実行するホラー さて、欲しい機能は素晴らしいのだけれど、直接のSQL文入力・実行でホラーとなる状況はいくつかあります。特に以下の２点。\n私の目的はあくまでもクエリーでありSELECTのSQL文だけの実行です。しかし、INSERTやUPDATEさらにDELETEもSQL文なので入力されて実行されたら、完全なホラーです。これらのSQL文が入力させないようにするか、入力しても実行を避ける方法が必要です。\n次は、クエリーの実行時間がやたらに長くなりシステムの負荷となることです。入力するクエリーの実行に要する時間は、クエリーが複雑となると実行する前にわかるものではありません。もし、クエリーの実行時間が指定した実行所要時間以上となったら実行を自動的に打ち切りにしたいです。\nということで、これらを避ける対策を前もって考えます。\nLaravelでどうSQL文を実行する？ ホラーの対策を考える前に、まず、LaravelでSQL文を実行してみましょう。tinkerで試してみます。\n\u0026gt;\u0026gt;\u0026gt; DB::select(\u0026#34;select count(*) as \u0026#39;会員数\u0026#39; from users\u0026#34;); =\u0026gt; [ {#3514 +\u0026#34;会員数\u0026#34;: 2, }, ] 簡単ですね。項目名にもUTF8なら日本語でエイリアスとできます。\nしかし、恐ろしいのは先に述べたように、こういうこともできてしまいます。\n\u0026gt;\u0026gt;\u0026gt; DB::select(\u0026#34;delete from users\u0026#34;); =\u0026gt; [] \u0026gt;\u0026gt;\u0026gt; User::all(); =\u0026gt; Illuminate\\Database\\Eloquent\\Collection {#3529 all: [], } DBテーブルは空になってしまいました。\nDB更新のSQL文の実行を防ぐ SQL文を簡単に実行できることがわかったところで、ホラーの対策を考えてみます。いくつかあります。\nその１ 簡単なのはSQL文にinsert, update, deleteなどが含まれていたら、プログラムで実行させない。preg_matchとか使用してバリデーションです。しかし、updated_atとかの項目がSQL文にあったら、それもマッチしてしまうので、updateの前後にスペースがあるかなどのマッチのパターンの調整が必要です。 その２ DBのユーザーを読み込み専用とする。これがベストですがいちいちそのために読み込み専用のDBユーザー作成する必要があります。ホストするプロバイダーによっては読み込み専用と読み書きのユーザーを分けて提供してくれるところもあります。.envあるいはconf/database.phpでの設定に工夫も必要となります。 その３ DBの読み込みセッションを使う。私が使用しているMySQL5.7では以下のようにたとえDBユーザーの権限に書き込みがあっても、以下のような読み込みセッションでDB更新の句クエリーを実行不可とできます。以下のDB::statement('SET SESSION TRANSACTION READ ONLY')の実行後は、レコード削除のSQL文のはエラーとなります。 \u0026gt;\u0026gt;\u0026gt; DB::select(\u0026#39;select count(*) from users\u0026#39;) =\u0026gt; [ {#3436 +\u0026#34;count(*)\u0026#34;: 1, }, ] \u0026gt;\u0026gt;\u0026gt; DB::statement(\u0026#39;SET SESSION TRANSACTION READ ONLY\u0026#39;) =\u0026gt; true \u0026gt;\u0026gt;\u0026gt; DB::select(\u0026#39;delete from users\u0026#39;) Illuminate\\Database\\QueryException with message \u0026#39;SQLSTATE[25006]: Read only sql transaction: 1792 Cannot execute statement in a READ ONLY transaction. (SQL: delete from users)\u0026#39; \u0026gt;\u0026gt;\u0026gt; DB::select(\u0026#39;select count(*) from users\u0026#39;) =\u0026gt; [ {#3441 +\u0026#34;count(*)\u0026#34;: 1, }, ] クエリーの実行所有時間の制限 こちらは、DBセッションにおける最大実行時間を設定することで、制限をかけることが可能です。以下は、最大実行時間を1000ミリ秒、つまり１秒としました。２秒のスリープの実行では１秒を超えたときにエラーとなります。\n\u0026gt;\u0026gt;\u0026gt; DB::statement(\u0026#39;SET SESSION MAX_EXECUTION_TIME=1000\u0026#39;) =\u0026gt; true \u0026gt;\u0026gt;\u0026gt; DB::select(\u0026#39;select sleep(1) from users\u0026#39;) =\u0026gt; [ {#3438 +\u0026#34;sleep(1)\u0026#34;: 0, }, ] \u0026gt;\u0026gt;\u0026gt; DB::select(\u0026#39;select sleep(2) from users\u0026#39;) Illuminate\\Database\\QueryException with message \u0026#39;SQLSTATE[HY000]: General error: 3024 Query execution was interrupted, maximum statement execution time exceeded (SQL: select sleep(2) from users)\u0026#39; 最後に 安全にクエリのSQL文の実行の仕方が解ったところで、実行したときにクエリーの実際の所有時間も知りたいですね。 それは以下のように、前もってDB::listen()を実行すれば、実行後に取得できます。ここでも時間単位はミリ秒です。\n\u0026gt;\u0026gt;\u0026gt; $time = 0 =\u0026gt; 0 \u0026gt;\u0026gt;\u0026gt; DB::listen(function($query) use (\u0026amp;$time) { $time = $query-\u0026gt;time; }) =\u0026gt; null \u0026gt;\u0026gt;\u0026gt; DB::select(\u0026#39;select count(*) from users\u0026#39;) =\u0026gt; [ {#3438 +\u0026#34;count(*)\u0026#34;: 1, }, ] \u0026gt;\u0026gt;\u0026gt; $time =\u0026gt; 0.48 ","date":"2021-12-14T07:46:54+09:00","permalink":"https://www.larajapan.com/2021/12/14/sql%E6%96%87%E3%82%92%E7%94%BB%E9%9D%A2%E3%81%A7%E5%85%A5%E5%8A%9B%E3%81%97%E3%81%A6%E5%AE%89%E5%85%A8%E3%81%AB%E5%AE%9F%E8%A1%8C%E3%81%97%E3%81%A6%E7%B5%90%E6%9E%9C%E3%82%92%E8%BF%94%E3%81%99/","title":"SQL文を画面で入力して安全に実行して結果を返す"},{"content":"LaravelではFeatureテストが簡単に行えるように、便利なテスト用メソッドが用意されています。今回はそちらを使っていてハマってしまった意図せぬ挙動について紹介します。\nHTTPテスト 例えば特定のURLにリクエストを投げ、正しいレスポンスが返されるか？などのFeatureテストをする際に、Laravelでは以下のようなテストが書けます。\ntests/Feature/ExampleTest.php \u0026lt;?php namespace Tests\\Feature; use Illuminate\\Foundation\\Testing\\WithoutMiddleware; use Tests\\TestCase; class ExampleTest extends TestCase { public function test_a_basic_request() { $response = $this-\u0026gt;get(\u0026#39;/\u0026#39;); // HTTPリクエストをシミュレート $response-\u0026gt;assertStatus(200); // レスポンスのステータスコードテスト $response-\u0026gt;assertSee(\u0026#39;Laravel\u0026#39;); // レスポンスに指定した文字列が含まれているかテスト } } 上のテストでは/にGETリクエストを投げ、リクエストが正常に処理されたかステータスコード200をチェック、そして返されたレスポンスにLaravelという文字列が含まれているかをテストします。\nインストール直後の状態では/にアクセスすると以下のページを返しますので、テストは成功するはずです。\nURLに日本語が含まれている場合の挙動 さて、ここからが本題です。私がハマったのはURLに日本語が含まれている場合のテストです。 他の開発者のwindows環境ではパスするテストが、私のmacOS環境では必ず失敗してしまう、 という状況が発生しました。\nどんなテストかを説明する為に例を用意しました。以下のようなページがあるとします。\n入力した検索ワードをそのまま出力するシンプルなページです。\nviewはこんな感じ。\nresources/views/search.blade.php \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;title\u0026gt;Search\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;form action=\u0026#34;/search\u0026#34; method=\u0026#34;get\u0026#34;\u0026gt; \u0026lt;input type=\u0026#34;text\u0026#34; name=\u0026#34;word\u0026#34; value=\u0026#34;\u0026#34;\u0026gt; \u0026lt;input type=\u0026#34;submit\u0026#34; value=\u0026#34;検索\u0026#34;\u0026gt; \u0026lt;/form\u0026gt; \u0026lt;h1\u0026gt;{{ $word }}\u0026lt;/h1\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; routeはこんな感じ。\nroutes/web.php \u0026lt;?php use Illuminate\\Http\\Request; use Illuminate\\Support\\Facades\\Route; Route::get(\u0026#39;/search\u0026#39;, function(Request $request) { $word = $request-\u0026gt;get(\u0026#39;word\u0026#39;, \u0026#39;\u0026#39;); return view(\u0026#39;search\u0026#39;, compact(\u0026#39;word\u0026#39;)); }); 前項のExampleTestをこのページに関するテストに書き換えてみましょう。\ntests/Feature/ExampleTest.php \u0026lt;?php namespace Tests\\Feature; use Illuminate\\Foundation\\Testing\\RefreshDatabase; use Tests\\TestCase; class ExampleTest extends TestCase { public function test_search_page() { $response = $this-\u0026gt;get(\u0026#39;/search?word=ほげ\u0026#39;); $response-\u0026gt;assertStatus(200); $response-\u0026gt;assertSee(\u0026#39;ほげ\u0026#39;); } } \u0026ldquo;ほげ\u0026quot;というワードで検索した場合をシミュレートする為、/search?word=ほげにリクエストを投げ、レスポンスに文字列「ほげ」が含まれているかテストしています。\nテストを実行してみましょう。\nvendor/bin/phpunit --filter ExampleTest PHPUnit 9.5.10 by Sebastian Bergmann and contributors. .F 2 / 2 (100%) Time: 00:00.138, Memory: 20.00 MB There was 1 failure: 1) Tests\\Feature\\ExampleTest::test_search_page Failed asserting that \u0026#39;\u0026lt;!DOCTYPE html\u0026gt;\\n ~~省略~~ \u0026lt;h1\u0026gt;\u0026lt;/h1\u0026gt;\\n \u0026lt;/body\u0026gt;\\n \u0026lt;/html\u0026gt;\u0026#39; contains \u0026#34;ほげ\u0026#34;. レスポンスに「ほげ」が含まれていない、との事でテストが失敗してしまいました。確かに、レスポンス内で検索ワードが表示される箇所が\n\u0026lt;h1\u0026gt;\u0026lt;/h1\u0026gt;\\n となっています。\nちなみに、検索文字列を「hoge」にした場合はパスします。\n~~~ public function test_search_page() { $response = $this-\u0026gt;get(\u0026#39;/search?word=hoge\u0026#39;); // hoge に変更 $response-\u0026gt;assertStatus(200); $response-\u0026gt;assertSee(\u0026#39;hoge\u0026#39;); // hoge に変更 } ~~~ vendor/bin/phpunit --filter ExampleTest PHPUnit 9.5.10 by Sebastian Bergmann and contributors. .. 2 / 2 (100%) Time: 00:00.138, Memory: 20.00 MB OK (2 tests, 3 assertions) なぜでしょう？\nparse_url()のバグ $this-\u0026gt;get()のソースを解析してみると、 vendor/symfony/http-foundation/Request.php 355行目のparse_url()にてURLからパースしたクエリ文字列が文字化けしていました。\n以下はtinkerで同じコードを実行した際の結果です。\nparse_url(\u0026#39;http://127.0.0.1:8000/search?word=ほげ\u0026#39;); =\u0026gt; [ \u0026#34;scheme\u0026#34; =\u0026gt; \u0026#34;http\u0026#34;, \u0026#34;host\u0026#34; =\u0026gt; \u0026#34;127.0.0.1\u0026#34;, \u0026#34;port\u0026#34; =\u0026gt; 8000, \u0026#34;path\u0026#34; =\u0026gt; \u0026#34;/search\u0026#34;, \u0026#34;query\u0026#34; =\u0026gt; b\u0026#34;word=ã_»ã__\u0026#34;, ] 文字化けしていますね。ググってみるとGitHubで以下のIssueを見つけました。\nUTF-8 URIs are broken by parse_url when locale is set\nparse_url()はplatform(macなど)やlocaleの設定によってパースした文字列が壊れる場合があるとのこと。2010年から報告されているこちらのPHPのバグのようです。\nurlencode()で解決 回避策として予め日本語部分をurlencode()でエンコードすれば良さそうです。 ExampleTestを以下のように修正しました。\ntests/Feature/ExampleTest.php ~~~ public function test_search_page() { $word = urlencode(\u0026#39;ほげ\u0026#39;); // 予めURLエンコード $response = $this-\u0026gt;get(\u0026#39;/search?word=\u0026#39;.$word); $response-\u0026gt;assertStatus(200); $response-\u0026gt;assertSee(\u0026#39;ほげ\u0026#39;); } ~~~ テストが通るようになりました。\nvendor/bin/phpunit --filter ExampleTest PHPUnit 9.5.10 by Sebastian Bergmann and contributors. .. 2 / 2 (100%) Time: 00:00.136, Memory: 20.00 MB OK (2 tests, 3 assertions) まとめ HTTPテストで日本語を含むURLにリクエストを投げる際は、日本語部分をURLエンコードするのが良さそうです。\n","date":"2021-12-06T12:01:33+09:00","permalink":"https://www.larajapan.com/2021/12/06/http%E3%83%86%E3%82%B9%E3%83%88%E3%81%A7url%E3%81%AB%E6%97%A5%E6%9C%AC%E8%AA%9E%E3%81%8C%E5%90%AB%E3%81%BE%E3%82%8C%E3%81%A6%E3%81%84%E3%82%8B%E5%A0%B4%E5%90%88%E3%81%AE%E6%84%8F%E5%9B%B3%E3%81%9B/","title":"HTTPテストでURLに日本語が含まれている場合の意図せぬ挙動"},{"content":"ブレードで変数の値を表示するのによく使う括弧。{{ }} と {!! !!}２種類あります。これらに関しての話です。\n{{ }} と {!! !!}の違い {{ }} と {!! !!}の違いは、いたって簡単です。前者はhtmlをエスケープして、後者はエスケープしません。と言っても？？なので、具体的に以下のようなブレードを作成して、違いを見てみましょう。\nresources/views/test.blade.php {{ $x }} {!! $x !!} tinkerでこの$xに値を入れてブレードをビューすると、以下のようなHTML文が出力されます。\n\u0026gt;\u0026gt;\u0026gt; view(\u0026#39;test\u0026#39;)-\u0026gt;with([\u0026#39;x\u0026#39; =\u0026gt; \u0026#39;\u0026lt;h1\u0026gt;hello\u0026lt;/h1\u0026gt;\u0026#39;])-\u0026gt;render(); =\u0026gt; \u0026#34;\u0026#34;\u0026#34; \u0026amp;lt;h1\u0026amp;gt;hello\u0026amp;lt;/h1\u0026amp;gt;\\n \u0026lt;h1\u0026gt;hello\u0026lt;/h1\u0026gt;\\n \u0026#34;\u0026#34;\u0026#34; 前者の表示は、HTMLのタグが見事にエスケープされていますが、後者はHTMLのタグがそのまま残っています。ブラウザでこれらを閲覧すると、\nと表示の違いがはっきりします。\nPHPにコンパイルされたブレード 先の例で使用したtest.blade.phpは、サーバーから表示される前にphpにコンパイルされてstorage/framework/viewsにキャッシュされます。コンパイルされたブレードの中身を見ると、\nstorage/framework/views/4ad4f1be91eef212ba8e4b67c67ee18a0f640a9a.php \u0026lt;?php echo e($x); ?\u0026gt; \u0026lt;?php echo $x; ?\u0026gt; \u0026lt;?php /**PATH /var/www/repos/l8x/resources/views/test.blade.php ENDPATH**/ ?\u0026gt; お馴染みのphpファイルです（ブレードはphpファイル拡張子ですがphpファイルではありません）。なるほど単に標準のPHP関数のecho()を使用しています。しかし、前者において、e()という関数は標準のPHP関数ではありません。調べてみると、これはLaravelのヘルパーです。以下にその定義を掲載します。\nvendor/laravel/framework/src/Illuminate/Support/helpers.php ... /** * Encode HTML special characters in a string. * * @param \\Illuminate\\Contracts\\Support\\DeferringDisplayableValue|\\Illuminate\\Contracts\\Support\\Htmlable|string|null $value * @param bool $doubleEncode * @return string */ function e($value, $doubleEncode = true) { if ($value instanceof DeferringDisplayableValue) { $value = $value-\u0026gt;resolveDisplayableValue(); } if ($value instanceof Htmlable) { return $value-\u0026gt;toHtml(); } return htmlspecialchars($value ?? \u0026#39;\u0026#39;, ENT_QUOTES, \u0026#39;UTF-8\u0026#39;, $doubleEncode); } ... 標準関数のhtmlspecialchars()が最終的にはHTMLタグをエスケープしていることわかります。\nHtmlString さて、ブレードで使用されている括弧の意味が解ったところで、先のtest.blade.phpを以下のように編集します。csrf_field()は、LaravelのCSRFトークンのためのヘルパー関数です。\nresources/views/test.blade.php {{ csrf_field() }} これをtinkerで先と同様にレンダリングします。今回は変数の渡しなしです。\n\u0026gt;\u0026gt;\u0026gt; view(\u0026#39;test\u0026#39;)-\u0026gt;render() =\u0026gt; \u0026#34;\u0026lt;input type=\u0026#34;hidden\u0026#34; name=\u0026#34;_token\u0026#34; value=\u0026#34;\u0026#34;\u0026gt;\\n\u0026#34; おかしいですね。二重波括弧なのに、HTMLタグがエスケープされていません。どうしてでしょう？\ncsrf_field()の定義を見てみましょう。\nvendor/laravel/framework/src/Illuminate/Support/helpers.php ... /** * Generate a CSRF token form field. * * @return \\Illuminate\\Support\\HtmlString */ function csrf_field() { return new HtmlString(\u0026#39;\u0026lt;input type=\u0026#34;hidden\u0026#34; name=\u0026#34;_token\u0026#34; value=\u0026#34;\u0026#39;.csrf_token().\u0026#39;\u0026#34;\u0026gt;\u0026#39;); } ... この関数が返しているのは、文字列ではなくHtmlStringのオブジェクトです。このオブジェクトがブレードでの括弧の変数の値とすると、エスケープ処理がされないのです。tinkerで試してみましょう。\n\u0026gt;\u0026gt;\u0026gt; use Illuminate\\Support\\HtmlString; \u0026gt;\u0026gt;\u0026gt; $x = new HtmlString(\u0026#39;\u0026lt;h1\u0026gt;hello\u0026lt;/h1\u0026gt;\u0026#39;); =\u0026gt; Illuminate\\Support\\HtmlString {#3517 html: \u0026#34;\u0026lt;h1\u0026gt;hello\u0026lt;/h1\u0026gt;\u0026#34;, } \u0026gt;\u0026gt;\u0026gt; e($x) =\u0026gt; \u0026#34;\u0026lt;h1\u0026gt;hello\u0026lt;/h1\u0026gt;\u0026#34; HtmlStringを使う意味 表示される値にHTMLタグが含まれるかどうか前もってわかっていれば、{{ }} と {!! !!}の使いわけできますが、たいていはブレードを作成するのは開発者ではなくデザイナーさんです。HTML文を含むときに文字列ではなく、HtmlStringのオブジェクトとすれば、迷わずにいつも{{ }}の使用となります。\n","date":"2021-11-24T07:19:05+09:00","permalink":"https://www.larajapan.com/2021/11/24/laravel%E3%81%AE%E3%83%96%E3%83%AC%E3%83%BC%E3%83%89%E3%81%A7%E3%81%AE-%E3%81%A8-%E3%81%AE%E9%81%95%E3%81%84/","title":"Laravelのブレードでの {{ }} と {!! !!} の違い"},{"content":"Laravel 8とは直接は関係ないのですが、以前ここで紹介したコード整形プログラムのphp-cs-fixerもバージョンアップとなりました。それにより使用していたその設定ファイルも変更の必要あります。\nphp-cs-fixerはコードを整形するプログラムで、開発者がひとりでももちろんですが複数となったときに特にコードのスタイルの統一に便利です。スタイルの指定、例えばインデントをタブでなく４つの空白文字とする、とかの指定は、すべてphp-cs-fixerの設定ファイルで行います。\nまず、バージョン3となって、その設定ファイルとキャッシュファイル名が以下のように変更となりました。\nバージョン２では、\n.php-cs .php-cs.cache がバージョン３では、\n.php-cs-fixer.php .php-cs-fixer.cache また、スタイル指定のルールも少し変更があります。Laravelのプロジェクトのために、以前と同様に以下のスレッドから取得して\nhttps://gist.github.com/laravel-shift/cab527923ed2a109dda047b97d53c200\n私のgistとしました。この記事の最後に掲載したのでフリーで使用してください！\n設定のルールに関しては以下に説明あります。調査が必要なら参考に。\nhttps://github.com/FriendsOfPHP/PHP-CS-Fixer/blob/master/doc/rules/index.rst\nphp-cs-fixerの使用はいたって簡単です。\n新規に使用するなら、まず\n$ composer require friendsofphp/php-cs-fixer --dev を実行してインストールし、設定ファイル.php-cs-fixer.phpを私のgistから作成して、Laravelのプロジェクトのルートディレクトリから以下を実行すれば、必要な整形を自動でしてくれます。\n$ vendor/bin/php-cs-fixer fix 以下がLaravelのプロジェクト用の設定ファイルです。\n","date":"2021-11-17T13:00:34+09:00","permalink":"https://www.larajapan.com/2021/11/17/laravel-8-x%E6%9B%B4%E6%96%B0%E3%81%A7%E5%A4%89%E3%82%8F%E3%81%A3%E3%81%9F%E3%81%93%E3%81%A8%EF%BC%88%EF%BC%94%EF%BC%89php-cs-fixer/","title":"Laravel 8.x更新で変わったこと（４）php-cs-fixer"},{"content":"DBから取得した値を自動で加工してくれる便利な機能のアクセサですが、時としてアクセサを無視してDBから取得した素の値が欲しいケースがあります。そんな時に使える方法を３つ紹介します。\nどういうケース？ 例えば、公式ドキュメントの「アクセサの定義」には例として以下のコードが紹介されています。\n\u0026lt;?php namespace App; use Illuminate\\Database\\Eloquent\\Model; class User extends Model { /** * ユーザーのファーストネームを取得 * * @param string $value * @return string */ public function getFirstNameAttribute($value) { return ucfirst($value); } } そして、定義したアクセサにより名前の出力は頭文字が大文字となります。\n$user-\u0026gt;first_name; // DBに保存されている素の値は\u0026#34;hikaru\u0026#34; \u0026gt;\u0026gt; \u0026#34;Hikaru\u0026#34; 今回はこのようにアクセサを定義したものの、例外的にDBに保存されている素の値である\u0026quot;hikaru\u0026quot;が欲しくなった、という時に使える方法の解説です。\n1. $attributes まず１つ目に、アクセサを定義したクラス内で使える方法です。DBから取得した素の値は$attributesというprotectedプロパティに格納されているため、Modelクラス内からアクセス可能です。例えば前項のUserクラスにgetPlainFirstName()を追加してみましょう。\n\u0026lt;?php namespace App; use Illuminate\\Database\\Eloquent\\Model; class User extends Model { /** * ユーザーのファーストネームを取得 * * @param string $value * @return string */ public function getFirstNameAttribute($value) { return ucfirst($value); } public function getPlainFirstName() { return $this-\u0026gt;attributes[\u0026#39;first_name\u0026#39;]; } } メソッドを呼び出すと素の値が取得できます。\n$user-\u0026gt;getPlainFirstName(); \u0026gt;\u0026gt; \u0026#34;hikaru\u0026#34; 因みに、$attributesはvendor/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.phpにて定義されています。\n2. getRawOriginal() 続いて、Userクラス外から素の値にアクセスする方法として、getRawOriginal()が使えます。\n$user-\u0026gt;getRawOriginal(\u0026#39;first_name\u0026#39;); \u0026gt;\u0026gt; \u0026#34;hikaru\u0026#34; １点注意が必要なのは、このメソッドは$originalというプロパティから値を取得しています。$originalにはDBから取得した時点の値を保持する配列が格納されており、getDirty()などで変更が加わった項目を判別する際などに使われています。\nつまり、DBから取得した後に変更した値は取得できません。\n$user-\u0026gt;first_name = \u0026#39;kenji\u0026#39;; // 名前を\u0026#34;hikaru\u0026#34;から\u0026#34;kenji\u0026#34;に変更 $user-\u0026gt;getRawOriginal(\u0026#39;first_name\u0026#39;); // $originalから値を取得するので\u0026#34;hikaru\u0026#34;のまま \u0026gt;\u0026gt; \u0026#34;hikaru\u0026#34; こちらのメソッドもHasAttributes.phpに定義されているのでソースを確認してみてください。\n3. getAttributes() 最後に紹介するgetAttributes()は$attributesを返すメソッドです。Userクラス外からアクセスでき、DBから取得した後に値を変更しても変更後の値が取得できます。getRawOriginal()とは異なり引数でキーを指定できない（$attributesそのものを返す）為、取得後に項目を指定する必要があります。\n$user-\u0026gt;getAttributes()[\u0026#39;first_name\u0026#39;]; \u0026gt;\u0026gt; \u0026#34;hikaru\u0026#34; $user-\u0026gt;first_name = \u0026#34;kenji\u0026#34;; $user-\u0026gt;getAttributes()[\u0026#39;first_name\u0026#39;]; // 変更後の値が取得できる \u0026gt;\u0026gt; \u0026#34;kenji\u0026#34; まとめ 以上、アクセサを無視してDBから取得した素の値を取得する方法３つの紹介でした。 紹介しておいてなのですが、アクセサを無視しなければならない箇所がいくつもある場合は今回紹介した方法で置き換えるよりも、本当にそのアクセサが必要なのか？見直す方が良いかと思います。\n","date":"2021-11-13T22:46:26+09:00","permalink":"https://www.larajapan.com/2021/11/13/%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B5%E3%82%92%E7%84%A1%E8%A6%96%E3%81%97%E3%81%A6%E5%80%A4%E3%82%92%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B/","title":"アクセサを無視して値を取得する"},{"content":"今回は前回紹介した画像挿入を応用して顔写真付き名簿を作成してみましょう。\n顔写真付き名簿を作る 例えば、以下のように行ごとにA列に顔写真とB列に名前を出力したいとします。 今回はNameListExportというクラスで実装していきますので予め以下のコマンドで生成して下さい。\nphp artisan make:export NameListExport また、エクセルに出力する画像データはpublic/img配下にあるとします。\nデータに紐付いた画像の挿入 前回の画像挿入と異なり、今回はB列に出力する名前に応じてA列の顔写真が決まります。つまり、名前情報と顔写真（正確にはその画像ファイルパス）が紐付いています。実際の状況ではそれらはDBなどから取得するかと思いますが、今回はcollectionで$membersという変数を用意します。\nまず、NameListExportを以下のように実装しました。$membersはコンストラクタにて外部から渡し、プロパティにセットしています。collection()内で取得すれば良いのでは？と思うかもしれませんが、そうするとdrawings()内で参照することができません。なぜなら、drawings()がcollection()より先に処理されるからです。(詳しくは vendor/maatwebsite/excel/src/Sheet.php のexport()をご確認下さい。)\napp/Exports/NameListExport.php \u0026lt;?php namespace App\\Exports; use Maatwebsite\\Excel\\Concerns\\Exportable; use Maatwebsite\\Excel\\Concerns\\FromCollection; use Maatwebsite\\Excel\\Concerns\\WithDrawings; use Maatwebsite\\Excel\\Concerns\\WithHeadings; use Maatwebsite\\Excel\\Concerns\\WithMapping; use \\PhpOffice\\PhpSpreadsheet\\Worksheet\\Drawing; class NameListExport implements FromCollection, WithHeadings, WithDrawings, WithMapping { use Exportable; protected $members; public function __construct($members) { $this-\u0026gt;members = $members; } public function headings():array { return [ \u0026#39;顔写真\u0026#39;, \u0026#39;名前\u0026#39;, ]; } public function map($member):array { return [\u0026#39;\u0026#39;, $member[\u0026#39;name\u0026#39;]]; // A列は画像を出力するので空文字にしておく } public function collection() { return $this-\u0026gt;members; } public function drawings() { $drawings = []; $rowNum = 2; // 1行目はヘッダ行なので画像出力は２行目から foreach ($this-\u0026gt;members as $member) { $drawing = new Drawing; $drawing-\u0026gt;setPath($member[\u0026#39;file\u0026#39;]); $drawing-\u0026gt;setCoordinates(\u0026#39;A\u0026#39;.$rowNum); $drawing-\u0026gt;setWidthAndHeight(60, 60); // 画像サイズ調整 $drawings[] = $drawing; $rowNum++; } return $drawings; } } それではtinkerで出力してみましょう。\n$members = collect([ [\u0026#39;name\u0026#39; =\u0026gt; \u0026#39;ミケ\u0026#39;, \u0026#39;file\u0026#39; =\u0026gt; public_path(\u0026#39;img/mike.jpg\u0026#39;)], [\u0026#39;name\u0026#39; =\u0026gt; \u0026#39;小虎\u0026#39;, \u0026#39;file\u0026#39; =\u0026gt; public_path(\u0026#39;img/kotora.jpg\u0026#39;)], [\u0026#39;name\u0026#39; =\u0026gt; \u0026#39;おこげ\u0026#39;, \u0026#39;file\u0026#39; =\u0026gt; public_path(\u0026#39;img/okoge.jpg\u0026#39;)], [\u0026#39;name\u0026#39; =\u0026gt; \u0026#39;くろ\u0026#39;, \u0026#39;file\u0026#39; =\u0026gt; public_path(\u0026#39;img/kuro.jpg\u0026#39;)], ]); use App\\Exports\\NameListExport; (new NameListExport($members))-\u0026gt;store(\u0026#39;name_list.xlsx\u0026#39;); 出力結果が以下になりました。（セルの幅と高さを調整してます）\nあれ、名前の行がズレてしまいました。\nどうやら$drawing-\u0026gt;setCoordinates()で２〜５行目に画像をセットしたことで、データを出力する際の開始行がズレてしまったようです。\nこの件についてGithub上でDiscussionされていましたが、Laravel Excelの開発側としてはそこはPhpSpreadsheet側のロジックなのでどうにもできないとのこと。。。。\nAfterSheetイベントで画像挿入 最終的に力技ですが、Laravel Excelのdrawings()は使用せず、AfterSheetイベント（シート作成プロセスの最後に呼ばれるイベント）にて画像をセットするようにしました。これならデータ→画像の順に挿入されるのでデータの開始行がズレません。 NameListExportを以下のように編集しました。WithDrawingの代わりにWithEventsをimplementしています。drawings()は中身はそのままですがgetDrawings()に変更し、registerEvents()内から呼び出すようにしました。そして、取得した$drawingsをAfterSheet内でWorkSheetにセットしています。\napp/Exports/NameListExport.php \u0026lt;?php namespace App\\Exports; use Maatwebsite\\Excel\\Events\\AfterSheet; use Maatwebsite\\Excel\\Concerns\\Exportable; use Maatwebsite\\Excel\\Concerns\\WithEvents; use Maatwebsite\\Excel\\Concerns\\WithMapping; use Maatwebsite\\Excel\\Concerns\\WithHeadings; use Maatwebsite\\Excel\\Concerns\\FromCollection; use \\PhpOffice\\PhpSpreadsheet\\Worksheet\\Drawing; class NameListExport implements FromCollection, WithHeadings, WithMapping, WithEvents { use Exportable; protected $members; public function __construct($members) { $this-\u0026gt;members = $members; } public function headings():array { return [ \u0026#39;顔写真\u0026#39;, \u0026#39;名前\u0026#39;, ]; } public function map($member):array { return [\u0026#39;\u0026#39;, $member[\u0026#39;name\u0026#39;]]; // A列は画像を出力するので空文字にしておく } public function collection() { return $this-\u0026gt;members; } public function getDrawings() { $drawings = []; $rowNum = 2; // 1行目はヘッダ行なので画像出力は２行目から foreach ($this-\u0026gt;members as $member) { $drawing = new Drawing; $drawing-\u0026gt;setPath($member[\u0026#39;file\u0026#39;]); $drawing-\u0026gt;setCoordinates(\u0026#39;A\u0026#39;.$rowNum); $drawing-\u0026gt;setWidthAndHeight(60, 60); $drawings[] = $drawing; $rowNum++; } return $drawings; } public function registerEvents(): array { $drawings = $this-\u0026gt;getDrawings(); return [ AfterSheet::class =\u0026gt; function (AfterSheet $event) use ($drawings) { $workSheet = $event-\u0026gt;sheet-\u0026gt;getDelegate(); // 画像挿入処理 foreach ($drawings as $drawing) { $drawing-\u0026gt;setWorkSheet($workSheet); } } ]; } } 再度tinkerで出力すると行ズレが無い期待通りの出力が得られるはずです。\nまとめ 画像挿入に関しては正直Laravel ExcelのWithDrawingsを使うメリットがあまり感じられませんでした。Laravel Excel側でやってくれるのはWorkSheetへのセットだけですし、それもデータの挿入と併用すると行ズレ問題が発生してしまいます。\n今回色々調べていて実感したのはLaravel ExcelはあくまでPhpShreadsheetのwrapperなので、Laravel Excel側でうまく処理できないケースについてはWithEventsなどを使って直接PhpSpreadsheet側で処理してしまうのが良さそうです。\n","date":"2021-11-11T12:36:08+09:00","permalink":"https://www.larajapan.com/2021/11/11/%E3%81%9D%E3%81%86%E3%81%A0%E3%80%81laravel-excel%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E3%81%BF%E3%82%88%E3%81%86%EF%BC%88%EF%BC%96%EF%BC%89%E9%A1%94%E5%86%99%E7%9C%9F%E4%BB%98%E3%81%8D%E5%90%8D/","title":"そうだ、Laravel Excelを使ってみよう（６）顔写真付き名簿を作成しよう"},{"content":"さらに引き続いて、最新のバージョンのLaravel 8.xの話です。今回はfactoryの話です。\n結構変わりました。これもL7.xと比較してみましょう。\nL7.xでは、namespaceがありません。\napp/database/factories/UserFactory.php /** @var \\Illuminate\\Database\\Eloquent\\Factory $factory */ use App\\User; use Faker\\Generator as Faker; use Illuminate\\Support\\Str; /* |-------------------------------------------------------------------------- | Model Factories |-------------------------------------------------------------------------- | | This directory should contain each of the model factory definitions for | your application. Factories provide a convenient way to generate new | model instances for testing / seeding your application\u0026#39;s database. | */ $factory-\u0026gt;define(User::class, function (Faker $faker) { return [ \u0026#39;name\u0026#39; =\u0026gt; $faker-\u0026gt;name, \u0026#39;email\u0026#39; =\u0026gt; $faker-\u0026gt;unique()-\u0026gt;safeEmail, \u0026#39;email_verified_at\u0026#39; =\u0026gt; now(), \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi\u0026#39;, // password \u0026#39;remember_token\u0026#39; =\u0026gt; Str::random(10), ]; }); L8.xでは、以下のようにnamespaceが入り、Factoryを継承したクラスとなりました。\napp/database/factories/UserFactory.php namespace Database\\Factories; use App\\Models\\User; use Illuminate\\Database\\Eloquent\\Factories\\Factory; use Illuminate\\Support\\Str; class UserFactory extends Factory { /** * The name of the factory\u0026#39;s corresponding model. * * @var string */ protected $model = User::class; /** * Define the model\u0026#39;s default state. * * @return array */ public function definition() { return [ \u0026#39;name\u0026#39; =\u0026gt; $this-\u0026gt;faker-\u0026gt;name(), \u0026#39;email\u0026#39; =\u0026gt; $this-\u0026gt;faker-\u0026gt;unique()-\u0026gt;safeEmail(), \u0026#39;email_verified_at\u0026#39; =\u0026gt; now(), \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi\u0026#39;, // password \u0026#39;remember_token\u0026#39; =\u0026gt; Str::random(10), ]; } /** * Indicate that the model\u0026#39;s email address should be unverified. * * @return \\Illuminate\\Database\\Eloquent\\Factories\\Factory */ public function unverified() { return $this-\u0026gt;state(function (array $attributes) { return [ \u0026#39;email_verified_at\u0026#39; =\u0026gt; null, ]; }); } } 使用の方は、\nL7.xでは、以下のようにクラスのないphp関数だったのが、\nfactory(App\\User::class, 50)-\u0026gt;create(); // Userのレコードを50作成 L8.xでは、\nUser::factory()-\u0026gt;count(50)-\u0026gt;create(); クラスメソッドとなります。\nUser::factory()として使えるのは、以下のようにUserモデルにおいてHasFactoryのトレイトを使用しているからです。\napp/Models/User.php namespace App\\Models; use Illuminate\\Contracts\\Auth\\MustVerifyEmail; use Illuminate\\Database\\Eloquent\\Factories\\HasFactory; use Illuminate\\Foundation\\Auth\\User as Authenticatable; use Illuminate\\Notifications\\Notifiable; use Laravel\\Sanctum\\HasApiTokens; class User extends Authenticatable { use HasApiTokens, HasFactory, Notifiable; .. 最後に、これもLaravel ShiftのアップグレードサービスですべてのFactoryの定義の変換とモデルのクラスの定義にuse HasFactory;を追加してくれるところまで自動変換してくれます。\n","date":"2021-11-05T11:22:00+09:00","permalink":"https://www.larajapan.com/2021/11/05/laravel-8-x%E6%9B%B4%E6%96%B0%E3%81%A7%E5%A4%89%E3%82%8F%E3%81%A3%E3%81%9F%E3%81%93%E3%81%A8%EF%BC%88%EF%BC%93%EF%BC%89factory%E3%81%8C%E3%82%AF%E3%83%A9%E3%82%B9%E5%8C%96%E3%81%95%E3%82%8C%E3%82%8B/","title":"Laravel 8.x更新で変わったこと（３）factoryがクラス化される"},{"content":"引き続いて、Laravel 8.x更新で変わったことです。今回はRoute関連の話です。\nLaravel 8.x以前では、routes/web.phpの中身はこんな感じでした。\nroutes/web.php Route::namespace(\u0026#39;Auth\u0026#39;)-\u0026gt;middleware(\u0026#39;guest\u0026#39;)-\u0026gt;group(function () { // Authentication Routes... Route::get(\u0026#39;\u0026#39;, \u0026#39;LoginController@showLoginForm\u0026#39;); Route::get(\u0026#39;login\u0026#39;, \u0026#39;LoginController@showLoginForm\u0026#39;)-\u0026gt;name(\u0026#39;login\u0026#39;); Route::post(\u0026#39;login\u0026#39;, \u0026#39;LoginController@login\u0026#39;)-\u0026gt;name(\u0026#39;login-post\u0026#39;); // Password Reset Routes... Route::get(\u0026#39;password/reset\u0026#39;, \u0026#39;ForgotPasswordController@showLinkRequestForm\u0026#39;)-\u0026gt;name(\u0026#39;password.forgot\u0026#39;); Route::post(\u0026#39;password/email\u0026#39;, \u0026#39;ForgotPasswordController@sendResetLinkEmail\u0026#39;)-\u0026gt;name(\u0026#39;password.email\u0026#39;); Route::get(\u0026#39;password/reset/{token}\u0026#39;, \u0026#39;ResetPasswordController@showResetForm\u0026#39;)-\u0026gt;name(\u0026#39;password.reset\u0026#39;); Route::post(\u0026#39;password/reset\u0026#39;, \u0026#39;ResetPasswordController@reset\u0026#39;)-\u0026gt;name(\u0026#39;password.reset-post\u0026#39;); }); これが、Laravel 8.xでは、\nroutes/web.php use App\\Http\\Controllers\\Admin; use App\\Http\\Controllers\\Auth; use App\\Http\\Controllers\\User; use Illuminate\\Support\\Facades\\Route; Route::middleware(\u0026#39;guest\u0026#39;)-\u0026gt;group(function () { // Authentication Routes... Route::get(\u0026#39;\u0026#39;, [Auth\\LoginController::class, \u0026#39;showLoginForm\u0026#39;]); Route::get(\u0026#39;login\u0026#39;, [Auth\\LoginController::class, \u0026#39;showLoginForm\u0026#39;])-\u0026gt;name(\u0026#39;login\u0026#39;); Route::post(\u0026#39;login\u0026#39;, [Auth\\LoginController::class, \u0026#39;login\u0026#39;])-\u0026gt;name(\u0026#39;login-post\u0026#39;); // Password Reset Routes... Route::get(\u0026#39;password/reset\u0026#39;, [Auth\\ForgotPasswordController::class, \u0026#39;showLinkRequestForm\u0026#39;])-\u0026gt;name(\u0026#39;password.forgot\u0026#39;); Route::post(\u0026#39;password/email\u0026#39;, [Auth\\ForgotPasswordController::class, \u0026#39;sendResetLinkEmail\u0026#39;])-\u0026gt;name(\u0026#39;password.email\u0026#39;); Route::get(\u0026#39;password/reset/{token}\u0026#39;, [Auth\\ResetPasswordController::class, \u0026#39;showResetForm\u0026#39;])-\u0026gt;name(\u0026#39;password.reset\u0026#39;); Route::post(\u0026#39;password/reset\u0026#39;, [Auth\\ResetPasswordController::class, \u0026#39;reset\u0026#39;])-\u0026gt;name(\u0026#39;password.reset-post\u0026#39;); }); となります。\n何が変わったかというと、今までのコントローラクラス名＠メソッド名（例：\u0026lsquo;LoginController@showLoginForm\u0026rsquo;）が、コントローラクラスパスとメソッド名の配列（[Auth\\LoginController::class, \u0026lsquo;showLoginForm\u0026rsquo;]）となりました。\nこの変更が動作するためには、RouteServiceProvider.phpのファイルの編集も必要です。 L7.xのバージョンでは、以下のように$namespaceがありますが、\napp/Providers/RouteServiceProvider.php namespace App\\Providers; use Illuminate\\Foundation\\Support\\Providers\\RouteServiceProvider as ServiceProvider; use Illuminate\\Support\\Facades\\Route; class RouteServiceProvider extends ServiceProvider { /** * This namespace is applied to your controller routes. * * In addition, it is set as the URL generator\u0026#39;s root namespace. * * @var string */ protected $namespace = \u0026#39;App\\Http\\Controllers\u0026#39;; ... /** * Define the \u0026#34;web\u0026#34; routes for the application. * * These routes all receive session state, CSRF protection, etc. * * @return void */ protected function mapWebRoutes() { Route::middleware(\u0026#39;web\u0026#39;) -\u0026gt;namespace($this-\u0026gt;namespace) -\u0026gt;group(base_path(\u0026#39;routes/web.php\u0026#39;)); } /** * Define the \u0026#34;api\u0026#34; routes for the application. * * These routes are typically stateless. * * @return void */ protected function mapApiRoutes() { Route::prefix(\u0026#39;api\u0026#39;) -\u0026gt;middleware(\u0026#39;api\u0026#39;) -\u0026gt;namespace($this-\u0026gt;namespace) -\u0026gt;group(base_path(\u0026#39;routes/api.php\u0026#39;)); } } L8.xのバージョンでは、それを削除します。mapWebRoute()とmapApiRoute()からもnamespace()を削除します。\napp/Providers/RouteServiceProvider.php namespace App\\Providers; use Illuminate\\Foundation\\Support\\Providers\\RouteServiceProvider as ServiceProvider; use Illuminate\\Support\\Facades\\Route; class RouteServiceProvider extends ServiceProvider { ... /** * Define the \u0026#34;web\u0026#34; routes for the application. * * These routes all receive session state, CSRF protection, etc. * * @return void */ protected function mapWebRoutes() { Route::middleware(\u0026#39;web\u0026#39;) -\u0026gt;group(base_path(\u0026#39;routes/web.php\u0026#39;)); } /** * Define the \u0026#34;api\u0026#34; routes for the application. * * These routes are typically stateless. * * @return void */ protected function mapApiRoutes() { Route::prefix(\u0026#39;api\u0026#39;) -\u0026gt;middleware(\u0026#39;api\u0026#39;) -\u0026gt;group(base_path(\u0026#39;routes/api.php\u0026#39;)); } } さて、上の変更によりプログラムの実行が速くなったとかのメリットはないと思いますが、この形式になることにより改善されたのは、vscodeのようなエディターにおいてweb.phpのファイル内から簡単にコントローラクラスの定義にジャンプできることです。エディターあるいはエディターのプラグインがクラスを認識できるようになったためです。\nとは言え、このコード変更を手動でやるのはとても面倒ですね。私の大きいプロジェクトでrouteがたくさんあり、242箇所も変更が必要な部分があります。このようなときには、Laravel Shiftのサービスを使用することを勧めます。以前に、ここで紹介していますが、あれから数年、サイトのデザインとか変わっていますが基本的には同じサービスです。\n最後に、上で編集したweb.phpやRouteServiceProvider.phpを何も編集せずにしておくことも可能です。それでも問題なくL8.xで動作します。\n","date":"2021-11-03T12:00:43+09:00","permalink":"https://www.larajapan.com/2021/11/03/laravel-8-x%E6%9B%B4%E6%96%B0%E3%81%A7%E5%A4%89%E3%82%8F%E3%81%A3%E3%81%9F%E3%81%93%E3%81%A8%EF%BC%88%EF%BC%92%EF%BC%89route%E9%96%A2%E9%80%A3%E3%81%AE%E5%A4%89%E6%9B%B4/","title":"Laravel 8.x更新で変わったこと（２）Route関連の変更"},{"content":"Laravel 7.xから8.xに更新して変わったことを紹介していきます。最初は、メンテナンスモードに関して。\nLaravel 7.xのメンテナンスモードに関して書いたのは、たった１年前のことです。 メンテナンス画面：php artisan up \u0026amp; downでは、\nLaravelの経験が長くなると、つまり初期のバージョンから使用していると、昔のやり方しか知らずに過ごしていることがあります。... ということでやり方学びなおします なんて書いていましたが、Laravel 8.xの更新でまたもや学びなおしです。\nphp artisan downのコマンドはそのままですが、それに追加するオプションが変わりました。\nまず、比較してみましょう。\nLaravel 7.xにおいて、\n$ php artisan down --help を実行すると、以下のようのなオプションがあります。英語のヘルプは日本語に私が訳しています。\nUsage: down [options] Options: --message[=MESSAGE] メンテナンス画面に表示するメッセージ --retry[=RETRY] HTTPヘッダーのRetry-Afterの秒数を設定 --allow[=ALLOW] メンテナンスモードでもアクセスできるIPアドレスを指定 同じコマンドラインをLaravel 8.xで実行すると、\nUsage: down [options] Options: --redirect[=REDIRECT] メンテナンス中は指定のURLに皆リダイレクト\u0026lt;strong\u0026gt; --render[=RENDER] メンテナンス中に表示する画面を指定 --retry[=RETRY] HTTPヘッダーのRetry-Afterの秒数を設定 --refresh[=REFRESH] HTTPヘッダーのRefreshの秒数を設定 --secret[=SECRET] メンテンナンスをバイパスするシークレットコードを指定 --status[=STATUS] メンテナンス中に返すHTTPのステータスコードを指定（デフォルト503） オプションの数が増えただけでなく、\u0026ndash;messageとか\u0026ndash;allowとかが無くなってしまいました。\nこれは問題です。　\u0026ndash;allowなしにどうやって開発者あるいは関係者だけがメンテナンス中のサイトにアクセスできるのでしょうか？\nLaravel 8.xはそれを、\u0026ndash;secretのオプションで解決します。実行は、例えば\n$ php artisan down --secret=secret-code としてメンテンナンスに入り、ブラウザーで以下のURLでアクセスすると、あたかもメンテナンスなしの状態で画面にアクセスできます。\nhttps://example.com/secret-code これはとても素晴らしいです。今まで自宅のIPが変わったりしていちいち調べて\u0026ndash;allowにIPアドレスを指定していましたが、その必要がなくなります。\n仕組みとしては、いたって簡単で上のようにシークレットコードでアクセスしたら、larave_maintenanceというクッキーにその値を暗号化して入れて、それが正しいかをチェックしているようです。\nもちろん、先の例のようにsecret-codeではあまりにも簡単すぎるので実際には以下のようにtinkerで値を作成することを勧めます。\n\u0026gt;\u0026gt;\u0026gt; Str::random() =\u0026gt; \u0026#34;mdcSOnkB5fT2rGAC\u0026#34; \u0026gt;\u0026gt;\u0026gt; (string) Str::uuid() =\u0026gt; \u0026#34;fe3a8010-bdda-4972-a0bb-ab96c17841a6\u0026#34; \u0026gt;\u0026gt;\u0026gt; ","date":"2021-10-31T12:48:59+09:00","permalink":"https://www.larajapan.com/2021/10/31/laravel-8-x%E6%9B%B4%E6%96%B0%E3%81%A7%E5%A4%89%E3%82%8F%E3%81%A3%E3%81%9F%E3%81%93%E3%81%A8%EF%BC%88%EF%BC%91%EF%BC%89%E3%83%A1%E3%83%B3%E3%83%86%E3%83%8A%E3%83%B3%E3%82%B9%E4%B8%AD%E3%81%AE%E3%82%A2/","title":"Laravel 8.x更新で変わったこと（１）メンテナンス中のアクセス"},{"content":"FormRequestの話はいつも入力値のバリデーションが中心ですが、入力値とは関係ないアクセスのオーソリ（認可）もFormRequest内で設定可能です。その説明のための簡単な例として、登録したユーザーが自分の名前などのプロフィールを編集するするときに、最後の編集から24時間経過しないと次の変更ができない、というのはどうでしょう。\nまず、FormRequestをaritisanの実行で作成します。\n$ php artisan make:request UserEditRequest 以下のようにオーソリのための関数、authorize()を定義します。\napp/Http/Request/UserEditRequest.php namespace App\\Http\\Requests; use Illuminate\\Foundation\\Http\\FormRequest; class UserEditRequest extends FormRequest { /** * Determine if the user is authorized to make this request. * * @return bool */ public function authorize() { $user = auth()-\u0026gt;user(); // レコードの更新日と現在の差分を時間で計算 $diff = Carbon::parse($user-\u0026gt;updated_at)-\u0026gt;diffInHours(Carbon::now()); return ($diff \u0026gt; 24); //差分が24時間超ならＯＫ、そうでないならアクセス不可 } /** * Get the validation rules that apply to the request. * * @return array */ public function rules() { return [ \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, ]; } } 次に、編集画面のコントローラで先のFormRequestを以下のように使用して、\nnamespace App\\Http\\Controllers; use App\\Http\\Requests\\UserEditRequest; class UserController extends Controller { /** * Create a new controller instance. * * @return void */ public function __construct() { $this-\u0026gt;middleware(\u0026#39;auth\u0026#39;); } /** * Show the form for editing the specified resource. * * @return \\Illuminate\\Http\\Response */ public function edit() { $user = auth()-\u0026gt;user(); return view(\u0026#39;edit\u0026#39;); } /** * Update the specified resource in storage. * * @param \\App\\Http\\Requests\\UserEditRequest $request * @return \\Illuminate\\Http\\Response */ public function update(UserEditRequest $request) //　ここでFormRequestを使用 { $user = auth()-\u0026gt;user(); $user-\u0026gt;update($request-\u0026gt;validated()); return redirect()-\u0026gt;route(\u0026#39;home\u0026#39;); } } 過去24時間内にすでにレコードを変更したという仮定で、編集画面の保存ボタンを押すと以下のように403の画面に遷移します。\nしかしこれでは、どうしてエラーとなったかユーザーには謎ですね。 拒否された理由のメッセージを表示したいです。\n以下のように、FormRequestのfailedAuthorization()を使えば、\napp/Http/Requests/UserEditRequest.php ... /** * Handle a failed authorization attempt. * * @return void * * @throws \\Illuminate\\Auth\\Access\\AuthorizationException */ protected function failedAuthorization() { throw new AuthorizationException(\u0026#39;最後の編集から２４時間経過しないと編集可能となりません\u0026#39;); } ... 今度は親切なメッセージのエラー表示となります。\n","date":"2021-10-24T02:23:14+09:00","permalink":"https://www.larajapan.com/2021/10/24/formrequest%E3%81%A7%E3%82%AA%E3%83%BC%E3%82%BD%E3%83%AA%EF%BC%88%E8%AA%8D%E5%8F%AF%EF%BC%89/","title":"FormRequestでオーソリ（認可）"},{"content":"以前ご紹介したLaravel Excelの続きです。今回はExcelシートに画像を挿入する方法についてご紹介いたします。\nWithDrawings 公式ドキュメントに画像を挿入する手順について説明がありますのでそちらを参考に進めて行きます。 まず、挿入する画像をpublic/img配下に配置します。ファイル名はとりあえずmike.jpgとしました。適当な画像が無ければ以下のサンプル画像を使って頂いても構いませんよ！\n次に、Exportクラスを作成します、今回はImageExportとしました。\nphp artisan make:export ImageExport 次に、ImageExportクラスにてWithDrawingsをimplementし、drawings()を実装します。drawings()内で使うDrawingクラスはPhpSpreadsheetのクラスなのでそちらをuseするのも忘れずに。\napp/Exports/ImageExport.php namespace App\\Exports; use Maatwebsite\\Excel\\Concerns\\Exportable; use Maatwebsite\\Excel\\Concerns\\WithDrawings; use PhpOffice\\PhpSpreadsheet\\Worksheet\\Drawing; class ImageExport implements WithDrawings { use Exportable; public function drawings() { $drawing = new Drawing(); $drawing-\u0026gt;setPath(public_path(\u0026#39;/img/mike.jpg\u0026#39;)); // 画像のパス指定 $drawing-\u0026gt;setCoordinates(\u0026#39;A1\u0026#39;); // 出力先のセルを指定 return $drawing; } } 一度、tinkerから出力してみましょう。以下を実行すると storage/app/test.xlsx が出力されるはずです。\nuse App\\Exports\\ImageExport; (new ImageExport)-\u0026gt;store(\u0026#39;test.xlsx\u0026#39;); =\u0026gt; true 出力されたファイルはこんな感じです。setCoordinates()で指定した通り、画像の左上がセルのA1に揃えられています。\n因みにドキュメントにもある通り、複数の画像を出力する際はdrawings()からDrawingインスタンスの配列をreturnします。試しにA1とC1にそれぞれ同じ画像を出力してみましょう。\napp/Exports/ImageExport.php ... public function drawings() { $drawing1 = new Drawing(); $drawing1-\u0026gt;setPath(public_path(\u0026#39;/img/mike.jpg\u0026#39;)); $drawing1-\u0026gt;setCoordinates(\u0026#39;A1\u0026#39;); $drawing2 = new Drawing(); $drawing2-\u0026gt;setPath(public_path(\u0026#39;/img/mike.jpg\u0026#39;)); $drawing2-\u0026gt;setCoordinates(\u0026#39;C1\u0026#39;); return [$drawing1, $drawing2]; } ... 出力は以下のようになります。\n画像の幅や高さを変更する 前項で出力した画像は元の画像のサイズのまま出力されています。出力する画像の幅や高さを変更したい場合はsetWidth(),setHeight()が使えます。drawings()を編集して高さを２倍にした画像、幅を２倍にした画像をそれぞれ出力してみましょう。\napp/Exports/ImageExport.php ... public function drawings() { // 元のサイズ $drawing1 = new Drawing(); $drawing1-\u0026gt;setPath(public_path(\u0026#39;/img/mike.jpg\u0026#39;)); $drawing1-\u0026gt;setCoordinates(\u0026#39;A1\u0026#39;); // 高さ２倍 $drawing2 = new Drawing(); $drawing2-\u0026gt;setPath(public_path(\u0026#39;/img/mike.jpg\u0026#39;)); $drawing2-\u0026gt;setCoordinates(\u0026#39;C1\u0026#39;); $drawing2-\u0026gt;setHeight($drawing1-\u0026gt;getHeight() * 2); // 幅２倍 $drawing3 = new Drawing(); $drawing3-\u0026gt;setPath(public_path(\u0026#39;/img/mike.jpg\u0026#39;)); $drawing3-\u0026gt;setCoordinates(\u0026#39;G1\u0026#39;); $drawing3-\u0026gt;setWidth($drawing1-\u0026gt;getWidth() * 2); return [$drawing1, $drawing2, $drawing3]; } ... 出力は以下になりました。\nあれ、同じサイズになりましたね、もしかして元のアスペクト比のままサイズ変更される？\nどうやらそのようです。 Drawingの親クラスであるBaseDrawingクラス(vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/BaseDrawing.php)を確認するとsetWidth(), setHeight()にて、resizeProportionalがtrueなら元のアスペクト比を維持しています。そして、デフォルトのresizeProportionalはtrueです。\nもし、アスペクト比を無視して幅や高さを変更したい場合は、\n... // 高さ２倍 $drawing2 = new Drawing(); $drawing2-\u0026gt;setPath(public_path(\u0026#39;/img/mike.jpg\u0026#39;)); $drawing2-\u0026gt;setCoordinates(\u0026#39;C1\u0026#39;); $drawing2-\u0026gt;setResizeProportional(false); //アスペクト比を無視 $drawing2-\u0026gt;setHeight($drawing1-\u0026gt;getHeight() * 2); // 幅２倍 $drawing3 = new Drawing(); $drawing3-\u0026gt;setPath(public_path(\u0026#39;/img/mike.jpg\u0026#39;)); $drawing3-\u0026gt;setCoordinates(\u0026#39;G1\u0026#39;); $drawing3-\u0026gt;setResizeProportional(false); //アスペクト比を無視 $drawing3-\u0026gt;setWidth($drawing1-\u0026gt;getWidth() * 2); ... とすれば良いですね。出力は以下になりました。\nBaseDrawingクラス を確認していると他にも便利そうなメソッドがありました。例えば、setWidthAndHeight()は指定した幅と高さに収まるように画像をリサイズしてくれます。画像をセル内に収めたい場合などに使えそうですね！\n","date":"2021-10-18T03:41:05+09:00","permalink":"https://www.larajapan.com/2021/10/18/%E3%81%9D%E3%81%86%E3%81%A0%E3%80%81laravel-excel%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E3%81%BF%E3%82%88%E3%81%86%EF%BC%88%EF%BC%95%EF%BC%89%E7%94%BB%E5%83%8F%E3%82%92%E6%8C%BF%E5%85%A5%E3%81%99/","title":"そうだ、Laravel Excelを使ってみよう（５）画像を挿入する"},{"content":"「Laravel Collection（２） Collectionを返すメソッド」ではfilterメソッドを紹介しました。今度はfilterメソッドのように条件でCollectionを絞るwhereメソッドの話です。EloquentやSQL文で慣れているならwhereの方がわかりやすいかもしれません。\nデータの準備 用意するデータは、Laravel Collection（１）１つの値を返すメソッド　をもとに、以下のDBレコードがすでに作成されていると仮定します。 mysql\u0026gt; select id, name, birth_date from users; +----+------------------+------------+ | id | name | birth_date | +----+------------------+------------+ | 1 | 石田 裕美子 | 1991-05-26 | | 2 | 佐藤 香織 | 2002-01-12 | | 3 | 杉山 明美 | 2000-03-13 | +----+------------------+------------+ 3 rows in set (0.00 sec) 今回は女の人だけになりました。\nwhere まず、filter()を使った例です。\n\u0026gt;\u0026gt;\u0026gt; User::all()-\u0026gt;filter(function ($user) { return $user-\u0026gt;birth_date \u0026lt; \u0026#39;2000-01-01\u0026#39;; }) =\u0026gt; Illuminate\\Database\\Eloquent\\Collection {#4317 all: [ App\\User {#4260 id: 1, name: \u0026#34;石田 裕美子\u0026#34;, email: \u0026#34;sasaki.naoko@example.net\u0026#34;, email_verified_at: \u0026#34;2021-10-07 02:41:15\u0026#34;, #password: \u0026#34;$2y$10$53mZGiOMkJEVm1VDij642eWGi.i4/bWt3fElQjQvp86dfG/9MK5mW\u0026#34;, #remember_token: \u0026#34;fCSd0AlA84\u0026#34;, birth_date: \u0026#34;1991-05-26\u0026#34;, created_at: \u0026#34;2021-10-07 02:41:16\u0026#34;, updated_at: \u0026#34;2021-10-07 02:41:16\u0026#34;, }, ], } これと同じ処理をwhere()で行うと、\n\u0026gt;\u0026gt;\u0026gt; User::all()-\u0026gt;where(\u0026#39;birth_date\u0026#39;, \u0026#39;\u0026lt;\u0026#39;, \u0026#39;2000-01-01\u0026#39;) =\u0026gt; Illuminate\\Database\\Eloquent\\Collection {#3393 all: [ App\\User {#4105 id: 1, name: \u0026#34;石田 裕美子\u0026#34;, email: \u0026#34;sasaki.naoko@example.net\u0026#34;, email_verified_at: \u0026#34;2021-10-07 02:41:15\u0026#34;, #password: \u0026#34;$2y$10$53mZGiOMkJEVm1VDij642eWGi.i4/bWt3fElQjQvp86dfG/9MK5mW\u0026#34;, #remember_token: \u0026#34;fCSd0AlA84\u0026#34;, birth_date: \u0026#34;1991-05-26\u0026#34;, created_at: \u0026#34;2021-10-07 02:41:16\u0026#34;, updated_at: \u0026#34;2021-10-07 02:41:16\u0026#34;, }, ], } 短くてわかりやすいです。\n今度はwhere()を２つ繋げます。\n\u0026gt;\u0026gt;\u0026gt; User::all()-\u0026gt;where(\u0026#39;birth_date\u0026#39;, \u0026#39;\u0026gt;=\u0026#39;, \u0026#39;1990-01-01\u0026#39;) -\u0026gt;where(\u0026#39;birth_date\u0026#39;, \u0026#39;\u0026lt;=\u0026#39;, \u0026#39;2000-01-01\u0026#39;) =\u0026gt; Illuminate\\Database\\Eloquent\\Collection {#4171 all: [ App\\User {#3376 id: 1, name: \u0026#34;石田 裕美子\u0026#34;, email: \u0026#34;sasaki.naoko@example.net\u0026#34;, email_verified_at: \u0026#34;2021-10-07 02:41:15\u0026#34;, #password: \u0026#34;$2y$10$53mZGiOMkJEVm1VDij642eWGi.i4/bWt3fElQjQvp86dfG/9MK5mW\u0026#34;, #remember_token: \u0026#34;fCSd0AlA84\u0026#34;, birth_date: \u0026#34;1991-05-26\u0026#34;, created_at: \u0026#34;2021-10-07 02:41:16\u0026#34;, updated_at: \u0026#34;2021-10-07 02:41:16\u0026#34;, }, ], } これは、Laravel Collection（２） Collectionを返すメソッド　で掲載したfilter()を使用した以下とまったく同じ処理なことわかりますか？\n\u0026gt;\u0026gt;\u0026gt; User::all()-\u0026gt;filter(function ($user) { if ($user-\u0026gt;birth_date \u0026gt; \u0026#39;2000-01-01\u0026#39;) return false; if ($user-\u0026gt;birth_date \u0026lt; \u0026#39;1990-01-01\u0026#39;) return false; return true; }) whereBetween()というメソッドもありますよ。これを使えばもっと短くなります。\n\u0026gt;\u0026gt;\u0026gt; User::all()-\u0026gt;whereBetween(\u0026#39;birth_date\u0026#39;, [\u0026#39;1990-01-01\u0026#39;, \u0026#39;2000-01-01\u0026#39;]) =\u0026gt; Illuminate\\Database\\Eloquent\\Collection {#3383 all: [ App\\User {#3412 id: 1, name: \u0026#34;石田 裕美子\u0026#34;, email: \u0026#34;sasaki.naoko@example.net\u0026#34;, email_verified_at: \u0026#34;2021-10-07 02:41:15\u0026#34;, #password: \u0026#34;$2y$10$53mZGiOMkJEVm1VDij642eWGi.i4/bWt3fElQjQvp86dfG/9MK5mW\u0026#34;, #remember_token: \u0026#34;fCSd0AlA84\u0026#34;, birth_date: \u0026#34;1991-05-26\u0026#34;, created_at: \u0026#34;2021-10-07 02:41:16\u0026#34;, updated_at: \u0026#34;2021-10-07 02:41:16\u0026#34;, }, ], } 対象のレコードを条件で絞るという点では、CollectionのwhereメソッドはEloquentやDBクエリのwhereメソッドと同じようなメソッドです。しかし両者のwhereメソッドはどこが違うのでしょうか。\nEloquentとCollectionでのwhereメソッドの違い この違いを理解するには、裏で実行されているSQL文を見る必要があります。 注意：以下のtinkerで使用されているsql()はユーザー定義です。こちらを参照してください。\nまず、Eloquentのクエリでのwhereの実行です。\n\u0026gt;\u0026gt;\u0026gt; User::where(\u0026#39;birth_date\u0026#39;, \u0026#39;\u0026lt;\u0026#39;, \u0026#39;2000-01-01\u0026#39;)-\u0026gt;get() =\u0026gt; Illuminate\\Database\\Eloquent\\Collection {#4172 all: [ App\\User {#4104 id: 1, name: \u0026#34;石田 裕美子\u0026#34;, email: \u0026#34;sasaki.naoko@example.net\u0026#34;, email_verified_at: \u0026#34;2021-10-07 02:41:15\u0026#34;, #password: \u0026#34;$2y$10$53mZGiOMkJEVm1VDij642eWGi.i4/bWt3fElQjQvp86dfG/9MK5mW\u0026#34;, #remember_token: \u0026#34;fCSd0AlA84\u0026#34;, birth_date: \u0026#34;1991-05-26\u0026#34;, created_at: \u0026#34;2021-10-07 02:41:16\u0026#34;, updated_at: \u0026#34;2021-10-07 02:41:16\u0026#34;, }, ], } \u0026gt;\u0026gt;\u0026gt; sql() =\u0026gt; [ [ \u0026#34;query\u0026#34; =\u0026gt; \u0026#34;select * from `users` where `birth_date` \u0026lt; ?\u0026#34;, \u0026#34;bindings\u0026#34; =\u0026gt; [ \u0026#34;2000-01-01\u0026#34;, ], \u0026#34;time\u0026#34; =\u0026gt; 5.02, ], ] SQL文にはしっかりSQLのwhere節が含まれています。つまりDBレベルで条件で絞っています。\n次に、Collectionでのwhere()の実行です。\n\u0026gt;\u0026gt;\u0026gt; User::all()-\u0026gt;where(\u0026#39;birth_date\u0026#39;, \u0026#39;\u0026lt;\u0026#39;, \u0026#39;2000-01-01\u0026#39;) =\u0026gt; Illuminate\\Database\\Eloquent\\Collection {#3393 all: [ App\\User {#4105 id: 1, name: \u0026#34;石田 裕美子\u0026#34;, email: \u0026#34;sasaki.naoko@example.net\u0026#34;, email_verified_at: \u0026#34;2021-10-07 02:41:15\u0026#34;, #password: \u0026#34;$2y$10$53mZGiOMkJEVm1VDij642eWGi.i4/bWt3fElQjQvp86dfG/9MK5mW\u0026#34;, #remember_token: \u0026#34;fCSd0AlA84\u0026#34;, birth_date: \u0026#34;1991-05-26\u0026#34;, created_at: \u0026#34;2021-10-07 02:41:16\u0026#34;, updated_at: \u0026#34;2021-10-07 02:41:16\u0026#34;, }, ], } \u0026gt;\u0026gt;\u0026gt; sql() =\u0026gt; [ [ \u0026#34;query\u0026#34; =\u0026gt; \u0026#34;select * from `users`\u0026#34;, \u0026#34;bindings\u0026#34; =\u0026gt; [], \u0026#34;time\u0026#34; =\u0026gt; 4.45, ], ] \u0026gt;\u0026gt;\u0026gt; こちらのSQL文にはwhere節は含まれておらず、DBのusersテーブルからすべてのレコードを取得しています。そしてその中から条件に適うレコードをプログラムで抽出しています。今回はDBには３つしかレコードがありませんが、これが何千ともあるならそれらすべてをメモリに保持することになるのでリソースの無駄遣いとなり、さらにはパフォーマンスの低下ともなります。\nとなると、いつEloquentのクエリでwhere()を使うか、いつCollectionでwhere()を使うかを十分注意すること重要になります。Eloquent、つまりDBレベルでできるだけ取得するレコードを絞って、その結果のCollectionにおいてさらにいろいろな条件で絞るとかの場合にCollectionのwhereメソッドが使われるのが適切です。\n","date":"2021-10-10T02:43:23+09:00","permalink":"https://www.larajapan.com/2021/10/10/laravel-collection%EF%BC%88%EF%BC%98%EF%BC%89-filter%E3%81%AE%E4%BB%A3%E3%82%8F%E3%82%8A%E3%81%ABwhere/","title":"Laravel Collection（８） filterの代わりにwhere"},{"content":"もしかして、最新のLaravelのバージョンを紹介できるのは初めて？いつも遅れて現在のひとつ前のバージョンを紹介してきたのですが、半年に１回のリリースが年に１回となり、さらに次のバージョンのリリースが遅れて、ついに私に追いつかれてしまったのです。\nバージョン 毎回のように、まずサポートのスケジュールを見てみましょう。 なるほど次のバージョン９がLTS（長期サポート）となりますね。\n新規プロジェクト composerはすでにシステムにインストールされていると仮定して、以下のコマンドがl8xのディレクトリ名でLaravelをインストールします。ちなみに、phpは最低7.3のバージョンが必要です。私の環境ではすでに7.4なので大丈夫。\n$ composer create-project --prefer-dist laravel/laravel:^8.0 l8x ウェブサーバーなどが書き込みを行うディレクトリにはパーミッションが必要です。\n$ cd l8x $ chmod -R a+w storage bootstrap/cache 以下を実行して、\n$ php artisan serve ブラウザでhttp://localhost:8000にアクセスすると、ページを見ることができます。\n前の7.xのバージョンとは変わりましたね。TailWind CSSの使用のためかな。\nこれ以降の変更をみるために、この時点でパージョン管理をしておきましょう。\n$ cd l8x $ git init $ git add . $ git commit -am \u0026#34;init\u0026#34; ユーザー認証のパッケージ さて、ここからが前のバージョンと大幅に変わったところです。Laravelは基本的にいわゆるバックエンド、サーバーアプリなのですが、使用するフロントエンドにも好みがあり、TailWind、VueJS, LivewireをLaravelの認証のパッケージを積極的に採用しています。今までのLaravel UIパッケージの代わりに、Laravel Breezeの登場です。そよ風のように優しく（易しく？）インストールということでしょうか。\nまず、必要なパッケージのインストールから、\n$ composer require laravel/breeze --dev 次に、以下を実行して会員登録画面やログイン画面などを作成します。bladeだけでなく、vueやreactの指定も可能です。\n$ php artisan breeze:install blade これにより以下のファイルが追加あるいは編集されています。\nnew file: app/Http/Controllers/Auth/AuthenticatedSessionController.php new file: app/Http/Controllers/Auth/ConfirmablePasswordController.php new file: app/Http/Controllers/Auth/EmailVerificationNotificationController.php new file: app/Http/Controllers/Auth/EmailVerificationPromptController.php new file: app/Http/Controllers/Auth/NewPasswordController.php new file: app/Http/Controllers/Auth/PasswordResetLinkController.php new file: app/Http/Controllers/Auth/RegisteredUserController.php new file: app/Http/Controllers/Auth/VerifyEmailController.php new file: app/Http/Requests/Auth/LoginRequest.php modified: app/Providers/RouteServiceProvider.php new file: app/View/Components/AppLayout.php new file: app/View/Components/GuestLayout.php modified: composer.json modified: composer.lock modified: package.json modified: resources/css/app.css modified: resources/js/app.js new file: resources/views/auth/confirm-password.blade.php new file: resources/views/auth/forgot-password.blade.php new file: resources/views/auth/login.blade.php new file: resources/views/auth/register.blade.php new file: resources/views/auth/reset-password.blade.php new file: resources/views/auth/verify-email.blade.php new file: resources/views/components/application-logo.blade.php new file: resources/views/components/auth-card.blade.php new file: resources/views/components/auth-session-status.blade.php new file: resources/views/components/auth-validation-errors.blade.php new file: resources/views/components/button.blade.php new file: resources/views/components/dropdown-link.blade.php new file: resources/views/components/dropdown.blade.php new file: resources/views/components/input.blade.php new file: resources/views/components/label.blade.php new file: resources/views/components/nav-link.blade.php new file: resources/views/components/responsive-nav-link.blade.php new file: resources/views/dashboard.blade.php new file: resources/views/layouts/app.blade.php new file: resources/views/layouts/guest.blade.php new file: resources/views/layouts/navigation.blade.php modified: resources/views/welcome.blade.php new file: routes/auth.php modified: routes/web.php new file: tailwind.config.js new file: tests/Feature/Auth/AuthenticationTest.php new file: tests/Feature/Auth/EmailVerificationTest.php new file: tests/Feature/Auth/PasswordConfirmationTest.php new file: tests/Feature/Auth/PasswordResetTest.php new file: tests/Feature/Auth/RegistrationTest.php modified: webpack.mix.js 前バージョンよりインストールされるファイルの数が倍以上になっています。これは、ブレードのコンポーネント（ressources/views/components/*）を多用しているからです。コンポーネントはまだ実際に使用したことないですが、将来その日がやってきそうです。\n今度は、javascriptやcssのトランスパイルのために、nodeのパッケージのインストールとwebpackの実行です。\n$ npm install \u0026amp;\u0026amp; npm run dev 次はDBの設定です。mysqlのコマンドを実行して、l8xのデータベースを作成します。\n$ mysql -u root -p mysql\u0026gt; create database l8x; .envファイルを次のように編集してDB情報を入れます。\nDB_DATABASE=l8x DB_USERNAME=DBユーザー名 DB_PASSWORD=DBパスワード\n以下のコマンドを実行してDBテーブルを作成します。\n$ php artisan migrate 以下のように、５つのテーブルが作成されます。\nmysql\u0026gt; show tables; +------------------------+ | Tables_in_l8x | +------------------------+ | failed_jobs | | migrations | | password_resets | | personal_access_tokens | | users | +------------------------+ 5 rows in set (0.00 sec) L7.xと比べて１つテーブルが増えていますね。personal_access_tokensのテーブルです。これはLaravel Sanctumで使用されるトークンのためらしい。楽しい将来の調査とします。\n再度、ブラウザでhttp://localhost:8000/registerにアクセスします。\nと入力して、「Register」ボタンをクリックすると、\n新規会員登録成功です。\nLaravel UIパッケージはどこへ？ さて、前バージョンまで使用していた、Laravel UIパッケージはもう使えないのでしょうか？ そんなことはないです。L8.xを対応しています。\nしかし、残念なことにレガシー扱いになってしまいました。シンプルさが私には良かったのですが。\nThis legacy package is a very simple authentication scaffolding built on the Bootstrap CSS framework. While it continues to work with the latest version of Laravel, you should consider using Laravel Breeze for new projects. Or, for something more robust, consider Laravel Jetstream. ちなみにパッケージのインストールは以下となります。もちろん、Laravel Breezeを削除してからのインストールとなりますので注意を。\n$ composer require laravel/ui \u0026#34;^3.0\u0026#34; $ php artisan ui bootstrap --auth この後の手順は、上と同じです。\n","date":"2021-10-04T06:11:14+09:00","permalink":"https://www.larajapan.com/2021/10/04/laravel-8-x%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB/","title":"Laravel 8.xのインストール"},{"content":"前回は１行１項目で３行の入力フォームの話でした。今回は難度をアップして１行３項目で３行の入力フォームの話です。\n複数行・複数項目の入力フォーム 今回のフォームはこんな感じです。前回より複雑でしょう。\nコントローラは、前回と同様ですが、FormRequestはItemsRequestとします。\napp/Controllers/Form2Controller.php namespace App\\Http\\Controllers; use App\\Http\\Requests\\ItemsRequest; use Illuminate\\Http\\Request; class Form2Controller extends Controller { /** * Show the form for creating a new resource. * * @return \\Illuminate\\Http\\Response */ public function create() { return view(\u0026#39;form2\u0026#39;); } /** * Store a newly created resource in storage. * * @param App\\Http\\Requests\\ItemsRequest $request * @return \\Illuminate\\Http\\Response */ public function store(ItemsRequest $request) { ddd($request-\u0026gt;validated()); } } ブレードは、\nresources/views/form2.blade.php ... \u0026lt;div class=\u0026#34;container-fluid\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;row\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;col-md-12 col-lg-12 mt-2\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;card card-primary\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;card-header\u0026#34;\u0026gt; \u0026lt;h3 class=\u0026#34;card-title\u0026#34;\u0026gt;複数行のフォーム\u0026lt;/h3\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;form method=\u0026#34;POST\u0026#34; action=\u0026#34;{{ route(\u0026#39;form2.store\u0026#39;) }}\u0026#34; class=\u0026#34;form-horizontal\u0026#34; novalidate=\u0026#34;\u0026#34;\u0026gt; @csrf \u0026lt;div class=\u0026#34;card-body\u0026#34;\u0026gt; @error(\u0026#39;names\u0026#39;) \u0026lt;div class=\u0026#34;invalid-feedback d-block\u0026#34;\u0026gt;{{ $message }}\u0026lt;/div\u0026gt; @enderror \u0026lt;div class=\u0026#34;table-responsive\u0026#34;\u0026gt; \u0026lt;table class=\u0026#34;table table-bordered table-hover table-sm\u0026#34;\u0026gt; \u0026lt;thead\u0026gt; \u0026lt;tr\u0026gt; \u0026lt;th\u0026gt;アイテム名\u0026lt;/th\u0026gt; \u0026lt;th class=\u0026#34;col-2\u0026#34;\u0026gt;価格\u0026lt;/th\u0026gt; \u0026lt;th class=\u0026#34;col-1\u0026#34;\u0026gt;個数\u0026lt;/th\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;/thead\u0026gt; \u0026lt;tbody\u0026gt; @for ($i = 0; $i \u0026lt; 3; $i++) \u0026lt;tr\u0026gt; \u0026lt;td\u0026gt; \u0026lt;input class=\u0026#34;form-control\u0026#34; maxlength=\u0026#34;255\u0026#34; name=\u0026#34;names[]\u0026#34; type=\u0026#34;text\u0026#34; value=\u0026#34;{{ old(\u0026#39;names.\u0026#39;.$i) }}\u0026#34;\u0026gt; @error(\u0026#39;names.\u0026#39;.$i) \u0026lt;span class=\u0026#34;invalid-feedback d-block\u0026#34;\u0026gt;{{ $message }}\u0026lt;/span\u0026gt; @enderror \u0026lt;/td\u0026gt; \u0026lt;td\u0026gt; \u0026lt;input class=\u0026#34;form-control\u0026#34; maxlength=\u0026#34;10\u0026#34; name=\u0026#34;prices[]\u0026#34; type=\u0026#34;text\u0026#34; value=\u0026#34;{{ old(\u0026#39;prices.\u0026#39;.$i) }}\u0026#34;\u0026gt; @error(\u0026#39;prices.\u0026#39;.$i) \u0026lt;span class=\u0026#34;invalid-feedback d-block\u0026#34;\u0026gt;{{ $message }}\u0026lt;/span\u0026gt; @enderror \u0026lt;/td\u0026gt; \u0026lt;td\u0026gt; \u0026lt;input class=\u0026#34;form-control\u0026#34; maxlength=\u0026#34;5\u0026#34; name=\u0026#34;quantities[]\u0026#34; type=\u0026#34;text\u0026#34; value=\u0026#34;{{ old(\u0026#39;quantities.\u0026#39;.$i) }}\u0026#34;\u0026gt; @error(\u0026#39;quantities.\u0026#39;.$i) \u0026lt;span class=\u0026#34;invalid-feedback d-block\u0026#34;\u0026gt;{{ $message }}\u0026lt;/span\u0026gt; @enderror \u0026lt;/td\u0026gt; \u0026lt;/tr\u0026gt; @endfor \u0026lt;/tbody\u0026gt; \u0026lt;/table\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;card-footer\u0026#34;\u0026gt; \u0026lt;button class=\u0026#34;btn btn-primary float-right mr-2\u0026#34; type=\u0026#34;submit\u0026#34;\u0026gt;保存\u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/form\u0026gt; \u0026lt;/div\u0026gt;\u0026lt;!-- card --\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt;\u0026lt;!-- row --\u0026gt; \u0026lt;/div\u0026gt; ... FormRequestは、\napp/Http/Requests/ItemsRequest.php namespace App\\Http\\Requests; use Illuminate\\Foundation\\Http\\FormRequest; use Illuminate\\Contracts\\Validation\\Validator; use Illuminate\\Validation\\ValidationException; class ItemsRequest extends FormRequest { protected function prepareForValidation() { $names = $prices = $quantities = []; // nullを削除するために入力データを作成しなおす foreach($this-\u0026gt;names as $i =\u0026gt; $name) { if ($name === null) { continue; } array_push($names, $name); array_push($prices, $this-\u0026gt;prices[$i]); array_push($quantities, $this-\u0026gt;quantities[$i]); } $this-\u0026gt;merge([ \u0026#39;names\u0026#39; =\u0026gt; $names, \u0026#39;prices\u0026#39; =\u0026gt; $prices, \u0026#39;quantities\u0026#39; =\u0026gt; $quantities, ]); } /** * Get the validation rules that apply to the request. * * @return array */ public function rules() { return [ \u0026#39;names\u0026#39; =\u0026gt; \u0026#39;required|array\u0026#39;, // 必ず１行の入力が必要 \u0026#39;names.*\u0026#39; =\u0026gt; \u0026#39;nullable|string\u0026#39;, \u0026#39;prices.*\u0026#39; =\u0026gt; \u0026#39;required_with:names.*|integer\u0026#39;, \u0026#39;quantities.*\u0026#39; =\u0026gt; \u0026#39;required_with:names.*|integer|min:1\u0026#39;, ]; } public function attributes() { return [ \u0026#39;names.*\u0026#39; =\u0026gt; \u0026#39;アイテム名\u0026#39;, \u0026#39;prices.*\u0026#39; =\u0026gt; \u0026#39;価格\u0026#39;, \u0026#39;quantities.*\u0026#39; =\u0026gt; \u0026#39;個数\u0026#39;, ]; } public function messages() { return [ \u0026#39;names.required\u0026#39; =\u0026gt; \u0026#39;必ず１つの入力が必要です\u0026#39;, \u0026#39;prices.*.required_with\u0026#39; =\u0026gt; \u0026#39;「アイテム名」が入力されているときは「価格」は必須です.\u0026#39;, \u0026#39;prices.*.integer\u0026#39; =\u0026gt; \u0026#39;「価格」は整数の入力が必要です.\u0026#39;, \u0026#39;quantities.*.required_with\u0026#39; =\u0026gt; \u0026#39;「アイテム名」が入力されているときは「個数」は必須です.\u0026#39;, \u0026#39;quantities.*.integer\u0026#39; =\u0026gt; \u0026#39;「個数」は整数の入力が必要です.\u0026#39;, \u0026#39;quantities.*.min:1\u0026#39; =\u0026gt; \u0026#39;「個数」は最低１です.\u0026#39;, ]; } } エラーの出力はこんな感じです。\nエラーの出力を見やすくする 先のエラーの出力、個数や価格の列の幅が短いので醜いですね。\n考えついたのは、エラーのテキストを一緒にして１行すべてを使って表示しては。ということで、エラーを加工が必要です。Laravelにはしっかりそのための関数もあるのです。\nItemRequest.phpに以下のように、failedValidation()なる関数を追加します。\napp/Http/Requests/ItemsRequest.php namespace App\\Http\\Requests; use Illuminate\\Foundation\\Http\\FormRequest; use Illuminate\\Contracts\\Validation\\Validator; use Illuminate\\Validation\\ValidationException; class ItemsRequest extends FormRequest { ... /** * Override the parent function * * @param \\Illuminate\\Contracts\\Validation\\Validator $validator * @return void * * @throws \\Illuminate\\Validation\\ValidationException */ protected function failedValidation(Validator $validator) { $errorsPerRow = []; foreach ($validator-\u0026gt;errors()-\u0026gt;getMessages() as $field =\u0026gt; $messages) { $i = explode(\u0026#39;.\u0026#39;, $field)[1] ?? null; // 例えば、name.1なら1を取り出す $errorsPerRow[\u0026#39;row.\u0026#39;.$i][] = $messages[0]; // 各行でのすべてのエラーを配列に } foreach ($errorsPerRow as $key =\u0026gt; $messages) { $validator-\u0026gt;errors()-\u0026gt;add($key, implode(\u0026#39; \u0026#39;, $messages)); //　各行でのエラーを１つにまとめる } throw (new ValidationException($validator)) -\u0026gt;errorBag($this-\u0026gt;errorBag) -\u0026gt;redirectTo($this-\u0026gt;getRedirectUrl()); } } 上の関数は、バリデーションが失敗したときにコールされる関数で、関数内で、各行のすべての項目のエラーを合わせて、row.0などのようなキーにエラーメッセージを割り当てて、新たなエラーを作成します。\nそしてそれを、ブレードで以下のように出力できるようにします。\n... \u0026lt;form method=\u0026#34;POST\u0026#34; action=\u0026#34;{{ route(\u0026#39;form2.store\u0026#39;) }}\u0026#34; class=\u0026#34;form-horizontal\u0026#34; novalidate=\u0026#34;\u0026#34;\u0026gt; @csrf \u0026lt;div class=\u0026#34;card-body\u0026#34;\u0026gt; @error(\u0026#39;names\u0026#39;) \u0026lt;div class=\u0026#34;invalid-feedback d-block\u0026#34;\u0026gt;{{ $message }}\u0026lt;/div\u0026gt; @enderror \u0026lt;div class=\u0026#34;table-responsive\u0026#34;\u0026gt; \u0026lt;table class=\u0026#34;table table-bordered table-hover table-sm\u0026#34;\u0026gt; \u0026lt;thead\u0026gt; \u0026lt;tr\u0026gt; \u0026lt;th\u0026gt;アイテム名\u0026lt;/th\u0026gt; \u0026lt;th class=\u0026#34;col-2\u0026#34;\u0026gt;価格\u0026lt;/th\u0026gt; \u0026lt;th class=\u0026#34;col-1\u0026#34;\u0026gt;個数\u0026lt;/th\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;/thead\u0026gt; \u0026lt;tbody\u0026gt; @for ($i = 0; $i \u0026lt; 3; $i++) \u0026lt;tr\u0026gt; \u0026lt;td\u0026gt; \u0026lt;input class=\u0026#34;form-control\u0026#34; maxlength=\u0026#34;255\u0026#34; name=\u0026#34;names[]\u0026#34; type=\u0026#34;text\u0026#34; value=\u0026#34;{{ old(\u0026#39;names.\u0026#39;.$i) }}\u0026#34;\u0026gt; \u0026lt;/td\u0026gt; \u0026lt;td\u0026gt; \u0026lt;input class=\u0026#34;form-control\u0026#34; maxlength=\u0026#34;10\u0026#34; name=\u0026#34;prices[]\u0026#34; type=\u0026#34;text\u0026#34; value=\u0026#34;{{ old(\u0026#39;prices.\u0026#39;.$i) }}\u0026#34;\u0026gt; \u0026lt;/td\u0026gt; \u0026lt;td\u0026gt; \u0026lt;input class=\u0026#34;form-control\u0026#34; maxlength=\u0026#34;5\u0026#34; name=\u0026#34;quantities[]\u0026#34; type=\u0026#34;text\u0026#34; value=\u0026#34;{{ old(\u0026#39;quantities.\u0026#39;.$i) }}\u0026#34;\u0026gt; \u0026lt;/td\u0026gt; \u0026lt;/tr\u0026gt; @error(\u0026#39;row.\u0026#39;.$i) \u0026lt;tr\u0026gt; \u0026lt;td colspan=\u0026#34;3\u0026#34;\u0026gt; \u0026lt;span class=\u0026#34;invalid-feedback d-block\u0026#34;\u0026gt;{{ $message }}\u0026lt;/span\u0026gt; \u0026lt;/td\u0026gt; \u0026lt;/tr\u0026gt; @enderror @endfor \u0026lt;/tbody\u0026gt; \u0026lt;/table\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;card-footer\u0026#34;\u0026gt; \u0026lt;button class=\u0026#34;btn btn-primary float-right mr-2\u0026#34; type=\u0026#34;submit\u0026#34;\u0026gt;保存\u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/form\u0026gt; ... 各項目のエラー表示を削除して、 @error(\u0026lsquo;row.\u0026rsquo;.$i)で行ごとのエラーの表示としています。\n結果は以下のように表示が改善されたエラー出力となります。\n最後に いろいろな形態の入力フォームがあるなか、前回も今回も追加専用のフォームの話でしたが、もちろんコードを変更すれば編集のフォームともなります。","date":"2021-09-12T12:42:49+09:00","permalink":"https://www.larajapan.com/2021/09/12/%E9%85%8D%E5%88%97%E5%80%A4%E3%81%AE%E5%85%A5%E5%8A%9B%E3%81%AE%E3%81%9F%E3%82%81%E3%81%AEformrequest%EF%BC%88%EF%BC%92%EF%BC%89%E3%82%A8%E3%83%A9%E3%83%BC%E3%82%92%E8%A6%8B%E3%82%84%E3%81%99%E3%81%8F/","title":"配列値の入力のためのFormRequest（２）エラーを見やすく"},{"content":"配列値の入力、つまり複数行に同じ入力項目を持つフォームの話です。お客様の登録などのフォームとは違って、バリデーションや画面でのエラーの表示など複雑な部分が多いです。しかし、コントローラで対応するより、FormRequestを使うとすっきりしたコードとなります。\n配列値の入力フォーム 各行の入力項目が１つのケースから作成してみます。以下のような画面です。行数は固定で、最大３つまでのメールアドレスの入力が可能です。\nコントローラのコードは、極力シンプルにして、投稿が成功したらバリデートした値を出力します。\napp/Http/Controllers/FormController.php namespace App\\Http\\Controllers; use App\\Http\\Requests\\EmailsRequest; use Illuminate\\Http\\Request; class FormController extends Controller { /** * Show the form for creating a new resource. * * @return \\Illuminate\\Http\\Response */ public function create() { return view(\u0026#39;form\u0026#39;); } /** * Store a newly created resource in storage. * * @param App\\Http\\Requests\\EmailsRequest $request * @return \\Illuminate\\Http\\Response */ public function store(EmailsRequest $request) { ddd($request-\u0026gt;validated()); } } ブレードは、\nresources/views/form.blade.php ... \u0026lt;div class=\u0026#34;container-fluid\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;row\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;col-md-12 col-lg-12 mt-2\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;card card-primary\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;card-header\u0026#34;\u0026gt; \u0026lt;h3 class=\u0026#34;card-title\u0026#34;\u0026gt;複数行のフォーム\u0026lt;/h3\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;form method=\u0026#34;POST\u0026#34; action=\u0026#34;{{ route(\u0026#39;form.store\u0026#39;) }}\u0026#34; class=\u0026#34;form-horizontal\u0026#34; novalidate=\u0026#34;\u0026#34;\u0026gt; @csrf \u0026lt;div class=\u0026#34;card-body\u0026#34;\u0026gt; @error(\u0026#39;emails\u0026#39;) \u0026lt;div class=\u0026#34;invalid-feedback d-block\u0026#34;\u0026gt;{{ $message }}\u0026lt;/div\u0026gt; @enderror \u0026lt;div class=\u0026#34;table-responsive\u0026#34;\u0026gt; \u0026lt;table class=\u0026#34;table table-bordered table-hover table-sm\u0026#34;\u0026gt; \u0026lt;thead\u0026gt; \u0026lt;tr\u0026gt; \u0026lt;th\u0026gt;メールアドレス\u0026lt;/th\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;/thead\u0026gt; \u0026lt;tbody\u0026gt; @for ($i = 0; $i \u0026lt; 3; $i++) \u0026lt;tr\u0026gt; \u0026lt;td\u0026gt; \u0026lt;input class=\u0026#34;form-control\u0026#34; maxlength=\u0026#34;255\u0026#34; name=\u0026#34;emails[]\u0026#34; type=\u0026#34;text\u0026#34; value=\u0026#34;{{ old(\u0026#39;emails.\u0026#39;.$i) }}\u0026#34;\u0026gt; @error(\u0026#39;emails.\u0026#39;.$i) \u0026lt;span class=\u0026#34;invalid-feedback d-block\u0026#34;\u0026gt;{{ $message }}\u0026lt;/span\u0026gt; @enderror \u0026lt;/td\u0026gt; \u0026lt;/tr\u0026gt; @endfor \u0026lt;/tbody\u0026gt; \u0026lt;/table\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;card-footer\u0026#34;\u0026gt; \u0026lt;button class=\u0026#34;btn btn-primary float-right mr-2\u0026#34; type=\u0026#34;submit\u0026#34;\u0026gt;保存\u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/form\u0026gt; \u0026lt;/div\u0026gt;\u0026lt;!-- card --\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt;\u0026lt;!-- row --\u0026gt; \u0026lt;/div\u0026gt; ... そして、FormRequestは、\napp/Http/Requests/EmailsRequest.php namespace App\\Http\\Requests; use Illuminate\\Foundation\\Http\\FormRequest; class EmaisRequest extends FormRequest { /** * Get the validation rules that apply to the request. * * @return array */ public function rules() { return [ \u0026#39;emails.*\u0026#39; =\u0026gt; \u0026#39;nullable|email|distinct\u0026#39;, // nullableとしているのは空行を許すため ]; } public function messages() { return [ \u0026#39;emails.*.email\u0026#39; =\u0026gt; \u0026#39;不正なメールです\u0026#39;, \u0026#39;emails.*.distinct\u0026#39; =\u0026gt; \u0026#39;重複の入力があります\u0026#39; ]; } } 実際に入力するとエラーはこんな感じで出力されます。\nそしてバリデートされた値はこんな感じです。\nしかし、これではDBに保存するときにいちいちnullを外す必要ありますね。これもFormRequestで処理したいです。\n入力値の加工 FormRequestで入力値を加工するには、以下のようにprepareForValidation()を追加して処理します。\napp/Http/Requests/EmailsRequest.php ... class EmailsRequest extends FormRequest { protected function prepareForValidation() { $emails = collect($this-\u0026gt;emails) -\u0026gt;filter(function ($email) { return $email !== null; //ここでnull値を削除 })-\u0026gt;all(); $this-\u0026gt;merge([ \u0026#39;emails\u0026#39; =\u0026gt; $emails, ]); } /** * Get the validation rules that apply to the request. * * @return array */ public function rules() { return [ \u0026#39;emails\u0026#39; =\u0026gt; \u0026#39;required|array\u0026#39;, //入力を必須とする \u0026#39;emails.*\u0026#39; =\u0026gt; \u0026#39;nullable|email|distinct\u0026#39;, ]; } public function messages() { return [ \u0026#39;emails.required\u0026#39; =\u0026gt; \u0026#39;必ず１つの入力が必要です\u0026#39;, \u0026#39;emails.*.email\u0026#39; =\u0026gt; \u0026#39;不正なメールです\u0026#39;, \u0026#39;emails.*.distinct\u0026#39; =\u0026gt; \u0026#39;重複の入力があります\u0026#39; ]; } } 最低１つの入力を条件とするために、\u0026lsquo;emails\u0026rsquo; =\u0026gt; \u0026lsquo;required|array\u0026rsquo;, もrules()も追加しています。\nさて、先の投稿でバリデートされたデータは、今度は以下のようにnullの値はなくなりました。\nそして、何も入力がなく投稿された場合は以下のようなエラーとなります。\nちなみに、最低２つの入力を条件としたいときは、ルールを、\u0026lsquo;emails\u0026rsquo; =\u0026gt; \u0026lsquo;required|array|min:2\u0026rsquo;, とすればよいです。\n","date":"2021-09-07T07:25:55+09:00","permalink":"https://www.larajapan.com/2021/09/07/%E9%85%8D%E5%88%97%E5%80%A4%E3%81%AE%E5%85%A5%E5%8A%9B%E3%81%AE%E3%81%9F%E3%82%81%E3%81%AEformrequest/","title":"配列値の入力のためのFormRequest"},{"content":"以前に「ティンカーでリクエスト（Request）」で、ティンカーにおいてRequestのオブジェクトをシミュレートできることを紹介しました。あれから数年、ヘルパー関数のrequest()にもティンカーで値を追加できる技を取得しました。\nrequest()に値がない まず、以前のティンカーでのRequestオブジェクトの作成をここで再現します。 Psy Shell v0.10.8 (PHP 7.4.21 — cli) by Justin Hileman \u0026gt;\u0026gt;\u0026gt; $request = \\Illuminate\\Http\\Request::create(\u0026#39;http://localhost\u0026#39;, \u0026#39;GET\u0026#39;, [\u0026#39;name\u0026#39; =\u0026gt; \u0026#39;test\u0026#39;]); =\u0026gt; Illuminate\\Http\\Request {#5238 +attributes: Symfony\\Component\\HttpFoundation\\ParameterBag {#5234}, +request: Symfony\\Component\\HttpFoundation\\InputBag {#5236}, +query: Symfony\\Component\\HttpFoundation\\InputBag {#5235}, +server: Symfony\\Component\\HttpFoundation\\ServerBag {#5231}, +files: Symfony\\Component\\HttpFoundation\\FileBag {#5232}, +cookies: Symfony\\Component\\HttpFoundation\\InputBag {#5233}, +headers: Symfony\\Component\\HttpFoundation\\HeaderBag {#5230}, } \u0026gt;\u0026gt;\u0026gt; $request-\u0026gt;all() =\u0026gt; [ \u0026#34;name\u0026#34; =\u0026gt; \u0026#34;test\u0026#34;, ] \u0026gt;\u0026gt;\u0026gt; $request-\u0026gt;name =\u0026gt; \u0026#34;test\u0026#34; \u0026gt;\u0026gt;\u0026gt; そして、このRequestオブジェクトを利用して簡単にバリデーションのテストもできます。\n\u0026gt;\u0026gt;\u0026gt; $request-\u0026gt;validate([\u0026#39;name\u0026#39; =\u0026gt; \u0026#39;required|alpha\u0026#39;]) =\u0026gt; [ \u0026#34;name\u0026#34; =\u0026gt; \u0026#34;test\u0026#34;, ] \u0026gt;\u0026gt;\u0026gt; $request-\u0026gt;validate([\u0026#39;name\u0026#39; =\u0026gt; \u0026#39;required|numeric\u0026#39;]) Illuminate\\Validation\\ValidationException with message \u0026#39;The given data was invalid.\u0026#39; \u0026gt;\u0026gt;\u0026gt; しかし、このオブジェクトでは、ヘルバーを使うと期待した値が返ってきません。\n\u0026gt;\u0026gt;\u0026gt; request(\u0026#39;name\u0026#39;) =\u0026gt; null　// \u0026#34;test\u0026#34;と返ってきて欲しい！ どうしてでしょう？\nグローバルのRequestオブジェクト request()の定義を見ると、\nvendor/laravel/framework/src/Illuminate/Foundation/helpers.php ... /** * Get an instance of the current request or an input item from the request. * * @param array|string|null $key * @param mixed $default * @return \\Illuminate\\Http\\Request|string|array */ function request($key = null, $default = null) { if (is_null($key)) { return app(\u0026#39;request\u0026#39;); } if (is_array($key)) { return app(\u0026#39;request\u0026#39;)-\u0026gt;only($key); } $value = app(\u0026#39;request\u0026#39;)-\u0026gt;__get($key); return is_null($value) ? value($default) : $value; } ... app(\u0026lsquo;request\u0026rsquo;)というのがヘルパーで使用しているRequestのオブジェクトのようです。 ティンカーでタイプしてみると、\n\u0026gt;\u0026gt;\u0026gt; app(\u0026#39;request\u0026#39;); =\u0026gt; Illuminate\\Http\\Request {#77 +attributes: Symfony\\Component\\HttpFoundation\\ParameterBag {#68}, +request: Symfony\\Component\\HttpFoundation\\InputBag {#70}, +query: Symfony\\Component\\HttpFoundation\\InputBag {#65}, +server: Symfony\\Component\\HttpFoundation\\ServerBag {#69}, +files: Symfony\\Component\\HttpFoundation\\FileBag {#74}, +cookies: Symfony\\Component\\HttpFoundation\\InputBag {#73}, +headers: Symfony\\Component\\HttpFoundation\\HeaderBag {#76}, } 先に作成したRequestのオブジェクトのIDは、#5238なのにこちらは#77で、Laravelのプログラムの実行（ティンカーの実行）のかなり初期に作成されたオブジェクトということがわかります。そして、そのオブジェクトはLaravelがサービスコンテナーとして管理するグローバルにアクセス可能なRequestのオブジェクトです。\nそして、以下のようにこのオブジェクトに値を追加することも可能なのです。\n\u0026gt;\u0026gt;\u0026gt; app(\u0026#39;request\u0026#39;)-\u0026gt;all(); =\u0026gt; [] \u0026gt;\u0026gt;\u0026gt; app(\u0026#39;request\u0026#39;)-\u0026gt;merge([\u0026#39;name\u0026#39; =\u0026gt; \u0026#39;test\u0026#39;]); =\u0026gt; Illuminate\\Http\\Request {#77 +attributes: Symfony\\Component\\HttpFoundation\\ParameterBag {#68}, +request: Symfony\\Component\\HttpFoundation\\InputBag {#70}, +query: Symfony\\Component\\HttpFoundation\\InputBag {#65}, +server: Symfony\\Component\\HttpFoundation\\ServerBag {#69}, +files: Symfony\\Component\\HttpFoundation\\FileBag {#74}, +cookies: Symfony\\Component\\HttpFoundation\\InputBag {#73}, +headers: Symfony\\Component\\HttpFoundation\\HeaderBag {#76}, } \u0026gt;\u0026gt;\u0026gt; app(\u0026#39;request\u0026#39;)-\u0026gt;all(); =\u0026gt; [ \u0026#34;name\u0026#34; =\u0026gt; \u0026#34;test\u0026#34;, ] \u0026gt;\u0026gt;\u0026gt; app(\u0026#39;request\u0026#39;)-\u0026gt;name =\u0026gt; \u0026#34;test\u0026#34; そして、ヘルパーでも値を取得できるようになります。\n\u0026gt;\u0026gt;\u0026gt; request(\u0026#39;name\u0026#39;) =\u0026gt; \u0026#34;test\u0026#34; ","date":"2021-08-29T05:37:09+09:00","permalink":"https://www.larajapan.com/2021/08/29/request%E3%83%98%E3%83%AB%E3%83%91%E3%83%BC%E3%81%8C%E5%A4%89%EF%BC%9F/","title":"request()ヘルパーが変？"},{"content":"集計用のコマンドを書いていてクエリビルダでfrom句にサブクエリを使いたいケースが発生しました。公式ドキュメントを見てもwhere句かjoin句でのサブクエリしか書かれていません。恐らくこうやるのでは？と、試してみたら期待通りに動いてしまい、結局、不安になってソースを確認したのでその共有です。\nfrom句のサブクエリが必要なケースとは？ 例えばECサイトにおける顧客分析で注文回数別にユーザ数を算出する場合を考えてみましょう。 以下のような注文テーブルがDBにあったとして、ユーザが商品を注文する度にこのテーブルにレコードが挿入されます。\nそして、以下のようなデータを出力したいとします。\n１回購入したユーザは60人、２回購入したユーザは50人、３回購入したユーザは10人、、、といった具合です。\nこれを出力する為のsql文は以下になります。\nselect invoice_count, count(user_id) as user_count from ( select user_id, count(invoice_id) as invoice_count from invoices group by user_id ) as invoices_by_user group by invoice_count; from句にユーザごとの購入回数を取得するサブクエリを指定する必要があり、これをどうやってクエリビルダで組み立てるか？が今回のゴールです。\n※次項から解説になりますが、手を動かしながら確認したい方は記事の最後のおまけ、検証データ準備をご参照下さい。\nDB::table($subQuery) 冒頭にて触れた通り、公式ドキュメントに載っているのはwhere句やjoin句にsubQueryを指定する方法です。いずれもClosureを指定して実現しています。ということは、table()に同様にClosureを渡せば良いのでは？、と思いやってみました。 まず、$subQueryという変数にClosureを格納します。Closureの引数にクエリビルダを指定し、サブクエリにて実行したいクエリを定義します。\n$subQuery = function ($query) { $query-\u0026gt;from(\u0026#39;invoices\u0026#39;) -\u0026gt;selectRaw(\u0026#39;user_id, count(invoice_id) as invoice_count\u0026#39;) -\u0026gt;groupBy(\u0026#39;user_id\u0026#39;); }; 次に、それをtable()の引数に指定してみます。\n$query = DB::table($subQuery) -\u0026gt;selectRaw(\u0026#39;invoice_count, count(user_id) as user_count\u0026#39;) -\u0026gt;groupBy(\u0026#39;invoice_count\u0026#39;); 最後に、toSql()でクエリを確認してみましょう。（見辛いので改行しています）\n\u0026gt;\u0026gt;\u0026gt; $query-\u0026gt;toSql(); =\u0026gt; \u0026#34;select invoice_count, count(user_id) as user_count from ( select user_id, count(invoice_id) as invoice_count from `invoices` group by `user_id` ) as `` group by `invoice_count`\u0026#34; 予想通り(?)、from句のサブクエリを指定することが出来ました。しかし、よく見てみるとサブクエリのエイリアスが空文字(``)になってしまっています。ここで私はこれが正しい使い方なのか不安になり、table()を解析することにしました。\nソース確認 まず、DBファサードを見てみます。すると、Docコメントにてtableメソッドについて記述されています。 vendor/laravel/framework/src/Illuminate/Support/Facades/DB.php namespace Illuminate\\Support\\Facades; /** * @method static \\Illuminate\\Database\\ConnectionInterface connection(string $name = null) * @method static \\Illuminate\\Database\\Query\\Builder table(string $table, string $as = null) \u0026lt;- ココ ... 第二引数にstring $asを取っているので、エイリアスはそこに指定すれば良さそうですね。せっかくなので、メソッドを見てみます。メソッドは下記のどちらかに定義されているようです。\nvendor/laravel/framework/src/Illuminate/Support/Facades/DB.php ... * @see \\Illuminate\\Database\\DatabaseManager * @see \\Illuminate\\Database\\Connection ... 確認したところ、\\Illuminate\\Database\\Connectionにありました。\nvendor/laravel/framework/src/Illuminate/Database/Connection.php /** * Begin a fluent query against a database table. * * @param \\Closure|\\Illuminate\\Database\\Query\\Builder|string $table * @param string|null $as * @return \\Illuminate\\Database\\Query\\Builder */ public function table($table, $as = null) { return $this-\u0026gt;query()-\u0026gt;from($table, $as); } なるほど、第一引数の$tableにはstring以外に、Closureやクエリビルダも渡すことができるみたいです。そして、内部的にはfrom()を呼び出しています。from()はどうなっているのでしょうか？\nvendor/laravel/framework/src/Illuminate/Database/Query/Builder.php /** * Set the table which the query is targeting. * * @param \\Closure|\\Illuminate\\Database\\Query\\Builder|string $table * @param string|null $as * @return $this */ public function from($table, $as = null) { if ($this-\u0026gt;isQueryable($table)) { return $this-\u0026gt;fromSub($table, $as); } $this-\u0026gt;from = $as ? \u0026#34;{$table} as {$as}\u0026#34; : $table; return $this; } なるほどなるほど、$tableがClosureやクエリビルダであればisQueryable()がtrueとなりfromSub()を呼び出すようです。join句にサブクエリを指定するメソッドがjoinSub()なので、from句ならfromSubというわけですね。\nまとめ クエリビルダでサブクエリを指定するには以下のいずれかの方法で実現できます。 // tableメソッドで指定 DB::table($subQuery, $as)-\u0026gt;...; // fromメソッドで指定 DB::query()-\u0026gt;from($subQuery, $as)-\u0026gt;...; // fromSubで指定 DB::query()-\u0026gt;fromSub($subQuery, $as)-\u0026gt;...; 上記の$subQueryは、ClosureでもクエリビルダでもOKです。以下はクエリビルダで指定してみた例です。\n$subQuery = DB::table(\u0026#39;invoices\u0026#39;) -\u0026gt;selectRaw(\u0026#39;user_id, count(invoice_id) as invoice_count\u0026#39;) -\u0026gt;groupBy(\u0026#39;user_id\u0026#39;); $query = DB::table($subQuery, \u0026#39;sub\u0026#39;) // エイリアスをsubとしてみました。 -\u0026gt;selectRaw(\u0026#39;invoice_count, count(user_id) as user_count\u0026#39;) -\u0026gt;groupBy(\u0026#39;invoice_count\u0026#39;); $query-\u0026gt;toSql(); =\u0026gt; \u0026#34;select invoice_count, count(user_id) as user_count from ( select user_id, count(invoice_id) as invoice_count from `invoices` group by `user_id` ) as `sub` // as sub になりました。 group by `invoice_count`\u0026#34; たまにドキュメントにて網羅されていないメソッドなどがありますが、そんな時は少しソースを覗いてみると良いかもしれませんね。\nおまけ、検証データ 実際にデータを入れて手を動かしながら確認してみたい方は以下の手順でデータを用意できます。 Laravel 7.xのインストールにてDBのセットアップまで完了している状態からの手順となります。 Model、migrationファイル、Factoryクラス作成\nphp artisan make:model Invoice -m -f invoicesのmigrationファイルを編集します。使用するのはinvoice_idとmember_idのみなので、そちらを追加。\ndatabase/migrations/2021_07_28_035716_create_invoices_table.php /** * Run the migrations. * * @return void */ public function up() { Schema::create(\u0026#39;invoices\u0026#39;, function (Blueprint $table) { $table-\u0026gt;id(\u0026#39;invoice_id\u0026#39;); $table-\u0026gt;integer(\u0026#39;user_id\u0026#39;)-\u0026gt;default(0); $table-\u0026gt;timestamps(); }); } ... DBに反映。\nphp artisan migrate Seederで検証用データを作成。今回はinvoicesテーブルしか使わないので、わざわざ専用のSeederクラスを作成せず、DatabaseSeeder.phpのrun()に直接記述しました。\ndatabase/seeds/DatabaseSeeder.php use App\\Invoice; use Illuminate\\Database\\Seeder; class DatabaseSeeder extends Seeder { /** * Seed the application\u0026#39;s database. * * @return void */ public function run() { $params = []; for ($i=0; $i\u0026lt;100; $i++) { // user_id毎に 0~10 のランダムな数の注文レコードを作成する for ($j=0; $j \u0026lt; rand(0, 10); $j++) { $params[] = [ \u0026#39;user_id\u0026#39; =\u0026gt; $i, ]; } } Invoice::insert($params); } } ユーザ毎に購入回数をばらつかせたいのでrand()を使用して０から１０の注文レコードを、１００ユーザ分用意するようにしました。また、データの挿入が速く終わるように、以前bulk insertで大量のデータをDBに登録するで紹介した方法を採っています。\nseederを実行して、DBにレコードを挿入。\nphp artisan db:seed mysql上で登録されたデータを確認してみます。 冒頭の、from句のサブクエリが必要なケースとは？で示したクエリを実行してみましょう。\n+---------------+------------+ | invoice_count | user_count | +---------------+------------+ | 1 | 21 | | 2 | 14 | | 3 | 21 | | 4 | 13 | | 5 | 14 | | 6 | 4 | | 7 | 5 | +---------------+------------+ 7 rows in set (0.01 sec) いい感じにバラけていますね！検証用データの準備は以上です。\n","date":"2021-08-03T23:53:37+09:00","permalink":"https://www.larajapan.com/2021/08/03/from%E5%8F%A5%E3%81%AE%E3%82%B5%E3%83%96%E3%82%AF%E3%82%A8%E3%83%AA/","title":"from句のサブクエリ"},{"content":"Collectionの中でとても好きなメソッドは、groupBy()です。Laravelが存在しなかった時代ではよくDBクエリから返ってくる１次元の配列をグループ化するため、汚いコードを書いて２次元の配列に変換していたものです。しかし、CollectionのgroupByがあるとコードがシンプルで綺麗に書けること書けること。\nこのような画面が欲しい 以下のようなデータがDBにあると仮定して、\nmysql\u0026gt; select id, name, gender from users; +----+---------------+-----------+ | id | name | gender | +----+---------------+-----------+ | 1 | 山田 千代 | 無回答 | | 2 | 佐藤 健一 | 男 | | 3 | 加藤 篤司 | 無回答 | | 4 | 山口 花子 | 女 | +----+---------------+-----------+ 4 rows in set (0.01 sec) このデータを性別でグループ化して、以下の画面の表示となるようにしたいのです。\nDBレベルでのデータのグループ化 DBレベルでは、データをグループ化して例えば性別（gender）ごとのユーザー数とかを以下のように取得できます。\n\u0026gt;\u0026gt;\u0026gt; User::groupBy(\u0026#39;gender\u0026#39;)-\u0026gt;selectRaw(\u0026#39;gender, count(*)\u0026#39;)-\u0026gt;get(); =\u0026gt; Illuminate\\Database\\Eloquent\\Collection {#3422 all: [ App\\User {#4345 gender: \u0026#34;女\u0026#34;, count(*): 1, }, App\\User {#4348 gender: \u0026#34;無回答\u0026#34;, count(*): 2, }, App\\User {#4349 gender: \u0026#34;男\u0026#34;, count(*): 1, }, ], } ちなみに実行されたSQL文は、\n\u0026gt;\u0026gt;\u0026gt; sql() =\u0026gt; [ [ \u0026#34;query\u0026#34; =\u0026gt; \u0026#34;select gender, count(*) from `users` group by `gender`\u0026#34;, \u0026#34;bindings\u0026#34; =\u0026gt; [], \u0026#34;time\u0026#34; =\u0026gt; 0.56, ], ] しかしこれでは先の欲しい画面のデータを表示するには、さらにそれぞれの性別において個々のDBレコードの情報取得が必要となります。つまり、以下のようなSQLの実行が３回必要となります。\n\u0026gt;\u0026gt;\u0026gt; User::where(\u0026#39;gender\u0026#39;, \u0026#39;=\u0026#39;, \u0026#39;男\u0026#39;)-\u0026gt;get(); =\u0026gt; Illuminate\\Database\\Eloquent\\Collection {#4345 all: [ App\\User {#4348 id: 2, name: \u0026#34;佐藤 健一\u0026#34;, gender: \u0026#34;男\u0026#34;, ... }, ], } しかし、これではトータル４回のSQL文の実行となってしまい効率的でありません。\nCollectionでのグループ化 今度は、DBからレコードを全部取得してからデータをグループ化します。\n\u0026gt;\u0026gt;\u0026gt; User::all()-\u0026gt;groupBy(\u0026#39;gender\u0026#39;); =\u0026gt; Illuminate\\Database\\Eloquent\\Collection {#4336 all: [ \u0026#34;無回答\u0026#34; =\u0026gt; Illuminate\\Database\\Eloquent\\Collection {#4120 all: [ App\\User {#3392 id: 1, name: \u0026#34;山田 千代\u0026#34;, ... }, App\\User {#4361 id: 3, name: \u0026#34;加藤 篤司\u0026#34;, ... }, ], }, \u0026#34;男\u0026#34; =\u0026gt; Illuminate\\Database\\Eloquent\\Collection {#4345 all: [ App\\User {#4188 id: 2, name: \u0026#34;佐藤 健一\u0026#34;, ... }, ], }, \u0026#34;女\u0026#34; =\u0026gt; Illuminate\\Database\\Eloquent\\Collection {#4276 all: [ App\\User {#3403 id: 4, name: \u0026#34;山口 花子\u0026#34;, ... }, ], }, ], } DBレベルのグループ化と違って、DBから取得した後にCollectionのgroupBy()を適用なので、データをグループ化するだけでなく個々のデータもコレクションにキープしてくれます。そして、実行されたSQL文をチェックしてみると以下のように１つのみです。\n\u0026gt;\u0026gt;\u0026gt; sql() =\u0026gt; [ [ \u0026#34;query\u0026#34; =\u0026gt; \u0026#34;select * from `users`\u0026#34;, \u0026#34;bindings\u0026#34; =\u0026gt; [], \u0026#34;time\u0026#34; =\u0026gt; 0.51, ], ] コントローラの作成 CollectionのgroupByを使用して必要な構造データを取得できたところで、コントローラを作成です。\napp/Http/Controlles/UserController.php namespace App\\Http\\Controllers; use App\\User; class UserController extends Controller { /** * Display a listing of the resource. * * @return \\Illuminate\\Http\\Response */ public function index() { return view(\u0026#39;users\u0026#39;) -\u0026gt;with([\u0026#39;genders\u0026#39; =\u0026gt; User::all()-\u0026gt;groupBy(\u0026#39;gender\u0026#39;)]); } } ブレードは以下のように、２つのループ（@foreach）を使い、グループのキーである性別と個々のレコードを分けて表示します。\nresources/views/users.blade.php @section(\u0026#39;content\u0026#39;) \u0026lt;div class=\u0026#34;container\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;row justify-content-center\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;col-md-8\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;card\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;card-header\u0026#34;\u0026gt;ユーザー性別分け\u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;card-body\u0026#34;\u0026gt; \u0026lt;table class=\u0026#34;table\u0026#34;\u0026gt; \u0026lt;thead\u0026gt; \u0026lt;tr\u0026gt; \u0026lt;td class=\u0026#34;col-1\u0026#34;\u0026gt;\u0026lt;/td\u0026gt; \u0026lt;td class=\u0026#34;col-1\u0026#34;\u0026gt;ID\u0026lt;/td\u0026gt; \u0026lt;td\u0026gt;名前\u0026lt;/td\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;/thead\u0026gt; \u0026lt;tbody\u0026gt; @foreach($genders as $gender =\u0026gt; $users) \u0026lt;tr\u0026gt; \u0026lt;td colspan=\u0026#34;3\u0026#34;\u0026gt;{{ $gender }}\u0026lt;/td\u0026gt; \u0026lt;/tr\u0026gt; @foreach($users as $user) \u0026lt;tr\u0026gt; \u0026lt;td\u0026gt;\u0026lt;/td\u0026gt; \u0026lt;td class=\u0026#34;text-right\u0026#34;\u0026gt;{{ $user-\u0026gt;id }}\u0026lt;/td\u0026gt; \u0026lt;td\u0026gt;{{ $user-\u0026gt;name }}\u0026lt;/td\u0026gt; \u0026lt;/tr\u0026gt; @endforeach @endforeach \u0026lt;/tbody\u0026gt; \u0026lt;/table\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; @endsection これでめでたし欲しかった画面の完成です。\n","date":"2021-07-25T02:18:40+09:00","permalink":"https://www.larajapan.com/2021/07/25/laravel-collection%EF%BC%88%EF%BC%97%EF%BC%89%E3%82%88%E3%81%8F%E4%BD%BF%E3%81%86groupby/","title":"Laravel Collection（７）よく使うgroupBy"},{"content":"募集は終了しました。 私のお客さん食文化のビジネス拡大のために、Laravelのプログラマーを募集しています。もちろん欲しいスキルはいくつか（以下）あるのですが、私のように独立して、フリーランス系の人が欲しいです。社員となるのではなく、お互いにプロジェクトや知識を、将来共有できる人が良いです。副業として（半日など）も考慮します。\n給与や勤務条件 こちら 仕事内容 ●　ウェブアプリ開発・管理 ●　システム管理（システムを熟知してから） 必須のスキル！ ●　Laravel 5.5以上のアプリ開発経験 ●　gitを毎日使用している ●　Linuxを毎日使用している ●　jqueryなどのjavascriptの開発経験 ●　AWSのEC2、S3などのクラウドサービスの経験 これがあればもっと良い ●　phpunitでユニットテストを書ける ●　オンラインビジネスの仕組みの知識と理解がある（Eコマースのプロジェクトが多いので） ●　技術書の英語が問題なく読める（最近は自動翻訳が優れているからそうでもないかな） ●　ブログを書いている 最後に このブログを読んでいるあなたは、もう資格十分かも。こちらにご連絡を。資格足りないかも？と思っていてもとりあえず声をかけてください！\n","date":"2021-07-15T06:22:39+09:00","permalink":"https://www.larajapan.com/2021/07/15/laravel%E3%81%AE%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9E%E5%8B%9F%E9%9B%86/","title":"Laravelのプログラマ募集"},{"content":"前回に続き、Collectionのsortメソッドについて解説していきます。今回は複数の項目を使ったソートについてです。\n前回同様、以下のサンプルデータを使いますのでコピペして手を動かしつつ読み進めていただけると幸いです。\n// 再掲 $houses = collect([ [\u0026#39;id\u0026#39; =\u0026gt; 1, \u0026#39;price\u0026#39; =\u0026gt; 3000, \u0026#39;size\u0026#39; =\u0026gt; 100, \u0026#39;age\u0026#39; =\u0026gt; 20, \u0026#39;station_distance\u0026#39; =\u0026gt; 0.6], [\u0026#39;id\u0026#39; =\u0026gt; 10, \u0026#39;price\u0026#39; =\u0026gt; 3000, \u0026#39;size\u0026#39; =\u0026gt; 70, \u0026#39;age\u0026#39; =\u0026gt; 3, \u0026#39;station_distance\u0026#39; =\u0026gt; 1.1], [\u0026#39;id\u0026#39; =\u0026gt; 23, \u0026#39;price\u0026#39; =\u0026gt; 1600, \u0026#39;size\u0026#39; =\u0026gt; 60, \u0026#39;age\u0026#39; =\u0026gt; 13], [\u0026#39;id\u0026#39; =\u0026gt; 3, \u0026#39;price\u0026#39; =\u0026gt; 1600, \u0026#39;size\u0026#39; =\u0026gt; 50, \u0026#39;age\u0026#39; =\u0026gt; 11, \u0026#39;station_distance\u0026#39; =\u0026gt; 2.0], ]); 複数の項目によるソート 前回は物件情報を例にsort, sortBy, sortKeysを使い、広さや築年数など１つの項目をピックアップして並び替える方法について解説しました。しかし、時としてある項目で並び替えた後に値が同じであるならば、別の項目によって並び替えたいケースがあります。 例えば、物件情報を価格の安い順に並び替えた後、同じ価格の物件が複数あるならそれらを築年数が浅い順に並び替えたい、などの場合です。このような複雑な並び替えは、sort()の引数に比較ロジックをcallback関数で指定します。\nsort($callback) Laravel公式のドキュメントにはsort()にcallback関数を渡す場合について、実例が載っていませんが、 If your sorting needs are more advanced, you may pass a callback to sort with your own algorithm. Refer to the PHP documentation on uasort, which is what the collection's sort method calls under the hood. と書かれています。ので、PHPのuasortのドキュメントを確認しましょう。そちらの例1 基本的な uasort() の例にてcallback関数の例が示されています。 \u0026lt;?php // 比較用の関数 function cmp($a, $b) { if ($a == $b) { return 0; } return ($a \u0026lt; $b) ? -1 : 1; } なるほど、$aと$bを引数に取り大小を比較する関数ですね。$a \u0026lt; $b なら１、$a \u0026gt; $b なら−１、$a = $b なら０が返されます。\n例を参考に、まずは価格（昇順）で並び替えるcallback関数を書いてみましょう。\n\u0026gt;\u0026gt;\u0026gt; function cmpPrice($a, $b) { if ($a[\u0026#39;price\u0026#39;] == $b[\u0026#39;price\u0026#39;]) { return 0; } return ($a[\u0026#39;price\u0026#39;] \u0026lt; $b[\u0026#39;price\u0026#39;]) ? -1 : 1; } そして、それをsort()に指定してみましょう。\n\u0026gt;\u0026gt;\u0026gt; $houses-\u0026gt;sort(\u0026#39;cmpPrice\u0026#39;); =\u0026gt; Illuminate\\Support\\Collection {#3397 all: [ 2 =\u0026gt; [ \u0026#34;id\u0026#34; =\u0026gt; 23, \u0026#34;price\u0026#34; =\u0026gt; 1600, \u0026#34;size\u0026#34; =\u0026gt; 60, \u0026#34;age\u0026#34; =\u0026gt; 13, ], 3 =\u0026gt; [ \u0026#34;id\u0026#34; =\u0026gt; 3, \u0026#34;price\u0026#34; =\u0026gt; 1600, \u0026#34;size\u0026#34; =\u0026gt; 50, \u0026#34;age\u0026#34; =\u0026gt; 11, \u0026#34;station_distance\u0026#34; =\u0026gt; 2.0, ], 0 =\u0026gt; [ \u0026#34;id\u0026#34; =\u0026gt; 1, \u0026#34;price\u0026#34; =\u0026gt; 3000, \u0026#34;size\u0026#34; =\u0026gt; 100, \u0026#34;age\u0026#34; =\u0026gt; 20, \u0026#34;station_distance\u0026#34; =\u0026gt; 0.6, ], 1 =\u0026gt; [ \u0026#34;id\u0026#34; =\u0026gt; 10, \u0026#34;price\u0026#34; =\u0026gt; 3000, \u0026#34;size\u0026#34; =\u0026gt; 70, \u0026#34;age\u0026#34; =\u0026gt; 3, \u0026#34;station_distance\u0026#34; =\u0026gt; 1.1, ], ], } 価格が安い順にソートされましたね。正しくソートされているか不安な場合は前回紹介したsortBy()を使って確認してみましょう。\n\u0026gt;\u0026gt;\u0026gt; $houses-\u0026gt;sort(\u0026#39;cmpPrice\u0026#39;) == $houses-\u0026gt;sortBy(\u0026#39;price\u0026#39;); =\u0026gt; true 因みに、cmpPriceは昇順にソートされますが、\nreturn ($a[\u0026#39;price\u0026#39;] \u0026lt; $b[\u0026#39;price\u0026#39;]) ? -1 : 1; この部分を\nreturn ($a[\u0026#39;price\u0026#39;] \u0026lt; $b[\u0026#39;price\u0026#39;]) ? 1 : -1; とプラスマイナスを逆にすれば、降順にする事もできます。\n続いて、価格が同じだった場合に築年数で比較するロジックを追加してみましょう。 価格の比較では名前付き関数を用意しましたが、次のコードではsort()内で無名関数で指定しています。laravelではこちらが一般的です。コードは以下のようになります。\n\u0026gt;\u0026gt;\u0026gt; $houses-\u0026gt;sort(function ($a, $b) { if ($a[\u0026#39;price\u0026#39;] == $b[\u0026#39;price\u0026#39;]) { if ($a[\u0026#39;age\u0026#39;] == $b[\u0026#39;age\u0026#39;]) { return 0; } return ($a[\u0026#39;age\u0026#39;] \u0026lt; $b[\u0026#39;age\u0026#39;]) ? -1 : 1; } return ($a[\u0026#39;price\u0026#39;] \u0026lt; $b[\u0026#39;price\u0026#39;]) ? -1 : 1; }); =\u0026gt; Illuminate\\Support\\Collection {#3397 all: [ 3 =\u0026gt; [ \u0026#34;id\u0026#34; =\u0026gt; 3, \u0026#34;price\u0026#34; =\u0026gt; 1600, \u0026#34;size\u0026#34; =\u0026gt; 50, \u0026#34;age\u0026#34; =\u0026gt; 11, \u0026#34;station_distance\u0026#34; =\u0026gt; 2.0, ], 2 =\u0026gt; [ \u0026#34;id\u0026#34; =\u0026gt; 23, \u0026#34;price\u0026#34; =\u0026gt; 1600, \u0026#34;size\u0026#34; =\u0026gt; 60, \u0026#34;age\u0026#34; =\u0026gt; 13, ], 1 =\u0026gt; [ \u0026#34;id\u0026#34; =\u0026gt; 10, \u0026#34;price\u0026#34; =\u0026gt; 3000, \u0026#34;size\u0026#34; =\u0026gt; 70, \u0026#34;age\u0026#34; =\u0026gt; 3, \u0026#34;station_distance\u0026#34; =\u0026gt; 1.1, ], 0 =\u0026gt; [ \u0026#34;id\u0026#34; =\u0026gt; 1, \u0026#34;price\u0026#34; =\u0026gt; 3000, \u0026#34;size\u0026#34; =\u0026gt; 100, \u0026#34;age\u0026#34; =\u0026gt; 20, \u0026#34;station_distance\u0026#34; =\u0026gt; 0.6, ], ], } 価格だけで並び替えた際と見比べてください、同じ価格の場合に築浅順に並び替えられていますね。\n今回は価格と築年数のみを考慮しましたが、同様に比較ロジックを追加していけば更に細かく並び順を指定できます。しかし、上記のコードではifの入れ子が増え、とても読み辛いコードになってしまいます。でも安心して下さい、次に紹介する宇宙船演算子とエルビス演算子の合せ技でとてもスッキリさせることができます！\n宇宙船演算子(\u003c=\u003e)とエルビス演算子(?:)の合せ技 ２つの演算子について軽くおさらいしておきましょう。\n宇宙船演算子 宇宙演算子はPHP7から追加された比較演算子で以下のように記述します。\n$a \u0026lt;=\u0026gt; $b; $aと$bを比較し、$a \u0026gt; $bなら１、$a = $bなら0、 $a \u0026lt; $bなら-1を返します。\nお気づきかと思いますが、まさに前項で作成したcallback関数と同じ結果を返してくれます。\nそれにしても面白い名前ですね。初めてこの演算子を見た時は、使い方よりもまず名前の由来を検索してしまいました。諸説色々あるみたいですが、中でもStar Warsのダースベーダー専用タイファイターに似ている！というのが有力なのだとか。。。気になる人はググってみて下さい！\nエルビス演算子 もう１つキーとなるのがエルビス演算子です。こちらはPHP5.3からあるので馴染みのある演算子かと思います。\n$a ?: $b; $aがtrueなら$aを返し、falseなら$bを返します。\nエルビス演算子の由来は、歌手のエルヴィス・プレスリーの顔文字だそうです。。。どこが？っとなりますが、確かにハテナマークがリーゼントに見えなくも無いかも・・・。私はよくNULL合体演算子と混同してしまうのですが、こちらも名前の由来を知ってから忘れなくなりました。\n宇宙船演算子とエルビス演算子を組み合わせると前項のコードは以下のように書き換えられます。\n\u0026gt;\u0026gt;\u0026gt; $houses-\u0026gt;sort(function ($a, $b) { return $a[\u0026#39;price\u0026#39;] \u0026lt;=\u0026gt; $b[\u0026#39;price\u0026#39;] ?: $a[\u0026#39;age\u0026#39;] \u0026lt;=\u0026gt; $b[\u0026#39;age\u0026#39;]; }); とてもスッキリしましたね！価格が同じ場合は $a[\u0026lsquo;price\u0026rsquo;] \u0026lt;=\u0026gt; $b[\u0026lsquo;price\u0026rsquo;]の結果が0となり、booleanに変換するとfalseなのでエルビス演算子により築年数の比較に進む、というわけです。\nさらに築年数が同じなら広さ、広さが同じなら駅からの距離、と並び替えたい場合は以下のように２行加えるだけです。\n\u0026gt;\u0026gt;\u0026gt; $houses-\u0026gt;sort(function ($a, $b) { return $a[\u0026#39;price\u0026#39;] \u0026lt;=\u0026gt; $b[\u0026#39;price\u0026#39;] ?: $a[\u0026#39;age\u0026#39;] \u0026lt;=\u0026gt; $b[\u0026#39;age\u0026#39;] ?: ($a[\u0026#39;size\u0026#39;] \u0026lt;=\u0026gt; $b[\u0026#39;size\u0026#39;]) * -1 ?: ($a[\u0026#39;station_distance\u0026#39;] \u0026lt;=\u0026gt; $b[\u0026#39;station_distance\u0026#39;]) * -1; }); 簡単ですね！\nsizeとstation_distanceは降順に並び替えたいので-1を掛けています、ご注意を。\nLaravel8のsortBy() ここまでLaravel7を前提に複数の項目で並び替える方法を紹介してきました。しかし実はこれ、Laravel8ではsortBy()でもっと簡単に同じことができます。\n公式ドキュメントを見ると、sortBy()に並べ替える属性と目的の並べ替えの方向で構成される配列が指定できる旨が書いてあります。よって、Laravel8では以下で同じことが出来ます。\n$houses-\u0026gt;sortBy([ [\u0026#39;price\u0026#39;, \u0026#39;asc\u0026#39;], [\u0026#39;age\u0026#39;, \u0026#39;asc\u0026#39;], [\u0026#39;size\u0026#39;, \u0026#39;desc\u0026#39;], [\u0026#39;station_distance\u0026#39;, \u0026#39;desc\u0026#39;], ]); こちらの方が分かりやすいですね、Laravel8にアップグレードした際はこちらを使っていきたいと思います。\n","date":"2021-07-12T00:00:42+09:00","permalink":"https://www.larajapan.com/2021/07/12/laravel-collection%EF%BC%88%EF%BC%96%EF%BC%89%E6%9B%B4%E3%81%AB%E3%82%BD%E3%83%BC%E3%83%88/","title":"Laravel Collection（６）複数のキーでソート"},{"content":"そろそろ引っ越しをしようかと思い、毎日物件情報サイトを物色しているhikaruです。\n物件選びは価格や広さ、築年数、その他様々な側面から比較していく必要があるのでなかなか骨の折れる作業ですね。物件情報がCollectionにまとめられていればサクッと私好み順に並び替えられるのに、としみじみCollectionのありがたみを実感している今日このごろです。\nということで、今回はCollectionのありがたいsort関連のメソッド３つ、sort, sortBy, sortKeysについて解説したいと思います。\nサンプルデータ 以降の解説にて使う物件情報を 物件ID 価格(万円) 広さ(㎡) 築年数 駅までの距離(km) としてコレクションにまとめました、コピペして活用してくださいね。 $houses = collect([ [\u0026#39;id\u0026#39; =\u0026gt; 1, \u0026#39;price\u0026#39; =\u0026gt; 3000, \u0026#39;size\u0026#39; =\u0026gt; 100, \u0026#39;age\u0026#39; =\u0026gt; 20, \u0026#39;station_distance\u0026#39; =\u0026gt; 0.6], [\u0026#39;id\u0026#39; =\u0026gt; 10, \u0026#39;price\u0026#39; =\u0026gt; 3000, \u0026#39;size\u0026#39; =\u0026gt; 70, \u0026#39;age\u0026#39; =\u0026gt; 3, \u0026#39;station_distance\u0026#39; =\u0026gt; 1.1], [\u0026#39;id\u0026#39; =\u0026gt; 23, \u0026#39;price\u0026#39; =\u0026gt; 1600, \u0026#39;size\u0026#39; =\u0026gt; 60, \u0026#39;age\u0026#39; =\u0026gt; 13], [\u0026#39;id\u0026#39; =\u0026gt; 3, \u0026#39;price\u0026#39; =\u0026gt; 1600, \u0026#39;size\u0026#39; =\u0026gt; 50, \u0026#39;age\u0026#39; =\u0026gt; 11, \u0026#39;station_distance\u0026#39; =\u0026gt; 2.0], ]); sort() sortメソッドは一次元なコレクションデータを並び替えるのに使用します、例えば築年数を抽出して浅い順に並び替えてみましょう。 \u0026gt;\u0026gt;\u0026gt; $houses-\u0026gt;pluck(\u0026#39;age\u0026#39;, \u0026#39;id\u0026#39;)-\u0026gt;sort(); =\u0026gt; Illuminate\\Support\\Collection {#3330 all: [ 10 =\u0026gt; 3, 3 =\u0026gt; 11, 23 =\u0026gt; 13, 1 =\u0026gt; 20, ], } 特定の項目を抽出するのはpluckメソッドでしたね、khino氏がLaravel Collection（２） Collectionを返すメソッドで解説しているので忘れてしまった人は復習しておきましょう。\n実行結果を見ると、小さい値から大きい値へ順に並び変えられているのが分かります。小から大へ並び変える事を昇順と言います。逆に大から小へ並び替える事を降順と言います。降順に並び替えるにはsortDescメソッドを使います。\n\u0026gt;\u0026gt;\u0026gt; $houses-\u0026gt;pluck(\u0026#39;age\u0026#39;, \u0026#39;id\u0026#39;)-\u0026gt;sortDesc(); =\u0026gt; Illuminate\\Support\\Collection {#3396 all: [ 1 =\u0026gt; 20, 23 =\u0026gt; 13, 3 =\u0026gt; 11, 10 =\u0026gt; 3, ], } また、この後説明するsortByもsortKeysも末尾にDescを付ける事で降順となります。覚えやすいですね！\nsortBy() sortByは指定した引数の項目によって並び替えるメソッドです。例えば、$housesを狭い順に並び替えてみましょう。 \u0026gt;\u0026gt;\u0026gt; $houses-\u0026gt;sortBy(\u0026#39;size\u0026#39;) =\u0026gt; Illuminate\\Support\\Collection {#3404 all: [ 3 =\u0026gt; [ \u0026#34;id\u0026#34; =\u0026gt; 3, \u0026#34;price\u0026#34; =\u0026gt; 1600, \u0026#34;size\u0026#34; =\u0026gt; 50, \u0026#34;age\u0026#34; =\u0026gt; 11, \u0026#34;station_distance\u0026#34; =\u0026gt; 2.0, ], 2 =\u0026gt; [ \u0026#34;id\u0026#34; =\u0026gt; 23, \u0026#34;price\u0026#34; =\u0026gt; 1600, \u0026#34;size\u0026#34; =\u0026gt; 60, \u0026#34;age\u0026#34; =\u0026gt; 13, ], 1 =\u0026gt; [ \u0026#34;id\u0026#34; =\u0026gt; 10, \u0026#34;price\u0026#34; =\u0026gt; 3000, \u0026#34;size\u0026#34; =\u0026gt; 70, \u0026#34;age\u0026#34; =\u0026gt; 3, \u0026#34;station_distance\u0026#34; =\u0026gt; 1.1, ], 0 =\u0026gt; [ \u0026#34;id\u0026#34; =\u0026gt; 1, \u0026#34;price\u0026#34; =\u0026gt; 3000, \u0026#34;size\u0026#34; =\u0026gt; 100, \u0026#34;age\u0026#34; =\u0026gt; 20, \u0026#34;station_distance\u0026#34; =\u0026gt; 0.6, ], ], } ところで、引数で指定した項目がセットされていない場合はどうなるのでしょう？station_distanceで並び替えてみましょう。\n\u0026gt;\u0026gt;\u0026gt; $houses-\u0026gt;sortBy(\u0026#39;station_distance\u0026#39;); =\u0026gt; Illuminate\\Support\\Collection {#3406 all: [ 2 =\u0026gt; [ \u0026#34;id\u0026#34; =\u0026gt; 23, \u0026#34;price\u0026#34; =\u0026gt; 1600, \u0026#34;size\u0026#34; =\u0026gt; 60, \u0026#34;age\u0026#34; =\u0026gt; 13, ], 0 =\u0026gt; [ \u0026#34;id\u0026#34; =\u0026gt; 1, \u0026#34;price\u0026#34; =\u0026gt; 3000, \u0026#34;size\u0026#34; =\u0026gt; 100, \u0026#34;age\u0026#34; =\u0026gt; 20, \u0026#34;station_distance\u0026#34; =\u0026gt; 0.6, ], 1 =\u0026gt; [ \u0026#34;id\u0026#34; =\u0026gt; 10, \u0026#34;price\u0026#34; =\u0026gt; 3000, \u0026#34;size\u0026#34; =\u0026gt; 70, \u0026#34;age\u0026#34; =\u0026gt; 3, \u0026#34;station_distance\u0026#34; =\u0026gt; 1.1, ], 3 =\u0026gt; [ \u0026#34;id\u0026#34; =\u0026gt; 3, \u0026#34;price\u0026#34; =\u0026gt; 1600, \u0026#34;size\u0026#34; =\u0026gt; 50, \u0026#34;age\u0026#34; =\u0026gt; 11, \u0026#34;station_distance\u0026#34; =\u0026gt; 2.0, ], ], } station_distanceがセットされていない要素が先頭に来ました。 セットされていない要素は0と同じ扱いなのでしょうか？ いえ、そうではありません。セットされていない値はnullとして扱われているのです。そして、マイナス値を含めてソートした場合も先頭にきます。以下のように１次元コレクションで確認すると分かりやすいです。\n\u0026gt;\u0026gt;\u0026gt; $col = collect([1, null, -1]); =\u0026gt; Illuminate\\Support\\Collection {#3388 all: [ 1, null, -1, ], } \u0026gt;\u0026gt;\u0026gt; $col-\u0026gt;sort() =\u0026gt; Illuminate\\Support\\Collection {#3406 all: [ 1 =\u0026gt; null, 2 =\u0026gt; -1, 0 =\u0026gt; 1, ], } \u0026gt;\u0026gt;\u0026gt; $col-\u0026gt;sortDesc() =\u0026gt; Illuminate\\Support\\Collection {#3398 all: [ 0 =\u0026gt; 1, 2 =\u0026gt; -1, 1 =\u0026gt; null, ], } これは配列のsort関数と同じ仕様です。nullとは値を持たない事を表現する型の唯一の値であり、他の型（boolean, int, float, stringなど）が混在する配列においては、昇順では先頭に、降順では末尾に並び替えされるのです。\nsortKeys() 最後にCollectionのキーによってソートするsortKeysについて見てみましょう。$housesをidをキーにしてソートしてみましょう。特定の項目をkeyに指定するにはkeyByメソッドを使います。 \u0026gt;\u0026gt;\u0026gt; $houses-\u0026gt;keyBy(\u0026#39;id\u0026#39;)-\u0026gt;sortKeys() =\u0026gt; Illuminate\\Support\\Collection {#3414 all: [ 1 =\u0026gt; [ \u0026#34;id\u0026#34; =\u0026gt; 1, \u0026#34;price\u0026#34; =\u0026gt; 3000, \u0026#34;size\u0026#34; =\u0026gt; 100, \u0026#34;age\u0026#34; =\u0026gt; 20, \u0026#34;station_distance\u0026#34; =\u0026gt; 0.6, ], 3 =\u0026gt; [ \u0026#34;id\u0026#34; =\u0026gt; 3, \u0026#34;price\u0026#34; =\u0026gt; 1600, \u0026#34;size\u0026#34; =\u0026gt; 50, \u0026#34;age\u0026#34; =\u0026gt; 11, \u0026#34;station_distance\u0026#34; =\u0026gt; 2.0, ], 10 =\u0026gt; [ \u0026#34;id\u0026#34; =\u0026gt; 10, \u0026#34;price\u0026#34; =\u0026gt; 3000, \u0026#34;size\u0026#34; =\u0026gt; 70, \u0026#34;age\u0026#34; =\u0026gt; 3, \u0026#34;station_distance\u0026#34; =\u0026gt; 1.1, ], 23 =\u0026gt; [ \u0026#34;id\u0026#34; =\u0026gt; 23, \u0026#34;price\u0026#34; =\u0026gt; 1600, \u0026#34;size\u0026#34; =\u0026gt; 60, \u0026#34;age\u0026#34; =\u0026gt; 13, ], ], } まとめ 以上がCollectionの基本的なsort系メソッドの使い方になります。次回は一歩踏み込んだソートについて解説していきたいと思います。","date":"2021-07-04T11:45:47+09:00","permalink":"https://www.larajapan.com/2021/07/04/laravel-collection-%EF%BC%88%EF%BC%95%EF%BC%89%E3%82%BD%E3%83%BC%E3%83%88/","title":"Laravel Collection（５）ソート"},{"content":"Collectionを使っての合計の計算は簡単です。例えば、注文の総個数の計算は$collection-\u0026gt;sum(\u0026lsquo;quantity\u0026rsquo;)です。金額と個数の掛け算で総合計金額の計算はどうでしょう。直感的に$collection-\u0026gt;sum(\u0026lsquo;price*quantity\u0026rsquo;)では、と思いますがそれはうまくいきません。さて、どう計算するのでしょう？\nデータの準備 準備として、まず以下migrationファイルを作成して、DBテーブルを作成します。\ndatabase/migrations/2021_06_26_164123_create_order_items_table.php use Illuminate\\Database\\Migrations\\Migration; use Illuminate\\Database\\Schema\\Blueprint; use Illuminate\\Support\\Facades\\Schema; class CreateOrderItemsTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create(\u0026#39;order_items\u0026#39;, function (Blueprint $table) { $table-\u0026gt;id(); $table-\u0026gt;unsignedBigInteger(\u0026#39;order_id\u0026#39;)-\u0026gt;index(); $table-\u0026gt;string(\u0026#39;name\u0026#39;); $table-\u0026gt;decimal(\u0026#39;price\u0026#39;, 8, 0); $table-\u0026gt;unsignedInteger(\u0026#39;quantity\u0026#39;); $table-\u0026gt;timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists(\u0026#39;order_items\u0026#39;); } } 今度は、factoryの作成です。\ndatabase/factories/OrderItemFactory.php /** @var \\Illuminate\\Database\\Eloquent\\Factory $factory */ use App\\OrderItem; use Faker\\Generator as Faker; use Illuminate\\Support\\Str; $factory-\u0026gt;define(OrderItem::class, function (Faker $faker) { return [ \u0026#39;order_id\u0026#39; =\u0026gt; $faker-\u0026gt;numberBetween(1, 3), \u0026#39;name\u0026#39; =\u0026gt; Str::upper($faker-\u0026gt;word()), \u0026#39;price\u0026#39; =\u0026gt; $faker-\u0026gt;numberBetween(100, 1000), \u0026#39;quantity\u0026#39; =\u0026gt; $faker-\u0026gt;numberBetween(1, 10), ]; }); さて、次にtinkerでサンプルデータを作成します。\n\u0026gt;\u0026gt;\u0026gt; factory(App\\OrderItem::class, 9)-\u0026gt;create(); =\u0026gt; Illuminate\\Database\\Eloquent\\Collection {#4246 all: [ App\\OrderItem {#4247 id: 1, order_id: 1, name: \u0026#34;BEATAE\u0026#34;, price: \u0026#34;924\u0026#34;, quantity: 10, created_at: \u0026#34;2021-06-26 19:58:26\u0026#34;, updated_at: \u0026#34;2021-06-26 19:58:26\u0026#34;, }, App\\OrderItem {#4248 id: 2, order_id: 1, ... 作成されたデータをmysqlで閲覧するとこんな感じです。\nmysql\u0026gt; select id, order_id, price, quantity from order_items where order_id = 1; +----+----------+-------+----------+ | id | order_id | price | quantity | +----+----------+-------+----------+ | 1 | 1 | 924 | 10 | | 2 | 1 | 157 | 8 | | 3 | 1 | 294 | 7 | | 4 | 1 | 839 | 2 | | 6 | 1 | 450 | 6 | | 8 | 1 | 310 | 9 | +----+----------+-------+----------+ ちなみに、sqlでは先の合計金額の計算は、\nmysql\u0026gt; select sum(price * quantity) from order_items where order_id = 1; +-----------------------+ | sum(price * quantity) | +-----------------------+ | 19722 | +-----------------------+ と簡単に計算できます。\n合計金額の計算 データが揃ったところでCollectionを利用して計算です。 最初に、Eloquentで注文番号１のアイテムのCollectionを作成します。\n\u0026gt;\u0026gt;\u0026gt; use App\\OrdreItem; \u0026gt;\u0026gt;\u0026gt; $items = OrderItem::where(\u0026#39;order_id\u0026#39;, \u0026#39;=\u0026#39;, 1)-\u0026gt;get(); =\u0026gt; Illuminate\\Database\\Eloquent\\Collection {#3317 all: [ App\\OrderItem {#4249 id: 1, order_id: 1, name: \u0026#34;BEATAE\u0026#34;, price: \u0026#34;924\u0026#34;, quantity: 10, created_at: \u0026#34;2021-06-26 19:58:26\u0026#34;, updated_at: \u0026#34;2021-06-26 19:58:26\u0026#34;, }, App\\OrderItem {#4103 id: 2, order_id: 1, ... 試しに、先の間違った合計金額の計算を実行してみましょうか。さて、どうなるか。\n\u0026gt;\u0026gt;\u0026gt; \u0026gt;\u0026gt;\u0026gt; $items-\u0026gt;sum(\u0026#39;price*quantity\u0026#39;); =\u0026gt; 0 もちろん、sum()の引数は項目名を指定しなければならないので、\u0026lsquo;price*quantity\u0026rsquo;ような項目名はありもしないので合計はゼロです。\n正しい方法としては、map()とsum()の２つの関数を利用します。それぞれのアイテムの合計金額のCollectionを作成してその和を計算です。\n\u0026gt;\u0026gt;\u0026gt; $items-\u0026gt;map(function($item) { return $item-\u0026gt;price * $item-\u0026gt;quantity; })-\u0026gt;sum(); =\u0026gt; 19722 ちょっと長くなったけれどわかりやすい計算です。\nこの他には、reduce()を使用した計算もあります。\n\u0026gt;\u0026gt;\u0026gt; $items-\u0026gt;reduce(function($carry, $item) { return $carry + $item-\u0026gt;price * $item-\u0026gt;quantity;}, 0); =\u0026gt; 19722 reduce()で使われる$carryは第２の引数として与えらた値（この場合ゼロ）を初期値としてループの中で計算（金額ｘ個数）した値を前回の$carryに足していきます。こちらの方法も悪くはないですね。前のmapに比べては余計なCollectionを作成しない分だけ効率かもしれません。\n","date":"2021-06-28T22:49:23+09:00","permalink":"https://www.larajapan.com/2021/06/28/laravel-collection%EF%BC%884%EF%BC%89%E5%90%88%E8%A8%88%E3%81%AE%E8%A8%88%E7%AE%97/","title":"Laravel Collection（４）合計の計算"},{"content":"以前投稿したbulk insertで大量のデータをDBに登録するの記事では10万レコードの挿入をbulk insertで行い、通常のinsertと比べてどれだけ速くなるのか検証しました。結果は一目瞭然で、大量のレコードをDBに登録する際には強力な武器になる事が分かりました。\nしかし、前回のコードを実際の運用で使うには１つ問題があります。それはbulk insertの途中でエラーが発生した場合、DBに中途半端にレコードが残ってしまうという問題です。今回はこの問題を解決するために、前回のコードをtransactionに入れ、エラーが発生した際にロールバックされるように改修します。そして、transaction処理にする事でパフォーマンスにどのような影響があるか検証してみます。\nエラー発生時の挙動 改修を始める前にまずは途中でエラーが発生し処理が異常終了してしまった場合の挙動を確認してみましょう。 前回の記事で作成したUsersSeederを以下のように編集し、9999件目のレコード挿入でわざと重複エラーを発生させてみましょう。 database/seeds/UsersSeeder.php use App\\User; use Faker\\Factory as Faker; use Illuminate\\Database\\Seeder; class UsersSeeder extends Seeder { /** * Run the database seeds. * * @return void */ public function run() { $faker = Faker::create(\u0026#39;ja_JP\u0026#39;); $params = []; for ($i=0; $i \u0026lt; 100000; $i++) { $email = \u0026#34;test{$i}@example.com\u0026#34;; // 9999件目でemailが重複 if ($i == 9999) { $email = \u0026#34;test1@example.com\u0026#34;; } $params[] = [ \u0026#39;name\u0026#39; =\u0026gt; $faker-\u0026gt;name(), \u0026#39;email\u0026#39; =\u0026gt; $email, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;testtest\u0026#39;, ]; if (count($params) \u0026gt;= 1000) { User::insert($params); $params = []; } } } } seederを実行すると以下のように重複エラーが発生するはずです。\n% php artisan db:seed Seeding: UsersSeeder Illuminate\\Database\\QueryException : SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry \u0026#39;test1@example.com\u0026#39; for key \u0026#39;users_email_unique\u0026#39; (SQL: insert into `users` (`email`, `name`, `password`) values ... この時点でDB上のレコード件数を確認してみると。。。\nmysql\u0026gt; select count(*) from users; +----------+ | count(*) | +----------+ | 9000 | +----------+ 1 row in set (0.01 sec) 9999件目でエラーが発生し、処理がabortされたので最後の999件は登録されませんでしたが、それまでの9000件のレコードが登録されたままの中途半端な状態になっています。前回の記事ではseederの実行前にtruncateメソッドを実行し、テーブルを空にしていたため、再度seederを実行しても問題ありませんでした。しかし、エラーにより処理が途中で強制終了されたのであれば、失敗した中途半端な状態は残さず、実行前の状態に戻るのが望ましいです。\ntransaction処理の実装 それではtransaction処理を実装し、エラー発生時にロールバックするように修正してみましょう。Laravelでは以下のようにDBファサードを使うとtransaction処理が簡単に実装できます。\n// transaction開始 DB::beginTransaction(); try { // 何かしらのDB操作... // transactionコミット DB::commit(); } catch (\\Throwable $th) { // ロールバック DB::rollBack(); } 修正を加えたコードが以下になります。\ndatabase/seeds/UsersSeeder.php use App\\User; use Faker\\Factory as Faker; use Illuminate\\Database\\Seeder; use Illuminate\\Support\\Facades\\DB; class UsersSeeder extends Seeder { /** * Run the database seeds. * * @return void */ public function run() { DB::beginTransaction(); try { $this-\u0026gt;insertDummies(); DB::commit(); } catch (\\Throwable $th) { DB::rollBack(); throw new Exception(\u0026#39;failed seeding User, so rolled back.\u0026#39;); } } private function insertDummies() { $faker = Faker::create(\u0026#39;ja_JP\u0026#39;); $params = []; for ($i=0; $i \u0026lt; 100000; $i++) { $email = \u0026#34;test{$i}@example.com\u0026#34;; // 9999件目でemailが重複 if ($i == 9999) { $email = \u0026#34;test1@example.com\u0026#34;; } $params[] = [ \u0026#39;name\u0026#39; =\u0026gt; $faker-\u0026gt;name(), \u0026#39;email\u0026#39; =\u0026gt; $email, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;testtest\u0026#39;, ]; if (count($params) \u0026gt;= 1000) { User::insert($params); $params = []; } } } } runメソッドが冗長になってしまう為、レコード作成処理をinsertDummiesメソッドに切り出しました。 レコード作成処理の途中でエラーが発生した場合はDB::rollback()によってinsertDummiesメソッド実行前に巻き戻されるはずです。\n再度、seederを実行してみましょう。\n% php artisan db:seed Seeding: UsersSeeder Exception : failed seeding User, so rolled back. ... Errorが発生し期待通りロールバックされました。DB側を確認してみましょう。\nmysql\u0026gt; select count(*) from users; +----------+ | count(*) | +----------+ | 0 | +----------+ 1 row in set (0.00 sec) 実行前の状態に戻されていますね。\n処理速度に影響は？ 通常のinsert処理からbulk insertに切り替える事の最大のメリットはその速さでしたが、transaction処理に切り替える事で何か影響は在るでしょうか？チェックしてみましょう。\ninsertDummiesメソッド内のエラーを発生させていた以下の行を削除し、正常終了するように直します。\ndatabase/seeds/UsersSeeder.php ... // 9999件目でemailが重複 if ($i == 9999) { $email = \u0026#34;test1@example.com\u0026#34;; } ... 再度、seederを実行してみましょう。\n% php artisan db:seed Seeding: UsersSeeder Seeded: UsersSeeder (6.87 seconds) Database seeding completed successfully. 約6.9秒、transaction無しの状態で約5.8秒だったので約１秒遅くなりましたが、気にするほどでは無さそうですね。\n","date":"2021-06-19T03:00:16+09:00","permalink":"https://www.larajapan.com/2021/06/19/bulk-insert%E3%82%92transaction%E5%87%A6%E7%90%86%E3%81%AB%E3%81%99%E3%82%8B/","title":"bulk insertをtransaction処理にする"},{"content":"前回においてCollectionのeach()でUserのオブジェクトにageの属性を追加する処理をしましたが、今回はこれを発展させて、UserのCollectionにカスタムメソッドを追加します。\n目的 例えば、いつも大人（２０歳以上）だけのユーザーを対象にした処理を行いたいとします。 DBから取得したレコードにおいては、birth_dateの属性があるので現時点での大人だけのレコードは、以下のようにfilter()を利用して、\nUser::all()-\u0026gt;filter(function ($user) { return $user-\u0026gt;isAdult(); }) のような処理をすれば大人だけのレコード取得が可能です。\n上で使用されているisAdult()は以下のように定義します。歳の計算は、アクセッサ getAgeAttributeで行います。\napp/User.php namespace App; use Carbon\\Carbon; use Illuminate\\Contracts\\Auth\\MustVerifyEmail; use Illuminate\\Foundation\\Auth\\User as Authenticatable; use Illuminate\\Notifications\\Notifiable; class User extends Authenticatable { use Notifiable; ... public function getAgeAttribute() { return Carbon::parse($this-\u0026gt;birth_date)-\u0026gt;age; } public function isAdult() { return $this-\u0026gt;age \u0026gt;= 20; } } さて、これで実際に先ほどのコードをtinkerで実行すると、\n\u0026gt;\u0026gt;\u0026gt; User::all()-\u0026gt;filter(function ($user) { return $user-\u0026gt;isAdult(); }); =\u0026gt; Illuminate\\Database\\Eloquent\\Collection {#4179 all: [ 0 =\u0026gt; App\\User {#3306 id: 1, name: \u0026#34;坂本 充\u0026#34;, email: \u0026#34;nishida@example.net\u0026#34;, email_verified_at: \u0026#34;2021-05-30 21:55:14\u0026#34;, #password: \u0026#34;$2y$10$snEjTmJvBDu3uxd1bz0QDupihJA3zjvEH5J0pm/aLHG4UvpSfJe/q\u0026#34;, #remember_token: \u0026#34;j8hPjp8ob7\u0026#34;, birth_date: \u0026#34;1997-03-02\u0026#34;, created_at: \u0026#34;2021-05-30 21:55:14\u0026#34;, updated_at: \u0026#34;2021-05-30 21:55:14\u0026#34;, }, 2 =\u0026gt; App\\User {#3986 id: 3, name: \u0026#34;近藤 知実\u0026#34;, email: \u0026#34;atsushi.ito@example.net\u0026#34;, email_verified_at: \u0026#34;2021-05-30 21:55:14\u0026#34;, #password: \u0026#34;$2y$10$A7xldQvOiCfUOyNS57EEYu2/s77a70BPmGoOjzDSgTmveeRlcAD8.\u0026#34;, #remember_token: \u0026#34;qOFWQFb5ER\u0026#34;, birth_date: \u0026#34;1972-09-14\u0026#34;, created_at: \u0026#34;2021-05-30 21:55:14\u0026#34;, updated_at: \u0026#34;2021-05-30 21:55:14\u0026#34;, }, ], } と大人だけのレコードを返してくれます。\nさらに、コールバックの定義も必要なしに、以下のようにfilterをあたかも属性のようにしての使用も可能です。短縮されていいですね。\n\u0026gt;\u0026gt;\u0026gt; User::all()-\u0026gt;filter-\u0026gt;isAdult() =\u0026gt; Illuminate\\Database\\Eloquent\\Collection {#3299 all: [ 0 =\u0026gt; App\\User {#4238 id: 1, name: \u0026#34;坂本 充\u0026#34;, email: \u0026#34;nishida@example.net\u0026#34;, email_verified_at: \u0026#34;2021-05-30 21:55:14\u0026#34;, #password: \u0026#34;$2y$10$snEjTmJvBDu3uxd1bz0QDupihJA3zjvEH5J0pm/aLHG4UvpSfJe/q\u0026#34;, #remember_token: \u0026#34;j8hPjp8ob7\u0026#34;, birth_date: \u0026#34;1997-03-02\u0026#34;, created_at: \u0026#34;2021-05-30 21:55:14\u0026#34;, updated_at: \u0026#34;2021-05-30 21:55:14\u0026#34;, }, 2 =\u0026gt; App\\User {#4240 id: 3, name: \u0026#34;近藤 知実\u0026#34;, email: \u0026#34;atsushi.ito@example.net\u0026#34;, email_verified_at: \u0026#34;2021-05-30 21:55:14\u0026#34;, #password: \u0026#34;$2y$10$A7xldQvOiCfUOyNS57EEYu2/s77a70BPmGoOjzDSgTmveeRlcAD8.\u0026#34;, #remember_token: \u0026#34;qOFWQFb5ER\u0026#34;, birth_date: \u0026#34;1972-09-14\u0026#34;, created_at: \u0026#34;2021-05-30 21:55:14\u0026#34;, updated_at: \u0026#34;2021-05-30 21:55:14\u0026#34;, }, ], } \u0026gt;\u0026gt;\u0026gt; しかし、凄いことに、これをさらに進めて、以下のようにCollectionのカスタムメソッドの作成してUserのCollectionに適用することも可能なのです。\nUser::all()-\u0026gt;adults() まず、Collectionを継承したクラスを作成して、adults()のカスタムメソッドを定義します。\napp/Collections/UserCollection.php namespace App\\Collections; use Illuminate\\Database\\Eloquent\\Collection; class UserCollection extends Collection { public function adults() { return $this-\u0026gt;filter-\u0026gt;isAdult(); } } そして、そのカスタムクラスをモデルのクラス（User）のnewCollection()のメソッドで返り値とします。\napp/User.php namespace App; use App\\Collections\\UserCollection; use Carbon\\Carbon; use Illuminate\\Contracts\\Auth\\MustVerifyEmail; use Illuminate\\Foundation\\Auth\\User as Authenticatable; use Illuminate\\Notifications\\Notifiable; class User extends Authenticatable { use Notifiable; ... public function getAgeAttribute() { return Carbon::parse($this-\u0026gt;birth_date)-\u0026gt;age; } public function isAdult() { return $this-\u0026gt;age \u0026gt;= 20; } public function newCollection(array $models = []) { return new UserCollection($models); } } 実行してみましょう。\n\u0026gt;\u0026gt;\u0026gt; User::all()-\u0026gt;adults(); =\u0026gt; App\\Collections\\UserCollection {#3305 all: [ 0 =\u0026gt; App\\User {#4024 id: 1, name: \u0026#34;坂本 充\u0026#34;, email: \u0026#34;nishida@example.net\u0026#34;, email_verified_at: \u0026#34;2021-05-30 21:55:14\u0026#34;, #password: \u0026#34;$2y$10$snEjTmJvBDu3uxd1bz0QDupihJA3zjvEH5J0pm/aLHG4UvpSfJe/q\u0026#34;, #remember_token: \u0026#34;j8hPjp8ob7\u0026#34;, birth_date: \u0026#34;1997-03-02\u0026#34;, created_at: \u0026#34;2021-05-30 21:55:14\u0026#34;, updated_at: \u0026#34;2021-05-30 21:55:14\u0026#34;, }, 2 =\u0026gt; App\\User {#3986 id: 3, name: \u0026#34;近藤 知実\u0026#34;, email: \u0026#34;atsushi.ito@example.net\u0026#34;, email_verified_at: \u0026#34;2021-05-30 21:55:14\u0026#34;, #password: \u0026#34;$2y$10$A7xldQvOiCfUOyNS57EEYu2/s77a70BPmGoOjzDSgTmveeRlcAD8.\u0026#34;, #remember_token: \u0026#34;qOFWQFb5ER\u0026#34;, birth_date: \u0026#34;1972-09-14\u0026#34;, created_at: \u0026#34;2021-05-30 21:55:14\u0026#34;, updated_at: \u0026#34;2021-05-30 21:55:14\u0026#34;, }, ], } 同じ結果となりました。コードが短くなっただけでなくわかりやすくもなりました。\n最後に注意としてですが、今回の例はレコード数がとても多いときはパフォーマンスの点からすると非常に悪い例です。すべてのDBレコードを取得してコードにおいて抽出を行うわけですゆえに。パフォーマンスを考えるとDBのSQLレベルにおいて大人の条件を入れて実行すべきです。\n","date":"2021-06-13T09:23:55+09:00","permalink":"https://www.larajapan.com/2021/06/13/laravel-collection%EF%BC%88%EF%BC%93%EF%BC%89%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%A0%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89/","title":"Laravel Collection（３）カスタムメソッド"},{"content":"前回は、１つの値を返すCollectionのメソッドを紹介しましたが、今度はCollectionを返すメソッドの紹介です。まさにこれが実践でよく使われるものです。\nレコード まずは、前回と同様に、以下の３レコードのみがusersのDBテーブルに存在するとの仮定です。\nmysql\u0026gt; select id, name, birth_date from users; +----+---------------+------------+ | id | name | birth_date | +----+---------------+------------+ | 1 | 坂本 充 | 1997-03-02 | | 2 | 藤本 和也 | 2005-10-03 | | 3 | 近藤 知実 | 1972-09-14 | +----+---------------+------------+ 3 rows in set (0.00 sec) 指定した項目値の抽出 pluck()\nこのメソッドは、引数として与えた項目名の値をCollectionとして返します。\n\u0026gt;\u0026gt;\u0026gt; User::all()-\u0026gt;pluck(\u0026#39;birth_date\u0026#39;) =\u0026gt; Illuminate\\Support\\Collection {#3302 all: [ \u0026#34;1997-03-02\u0026#34;, \u0026#34;2005-10-03\u0026#34;, \u0026#34;1972-09-14\u0026#34;, ], } 引数として２つの項目名を与えると、後者をキーとした連想配列のCollectionを返します。\n\u0026gt;\u0026gt;\u0026gt; User::all()-\u0026gt;pluck(\u0026#39;name\u0026#39;, \u0026#39;id\u0026#39;) =\u0026gt; Illuminate\\Support\\Collection {#4258 all: [ 1 =\u0026gt; \u0026#34;坂本 充\u0026#34;, 2 =\u0026gt; \u0026#34;藤本 和也\u0026#34;, 3 =\u0026gt; \u0026#34;近藤 知実\u0026#34;, ], } 複数の項目値を抽出するには、\nmap()\nを使用します。\n\u0026gt;\u0026gt;\u0026gt; User::all()-\u0026gt;map(function ($user) { return $user-\u0026gt;only([\u0026#39;id\u0026#39;, \u0026#39;name\u0026#39;, \u0026#39;birth_date\u0026#39;]); }) =\u0026gt; Illuminate\\Support\\Collection {#4090 all: [ [ \u0026#34;id\u0026#34; =\u0026gt; 1, \u0026#34;name\u0026#34; =\u0026gt; \u0026#34;坂本 充\u0026#34;, \u0026#34;birth_date\u0026#34; =\u0026gt; \u0026#34;1997-03-02\u0026#34;, ], [ \u0026#34;id\u0026#34; =\u0026gt; 2, \u0026#34;name\u0026#34; =\u0026gt; \u0026#34;藤本 和也\u0026#34;, \u0026#34;birth_date\u0026#34; =\u0026gt; \u0026#34;2005-10-03\u0026#34;, ], [ \u0026#34;id\u0026#34; =\u0026gt; 3, \u0026#34;name\u0026#34; =\u0026gt; \u0026#34;近藤 知実\u0026#34;, \u0026#34;birth_date\u0026#34; =\u0026gt; \u0026#34;1972-09-14\u0026#34;, ], ], } 条件に合うレコードを抽出 filter()\nこのメソッドは、Collectionから条件に合う必要なレコードだけを抽出します。\n\u0026gt;\u0026gt;\u0026gt; User::all()-\u0026gt;filter(function ($user) { return $user-\u0026gt;birth_date \u0026lt; \u0026#39;1990-01-01\u0026#39;; }) =\u0026gt; Illuminate\\Database\\Eloquent\\Collection {#4025 all: [ 2 =\u0026gt; App\\User {#4179 id: 3, name: \u0026#34;近藤 知実\u0026#34;, email: \u0026#34;atsushi.ito@example.net\u0026#34;, email_verified_at: \u0026#34;2021-05-30 21:55:14\u0026#34;, #password: \u0026#34;$2y$10$A7xldQvOiCfUOyNS57EEYu2/s77a70BPmGoOjzDSgTmveeRlcAD8.\u0026#34;, #remember_token: \u0026#34;qOFWQFb5ER\u0026#34;, birth_date: \u0026#34;1972-09-14\u0026#34;, created_at: \u0026#34;2021-05-30 21:55:14\u0026#34;, updated_at: \u0026#34;2021-05-30 21:55:14\u0026#34;, }, ], } 条件を少し複雑にすると、\n\u0026gt;\u0026gt;\u0026gt; User::all()-\u0026gt;filter(function ($user) { if ($user-\u0026gt;birth_date \u0026gt; \u0026#39;2000-01-01\u0026#39;) return false; if ($user-\u0026gt;birth_date \u0026lt; \u0026#39;1990-01-01\u0026#39;) return false; return true; }) =\u0026gt; Illuminate\\Database\\Eloquent\\Collection {#4249 all: [ App\\User {#4246 id: 1, name: \u0026#34;坂本 充\u0026#34;, email: \u0026#34;nishida@example.net\u0026#34;, email_verified_at: \u0026#34;2021-05-30 21:55:14\u0026#34;, #password: \u0026#34;$2y$10$snEjTmJvBDu3uxd1bz0QDupihJA3zjvEH5J0pm/aLHG4UvpSfJe/q\u0026#34;, #remember_token: \u0026#34;j8hPjp8ob7\u0026#34;, birth_date: \u0026#34;1997-03-02\u0026#34;, created_at: \u0026#34;2021-05-30 21:55:14\u0026#34;, updated_at: \u0026#34;2021-05-30 21:55:14\u0026#34;, }, ], } というように、1990-01-01と2000-01-01の間に生まれたユーザーだけの取得も可能です。\nちなみに、reject()はfilter()の逆で、上と同じ結果が欲しいなら、以下のようtrue, falseがすべて逆になります。\n\u0026gt;\u0026gt;\u0026gt; User::all()-\u0026gt;reject(function ($user) { if ($user-\u0026gt;birth_date \u0026gt; \u0026#39;2000-01-01\u0026#39;) return true; if ($user-\u0026gt;birth_date \u0026lt; \u0026#39;1990-01-01\u0026#39;) return true; return false; }) それぞれのレコード（Userのオブジェクト）で属性処理 each()\nこのメソッドはまさに、よく使う以下のforeach()の関数のようなもので取得したそれぞれのDBレコードにおいて、属性の処理が可能です。\nforeach($rows as $row) { ... } 例えば、以下のようにUserのオブジェクトにageの属性を追加することができます。\n\u0026gt;\u0026gt;\u0026gt; use Carbon\\Carbon; \u0026gt;\u0026gt;\u0026gt; User::all()-\u0026gt;each(function ($user) { $user-\u0026gt;age = Carbon::parse($user-\u0026gt;birth_date)-\u0026gt;age; }) =\u0026gt; Illuminate\\Database\\Eloquent\\Collection {#3986 all: [ App\\User {#4234 id: 1, name: \u0026#34;坂本 充\u0026#34;, email: \u0026#34;nishida@example.net\u0026#34;, email_verified_at: \u0026#34;2021-05-30 21:55:14\u0026#34;, #password: \u0026#34;$2y$10$snEjTmJvBDu3uxd1bz0QDupihJA3zjvEH5J0pm/aLHG4UvpSfJe/q\u0026#34;, #remember_token: \u0026#34;j8hPjp8ob7\u0026#34;, birth_date: \u0026#34;1997-03-02\u0026#34;, created_at: \u0026#34;2021-05-30 21:55:14\u0026#34;, updated_at: \u0026#34;2021-05-30 21:55:14\u0026#34;, age: 24, }, App\\User {#4238 id: 2, name: \u0026#34;藤本 和也\u0026#34;, email: \u0026#34;yoshida.kana@example.com\u0026#34;, email_verified_at: \u0026#34;2021-05-30 21:55:14\u0026#34;, #password: \u0026#34;$2y$10$QiozldxVUo3LLj347mqHxeoDh5TEcjddDwc6vN8Pv5b4kPgPUgjri\u0026#34;, #remember_token: \u0026#34;frW94QFFZh\u0026#34;, birth_date: \u0026#34;2005-10-03\u0026#34;, created_at: \u0026#34;2021-05-30 21:55:14\u0026#34;, updated_at: \u0026#34;2021-05-30 21:55:14\u0026#34;, age: 15, }, App\\User {#4180 id: 3, name: \u0026#34;近藤 知実\u0026#34;, email: \u0026#34;atsushi.ito@example.net\u0026#34;, email_verified_at: \u0026#34;2021-05-30 21:55:14\u0026#34;, #password: \u0026#34;$2y$10$A7xldQvOiCfUOyNS57EEYu2/s77a70BPmGoOjzDSgTmveeRlcAD8.\u0026#34;, #remember_token: \u0026#34;qOFWQFb5ER\u0026#34;, birth_date: \u0026#34;1972-09-14\u0026#34;, created_at: \u0026#34;2021-05-30 21:55:14\u0026#34;, updated_at: \u0026#34;2021-05-30 21:55:14\u0026#34;, age: 48, }, ], } ","date":"2021-06-06T04:47:59+09:00","permalink":"https://www.larajapan.com/2021/06/06/laravel-collection%EF%BC%88%EF%BC%92%EF%BC%89-collection%E3%82%92%E8%BF%94%E3%81%99%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89/","title":"Laravel Collection（２） Collectionを返すメソッド"},{"content":"Laravelをフレームワークとして使う１つの魅力は、Collectionにあります。Eloquentでデータベースから取得した後のレコード処理がとてもエレガントにわかりやく書くことができます。というものの、Laravelのマニュアルに出てくるCollectionの例は配列ベースをcollect()したものばかりで物足りません。ということで、ここでCollectionの紹介を兼ねてDBからの取得にCollectionのメソッドを適用する例を掲載します。\n準備 まずは、DBレコード取得ための準備から。\nここでで作成したLaravelのプロジェクトをもとにします。\nそこで使用されている、DBテーブルにある項目を追加したいので、まずはmigrationを作成します。\n$ php artisan make:migration add_new_fields_to_users_table --table=users Created Migration: 2021_05_30_213829_add_new_fields_to_users_table 以下のようにusersのDBテーブルにbirth_date、つまり誕生日の項目を追加します。\ndatabase/migrations/2021_05_24_022418_add_new_fields_to_users_table.php use Illuminate\\Database\\Migrations\\Migration; use Illuminate\\Database\\Schema\\Blueprint; use Illuminate\\Support\\Facades\\Schema; class AddNewFieldsToUsersTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::table(\u0026#39;users\u0026#39;, function (Blueprint $table) { $table-\u0026gt;date(\u0026#39;birth_date\u0026#39;)-\u0026gt;nullable()-\u0026gt;after(\u0026#39;remember_token\u0026#39;); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::table(\u0026#39;users\u0026#39;, function (Blueprint $table) { $table-\u0026gt;dropColumn(\u0026#39;birth_date\u0026#39;); }); } } そして、migrationを実行してDBに項目を追加します。\n$ php artisan migrate Migrating: 2021_05_30_213829_add_new_fields_to_users_table Migrated: 2021_05_30_213829_add_new_fields_to_users_table (0.09 seconds) 次に、factoryで自動データ作成のために項目を追加します。\ndatabase/factories/UserFactory.php ... $factory-\u0026gt;define(User::class, function (Faker $faker) { return [ \u0026#39;name\u0026#39; =\u0026gt; $faker-\u0026gt;name(), \u0026#39;email\u0026#39; =\u0026gt; $faker-\u0026gt;unique()-\u0026gt;safeEmail(), \u0026#39;email_verified_at\u0026#39; =\u0026gt; now(), \u0026#39;password\u0026#39; =\u0026gt; Hash::make(\u0026#39;password\u0026#39;), \u0026#39;remember_token\u0026#39; =\u0026gt; Str::random(10), \u0026#39;birth_date\u0026#39; =\u0026gt; $faker-\u0026gt;date(), ]; }); tinkerでdate_birthを追加したusersのレコードを３つ作成します。\n\u0026gt;\u0026gt;\u0026gt; User::truncate() =\u0026gt; Illuminate\\Database\\Eloquent\\Builder {#3302} \u0026gt;\u0026gt;\u0026gt; factory(\\App\\User::class, 3)-\u0026gt;create() =\u0026gt; Illuminate\\Database\\Eloquent\\Collection {#4258 all: [ App\\User {#4254 name: \u0026#34;坂本 充\u0026#34;, email: \u0026#34;nishida@example.net\u0026#34;, email_verified_at: \u0026#34;2021-05-30 21:55:14\u0026#34;, #password: \u0026#34;$2y$10$snEjTmJvBDu3uxd1bz0QDupihJA3zjvEH5J0pm/aLHG4UvpSfJe/q\u0026#34;, #remember_token: \u0026#34;j8hPjp8ob7\u0026#34;, birth_date: \u0026#34;1997-03-02\u0026#34;, updated_at: \u0026#34;2021-05-30 21:55:14\u0026#34;, created_at: \u0026#34;2021-05-30 21:55:14\u0026#34;, id: 1, }, App\\User {#4252 name: \u0026#34;藤本 和也\u0026#34;, email: \u0026#34;yoshida.kana@example.com\u0026#34;, email_verified_at: \u0026#34;2021-05-30 21:55:14\u0026#34;, #password: \u0026#34;$2y$10$QiozldxVUo3LLj347mqHxeoDh5TEcjddDwc6vN8Pv5b4kPgPUgjri\u0026#34;, #remember_token: \u0026#34;frW94QFFZh\u0026#34;, birth_date: \u0026#34;2005-10-03\u0026#34;, updated_at: \u0026#34;2021-05-30 21:55:14\u0026#34;, created_at: \u0026#34;2021-05-30 21:55:14\u0026#34;, id: 2, }, App\\User {#4251 name: \u0026#34;近藤 知実\u0026#34;, email: \u0026#34;atsushi.ito@example.net\u0026#34;, email_verified_at: \u0026#34;2021-05-30 21:55:14\u0026#34;, #password: \u0026#34;$2y$10$A7xldQvOiCfUOyNS57EEYu2/s77a70BPmGoOjzDSgTmveeRlcAD8.\u0026#34;, #remember_token: \u0026#34;qOFWQFb5ER\u0026#34;, birth_date: \u0026#34;1972-09-14\u0026#34;, updated_at: \u0026#34;2021-05-30 21:55:14\u0026#34;, created_at: \u0026#34;2021-05-30 21:55:14\u0026#34;, id: 3, }, ], } Collectionから１つの値を返すメソッド DBレコードが揃ったところで、CollectionのメソッドをEloquentで取得したCollectionに適用です。 今回は、１つの値だけを返す簡単なメソッドの紹介をします。\nまずは、Collectionのレコード数が必要なら以下のようにcount()をチェーンして返すことできます。\n\u0026gt;\u0026gt;\u0026gt; User::all()-\u0026gt;count() =\u0026gt; 3 \u0026gt;\u0026gt;\u0026gt; User::where(\u0026#39;id\u0026#39;, \u0026#39;\u0026gt;\u0026#39;, 1)-\u0026gt;get()-\u0026gt;count() =\u0026gt; 2 ここDB処理でも同じことできますけれど、それに関しては「Eloquentでカウントするときの注意」で説明しています。\n次の例ではmax()、min()はそれぞれ最大値と最小値を返してくれます。この手の集計関数は他にも、avg()、median()、reduce()、sum()もあります。\n\u0026gt;\u0026gt;\u0026gt; User::all()-\u0026gt;max(\u0026#39;birth_date\u0026#39;) =\u0026gt; \u0026#34;2005-10-03\u0026#34; \u0026gt;\u0026gt;\u0026gt; User::all()-\u0026gt;min(\u0026#39;birth_date\u0026#39;) =\u0026gt; \u0026#34;1972-09-14\u0026#34; 次の例はブーリアン値を返すメソッドです。\ncontains()は、含んでいるならブーリアン値のtrueを返します。\n\u0026gt;\u0026gt;\u0026gt; User::all()-\u0026gt;contains(function($item) { return $item-\u0026gt;birth_date === \u0026#39;2005-10-03\u0026#39;; }) =\u0026gt; true また、EloquentのモデルのUserのプライマリキーを引数として渡すこともできます。これは通常のCollectionには適用不可でEloquentのCollectionのみに使用できるメソッドです。\n\u0026gt;\u0026gt;\u0026gt; User::all()-\u0026gt;contains(3) =\u0026gt; true \u0026gt;\u0026gt;\u0026gt; User::all()-\u0026gt;contains(4) =\u0026gt; false ブーリアン値を返すものには以下の空判定のメソッドもありますね。\n\u0026gt;\u0026gt;\u0026gt; User::all()-\u0026gt;isEmpty() =\u0026gt; false \u0026gt;\u0026gt;\u0026gt; User::all()-\u0026gt;isNotEmpty() =\u0026gt; true 次の例はオブジェクトを返すメソッドです。\n以下のように、最初と最後のレコードのオブジェクトを返すことが可能です。\n\u0026gt;\u0026gt;\u0026gt; User::all()-\u0026gt;first() =\u0026gt; App\\User {#4254 id: 1, name: \u0026#34;坂本 充\u0026#34;, email: \u0026#34;nishida@example.net\u0026#34;, email_verified_at: \u0026#34;2021-05-30 21:55:14\u0026#34;, #password: \u0026#34;$2y$10$snEjTmJvBDu3uxd1bz0QDupihJA3zjvEH5J0pm/aLHG4UvpSfJe/q\u0026#34;, #remember_token: \u0026#34;j8hPjp8ob7\u0026#34;, birth_date: \u0026#34;1997-03-02\u0026#34;, created_at: \u0026#34;2021-05-30 21:55:14\u0026#34;, updated_at: \u0026#34;2021-05-30 21:55:14\u0026#34;, } \u0026gt;\u0026gt;\u0026gt; User::all()-\u0026gt;last() =\u0026gt; App\\User {#4260 id: 3, name: \u0026#34;近藤 知実\u0026#34;, email: \u0026#34;atsushi.ito@example.net\u0026#34;, email_verified_at: \u0026#34;2021-05-30 21:55:14\u0026#34;, #password: \u0026#34;$2y$10$A7xldQvOiCfUOyNS57EEYu2/s77a70BPmGoOjzDSgTmveeRlcAD8.\u0026#34;, #remember_token: \u0026#34;qOFWQFb5ER\u0026#34;, birth_date: \u0026#34;1972-09-14\u0026#34;, created_at: \u0026#34;2021-05-30 21:55:14\u0026#34;, updated_at: \u0026#34;2021-05-30 21:55:14\u0026#34;, } 次回は、Collectionを返すメソッドを紹介します。\n","date":"2021-05-31T07:54:21+09:00","permalink":"https://www.larajapan.com/2021/05/31/laravel-collection%EF%BC%88%EF%BC%91%EF%BC%89%EF%BC%91%E3%81%A4%E3%81%AE%E5%80%A4%E3%82%92%E8%BF%94%E3%81%99%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89/","title":"Laravel Collection（１）１つの値を返すメソッド"},{"content":"毎日走らせているcronのジョブの１つが、なかなか時間が掛かるのでどうにか改善できないかと悩んでいました。DBへデータを挿入する箇所で時間が掛かっており、コードを確認するとforループで１レコードずつinsert処理を行っていました。bulk insertするように改修したところ、劇的に処理時間が短くなりました。今回はそんな妙薬、bulk insertについてです。\nBulk insertとは？ bulk insertとはDBにレコードを保存する際に、複数のレコードを１クエリでまとめて挿入する方法です。１レコードずつクエリを発行するよりも効率的で高速です。 Laravelでは以下のようにクエリビルダのinsertメソッドを使い実装します。\nDB::table(\u0026#39;users\u0026#39;)-\u0026gt;insert([ [\u0026#39;name\u0026#39; =\u0026gt; \u0026#39;John\u0026#39;, \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;john@example.com\u0026#39;, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;testtest\u0026#39;], [\u0026#39;name\u0026#39; =\u0026gt; \u0026#39;Ben\u0026#39;, \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;ben@example.com\u0026#39;, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;testtest\u0026#39;], [\u0026#39;name\u0026#39; =\u0026gt; \u0026#39;Tyler\u0026#39;, \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;tyler@example.com\u0026#39;, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;testtest\u0026#39;], ]); １レコードずつ挿入した場合とbulk insertを使った場合でどれ位処理時間が異なるのでしょうか？seederでusersテーブルに10万件のダミーレコードを登録するケースで比較してみます。\n１レコードずつinsertする場合 ※Laravelインストール後、DB接続設定やmigration実行完了時点の状態から進めています。 まずはseederを用意します。以下のコマンドでUsersSeederを作成してください。\nphp artisan make:seeder UsersSeeder 作成されたUsersSeederクラスを以下の様に編集します。\ndatabase/seeds/UsersSeeder.php use App\\User; use Faker\\Factory as Faker; use Illuminate\\Database\\Seeder; use Illuminate\\Support\\Facades\\Hash; class UsersSeeder extends Seeder { /** * Run the database seeds. * * @return void */ public function run() { $faker = Faker::create(\u0026#39;ja_JP\u0026#39;); for ($i=0; $i \u0026lt; 100000; $i++) { $param = [ \u0026#39;name\u0026#39; =\u0026gt; $faker-\u0026gt;name(), \u0026#39;email\u0026#39; =\u0026gt; \u0026#34;test{$i}@example.com\u0026#34;, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;testtest\u0026#39;, ]; User::create($param); } } } 上記のコードではforループ内でcreateメソッドを呼んでおり、10万回ループ毎にDBにレコードが挿入されます。\nemailにはループカウンターの$iが使用されています、これはusersテーブルに挿入する際にemailがユニークである必要があるためです。fakerのuniqueメソッドを使えばユニークなemailを生成することが出来ますが、10万件となると重複が発生し失敗してしまいます。\nseederを追加したら以下のコマンドでcomposerのオートローダを再生成し、UsersSeederが読み込まれるようにしましょう。\n$ composer dump-autoload DatabaseSeeder.php も編集し、db:seedでUsersSeederを呼び出すようにします。コマンド実行毎にusersテーブルをtruncateメソッドで初期化し、挿入するemailが重複しないようにしています。\ndatabase/seeds/DatabaseSeeder.php use Illuminate\\Database\\Seeder; use Illuminate\\Support\\Facades\\DB; class DatabaseSeeder extends Seeder { /** * Seed the application\u0026#39;s database. * * @return void */ public function run() { DB::table(\u0026#39;users\u0026#39;)-\u0026gt;truncate(); $this-\u0026gt;call(UsersSeeder::class); } } これで準備が出来ました、db:seedコマンドを実行してみましょう。\n$ php artisan db:seed Seeding: UsersSeeder Seeded: UsersSeeder (157.48 seconds) Database seeding completed successfully. 上記はM1 MacBookAirでの実行で、約２分半掛かりました。また、Windows + VirtualBoxの仮想環境では約２０分掛かっていました。記事の内容とは関係無いですが、Appleシリコン速いですね！\nbulk insertの場合 次に、UsersSeederを編集してbulk insertにしてみましょう。bulk insertにするには一度、挿入するデータを連想配列で用意し、insertメソッドに渡します。 database/seeds/UsersSeeder.php ... public function run() { $faker = Faker::create(\u0026#39;ja_JP\u0026#39;); $params = []; for ($i=0; $i \u0026lt; 100000; $i++) { $params[] = [ \u0026#39;name\u0026#39; =\u0026gt; $faker-\u0026gt;name(), \u0026#39;email\u0026#39; =\u0026gt; \u0026#34;test{$i}@example.com\u0026#34;, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;testtest\u0026#39;, ]; } User::insert($params); } db:seedを実行してみましょう。\n$ php artisan db:seed Seeding: UsersSeeder PHP Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 4194312 bytes) in ... 流石に10万件のデータを一つの変数に格納するとメモリの使用上限に達し、エラーが発生してしまいました。（memory_limitは初期設定の128MBです）\nini_set()でmemory_limitを上げても良いですが、一体どれだけメモリを食うのか分かりません。1000件毎にinsertメソッドを実行、$paramsを初期化し、メモリ使用量を抑える事にしました。以下が修正を加えたコードです。\ndatabase/seeds/UsersSeeder.php ... public function run() { $faker = Faker::create(\u0026#39;ja_JP\u0026#39;); $params = []; for ($i=0; $i \u0026lt; 100000; $i++) { $params[] = [ \u0026#39;name\u0026#39; =\u0026gt; $faker-\u0026gt;name(), \u0026#39;email\u0026#39; =\u0026gt; \u0026#34;test{$i}@example.com\u0026#34;, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;testtest\u0026#39;, ]; if (count($params) \u0026gt;= 1000) { User::insert($params); $params = []; } } } 蛇足ですが、こちらの記事、High-speed inserts with MySQLにて\nIt takes around 1,000 inserts per query to reach the maximum throughput と言及されているように、bulk insertの場合は１クエリ辺り1000件のインサートが最も効率が良いらしいです。 再度、db:seedを実行してみましょう。\n$ php artisan db:seed Seeding: UsersSeeder Seeded: UsersSeeder (5.8 seconds) Database seeding completed successfully. 約２分半掛かっていたのが、約６秒です。約２０分掛かっていた Windows + VirtualBox の環境でも約６秒でした。すごい差です！\ncreated_at, updated_atは自動入力されない 一点注意が必要なのは、bulk insertではModelクラスにて$timestampsをtrueに設定していても、created_atやupdated_atは自動入力されません。なぜならinsertメソッドはEloquent側ではなく、クエリビルダ側のメソッドだからです。したがって、insertするパラメタに明示的に含める必要があります。 まとめ 大量のレコードを挿入する場合、１レコードずつではなく、bulk insertで一括挿入することでとても速く処理できます。これは逆にDBへのクエリ発行がプログラムにおいて如何に重い処理か、という事を示しています。普段コードを書く際にも無駄なクエリを発行していないか気をつけねば、と思った次第です。","date":"2021-05-03T01:55:23+09:00","permalink":"https://www.larajapan.com/2021/05/03/bulk-insert%E3%81%A7%E5%A4%A7%E9%87%8F%E3%81%AE%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92db%E3%81%AB%E7%99%BB%E9%8C%B2%E3%81%99%E3%82%8B/","title":"bulk insertで大量のデータをDBに登録する"},{"content":"Artisanコマンドはコマンドラインで手動で実行するだけでなく、クロンジョブでの実行でも使用します。時間を設定して自動的に実行されるゆえに、実際に実行されたのか、どれだけ時間かかったのか、いくつのレコードを処理したのかなどをログに記録しておきたいです。そして、わかりやすいようにコマンドごとに違うログファイルを作成したいです。となると、config/logging.phpでそれぞれのコマンドのチャンネルを作成することになるのでしょうか？　つまり５０個コマンドあれば５０個のチャンネルの作成です。それは面倒だな、ということで良いアイデアを思いつきました。\n要は、コマンドごとにログファイル名を変えれば良いことなので、前回の月次ごとのログの作成をベースとして変更します。\nまず、config/logging.phpに新しいチャンネルcliを追加します。\nconfig/logging.php ... \u0026#39;channels\u0026#39; =\u0026gt; [ ... \u0026#39;monthly\u0026#39; =\u0026gt; [ \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;daily\u0026#39;, // use RotatingHandler \u0026#39;tap\u0026#39; =\u0026gt; [App\\Logging\\MonthlyFileNameForRotatingHandler::class], \u0026#39;path\u0026#39; =\u0026gt; storage_path(\u0026#39;logs/laravel.log\u0026#39;), \u0026#39;level\u0026#39; =\u0026gt; \u0026#39;info\u0026#39;, \u0026#39;permission\u0026#39; =\u0026gt; 0666, \u0026#39;days\u0026#39; =\u0026gt; 12, // １２ヶ月でローテーション ], \u0026#39;cli\u0026#39; =\u0026gt; [ \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;daily\u0026#39;, // use RotatingHandler \u0026#39;tap\u0026#39; =\u0026gt; [App\\Logging\\CommandFileNameForRotatingHandler::class], \u0026#39;path\u0026#39; =\u0026gt; storage_path(\u0026#39;logs/cli.log\u0026#39;), \u0026#39;level\u0026#39; =\u0026gt; \u0026#39;info\u0026#39;, \u0026#39;permission\u0026#39; =\u0026gt; 0666, \u0026#39;days\u0026#39; =\u0026gt; 12, // １２ヶ月でローテーション ], ... 前回のmonthlyのチャンネルの設定と比較すると、変わっているのはtapで指定したクラス名とpathのログファイル名だけです。\nそして、以下の新規のクラスの定義は、これまた前回のMonthlyFileNameForRotatingHandlerクラスとほとんど変わりません。\napp/Logging/CommandFileNameForRotatingHandler.php namespace App\\Logging; class CommandFileNameForRotatingHandler { /** * Customize the given logger instance. * * @param \\Illuminate\\Log\\Logger $logger * @return void */ public function __invoke($logger) { foreach ($logger-\u0026gt;getHandlers() as $handler) { $name = $_SERVER[\u0026#39;argv\u0026#39;][1]; //実行されるArtisanのコマンド名を取得して、以下でそれを使用 $handler-\u0026gt;setFilenameFormat(\u0026#34;{filename}-$name-{date}\u0026#34;, \u0026#39;Y-m\u0026#39;); } } } 変更した部分は、ファイル名の作成の部分だけです。 そして、この設定により以下のようなコマンドラインを実行するなら、\n$ php artisan command:log 作成されるログファイル名は、cli-command:log-2021-04.logのようになります。\n違うコマンドを実行するなら、\n$ php artisan task:something ログファイル名は、cli-task:something-2021-04.logとなります。\nもちろんArtisanのコマンドでは以下の例のように、Logの関数のコールが必要です。\napp/Console/Commands/LogCommand.php namespace App\\Console\\Commands; use Illuminate\\Console\\Command; use Illuminate\\Support\\Facades\\Log; class LogCommand extends Command { /** * The name and signature of the console command. * * @var string */ protected $signature = \u0026#39;command:log {action} {--option=}\u0026#39;; /** * The console command description. * * @var string */ protected $description = \u0026#39;Command description\u0026#39;; /** * Create a new command instance. * * @return void */ public function __construct() { parent::__construct(); } /** * Execute the console command. * * @return int */ public function handle() { Log::channel(\u0026#39;cli\u0026#39;)-\u0026gt;info(\u0026#39;start\u0026#39;, $_SERVER[\u0026#39;argv\u0026#39;]); // ここでcliのチャンネルを使用してコール return 0; } } 上のコマンドを実行すると、cli-command:log-2021-04.logの中身は、以下のようになります。 info()の２番目の引数には配列を渡すことが可能です。ここではコマンドラインの引数を記録します。\n$ php artisan command:log run --option=x $ cat storage/logs/cli-command:log-2021-04.log [2021-04-24 23:45:46] local.INFO: start [\u0026#34;artisan\u0026#34;,\u0026#34;command:log\u0026#34;,\u0026#34;run\u0026#34;,\u0026#34;--option=x\u0026#34;] → ログに関しての他の記事 ","date":"2021-04-26T00:00:32+09:00","permalink":"https://www.larajapan.com/2021/04/26/laravel%E3%81%A7%E3%83%AD%E3%82%B0%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AE%E4%BD%9C%E6%88%90%EF%BC%88%EF%BC%93%EF%BC%89%E3%82%B3%E3%83%9E%E3%83%B3%E3%83%89%E3%83%AD%E3%82%B0/","title":"Laravelでログファイルの作成（３）コマンドログファイル自動ネーミング"},{"content":"デフォルトのインストールでは、laravel.logは日次ではローテーションされません。つまり一生同じファイルにエラーなどが記録され続けます。それが嫌ならどうしましょう？\n日次でローテーション これは次のファイルの変更で簡単に設定可能です。\nconfig/logging.php ... \u0026#39;channels\u0026#39; =\u0026gt; [ \u0026#39;stack\u0026#39; =\u0026gt; [ \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;stack\u0026#39;, \u0026#39;channels\u0026#39; =\u0026gt; [\u0026#39;daily\u0026#39;], // これをsingleからdailyに変換 \u0026#39;ignore_exceptions\u0026#39; =\u0026gt; false, ], \u0026#39;single\u0026#39; =\u0026gt; [ \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;single\u0026#39;, \u0026#39;path\u0026#39; =\u0026gt; storage_path(\u0026#39;logs/laravel.log\u0026#39;), \u0026#39;level\u0026#39; =\u0026gt; \u0026#39;debug\u0026#39;, ], \u0026#39;daily\u0026#39; =\u0026gt; [ \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;daily\u0026#39;, \u0026#39;path\u0026#39; =\u0026gt; storage_path(\u0026#39;logs/laravel.log\u0026#39;), \u0026#39;level\u0026#39; =\u0026gt; \u0026#39;debug\u0026#39;, \u0026#39;days\u0026#39; =\u0026gt; 14, // 14日でローテーション ], ... こうすると、作成されるファイルは、laravel.logから、laravel-YYYY-MM-DD.logの形式に変わります。\n$ ls -l -rw-r--r-- 1 kenji kenji 51 Apr 9 14:51 laravel-2021-04-09.log -rw-r--r-- 1 kenji kenji 18525 Apr 3 13:14 laravel.log そして14日経過したら、それ以前に作成されたログファイルは削除されます。つまりファイルのローテーションが実行されます。\n月次でローテーション さて、日次のローテーションではあまりにも頻繁過ぎると思うなら、今度は月次に変更してみましょう。\nこれはちょっと準備が要ります。デフォルトやマニュアルには直接の例がないのでちょっと調査必要でしたが可能です。\nまず、以下のファイルを作成します。\napp/Logging/MonthlyFileNameForRotatingHandler.php namespace App\\Logging; class MonthlyFileNameForRotatingHandler { /** * Customize the given logger instance. * * @param \\Illuminate\\Log\\Logger $logger * @return void */ public function __invoke($logger) { foreach ($logger-\u0026gt;getHandlers() as $handler) { $handler-\u0026gt;setFilenameFormat(\u0026#39;{filename}-{date}\u0026#39;, \u0026#39;Y-m\u0026#39;); } } } それから、以下のように新規のチャンネルmonthlyの作成が必要です。\nconfig/logging.php ... \u0026#39;channels\u0026#39; =\u0026gt; [ \u0026#39;stack\u0026#39; =\u0026gt; [ \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;stack\u0026#39;, \u0026#39;channels\u0026#39; =\u0026gt; [\u0026#39;monthly\u0026#39;], // これをmonthlyに変換 \u0026#39;ignore_exceptions\u0026#39; =\u0026gt; false, ], ... \u0026#39;monthly\u0026#39; =\u0026gt; [ \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;daily\u0026#39;, // use RotatingHandler \u0026#39;tap\u0026#39; =\u0026gt; [App\\Logging\\MonthlyFileNameForRotatingHandler::class], \u0026#39;path\u0026#39; =\u0026gt; storage_path(\u0026#39;logs/laravel.log\u0026#39;), \u0026#39;level\u0026#39; =\u0026gt; \u0026#39;info\u0026#39;, \u0026#39;permission\u0026#39; =\u0026gt; 0666, \u0026#39;days\u0026#39; =\u0026gt; 12, // １２ヶ月でローテーション ], ... この設定をすると、今度はファイル名はlaravel-YYYY-MM.logの形式に変わります。以下のように。\n$ ls -l -rw-r--r-- 1 kenji kenji 143 Apr 9 14:55 laravel-2021-04-09.log -rw-rw-rw- 1 kenji kenji 45 Apr 9 15:12 laravel-2021-04.log 月次なのに\u0026lsquo;days\u0026rsquo; =\u0026gt; 12とちょっと設定が変ですが、ローテーションの仕組みはそこで指定した数のファイルをキープして他のファイルは削除するというものなので、この場合は12日ではなく12ヶ月となります。ちなみに、どのファイルが削除されるかはファイルの日付ではなくファイル名順で決まるのでローテーションのテストをするならダミーのファイルを作成してテスト可能です。\n最後に、ファイルのローテーションが要らないなら、そこには0と指定してください。永遠に作成された月次のファイルが残り続けます。\n→ ログに関しての他の記事 ","date":"2021-04-19T00:00:25+09:00","permalink":"https://www.larajapan.com/2021/04/19/laravel%E3%81%A7log%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AE%E4%BD%9C%E6%88%90%EF%BC%88%EF%BC%92%EF%BC%89%E3%83%AD%E3%82%B0%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%92%E3%83%AD%E3%83%BC%E3%83%86/","title":"LaravelでLogファイルの作成（２）ログファイルをローテーション"},{"content":"そうだ、そうだ、Laravel Excelを使ってみよう（３）日付のフォーマットの続きです。\nファイルの保存先を指定する デフォルトの保存先 前回までエクスポートしたファイルは storage/app 配下に出力されました。この出力先はconfig/filesystems.phpのdefaultに指定されたdiskに基づいています。初期設定ではdefaultにlocalが指定されており、localの設定を確認するとrootにstorage/appが指定されていますね。 ... \u0026#39;default\u0026#39; =\u0026gt; env(\u0026#39;FILESYSTEM_DRIVER\u0026#39;, \u0026#39;local\u0026#39;), ... \u0026#39;disks\u0026#39; =\u0026gt; [ \u0026#39;local\u0026#39; =\u0026gt; [ \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;local\u0026#39;, \u0026#39;root\u0026#39; =\u0026gt; storage_path(\u0026#39;app\u0026#39;), ], ... デフォルトの保存先がどこの設定を参照しているのか確認できたところで本題、エクスポートの保存先を変更してみます。\n相対パスで指定する もし、storage/app/public 配下にエクスポートしたい場合は以下のように、storeメソッドの第２引数に相対パスを指定することで変更できます。 Psy Shell v0.10.6 (PHP 7.2.34 — cli) by Justin Hileman \u0026gt;\u0026gt;\u0026gt; use App\\Exports\\PurchaseHistoryExport; \u0026gt;\u0026gt;\u0026gt; Excel::store(new PurchaseHistoryExport, \u0026#39;public/purchase_history.xlsx\u0026#39;); =\u0026gt; true storage/app 以外のディレクトリ、例えば storage/exports などに出力したい場合はどうでしょうか？ 試しに相対パスで指定してみます。\n\u0026gt;\u0026gt;\u0026gt; Excel::store(new PurchaseHistoryExport, \u0026#39;../exports/purchase_history.xlsx\u0026#39;); LogicException with message \u0026#39;Path is outside of the defined root, path: [../exports/purchase_history.xlsx]\u0026#39; エラーが出力されてしまいました。\n冒頭で触れた通り、config/filesystems.php のdefaultに指定されたdisk（初期設定ではlocal）に基づいています。よって、そのrootである storage/app 外のパスは指定できないみたいです。\nでは、どうするか？\nDiskで指定する diskの設定を追加しstoreメソッドに渡すことで、出力先のディレクトリを自由に変更できます。公式ドキュメントだとStoring exports on disk のCustom disks欄に説明が記載されてます。 まず、config/filesystems.php のdisksに設定を追加します。\nconfig/filesystems.php \u0026#39;disks\u0026#39; =\u0026gt; [ ... \u0026#39;exports\u0026#39; =\u0026gt; [ \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;local\u0026#39;, \u0026#39;root\u0026#39; =\u0026gt; storage_path(\u0026#39;exports\u0026#39;), // ←出力先のディレクトリパスを指定 ], ], そして、storeメソッドの第三引数に追加した設定のkeyを指定します。configに追加した設定を読み込ませるために、tinkerの再起動が必要ですのでご注意を！\nPsy Shell v0.10.6 (PHP 7.2.34 — cli) by Justin Hileman \u0026gt;\u0026gt;\u0026gt; use App\\Exports\\PurchaseHistoryExport; \u0026gt;\u0026gt;\u0026gt; Excel::store(new PurchaseHistoryExport, \u0026#39;purchase_history.xlsx\u0026#39;, \u0026#39;exports\u0026#39;); =\u0026gt; true storage/exports/purchase_history.xlsx にエクスポートされたはずです。\nまとめ 色々な分野でデータ活用が見直されている昨今、出力するデータも多岐に渡ると思います。とはいえ、出力先毎にdisk設定を追加していくとconfig/filesystems.phpが肥大化してしまい、管理しずらくなります。よって、disk設定と相対パス指定の両方をバランスよく組み合わせて運用していくのが良さそうです。 余談 当初、出力先を変更する方法について理解が進まず、ドキュメントとにらめっこしたり、ソースを追ってみたりしてました。最終的にGitHub上の過去のIssueから相対パスで指定できることを知り、diskの設定追加が必要なケースについても把握できました。このパッケージはGitHub上でのやり取りが盛んに行われているので行き詰まった時は是非そちらを参照してみて下さい。 GitHub: https://github.com/maatwebsite/Laravel-Excel\n","date":"2021-04-12T00:00:55+09:00","permalink":"https://www.larajapan.com/2021/04/12/%E3%81%9D%E3%81%86%E3%81%A0%E3%80%81laravel-excel%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E3%81%BF%E3%82%88%E3%81%86%EF%BC%88%EF%BC%94%EF%BC%89%E4%BF%9D%E5%AD%98%E5%85%88%E3%81%AE%E6%8C%87%E5%AE%9A/","title":"そうだ、Laravel Excelを使ってみよう（４）保存先の指定"},{"content":"ログファイルを作成するのは、デバッグのためや後のチェックに使われるために実行の記録を残すなど、いろいろな目的で使われます。Laravelでは、laravel.logというファイルがデフォルトでstorage/logsに作成されて、そこでいろいろな情報を記録するのに使われます。しかし、特定のプログラムの実行でlaravel.logではないファイルにログを残したいときはどうしましょう？\nLaravelではこれはいたって簡単。\nまず、config/logging.phpのファイルをエディターで開いて、以下のようにcustomのチャンネルを追加します。\nconfig/logging.php ... \u0026#39;channels\u0026#39; =\u0026gt; [ \u0026#39;stack\u0026#39; =\u0026gt; [ \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;stack\u0026#39;, \u0026#39;channels\u0026#39; =\u0026gt; [\u0026#39;single\u0026#39;], \u0026#39;ignore_exceptions\u0026#39; =\u0026gt; false, ], \u0026#39;single\u0026#39; =\u0026gt; [ \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;single\u0026#39;, \u0026#39;path\u0026#39; =\u0026gt; storage_path(\u0026#39;logs/laravel.log\u0026#39;), \u0026#39;level\u0026#39; =\u0026gt; \u0026#39;debug\u0026#39;, ], ... \u0026#39;custom\u0026#39; =\u0026gt; [ \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;single\u0026#39;, \u0026#39;path\u0026#39; =\u0026gt; storage_path(\u0026#39;logs/custom.log\u0026#39;), \u0026#39;level\u0026#39; =\u0026gt; \u0026#39;info\u0026#39;, \u0026#39;permission\u0026#39; =\u0026gt; 0666, ], ], ]; そして、tinkerでログを書き込みます。\n\u0026gt;\u0026gt;\u0026gt; Log::channel(\u0026#39;custom\u0026#39;)-\u0026gt;info(\u0026#39;テストだよ\u0026#39;); =\u0026gt; null 以下のように、custom.logのファイルが作成されています。上の設定ではパーミッションがlaravel.logと異なることに注意してください。デフォルトではpermissionは0644ですが上の設定では0666として誰もが読み書き自由となっています。\n$ ls -l storage/logs -rw-rw-rw- 1 kenji kenji 52 Apr 3 14:17 custom.log -rw-r--r-- 1 kenji kenji 18525 Apr 3 13:14 laravel.log そしてファイルの中身は、\n$ cat storage/logs/custom.log [2021-04-03 21:17:05] local.INFO: テストだよ ちゃんと書き込まれていますね。\nもう１つテストです。今度は、info()でなくdebug()の関数を使います。\n\u0026gt;\u0026gt;\u0026gt; Log::channel(\u0026#39;custom\u0026#39;)-\u0026gt;debug(\u0026#39;デバッグだよ\u0026#39;) =\u0026gt; null ファイルの中身をチェックすると、\n$ cat storage/logs/custom.log [2021-04-03 21:17:05] local.INFO: テストだよ 書き込みがありません。これは、設定でlevelをinfoと設定したためです。infoの下のレベルであるdebugでは書き込みが起こらないのです。\nちなみに以下がレベルのランクです。\nLog::emergency($message); Log::alert($message); Log::critical($message); Log::error($message); Log::warning($message); Log::notice($message); Log::info($message); Log::debug($message); → ログに関しての他の記事 ","date":"2021-04-05T00:00:17+09:00","permalink":"https://www.larajapan.com/2021/04/05/laravel%E3%81%A7log%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AE%E4%BD%9C%E6%88%90%EF%BC%88%EF%BC%91%EF%BC%89laravel-log%E3%81%A7%E3%81%AA%E3%81%84%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AB/","title":"LaravelでLogファイルの作成（１）laravel.logでないファイルに記録"},{"content":"そうだ、そうだ、Laravel Excelを使ってみよう（２）ヘッダ, map()の続きです。\n日付のフォーマット 注文の多いマーケッターさんなら、日付の表示を「YYYY/MM/DD」に変更して欲しい、と言うかもしれません。やってみましょう。 前回エクスポートしたファイルを見てみると作成日欄は「標準」となっており「日付」ではありません。\nカラムの形式を「日付」に変えた上で「YYYY/MM/DD」で表示させます。\nカラムの形式を指定するにはWithColumnFormattingをimplementし、columnFormatsメソッドを追加します。\napp/Exports/PurchaseHistoryExport.php ... use Maatwebsite\\Excel\\Concerns\\WithColumnFormatting; use PhpOffice\\PhpSpreadsheet\\Style\\NumberFormat; class PurchaseHistoryExport implements FromCollection, WithHeadings, WithStrictNullComparison, WithMapping, WithColumnFormatting { ... /** * @return array */ public function columnFormats() :array { return [ \u0026#39;B\u0026#39; =\u0026gt; NumberFormat::FORMAT_DATE_YYYYMMDDSLASH, ]; } ... NumberFormatクラスに指定可能なフォーマットが定数で定義されているので適当なものを選びます。日付に関しては以下のフォーマットが定義されていました。今回は\u0026rsquo;YYYY/MM/DD\u0026rsquo;としたいので、FORMAT_DATE_YYYYMMDDSLASHを選択しました。\nvendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat.php const FORMAT_DATE_YYYYMMDD2 = \u0026#39;yyyy-mm-dd\u0026#39;; const FORMAT_DATE_YYYYMMDD = \u0026#39;yyyy-mm-dd\u0026#39;; const FORMAT_DATE_DDMMYYYY = \u0026#39;dd/mm/yyyy\u0026#39;; const FORMAT_DATE_DMYSLASH = \u0026#39;d/m/yy\u0026#39;; const FORMAT_DATE_DMYMINUS = \u0026#39;d-m-yy\u0026#39;; const FORMAT_DATE_DMMINUS = \u0026#39;d-m\u0026#39;; const FORMAT_DATE_MYMINUS = \u0026#39;m-yy\u0026#39;; const FORMAT_DATE_XLSX14 = \u0026#39;mm-dd-yy\u0026#39;; const FORMAT_DATE_XLSX15 = \u0026#39;d-mmm-yy\u0026#39;; const FORMAT_DATE_XLSX16 = \u0026#39;d-mmm\u0026#39;; const FORMAT_DATE_XLSX17 = \u0026#39;mmm-yy\u0026#39;; const FORMAT_DATE_XLSX22 = \u0026#39;m/d/yy h:mm\u0026#39;; const FORMAT_DATE_DATETIME = \u0026#39;d/m/yy h:mm\u0026#39;; const FORMAT_DATE_TIME1 = \u0026#39;h:mm AM/PM\u0026#39;; const FORMAT_DATE_TIME2 = \u0026#39;h:mm:ss AM/PM\u0026#39;; const FORMAT_DATE_TIME3 = \u0026#39;h:mm\u0026#39;; const FORMAT_DATE_TIME4 = \u0026#39;h:mm:ss\u0026#39;; const FORMAT_DATE_TIME5 = \u0026#39;mm:ss\u0026#39;; const FORMAT_DATE_TIME6 = \u0026#39;h:mm:ss\u0026#39;; const FORMAT_DATE_TIME7 = \u0026#39;i:s.S\u0026#39;; const FORMAT_DATE_TIME8 = \u0026#39;h:mm:ss;@\u0026#39;; const FORMAT_DATE_YYYYMMDDSLASH = \u0026#39;yyyy/mm/dd;@\u0026#39;; 一旦、tinkerからエクスポートしてみます。\nPsy Shell v0.10.6 (PHP 7.2.34 — cli) by Justin Hileman \u0026gt;\u0026gt;\u0026gt; use App\\Exports\\PurchaseHistoryExport; \u0026gt;\u0026gt;\u0026gt; Excel::store(new PurchaseHistoryExport, \u0026#39;purchase_history.xlsx\u0026#39;); =\u0026gt; true するとカラムの表示形式が「標準」から「日付」変わりました。\nしかし、セルの表示形式は「2021-03-15 09:42:24」のままです。どうすれば良いでしょうか？\nシリアル値に変換 公式ドキュメントを確認すると、日付に関してPhpSpreadsheetパッケージのDate::dateTimeToExcel()を利用するように推奨されています。このメソッドはPHPのDateTimeオブジェクトをExcelのシリアル値に変換するメソッドです。なるほど、日付を正しくフォーマットするためには一度シリアル値に変換してエクスポートする必要があるようです。 シリアル値はExcel上で日時を管理するデータ形式です。１９００年１月１日を１とみなし、そこから何日目か？によって日付を管理しています（つまり、Excel版のtimestampですね）。例えば、Excelでセルに数字の２を入力して表示形式を「標準」から「日付」に変更してみて下さい。すると、「1900/1/2」と表示されるはずです。１９００年１月１日から２日目だからです。尚、時刻に関しては小数点以下で表現しています。\n話を元に戻して、mapメソッドを以下の様に修正しました。\nPhpOffice\\PhpSpreadsheetパッケージからDateクラスをuseしています。このパッケージはLaravel Excelをインストールした際に一緒に入っています。（余談ですが、そもそもLaravel Excel自体がこのパッケージをLaravel用に使いやすくしたラッパーパッケージなのだそうです。）\napp/Exports/PurchaseHistoryExport.php ... use PhpOffice\\PhpSpreadsheet\\Shared\\Date; ... /** * @param PurchaseHistory $row * @return array */ public function map($row) :array { return [ $row-\u0026gt;id, Date::dateTimeToExcel($row-\u0026gt;created_at), // \u0026lt;-修正箇所 $row-\u0026gt;user_id, $row-\u0026gt;product_name, $row-\u0026gt;price * $row-\u0026gt;quantity, // 合計金額 ]; } ... 再度tinkerからエクスポートして確認すると、「作成日」欄が「YYYY/MM/DD」の形式になりました。表示上は年月日のみですが、内部的にシリアル値で管理されているのでセルを選択すると時刻まで確認できます。良さそうですね！\n","date":"2021-04-01T05:14:43+09:00","permalink":"https://www.larajapan.com/2021/04/01/%E3%81%9D%E3%81%86%E3%81%A0%E3%80%81laravel-excel%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E3%81%BF%E3%82%88%E3%81%86-%E3%81%9D%E3%81%AE%EF%BC%93/","title":"そうだ、Laravel Excelを使ってみよう（３）日付のフォーマット"},{"content":"前回の記事、そうだ、そうだ、Laravel Excelを使ってみよう（１）セットアップの続きです。\nヘッダ行を追加 前回出力したファイル storage/app/purchase_history.xlsx を見るとデータのみが出力されていて、各列が何を示すのか分かりません。ヘッダ行を追加して分かりやすくします。WithHeadingsインタフェースを追加し、以下のメソッドをExportクラスに追加します。WithHeadingsインタフェースの制約により、戻り値の型をarrayに指定する必要があるのでご注意を。 app/Exports/PurchaseHistoryExport.php ... use Maatwebsite\\Excel\\Concerns\\WithHeadings; class PurchaseHistoryExport implements FromCollection, WithHeadings { /** * @return array */ public function headings() :array { return [ \u0026#39;ID\u0026#39;, \u0026#39;作成日\u0026#39;, \u0026#39;更新日\u0026#39;, \u0026#39;user_id\u0026#39;, \u0026#39;商品名\u0026#39;, \u0026#39;単価\u0026#39;, \u0026#39;数量\u0026#39; ]; } ... 再度tinkerから出力するとヘッダ行が追加されました。\nこちら出力して気づいたのですが、「数量」列に空欄になっているセルがありますね。なんでしょう？tinkerで該当のレコードを確認してみます。\nPsy Shell v0.10.6 (PHP 7.2.34 — cli) by Justin Hileman \u0026gt;\u0026gt;\u0026gt; App\\PurchaseHistory::find(7); =\u0026gt; App\\PurchaseHistory {#4090 id: 7, created_at: \u0026#34;2021-03-15 09:45:05\u0026#34;, updated_at: \u0026#34;2021-03-15 09:45:05\u0026#34;, user_id: 42426926, product_name: \u0026#34;provident\u0026#34;, price: 5352, quantity: 0, } なるほど、quantity: 0になっている箇所が空欄になっているのですね。Laravel Excelではデフォルトでは0をエクスポートするとnull扱いされ空のセルとなります。0を0として出力したい場合はWithStrictNullComparisonインタフェースをimplementする必要があります。\napp/Exports/PurchaseHistoryExport.php ... use Maatwebsite\\Excel\\Concerns\\WithStrictNullComparison; class PurchaseHistoryExport implements FromCollection, WithHeadings, WithStrictNullComparison { ... 再度エクスポートすると空欄だったセルに０が挿入されています\nここまででお気づきかもしれませんが、Laravel Excelの実装は基本的にインタフェース（公式ドキュメントではConcernと呼ばれている）を追加していきます。必要なものを必要な時にだけ、最小限の拡張で実装できるのでコードをシンプルに保つことができそうです。\n行ごとの処理を追加 前項まではDBから取得したデータをそのまま出力していましたが、「更新日・単価・数量」は不要、代わりに「合計金額」を出力して欲しい、と依頼されたとします。行ごとの処理を追加して出力内容を修正してみましょう。（この程度であれば、collectionメソッド側でDBから取得する際のクエリを修正した方が効率的ですが、例の為に敢えて行ごとの処理として実装します。） エクスポートする項目が変わるのでヘッダ行の設定を先に修正します。\napp/Exports/PurchaseHistoryExport.php public function headings() :array { return [ \u0026#39;ID\u0026#39;, \u0026#39;作成日\u0026#39;, \u0026#39;user_id\u0026#39;, \u0026#39;商品名\u0026#39;, \u0026#39;合計金額\u0026#39;, ]; } 次に、行ごとの処理を追加します。行ごとの処理はWithMappingインタフェースをimplementし、mapメソッドを追加して実装です。合計金額は単価 x 数量としました。\napp/Exports/PurchaseHistoryExport.php ... use Maatwebsite\\Excel\\Concerns\\WithMapping; class PurchaseHistoryExport implements FromCollection, WithHeadings, WithStrictNullComparison, WithMapping { ... /** * @param PurchaseHistory $row * @return array */ public function map($row) :array { return [ $row-\u0026gt;id, $row-\u0026gt;created_at, $row-\u0026gt;user_id, $row-\u0026gt;product_name, $row-\u0026gt;price * $row-\u0026gt;quantity, // 合計金額 ]; } ... エクスポートは以下になりました。\n更に続きます。\n","date":"2021-03-29T00:00:48+09:00","permalink":"https://www.larajapan.com/2021/03/29/%E3%81%9D%E3%81%86%E3%81%A0%E3%80%81laravel-excel%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E3%81%BF%E3%82%88%E3%81%86-%E3%81%9D%E3%81%AE%EF%BC%92/","title":"そうだ、Laravel Excelを使ってみよう（２）ヘッダ, map()"},{"content":"集計データをcsvにエクスポートする処理をLaravel Excelを使って置き換える機会があったので備忘録としてまとめてみます。Laravel Excelとはcsvやxlsxファイルなどへのデータのエクスポート、インポートがお手軽に実現できるパッケージです。\n公式ページに色々利点が列挙されていますが、今回の導入で期待しているのは以下の３点です。\n1 データをxlsxファイルにエクスポート 2 chunk処理によるエクスポート処理のパフォーマンスアップ 3 整理整頓されたクラスセットによりコードの見通しがよくなる\nまずはインストールから。\n環境 macOS Big Sur バージョン 11.1 PHP 7.4.15 Laravel Framework 7.30.4 新規プロジェクトが作成されている状態 installation 公式ドキュメントに沿ってパッケージをインストールします。 予めRequirementsを確認し、PHPのextensionなど入っていないものが有れば必要に応じて入れて下さい。私の環境ではgdのみ入っていなかったので以下の手順で入れました。(phpbrewを使っています)\nbrew install gd phpbrew ext install gd それではLaravel Excelをインストールします。\ncomposer require maatwebsite/excel 2021/02/24時点では3.1.27が入りました。\n購入履歴をエクスポートしてみる どのECサイトでも顧客動向を把握するために、商品の購入履歴を分析すると思います。マーケの人からそれに必要なデータをExcelで開ける形式で下さい、と依頼されたとしましょう。そんな時、従来であればBOMや文字コードを気にかけつつ、fputcsvなどでcsvファイルに出力していました。Laravel Excelを使えば直接xlsxファイルで出力できるので、それらの煩わしさから解放されそうです。 例えば以下のようなテーブルからデータをエクスポートするとします。\nダミーデータの準備 まずはモデルを作成、migartionファイルと後ほどダミーデータを生成するのでfactoryもオプションで指定して作成しておきます。 php artisan make:model -fm PurchaseHistory migrationファイルは以下のようにしました。\npublic function up() { Schema::create(\u0026#39;purchase_histories\u0026#39;, function (Blueprint $table) { $table-\u0026gt;id(); $table-\u0026gt;timestamps(); $table-\u0026gt;integer(\u0026#39;user_id\u0026#39;)-\u0026gt;default(0); $table-\u0026gt;string(\u0026#39;product_name\u0026#39;)-\u0026gt;default(\u0026#39;\u0026#39;); $table-\u0026gt;integer(\u0026#39;price\u0026#39;)-\u0026gt;default(0); $table-\u0026gt;smallInteger(\u0026#39;quantity\u0026#39;)-\u0026gt;default(0); }); } migrateします。\nphp artisan migrate 続いてfactory側も準備します。\n$factory-\u0026gt;define(PurchaseHistory::class, function (Faker $faker) { return [ \u0026#39;user_id\u0026#39; =\u0026gt; $faker-\u0026gt;randomNumber(), \u0026#39;product_name\u0026#39; =\u0026gt; $faker-\u0026gt;word(), \u0026#39;price\u0026#39; =\u0026gt; $faker-\u0026gt;randomNumber(4), \u0026#39;quantity\u0026#39; =\u0026gt; $faker-\u0026gt;randomNumber(1), ]; }); tinkerから100レコードほどダミーデータを生成しておきます\nfactory(\u0026#39;App\\PurchaseHistory\u0026#39;, 100)-\u0026gt;create(); PurchaseHistory::count(); \u0026gt;\u0026gt; 100 これでダミーデータの準備完了です。\nExportクラスを作成 以下のコマンドでExportファイルを生成して下さい。 php artisan make:export PurchaseHistoryExport app\\Exports\\PurchaseHistoryExport.php が生成されるはずです。このクラスでヘッダや、ソースからのデータ取得、出力データの整形などを指定していきます。デフォルトでcollectionメソッドが定義されていますので以下のように編集します。\nnamespace App\\Exports; use App\\PurchaseHistory; use Maatwebsite\\Excel\\Concerns\\FromCollection; class PurchaseHistoryExport implements FromCollection { /** * @return \\Illuminate\\Support\\Collection */ public function collection() { return PurchaseHistory::all(); } } これでpurchase_historyテーブルから抽出したデータを全てエクスポートすることになります。試しにtinkerからエクスポートしてみます。\nuse App\\Exports\\PurchaseHistoryExport; Excel::store(new PurchaseHistoryExport, \u0026#39;purchase_history.xlsx\u0026#39;); 第一引数に先程編集したExportクラス、第2引数に出力するファイル名を指定しています。trueが返されればエクスポート完了です。出力先はデフォルトでstorage/app/purchase_history.xlsxになります お手軽ですね！因みにファイル名の拡張子を\u0026rsquo;xlsx\u0026rsquo;から\u0026rsquo;csv\u0026rsquo;にするとcsv形式でも出力してくれます。その他、対応している拡張子が公式ドキュメントの方に載っていますのでチェックしてみて下さい。\n長くなってしまったので、続きは次の記事にします。\n","date":"2021-03-25T03:08:05+09:00","permalink":"https://www.larajapan.com/2021/03/25/%E3%81%9D%E3%81%86%E3%81%A0%E3%80%81laravel-excel%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E3%81%BF%E3%82%88%E3%81%86-%E3%81%9D%E3%81%AE%EF%BC%91/","title":"そうだ、Laravel Excelを使ってみよう（１）セットアップ"},{"content":"Laravelはプログラム実行において読み込むファイルの数は非常に多いです。実行速度を最適化するにはいろいろな方法がありますが、一番簡単に実行できる方法があります。遠い昔にも紹介しましたが、ルートのキャッシュです。しかし、バージョン７への更新によりちょっと問題が発生です。\nルートのキャッシュ お客さんのプロジェクトでは、routes/web.phpやapi.phpで定義されているルート（routeのことです）は結構あります。現在445あります。毎回毎回これを読み込みLaravelが処理するにはシステムに負荷がかかります。Laravelでは以前から、それを最適化してキャッシュするコマンドがあります。\nまず、L7.xでプロジェクトを作成して、その時点で動作確認したところで、ルートをキャッシュしまするのですが、\nその前にちょっとした作業が必要です。なぜなら、そのキャッシュのコマンドはクロージャのルートでエラーとなってしまうからです。それはL6.xでも同じことです。\nということで、以下の２つのファイルにおいてコメントアウトが必要です。\nroutes/api.php ... // 以下コメントアウト // Route::middleware(\u0026#39;auth:api\u0026#39;)-\u0026gt;get(\u0026#39;/user\u0026#39;, function (Request $request) { // return $request-\u0026gt;user(); // }); routes/web.php ... // 以下コメントアウト // Route::get(\u0026#39;/\u0026#39;, function () { // return view(\u0026#39;welcome\u0026#39;); //}); Auth::routes(); Route::get(\u0026#39;/home\u0026#39;, \u0026#39;HomeController@index\u0026#39;)-\u0026gt;name(\u0026#39;home\u0026#39;); これでルートのキャッシュ化のコマンドを実行です。\n$ php artisan route:cache Route cache cleared! Routes cached successfully! 成功しましたね。そしてディレクトリには以下のように、\nbootstrap/cache ├── packages.php ├── routes-v7.php └── services.php キャッシュされたroutes-v7.phpのファイルが作成されています。L6.xではファイル名は、routes.phpでした。\n重複のルート名はL6.xではOKだったのに L7.xになってこのコマンドの実行において前バージョンから１つ変わったことあります。L6.xにおいては、ルート名の重複が許されていたのですが、L7.xではそれが許されません。比較してみましょう。\nまず、L6.xの環境で、以下のようなルートの設定をします。\nroutes/web.php // Route::get(\u0026#39;/\u0026#39;, function () { // return view(\u0026#39;welcome\u0026#39;); // }); // Auth::routes(); // 左の関数から以下を抽出 Route::get(\u0026#39;login\u0026#39;, \u0026#39;Auth\\LoginController@showLoginForm\u0026#39;)-\u0026gt;name(\u0026#39;login\u0026#39;); Route::post(\u0026#39;login\u0026#39;, \u0026#39;Auth\\LoginController@login\u0026#39;)-\u0026gt;name(\u0026#39;login\u0026#39;); // Route::get(\u0026#39;/home\u0026#39;, \u0026#39;HomeController@index\u0026#39;)-\u0026gt;name(\u0026#39;home\u0026#39;); Auth::routes()で設定されているルートから、loginのルート定義を取り出して、意図的にgetとpostに同じloginという名前となるように設定します。-\u0026gt;name(\u0026rsquo;login\u0026rsquo;)の部分です。\n$ php artisan route:list を実行すると、\n+--------+----------+-------+-------+---------------------------------------------------------+------------+ | Domain | Method | URI | Name | Action | Middleware | +--------+----------+-------+-------+---------------------------------------------------------+------------+ | | GET|HEAD | login | login | App\\Http\\Controllers\\Auth\\LoginController@showLoginForm | web,guest | | | POST | login | login | App\\Http\\Controllers\\Auth\\LoginController@login | web,guest | +--------+----------+-------+-------+---------------------------------------------------------+------------+ そして、キャッシュのコマンドの実行もOKです。\n$ php artisan route:cache Route cache cleared! Routes cached successfully! 重複のルート名でも、GETとPOSTと使用されるメソッドが違うので使用には問題がありませんでした。\n以上は、L6.xでの話ですが、L7.xでは。。。\nL7.xでの問題 L7.xにおいて、先と同じようにルートをweb.phpで設定して、\n$ php artisan route:list を実行すると、\n+--------+----------+-------+-------+---------------------------------------------------------+------------+ | Domain | Method | URI | Name | Action | Middleware | +--------+----------+-------+-------+---------------------------------------------------------+------------+ | | GET|HEAD | login | login | App\\Http\\Controllers\\Auth\\LoginController@showLoginForm | web | | | | | | | guest | | | POST | login | login | App\\Http\\Controllers\\Auth\\LoginController@login | web | | | | | | | guest | +--------+----------+-------+-------+---------------------------------------------------------+------------+ OKそうですね。そして、キャッシュのコマンドを実行すると、\nRoute cache cleared! LogicException Unable to prepare route [login] for serialization. Another route has already been assigned name [login]. ... と、「もうloginの名前は使われているよ」というエラーが表示されます。\nそうなら、重複とならないようにルートの名前を変えてみます。\n... Route::get(\u0026#39;login\u0026#39;, \u0026#39;Auth\\LoginController@showLoginForm\u0026#39;)-\u0026gt;name(\u0026#39;login\u0026#39;); Route::post(\u0026#39;login\u0026#39;, \u0026#39;Auth\\LoginController@login\u0026#39;)-\u0026gt;name(\u0026#39;login-post\u0026#39;); ... 今度はキャッシュのコマンドの実行はOKとなりました。\n$ php artisan route:cache Route cache cleared! Routes cached successfully! そして、今度はpostのルートの名前を外してみます。オリジナルの定義と同じです。\n... Route::get(\u0026#39;login\u0026#39;, \u0026#39;Auth\\LoginController@showLoginForm\u0026#39;)-\u0026gt;name(\u0026#39;login\u0026#39;); Route::post(\u0026#39;login\u0026#39;, \u0026#39;Auth\\LoginController@login\u0026#39;); ... これでも、キャッシュのコマンドの実行は成功となります。\nどうなっているのか？と以下を実行してみると、\n$ php aritsan route:list +--------+----------+-------+-----------------------------+---------------------------------------------------------+------------+ | Domain | Method | URI | Name | Action | Middleware | +--------+----------+-------+-----------------------------+---------------------------------------------------------+------------+ | | GET|HEAD | login | login | App\\Http\\Controllers\\Auth\\LoginController@showLoginForm | web | | | | | | | guest | | | POST | login | generated::Jhj05XtQrWTdgVlP | App\\Http\\Controllers\\Auth\\LoginController@login | web | | | | | | | guest | +--------+----------+-------+-----------------------------+---------------------------------------------------------+------------+ 名が無かったルートには、 generated::Jhj05XtQrWTdgVlPと自動で名前が付けられていました。便利ですね。\n最後に ルート名がそのキャッシュ化において重複を許さなくなったL7.xの更新は、既存のコードをブレイクするという報告がいくつか出ていました。もちろんこれにより私のプロジェクトでも変更がいくつか必要となりました。ここの部分更新時には注意要です。\nそれから、ルートのキャッシュでもうひとつ注意することは、ルートの定義ファイルを変更したら必ず、キャッシュコマンドを再実行する必要があります。\nときどきこれを忘れてどうして変更が反映していないのか考え込んでしまう経験が過去にありました。それゆえに私の開発環境ではキャッシュせずに、本環境のみにおいてキャッシュ化を実行するようにしています。\n","date":"2021-03-21T04:16:59+09:00","permalink":"https://www.larajapan.com/2021/03/21/laravel-7-x%E6%9B%B4%E6%96%B0%E3%81%A7%E7%99%BA%E8%A6%8B%E3%81%97%E3%81%9F%E3%81%93%E3%81%A8%EF%BC%883%EF%BC%89%E3%83%AB%E3%83%BC%E3%83%88%E3%81%AE%E3%82%AD%E3%83%A3%E3%83%83%E3%82%B7%E3%83%A5/","title":"Laravel 7.x更新で発見したこと（3）ルートのキャッシュ"},{"content":"これも前回と同様に、開発したプログラムに今まであったバグがLaravelの更新により表面化したという話です。\n余分なスラッシュ 例えば、このブログで以下のようなURLあります。\n/2021/02/16/laravel-7-xのインストール\nこのURLに含まれるスラッシュに余分にスラッシュをつけます。以下では.comの直後にです。\n//2021/02/16/laravel-7-xのインストール\n上のリンクをクリックしたらわかりますが、余分なスラッシュを削除して最初のようなURLにして希望したページを表示してくれます。これは、ブラウザが修正してくれるのではなくサーバーで使用しているワードプレスのプログラムが修正してくれます。\nLaravel 7.x更新で余分なスラッシュが禁止となる これに気づいたのは、お客さんのサイトでLaravelを6から7に更新した後です。お客さんが発行したメルマガに入っていた販売商品のページのリンクに余分なスラッシュが含まれていたのです。そのためにリンク先のページにアクセスしても、404の「ページが見つかりません」のエラーページです。さあ大変ということで原因わからずに、即座にLaravel6にダウングレードと、冷や汗ものでした。\nお客さんの話では以前は余計なスラッシュが入っていてもアクセスできたということです。さて、いったいどういことでしょう、ということで調査すると、ありました。\nhttps://github.com/laravel/framework/pull/32260\nこれによると、今までURLにおける余分なスラッシュの扱いにおいて問題がありました。routeをキャッシュしたときとしないときでの統一性にも問題があったそうです。しかしこのPRは、Laravel 7.xのバージョンにマージされています。\nということで、今後は余分なスラッシュがURLに入らないように注意する必要あります。\nこれからその点をしっかり注意するとして、問題は過去のメルマガに含まれるリンクです。過去だからアクセスをする人は少ないと思いますが昨日のメルマガとかからはアクセスしますよね。ということで、Laravel 7.xが対応しないURLを対応しなければなりません。そうでなければいつまでもLaravelの更新ができないです。\nということで、急いで解決方法を見つけました。\n解決方法 解決方法は意外にも、phpのプログラムの変更ではなく、public/.htaccessのapacheのウェブサーバーの設定ファイルの修正でした。\n以下は、URLのドメイン名の直後の余分なスラッシュを削除してくれます。\npublic/.htaccess \u0026lt;IfModule mod_rewrite.c\u0026gt; \u0026lt;IfModule mod_negotiation.c\u0026gt; Options -MultiViews -Indexes \u0026lt;/IfModule\u0026gt; RewriteEngine On # Handle Authorization Header RewriteCond %{HTTP:Authorization} . RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] # Redirect Trailing Slashes If Not A Folder... RewriteCond %{REQUEST_FILENAME} !-d RewriteCond %{REQUEST_URI} (.+)/$ RewriteRule ^ %1 [L,R=301] # 余分なスラッシュを削除 RewriteCond %{THE_REQUEST} \\s//+ RewriteRule (.*) /$1 [R=301,L] # Handle Front Controller... RewriteCond %{REQUEST_FILENAME} !/assets RewriteCond %{REQUEST_FILENAME} !/server-status RewriteCond %{REQUEST_FILENAME} !-d RewriteCond %{REQUEST_FILENAME} !-f RewriteRule ^ index.php [L] \u0026lt;/IfModule\u0026gt; {THE_REQUEST}は、例えば GET /////2021/02/16/laravel-7-xのインストール HTTP/1.1のような値が含まれます。GETの後の空白文字が\\sで//+が複数のスラッシュをマッチの条件とします。$1の変数には、余計なスラッシュを含まない2021/02/16/laravel-7-xのインストールの値となるので、/$1で１つのスラッシュに置き換えてリダイレクトします。\nこれで晴れてLaravel 7.xへの更新ができます！\n","date":"2021-03-14T02:46:25+09:00","permalink":"https://www.larajapan.com/2021/03/14/laravel-7-x%E6%9B%B4%E6%96%B0%E3%81%A7%E7%99%BA%E8%A6%8B%E3%81%97%E3%81%9F%E3%81%93%E3%81%A8%EF%BC%88%EF%BC%92%EF%BC%89url%E3%81%AE%E4%BD%99%E5%88%86%E3%81%AA%E3%82%B9%E3%83%A9%E3%83%83%E3%82%B7/","title":"Laravel 7.x更新で発見したこと（２）URLの余分なスラッシュ"},{"content":"Laravel6からLaravel7へのバージョン更新で起こった問題を深～く追及していったら、私のプログラムのバグの発見とともにLaravelフレームワークの内部の変更の背景を知ることになりました。\ngroupByを使ったパジネーションの問題 Laravel6.xからLaravel7.xに更新して、動作テストを行っていたらこのようなこと起こりました。\nまず、テストケースの作成です。ユーザーの一日の歩行数を記録するとして以下の構造を持つstepsのテーブルを作成します。\nuser_idは、usersのuser.idとして、date_loggedが対象の日付、そしてstepsが歩行数です。\nmysql\u0026gt; describe steps; +-------------+----------------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +-------------+----------------------+------+-----+---------+----------------+ | id | bigint(20) unsigned | NO | PRI | NULL | auto_increment | | user_id | int(10) unsigned | NO | | NULL | | | date_logged | date | NO | | NULL | | | steps | smallint(5) unsigned | NO | | 0 | | | created_at | timestamp | YES | | NULL | | | updated_at | timestamp | YES | | NULL | | +-------------+----------------------+------+-----+---------+----------------+ 6 rows in set (0.00 sec) factory()を利用してDBテーブルにデータを入れます。こんなデータです。\nmysql\u0026gt; select * from steps; +----+---------+-------------+-------+---------------------+---------------------+ | id | user_id | date_logged | steps | created_at | updated_at | +----+---------+-------------+-------+---------------------+---------------------+ | 1 | 4 | 2021-02-20 | 1119 | 2021-02-26 15:53:46 | 2021-02-26 15:53:46 | | 2 | 3 | 2021-02-22 | 3955 | 2021-02-26 15:53:46 | 2021-02-26 15:53:46 | | 3 | 4 | 2021-02-21 | 294 | 2021-02-26 15:53:46 | 2021-02-26 15:53:46 | | 4 | 2 | 2021-02-22 | 1473 | 2021-02-26 15:53:46 | 2021-02-26 15:53:46 | | 5 | 1 | 2021-02-22 | 2858 | 2021-02-26 15:53:46 | 2021-02-26 15:53:46 | | 6 | 2 | 2021-02-21 | 1517 | 2021-02-26 15:53:46 | 2021-02-26 15:53:46 | | 7 | 3 | 2021-02-21 | 2913 | 2021-02-26 15:53:46 | 2021-02-26 15:53:46 | | 8 | 2 | 2021-02-21 | 3653 | 2021-02-26 15:53:46 | 2021-02-26 15:53:46 | | 9 | 1 | 2021-02-24 | 309 | 2021-02-26 15:53:46 | 2021-02-26 15:53:46 | | 10 | 1 | 2021-02-24 | 2985 | 2021-02-26 15:53:46 | 2021-02-26 15:53:46 | +----+---------+-------------+-------+---------------------+---------------------+ 10 rows in set (0.01 sec) さて、Laravel6.xの環境でtinkerを実行して以下のクエリを実行します。日付でグループ化してそれぞれの日付の総歩数を表示したいわけです。しかも、画面で表示するのでパジネーションを使います。\n\u0026gt;\u0026gt;\u0026gt; use App\\Step; \u0026gt;\u0026gt;\u0026gt; $pager = Step::select(\u0026#39;date_logged\u0026#39;, DB::raw(\u0026#39;sum(steps) as steps\u0026#39;), \u0026#39;date_logged\u0026#39;) -\u0026gt;groupBy(\u0026#39;steps.date_logged\u0026#39;) -\u0026gt;paginate(2) =\u0026gt; Illuminate\\Pagination\\LengthAwarePaginator {#4152 +onEachSide: 3, } \u0026gt;\u0026gt;\u0026gt; $pager-\u0026gt;toArray(); =\u0026gt; [ \u0026#34;current_page\u0026#34; =\u0026gt; 1, \u0026#34;data\u0026#34; =\u0026gt; [ [ \u0026#34;date_logged\u0026#34; =\u0026gt; \u0026#34;2021-02-20\u0026#34;, \u0026#34;steps\u0026#34; =\u0026gt; \u0026#34;1119\u0026#34;, ], [ \u0026#34;date_logged\u0026#34; =\u0026gt; \u0026#34;2021-02-21\u0026#34;, \u0026#34;steps\u0026#34; =\u0026gt; \u0026#34;8377\u0026#34;, ], ], \u0026#34;first_page_url\u0026#34; =\u0026gt; \u0026#34;http://localhost?page=1\u0026#34;, \u0026#34;from\u0026#34; =\u0026gt; 1, \u0026#34;last_page\u0026#34; =\u0026gt; 2, \u0026#34;last_page_url\u0026#34; =\u0026gt; \u0026#34;http://localhost?page=2\u0026#34;, \u0026#34;next_page_url\u0026#34; =\u0026gt; \u0026#34;http://localhost?page=2\u0026#34;, \u0026#34;path\u0026#34; =\u0026gt; \u0026#34;http://localhost\u0026#34;, \u0026#34;per_page\u0026#34; =\u0026gt; 2, \u0026#34;prev_page_url\u0026#34; =\u0026gt; null, \u0026#34;to\u0026#34; =\u0026gt; 2, \u0026#34;total\u0026#34; =\u0026gt; 4, ] 実行は問題ないですね。全部（total）で合計４つのレコードがあり、１ページあたり２レコード表示で最初の２つレコードを抽出（data）です。\nさて、今度は同じクエリをLaravel 7.xで実行すると、\n\u0026gt;\u0026gt;\u0026gt; use App\\Step; \u0026gt;\u0026gt;\u0026gt; $pager = Step::select(\u0026#39;date_logged\u0026#39;, DB::raw(\u0026#39;sum(steps) as steps\u0026#39;), \u0026#39;date_logged\u0026#39;) -\u0026gt;groupBy(\u0026#39;steps.date_logged\u0026#39;) -\u0026gt;paginate(2) Illuminate\\Database\\QueryException with message \u0026#39;SQLSTATE[42S21]: Column already exists: 1060 Duplicate column name \u0026#39;date_logged\u0026#39; (SQL: select count(*) as aggregate from (select `date_logged`, sum(steps) as steps, `date_logged` from `steps` group by `steps`.`date_logged`) as `aggregate_table`)\u0026#39; とクエリがSQLの実行エラーになっていしまいます。\nエラーは初歩的なミスで、クエリが返す項目に重複の項目があるよ、という指摘です。そうですね、select()でdate_loggedが重複しています。\nしかし、Laravel 6.xでは問題ありませんでしたね。また、以下のpaginate()をget()に置き換えたクエリの実行はLaravel 7.xでも問題はありません。\n\u0026gt;\u0026gt;\u0026gt; Step::select(\u0026#39;date_logged\u0026#39;, DB::raw(\u0026#39;sum(steps) as steps\u0026#39;), \u0026#39;date_logged\u0026#39;) -\u0026gt;groupBy(\u0026#39;steps.date_logged\u0026#39;) -\u0026gt;get(); =\u0026gt; Illuminate\\Database\\Eloquent\\Collection {#3658 all: [ App\\Step {#4220 date_logged: \u0026#34;2021-02-20\u0026#34;, steps: \u0026#34;1119\u0026#34;, }, App\\Step {#4219 date_logged: \u0026#34;2021-02-21\u0026#34;, steps: \u0026#34;8377\u0026#34;, }, App\\Step {#4065 date_logged: \u0026#34;2021-02-22\u0026#34;, steps: \u0026#34;8286\u0026#34;, }, App\\Step {#3337 date_logged: \u0026#34;2021-02-24\u0026#34;, steps: \u0026#34;3294\u0026#34;, }, ], } エラーの原因は何でしょう？paginate()の関数のコードがLaravel7.xに変わった？\nLaravel 6.xに戻って、実行されたクエリを見てみると、\n\u0026gt;\u0026gt;\u0026gt; $pager = Step::select(\u0026#39;date_logged\u0026#39;, DB::raw(\u0026#39;sum(steps) as steps\u0026#39;), \u0026#39;date_logged\u0026#39;) -\u0026gt;groupBy(\u0026#39;steps.date_logged\u0026#39;) -\u0026gt;paginate(2) =\u0026gt; Illuminate\\Pagination\\LengthAwarePaginator {#3241 +onEachSide: 3, } \u0026gt;\u0026gt;\u0026gt; sql(); =\u0026gt; [ [ \u0026#34;query\u0026#34; =\u0026gt; \u0026#34;select count(*) as aggregate from `steps` group by `steps`.`date_logged`\u0026#34;, \u0026#34;bindings\u0026#34; =\u0026gt; [], \u0026#34;time\u0026#34; =\u0026gt; 1.37, ], [ \u0026#34;query\u0026#34; =\u0026gt; \u0026#34;select `date_logged`, sum(steps) as steps, `date_logged` from `steps` group by `steps`.`date_logged` limit 2 offset 0\u0026#34;, \u0026#34;bindings\u0026#34; =\u0026gt; [], \u0026#34;time\u0026#34; =\u0026gt; 0.82, ], ] ] Laravel 7.xで重複を修正して再度実行すると、\n\u0026gt;\u0026gt;\u0026gt; use App\\Step; \u0026gt;\u0026gt;\u0026gt; $pager = Step::select(\u0026#39;date_logged\u0026#39;, DB::raw(\u0026#39;sum(steps) as steps\u0026#39;)) -\u0026gt;groupBy(\u0026#39;steps.date_logged\u0026#39;) -\u0026gt;paginate(2) =\u0026gt; Illuminate\\Pagination\\LengthAwarePaginator {#4274 +onEachSide: 3, } \u0026gt;\u0026gt;\u0026gt; sql() =\u0026gt; [ [ \u0026#34;query\u0026#34; =\u0026gt; \u0026#34;select count(*) as aggregate from (select `date_logged`, sum(steps) as steps from `steps` group by `steps`.`date_logged`) as `aggregate_table`\u0026#34;, \u0026#34;bindings\u0026#34; =\u0026gt; [], \u0026#34;time\u0026#34; =\u0026gt; 164.63, ], [ \u0026#34;query\u0026#34; =\u0026gt; \u0026#34;select `date_logged`, sum(steps) as steps from `steps` group by `steps`.`date_logged` limit 2 offset 0\u0026#34;, \u0026#34;bindings\u0026#34; =\u0026gt; [], \u0026#34;time\u0026#34; =\u0026gt; 0.42, ], ] どちらを見てもわかるように、パジネーションを作成するには、指定したレコードを取得するクエリを実行するだけなく、対象となる総レコード数のクエリも実行されます。もちろんそうでないと、全部でのページ数がわかりません。\nしかし、そのレコード数計算のためのクエリを比較してみると、Laravel 7.xでは対象のクエリをサブクエリとしてSQL文のFromに与えています。そして、そこの部分で項目の重複を許さないためにエラーとなった次第です。なるほど！\nまた、Laravel 7.xが一発でレコード数の結果を返しますが、Laravel 6.xのレコード数計算のクエリは結果が複数のレコードで返されますので、さらにそれ自体をカウントしなければいけません。多分にプログラムでカウントしていたのかもしれませんが、効率は良くないですね。\n調べてみると まず、Laravel 6.xのマニュアルでは、この部分\nCurrently, pagination operations that use a groupBy statement cannot be executed efficiently by Laravel. If you need to use a groupBy with a paginated result set, it is recommended that you query the database and create a paginator manually. 現在groupBy文を使用したパジネーションの操作は、Laravelで効率よく実行できません。groupByを使用したパジネーションを使用する必要がある場合はデータベースクエリを実行し、その結果を元にパジネーターを自前で作成してください）\nとあります。\nこの記述はLaravel 7.xのマニュアルにもありますが、最新の7xのコードでは修正されているはずです（次を読むとわかります）。Laravel 8.xではこの記述はありません。\nもう少し調べてみると、github.comのissuesにまさにそのマージありました。\n[7.x] Run pagination count as subquery for group by and havings\nPaginating queries with groupBy or having statements is a long-standing issue in Laravel going back to the very beginning of the framework with literal dozens of raised issues: #1892, #2761, #3105, #4306, #6985, #7372, #9567, #10632, #14123, #16320, #17406, #22883, #28931\nThis solution was suggested @acasar years ago but I wrote it off at the time - but honestly I think it\u0026rsquo;s a lot better than what we have now so I\u0026rsquo;m bringing it up again for consideration.\ngroupByあるいはhavingを使用したパジネーションは、フレームワークの開発当初からたくさん問題として取り上げられたLaravelの長年の課題でした。\n今回の@acasarが何年も前に提示した解決方法は、当時私が取り上げなかったものですが、正直言って現在使用されているものより良いので、ここで再度取り入れることにします。\nなるほど、Laravelの作者でもこういうことがあるのですね。というかたくさんの人が改良のアイデア（あるいは不平）をいつも与えて続けているのは好まれている証拠です。\n","date":"2021-03-05T12:36:06+09:00","permalink":"https://www.larajapan.com/2021/03/05/laravel-7-x%E6%9B%B4%E6%96%B0%E3%81%A7%E7%99%BA%E8%A6%8B%E3%81%97%E3%81%9F%E3%81%93%E3%81%A8%EF%BC%88%EF%BC%91%EF%BC%89/","title":"Laravel 7.x更新で発見したこと（１）パジネーションのクエリ"},{"content":"最新のLaravelのバージョンは8.xなことはわかっています。私が新しいもの嫌いというわけでもありません。しかし、リリースされてからちょっと遅れて（１年近く経って）、Laravelのバージョン7.xのインストールの手順の紹介です。\nバージョン 残念ながらバージョン7.xはLTS（長期サポート）ではありません。以下に見ての通りに来月でサポートが切れてしまいます。前バージョンの6.xの方がLTSサポートなのでサポート終了はまだ先です。\n上の表を見て気づきましたか？　そうバージョン９から、今までの半年に１回のメジャーバージョンの更新が１年に１回となります。Laravelも成熟してきたというのが理由らしいです。これで少しは追いつけるかな。\n新規プロジェクトの作成 composerはすでにシステムにインストールされていると仮定して、以下のコマンドがl7xのディレクトリ名でLaravelをインストールします。\n$ composer create-project --prefer-dist laravel/laravel:^7.0 l7x ウェブサーバーなどが書き込みを行うディレクトリにはパーミッションが必要です。\n$ cd l7x $ chmod -R a+w storage bootstrap/cache 以下を実行して、\n$ php artisan serve ブラウザでhttp://localhost:8000にアクセスすると、ページを見ることができます。\n必要なら、この時点でパージョン管理をするのも良いアイデアです。\n$ cd l7x $ git init $ git add . $ git commit -am \u0026#34;init\u0026#34; ユーザー認証のパッケージ Laravelではユーザー認証の機能が簡単にインストールできます。\nまず、必要なパッケージのインストールから、\n$ composer require laravel/ui \u0026#34;^2.0\u0026#34; 次に、以下を実行して会員登録画面やログイン画面などを作成します。boostrapだけでなく、vueやreactの指定も可能です。\n$ php artisan ui bootstrap --auth これにより以下のファイルが追加あるいは編集されています。\nnew file: app/Http/Controllers/Auth/ConfirmPasswordController.php new file: app/Http/Controllers/Auth/ForgotPasswordController.php new file: app/Http/Controllers/Auth/LoginController.php new file: app/Http/Controllers/Auth/RegisterController.php new file: app/Http/Controllers/Auth/ResetPasswordController.php new file: app/Http/Controllers/Auth/VerificationController.php new file: app/Http/Controllers/HomeController.php modified: composer.json modified: composer.lock modified: package.json modified: resources/js/bootstrap.js new file: resources/sass/_variables.scss modified: resources/sass/app.scss new file: resources/views/auth/login.blade.php new file: resources/views/auth/passwords/confirm.blade.php new file: resources/views/auth/passwords/email.blade.php new file: resources/views/auth/passwords/reset.blade.php new file: resources/views/auth/register.blade.php new file: resources/views/auth/verify.blade.php new file: resources/views/home.blade.php new file: resources/views/layouts/app.blade.php modified: routes/web.php 今度は、javascriptやcssのトランスパイルのために、nodeのパッケージのインストールとwebpackの実行です。\n$ npm install \u0026amp;\u0026amp; npm run dev 次はDBの設定です。mysqlのコマンドを実行して、l7xのデータベースを作成します。\n$ mysql -u root -p mysql\u0026gt; create database l7x; .envファイルを次のように編集してDB情報を入れます。\nDB_DATABASE=l7x DB_USERNAME=DBユーザー名 DB_PASSWORD=DBパスワード\n以下のコマンドを実行してDBテーブルを作成します。\n$ php artisan migrate 以下のように、４つのテーブルが作成されます。\nmysql\u0026gt; show tables; +-----------------+ | Tables_in_l7x | +-----------------+ | failed_jobs | | migrations | | password_resets | | users | +-----------------+ 4 rows in set (0.00 sec) 再度、ブラウザでhttp://localhost:8000/registerにアクセスします。\nと入力して、「Register」ボタンをクリックすると、\n新規会員登録成功です。\n","date":"2021-02-16T06:12:05+09:00","permalink":"https://www.larajapan.com/2021/02/16/laravel-7-x%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB/","title":"Laravel 7.xのインストール"},{"content":"前回はMailableを使ってのメール送信の話でした。今回は、もう１つLaravelからメールを送信する方法としてNotificationの話です。\nNotificationを使用してメールを送信 Mailableを使ってメールを送信するには、 use App\\Mail\\SomeMailableClass; Mail::to(\u0026#34;test@example.com\u0026#34;)-\u0026gt;send(new SomeMailableClass); が典型的な送信の仕方です。\n一方、今回紹介するNotificationは、Notifiableのトレイトを使ったモデルクラスにメールを送信する機能を与えます。\n例えば、このようにUserのクラスにNotifiableを追加すると、\nnamespace App; use Illuminate\\Contracts\\Auth\\MustVerifyEmail; use Illuminate\\Foundation\\Auth\\User as Authenticatable; use Illuminate\\Notifications\\Notifiable; class User extends Authenticatable implements MustVerifyEmail { use Notifiable; .. } 以下のように、Userのモデルのインスタンスのメソッドとして、notify()でメールを送信できます。\nuse App\\User; use App\\Notifications\\Hello; $user = User::find(1); $user-\u0026gt;notify(new Hello); 上では、$userのオブジェクトの項目である、$user-\u0026gt;emailが自動的にメール送信の宛先となります。\nさて、送信するメールの内容の方は、Notificationのクラス（上ではHelloのクラス）の作成が必要です。\n$ php artisan make:notification Hello を実行して、作成されたファイルを以下のように編集します。\napp/Notifications/Hello.php namespace App\\Notifications; use Illuminate\\Bus\\Queueable; use Illuminate\\Contracts\\Queue\\ShouldQueue; use Illuminate\\Notifications\\Messages\\MailMessage; use Illuminate\\Notifications\\Notification; class Hello extends Notification { use Queueable; /** * Create a new notification instance. * * @return void */ public function __construct() { // } /** * Get the notification\u0026#39;s delivery channels. * * @param mixed $notifiable * @return array */ public function via($notifiable) { return [\u0026#39;mail\u0026#39;]; } /** * Get the mail representation of the notification. * * @param mixed $notifiable * @return \\Illuminate\\Notifications\\Messages\\MailMessage */ public function toMail($notifiable) { return (new MailMessage) -\u0026gt;subject(\u0026#39;こんにちは\u0026#39;) -\u0026gt;greeting(\u0026#39;こんにちは！\u0026#39;) -\u0026gt;line(\u0026#39;メールの内容\u0026#39;) -\u0026gt;action(\u0026#39;ホームページへ\u0026#39;, url(\u0026#39;/\u0026#39;)) -\u0026gt;line(\u0026#39;ありがとうございます\u0026#39;); } /** * Get the array representation of the notification. * * @param mixed $notifiable * @return array */ public function toArray($notifiable) { return [ // ]; } } さてtinkerで送信してみます。\n\u0026gt;\u0026gt;\u0026gt; $user = User::find(1); [!] Aliasing \u0026#39;User\u0026#39; to \u0026#39;App\\User\u0026#39; for this Tinker session. =\u0026gt; App\\User {#4072 id: 1, name: \u0026#34;test\u0026#34;, email: \u0026#34;test@example.com\u0026#34;, email_verified_at: null, created_at: \u0026#34;2020-12-30 23:14:29\u0026#34;, updated_at: \u0026#34;2020-12-30 23:14:29\u0026#34;, } \u0026gt;\u0026gt;\u0026gt; $user-\u0026gt;notify(new App\\Notifications\\Hello); =\u0026gt; null MailTrapで送信したメールを見ると、\nもうすでにHTMLのメールとなっています。\n以下のように、文言はすべてMailMessageのクラスのメソッドで簡単に与えるだけOKです。 上の例で、まだ英語として残っているRegards, Laravelの部分を変えたいなら、以下のように、salutationのメソッドに引数を渡します。\n... public function toMail($notifiable) { return (new MailMessage) -\u0026gt;subject(\u0026#39;こんにちは\u0026#39;) -\u0026gt;greeting(\u0026#39;こんにちは！\u0026#39;) -\u0026gt;line(\u0026#39;メールの内容\u0026#39;) -\u0026gt;action(\u0026#39;ホームページへ\u0026#39;, url(\u0026#39;/\u0026#39;)) -\u0026gt;line(\u0026#39;ありがとうございます\u0026#39;) -\u0026gt;salutation(\u0026#39;Larajapan\u0026#39;); } .. 送信メールをカスタマイズ もっとHTMLメールをカスタマイズしたいなら、以下を実行してデフォルトのブレードを自分のプロジェクトのディレクトリに出力します。 $ php artisan vendor:publish --tag=laravel-notifications Copied Directory [/vendor/laravel/framework/src/Illuminate/Notifications/resources/views] To [/resources/views/vendor/notifications] Publishing complete. 作成された、email.blade.phpは以下のようなマークダウンのブレードです。Laravelのコンポーネントとマークダウンのシンボルが混ざっていて少しわかりづらいですが、上で使用されたメソッド名がブレードでは変数名となっているのがわかります。line()に関してはボタンの表示を境に上と下で、$introLinesと$outroLinesに分かれます。\nresources/views/vendor/notifications/email.blade.php @component(\u0026#39;mail::message\u0026#39;) {{-- Greeting --}} @if (! empty($greeting)) # {{ $greeting }} @else @if ($level === \u0026#39;error\u0026#39;) # @lang(\u0026#39;Whoops!\u0026#39;) @else # @lang(\u0026#39;Hello!\u0026#39;) @endif @endif {{-- Intro Lines --}} @foreach ($introLines as $line) {{ $line }} @endforeach {{-- Action Button --}} @isset($actionText) \u0026lt;?php switch ($level) { case \u0026#39;success\u0026#39;: case \u0026#39;error\u0026#39;: $color = $level; break; default: $color = \u0026#39;primary\u0026#39;; } ?\u0026gt; @component(\u0026#39;mail::button\u0026#39;, [\u0026#39;url\u0026#39; =\u0026gt; $actionUrl, \u0026#39;color\u0026#39; =\u0026gt; $color]) {{ $actionText }} @endcomponent @endisset {{-- Outro Lines --}} @foreach ($outroLines as $line) {{ $line }} @endforeach {{-- Salutation --}} @if (! empty($salutation)) {{ $salutation }} @else @lang(\u0026#39;Regards\u0026#39;),\u0026lt;br\u0026gt; {{ config(\u0026#39;app.name\u0026#39;) }} @endif {{-- Subcopy --}} @isset($actionText) @slot(\u0026#39;subcopy\u0026#39;) @lang( \u0026#34;If you’re having trouble clicking the \\\u0026#34;:actionText\\\u0026#34; button, copy and paste the URL below\\n\u0026#34;. \u0026#39;into your web browser:\u0026#39;, [ \u0026#39;actionText\u0026#39; =\u0026gt; $actionText, ] ) \u0026lt;span class=\u0026#34;break-all\u0026#34;\u0026gt;[{{ $displayableActionUrl }}]({{ $actionUrl }})\u0026lt;/span\u0026gt; @endslot @endisset @endcomponent もちろん、前回のように独自のマークダウンのブレードを作成して利用することも可能です。その場合は以下のように、markdown()メソッドにブレード名を入れます。\napp/Notifications/Hello.php ... /** * Get the mail representation of the notification. * * @param mixed $notifiable * @return \\Illuminate\\Notifications\\Messages\\MailMessage */ public function toMail($notifiable) { return (new MailMessage) -\u0026gt;subject(\u0026#39;こんにちは！マークダウン\u0026#39;) -\u0026gt;markdown(\u0026#39;emails.hello-markdown\u0026#39;); } ... ","date":"2021-02-07T02:45:46+09:00","permalink":"https://www.larajapan.com/2021/02/07/%E3%83%A1%E3%83%BC%E3%83%AB%E3%81%AE%E9%80%81%E4%BF%A1%EF%BC%88%EF%BC%96%EF%BC%89notification%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E3%83%A1%E3%83%BC%E3%83%AB%E3%82%92%E9%80%81%E4%BF%A1/","title":"メールの送信（６）Notificationを使ってメールを送信"},{"content":"HTMLメールをマークダウンで簡単に作成して送信できることができたので、今度はフォントや色などのカスタマイズを試みます。\n前回のマークダウンメールでに使われているコンポーネントやCSSの定義は、Laravelのパッケージの奥深くに隠れています。カスタマイズのためにはそれを日の目に出してあげる必要があります。まず以下をコマンドラインで実行します。\n$ php artisan vendor:publish --tag=laravel-mail resources/views/vendor/mailのディレクトリに以下のようにファイル作成されます。\nresources └── views ├── emails │ ├── hello.blade.php │ └── hello-markdown.blade.php ├── vendor │ └── mail │ ├── html │ │ ├── button.blade.php │ │ ├── footer.blade.php │ │ ├── header.blade.php │ │ ├── layout.blade.php │ │ ├── message.blade.php │ │ ├── panel.blade.php │ │ ├── subcopy.blade.php │ │ ├── table.blade.php │ │ └── themes │ │ └── default.css │ └── text │ ├── button.blade.php │ ├── footer.blade.php │ ├── header.blade.php │ ├── layout.blade.php │ ├── message.blade.php │ ├── panel.blade.php │ ├── subcopy.blade.php │ └── table.blade.php └── welcome.blade.php resources/views/vendor/mailのhtmlのディレクトリには、HTML用メールのコンポーネント、textにはテキストメール用のコンポーネントの定義のファイルが作成されます。\nこれらのファイルがいったん作成されると、Laravelのパッケージの定義でなくこれらのファイルの定義がマークダウンのメール送信に使用されます。\n今回は、ブレードのコンポーネントの理解などにば深入りはしたくないので、HTMLのカスタマイズにはCSSの定義だけを変更してみます。\nデフォルトのCSSは、上に見られるのように、html/themes/default.cssにあります。以下にその内容の一部を掲載します。\n/* Base */ body, body *:not(html):not(style):not(br):not(tr):not(code) { box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, \u0026#39;Segoe UI\u0026#39;, Roboto, Helvetica, Arial, sans-serif, \u0026#39;Apple Color Emoji\u0026#39;, \u0026#39;Segoe UI Emoji\u0026#39;, \u0026#39;Segoe UI Symbol\u0026#39;; position: relative; } body { -webkit-text-size-adjust: none; background-color: #ffffff; color: #718096; height: 100%; line-height: 1.4; margin: 0; padding: 0; width: 100% !important; } p, ul, ol, blockquote { line-height: 1.4; text-align: left; } a { color: #3869d4; } a img { border: none; } /* Typography */ h1 { color: #3d4852; font-size: 18px; font-weight: bold; margin-top: 0; text-align: left; } ... font-familyの定義を見てわかる通りに、フォントなどは日本語のフォントを意識はしていません。 まず、そこを変えてみます。このデフォルトのCSSファイルを直接編集してもよいですが、将来を考慮してこのファイルのコピー、larajapan.cssを作成して編集します。\nbodyのfont-familyのフォントとh1の色を変更します。\nresources/views/vendor/mail/html/themes/laravel.css /* Base */ body, body *:not(html):not(style):not(br):not(tr):not(code) { box-sizing: border-box; font-family: \u0026#34;Lucida Grande\u0026#34;, \u0026#34;Hiragino Kaku Gothic ProN\u0026#34;, Meiryo, sans-serif; /*日本語のフォントを指定 */ position: relative; } ... h1 { color: blue;　/* 青に変更 */ font-size: 18px; font-weight: bold; margin-top: 0; text-align: left; } ... さて、このCSSの変更を反映してメールを送信するには、いくつか方法あります。\n最初の方法は、config/mail.phpにおいて以下のように定義、\nconfig/mail.php return [ ... /* |-------------------------------------------------------------------------- | Markdown Mail Settings |-------------------------------------------------------------------------- | | If you are using Markdown based email rendering, you may configure your | theme and component paths here, allowing you to customize the design | of the emails. Or, you may simply stick with the Laravel defaults! | */ \u0026#39;markdown\u0026#39; =\u0026gt; [ \u0026#39;theme\u0026#39; =\u0026gt; \u0026#39;larajapan\u0026#39;, // defaultからlarajapanに変更 \u0026#39;paths\u0026#39; =\u0026gt; [ resource_path(\u0026#39;views/vendor/mail\u0026#39;), ], ], ]; 次の方法は、Mailableのクラスで定義。\napp/MailHelloMarkdown namespace App\\Mail; use Illuminate\\Bus\\Queueable; use Illuminate\\Contracts\\Queue\\ShouldQueue; use Illuminate\\Mail\\Mailable; use Illuminate\\Queue\\SerializesModels; class HelloMarkdown extends Mailable { use Queueable, SerializesModels; /** * Create a new message instance. * * @return void */ public function __construct() { $this-\u0026gt;theme = \u0026#39;larajapan\u0026#39;;　//ここで指定 } /** * Build the message. * * @return $this */ public function build() { return $this-\u0026gt;subject(\u0026#39;こんにちは！マークダウン\u0026#39;) -\u0026gt;markdown(\u0026#39;emails.hello-markdown\u0026#39;); } } 最後は、HelloMarkdownのインスタンスで変更します。以下はtinkerでの例。\nPsy Shell v0.10.6 (PHP 7.2.24 — cli) by Justin Hileman \u0026gt;\u0026gt;\u0026gt; $mailable = new App\\Mail\\HelloMarkdown; =\u0026gt; App\\Mail\\HelloMarkdown {#3283 +locale: null, +from: [], +to: [], +cc: [], +bcc: [], +replyTo: [], +subject: null, +view: null, +textView: null, +viewData: [], +attachments: [], +rawAttachments: [], +diskAttachments: [], +callbacks: [], +theme: null, +mailer: null, +connection: null, +queue: null, +chainConnection: null, +chainQueue: null, +delay: null, +middleware: [], +chained: [], } \u0026gt;\u0026gt;\u0026gt; $mailable-\u0026gt;theme = \u0026#39;larajapan\u0026#39;; =\u0026gt; \u0026#34;larajapan\u0026#34; \u0026gt;\u0026gt;\u0026gt; Mail::to(\u0026#39;test@example.com\u0026#39;)-\u0026gt;send($mailable); =\u0026gt; null さて、送信した結果をMailtrapで見てみます。\nh1の部分は変更した通りに、青色になっていますが、フォントはそうたいした変化ないですね。HTMLソースを見てみましょう。\n変更した定義のフォント（赤箱）になっていますね。MailtrapのCSSが上書きしているのかな？\nこのソースを見て気づいたと思いますが、HTMLメールでは、画面のHTMLと違って、\n\u0026lt;link rel=\u0026#34;stylesheet\u0026#34; href=\u0026#34;larajapan.css\u0026#34;\u0026gt; のような指定がソースにはなく、すべて定義に置き換えられています。\n","date":"2021-01-30T08:04:00+09:00","permalink":"https://www.larajapan.com/2021/01/30/%E3%83%A1%E3%83%BC%E3%83%AB%E3%81%AE%E9%80%81%E4%BF%A1%EF%BC%88%EF%BC%95%EF%BC%89html%E3%83%A1%E3%83%BC%E3%83%AB%E3%81%AE%E3%83%95%E3%82%A9%E3%83%B3%E3%83%88%E3%81%AA%E3%81%A9%E3%82%92%E3%82%AB/","title":"メールの送信（５）HTMLメールのフォントなどをカスタマイズ"},{"content":"コントローラでブレードを指定して画面を作成するように、メールでもブレードを使ってメールの作成が可能です。今回はそれを可能にしてくれるMailableのお話です。\nHTMLメールの送信 まず、Mailableの作成から。\n$ php artisan make:mail HelloHtml を実行すると、Mailableのクラス、HelloHtml.phpを作成してくれます。\nこれを以下のように編集します。\napp/Mail/HelloHtml.php namespace App\\Mail; use Illuminate\\Bus\\Queueable; use Illuminate\\Contracts\\Queue\\ShouldQueue; use Illuminate\\Mail\\Mailable; use Illuminate\\Queue\\SerializesModels; class HelloHtml extends Mailable { use Queueable, SerializesModels; /** * Create a new message instance. * * @return void */ public function __construct() { // } /** * Build the message. * * @return $this */ public function build() { return $this-\u0026gt;subject(\u0026#39;こんにちは！\u0026#39;) // メールの件名 -\u0026gt;view(\u0026#39;emails.hello\u0026#39;);　// ここでブレードを指定 } } ブレードの中身は、簡単に以下とします。\nresources/views/emails/hello.blade.php \u0026lt;html\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h1\u0026gt;こんにちは！\u0026lt;/h1\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; これをtinkerで送信します。\n$ php artisan tinker Psy Shell v0.10.6 (PHP 7.2.24 — cli) by Justin Hileman \u0026gt;\u0026gt;\u0026gt; use App\\Mail\\HelloHtml; \u0026gt;\u0026gt;\u0026gt; Mail::to(\u0026#39;test@example.com\u0026#39;)-\u0026gt;send(new HelloHtml); =\u0026gt; null Mailtrapで送信されたメールを見てましょう。\nHTMLメールとして届いていますね。\nここでひとつ修正です。前回にMailtrapの無料プランは３ヶ月までと書きましたが、それは大間違いで永遠に無料でした。毎月500通までの受信などの制限ありますが、私の使用頻度では十分です。\nマークダウンを使ってもっとHTMLの要素をメールに 先に送信したメールは、HTMLですがとてもベーシックでつまらないですね。せっかくのHTMLだから、画像を入れたり色変えたりボタンを入れたりしたいです。しかし、コントローラの画面と違って、いちいちデザイナーに頼むのもなんです。\nそこで、Laravelのマークダウンメールの登場です。\nマークダウンのためのMailableのクラスの作成は、先のMailableのクラスと同じで、先のコマンドの実行に、\u0026ndash;markdownの引数の指定するだけです。\n$ php artisan make:mail HelloMarkdown --markdown=emails.hello-markdown これで、HelloMarkdown.phpとhello-markdown.blade.phpが作成されます。\nこれらを以下のように編集します。\napp/Mail/HelloMarkdown.php namespace App\\Mail; use Illuminate\\Bus\\Queueable; use Illuminate\\Contracts\\Queue\\ShouldQueue; use Illuminate\\Mail\\Mailable; use Illuminate\\Queue\\SerializesModels; class HelloMarkdown extends Mailable { use Queueable, SerializesModels; /** * Create a new message instance. * * @return void */ public function __construct() { // } /** * Build the message. * * @return $this */ public function build() { return $this-\u0026gt;subject(\u0026#39;こんにちは！マークダウン\u0026#39;) -\u0026gt;markdown(\u0026#39;emails.hello-markdown\u0026#39;); // viewでなくmarkdownメソッドを使用 } } resources/views/emails/hello-markdown.blade.php @component(\u0026#39;mail::message\u0026#39;) # こんにちは ![Gmail](https://example.com/gmail.png) ここにメールの内容が入る @component(\u0026#39;mail::button\u0026#39;, [\u0026#39;url\u0026#39; =\u0026gt; \u0026#39;\u0026#39;]) ホームページへ @endcomponent ありがとうございます\u0026lt;br\u0026gt; {{ config(\u0026#39;app.name\u0026#39;) }} @endcomponent 先と同様に、tinkerで送信します。\nPsy Shell v0.10.6 (PHP 7.2.24 — cli) by Justin Hileman \u0026gt;\u0026gt;\u0026gt; use App\\Mail\\HelloMarkdown; \u0026gt;\u0026gt;\u0026gt; Mail::to(\u0026#39;test@example.com\u0026#39;)-\u0026gt;send(new HelloMarkdown); =\u0026gt; null 結果を、Mailtrapで見ると、以下のようになります。\n","date":"2021-01-23T03:25:11+09:00","permalink":"https://www.larajapan.com/2021/01/23/%E3%83%A1%E3%83%BC%E3%83%AB%E3%81%AE%E9%80%81%E4%BF%A1%EF%BC%88%EF%BC%94%EF%BC%89%E3%83%96%E3%83%AC%E3%83%BC%E3%83%89%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6html%E3%83%A1%E3%83%BC%E3%83%AB%E3%82%92/","title":"メールの送信（４）ブレードを使ってHTMLメールを作成"},{"content":"debugbarを使うとコントローラでのDB処理に使用されるSQL文をブラウザ画面でデバッグ文をわざわざ挿入することなしに見ることができます。しかし、php artisanのコマンドラインの実行では同様なことできません。さて、どうしたらよいでしょうか？\nまず、テストコマンドの作成から、\n$ php artisan make:command TestCommand この実行で、TestCommand.php が作成されます。 これを以下のように編集します。\napp/Console/Commands/TestCommand.php namespace App\\Console\\Commands; use App\\User; use Illuminate\\Console\\Command; class TestCommand extends Command { /** * The name and signature of the console command. * * @var string */ protected $signature = \u0026#39;command:test\u0026#39;; /** * The console command description. * * @var string */ protected $description = \u0026#39;テストコマンド\u0026#39;; /** * Create a new command instance. * * @return void */ public function __construct() { parent::__construct(); } /** * Execute the console command. * * @return int */ public function handle() { $count = User::count(); echo \u0026#34;会員の数：$count\\n\u0026#34;; return 0; } } まず、通常のコマンドラインからの実行で動作確認です。\n$ php artisan command:test 会員の数：1 大丈夫ですね。\nさて、その実行でどのようなSQL文が実行されているかをチェックするには、そう、tinkerの登場です。 以下はtinkerの１セッションですが、説明のために分割されているので注意を。\n$ php artisan tinker Psy Shell v0.10.4 (PHP 7.3.14 — cli) by Justin Hileman まず、DBクエリのログ機能をオンにします。\n\u0026gt;\u0026gt;\u0026gt; DB::enableQueryLog(); =\u0026gt; null 次に、コマンドの実行です。そうです、Artisan::call()の関数の引数にコマンド名を指定するだけなのです。\n\u0026gt;\u0026gt;\u0026gt; Artisan::call(\u0026#39;command:test\u0026#39;); 会員の数：1 =\u0026gt; 0 次に、貯めたログを出力します。\n\u0026gt;\u0026gt;\u0026gt; DB::getQueryLog(); =\u0026gt; [ [ \u0026#34;query\u0026#34; =\u0026gt; \u0026#34;select count(*) as aggregate from `users`\u0026#34;, \u0026#34;bindings\u0026#34; =\u0026gt; [], \u0026#34;time\u0026#34; =\u0026gt; 14.32, ], ] \u0026gt;\u0026gt;\u0026gt; SQL文が見れましたね。\nDB::enableQueryLog()などを毎回毎回tinkerのセッションでタイプするのが面倒なら、以前のこの記事のようにブートストラップのファイルを作成しておくと、\n$ php artisan tinker ~/tinker.php \u0026gt;\u0026gt;\u0026gt; Artisan::call(\u0026#39;command:test\u0026#39;); 会員の数：1 =\u0026gt; 0 \u0026gt;\u0026gt;\u0026gt; sql() =\u0026gt; [ [ \u0026#34;query\u0026#34; =\u0026gt; \u0026#34;select count(*) as aggregate from `users`\u0026#34;, \u0026#34;bindings\u0026#34; =\u0026gt; [], \u0026#34;time\u0026#34; =\u0026gt; 14.29, ], ] \u0026gt;\u0026gt;\u0026gt; と簡単になります。\n私は、シェルのエイリアスとして、.zshrcファイルに入れています。\n... alias tinker=\u0026#39;php artisan tinker ~/tinker.php\u0026#39; ... ","date":"2021-01-18T04:23:35+09:00","permalink":"https://www.larajapan.com/2021/01/18/tinker%E3%81%A7artisan%E3%82%92%E5%AE%9F%E8%A1%8C/","title":"tinkerでartisanを実行"},{"content":"前回紹介したMailtrapに比べて機能は劣りますが、十分代わりとなるしかもオープンスース（つまり無料）のMailhogの紹介です。このプログラムと知り合ったのは、Laravelの最新バージョン8のsailのパッケージで使用されていたからです。sailはDockerを使用して簡単に開発環境を作成できる素晴らしいものなのですが、Dockerの１つのコンテイナーとしてMailhogがインストールされます。sailの紹介は別の機会として、今回は単独でのインストールです。\nインストール まず、Mailhogはphpで作成されたプログラムでなく、Goの言語で書かれコンパイルされたバイナリのプログラムです。 以下がソースコードのレポジトリです。\nhttps://github.com/mailhog/MailHog\nしかし、ソースコードをコンパイルするやっかいなことは必要でありません。すでにコンパイルされたバイナリを以下からダウンロードできます。\nhttps://github.com/mailhog/MailHog/releases/v1.0.0\nこの最新のリリースはもう３年以上も前なのは少し気になりますが、上のリンク先の画面の下方にOSごとのバイナリのダウンロードリンクがあります。\nここでは、私のFedoraの仮想環境でLinuxバージョンを選択します。\nこれをダウンロードして、mailhogと改名して/usr/local/binに移します。また、chmodを実行して適切なパーミッションを与えます。\n# mv MailHog_linux_386 /usr/local/bin/mailhog # chmod 755 mailhog 次にコマンドラインで、mailhogを実行します。このリンク先のようなスクリプトを作成して、OSのサービスとして立ち上げてもOKです。\n$ mailhog 2021/01/07 08:42:42 Using in-memory storage 2021/01/07 08:42:42 [SMTP] Binding to address: 0.0.0.0:1025 [HTTP] Binding to address: 0.0.0.0:8025 2021/01/07 08:42:42 Serving under http://0.0.0.0:8025/ Creating API v1 with WebPath: Creating API v2 with WebPath: これでMailhogがメールを受信できる準備ができました。\nLaravelの環境変数の設定 Laravelのプロジェクトでは、.envを編集して以下のようにメール送信の設定をします。\n... MAIL_MAILER=smtp MAIL_HOST=0.0.0.0 MAIL_PORT=1025 MAIL_USERNAME=null MAIL_PASSWIRD=null MAIL_ENCRYPTION=null MAIL_FROM_ADDRESS=test@example.com MAIL_FROM_NAME=test ... MailTrapのような設定ですが、PortやEncryptが違うことに注意を。\nメール送信テスト 準備が整ったところで、tinkerでまずテキストメールを送信\n$ php artisan tinker Psy Shell v0.10.4 (PHP 7.2.24 — cli) by Justin Hileman \u0026gt;\u0026gt;\u0026gt; Mail::raw(\u0026#39;Hello\u0026#39;, function($m) { $m-\u0026gt;subject(\u0026#39;hello\u0026#39;)-\u0026gt;to(\u0026#39;test@example.com\u0026#39;); }); =\u0026gt; null \u0026gt;\u0026gt;\u0026gt; ブラウザを開いて、http://localhost:8025にアクセスすると、以下の画面が表示されます。\nメール届いていますね！\nメールの中身も閲覧できます。\n今度は、アプリからHTMLのメールを送信します。\nいいですね。ちゃんと表示されています。\nさて、HTMLソースはどうでしょう。\nう～ん。Quoted-printableがそのままです。例えば、上の画面の青背景は、「確認」の日本語なのですがこれじゃ読めません。もちろん、メールをファイルとしてダウンロードしてOutlookでHTMLソースを見ることが可能ですが、ちょいと不便です。\nここの部分残念ですが、自分のメールアカウントを使用せずに開発のテストとしては十分と思います。無料であることを考えたら、なおさら。\n最後に、先のmailhogのプログラムの実行を停止したら、デフォルトではメモリにメールを保存なので受信したメールはすべて消えてしまいます。それが嫌なら、mongodbにメールを保存するオプションもあります。\n","date":"2021-01-10T13:03:22+09:00","permalink":"https://www.larajapan.com/2021/01/10/%E3%83%A1%E3%83%BC%E3%83%AB%E3%81%AE%E9%80%81%E4%BF%A1%EF%BC%88%EF%BC%93%EF%BC%89%E3%83%A1%E3%83%BC%E3%83%AB%E5%8F%97%E4%BF%A1%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E3%80%80mailhog/","title":"メールの送信（３）メール受信サービス　Mailhog"},{"content":"LaravelのマニュアルのMailのセクションでは、Mailtrapというサービスが紹介されています。開発中はGmailのアドレスの送信すればいいものの、なにゆえに必要なのか疑問を持っていました。しかし、先日LaravelのNotificationのHTMLメールでHTMLのタグがどう使われているかチェックするのに困ったことがあり、Mailtrapのサービスを使用してみました。\nMailtrapのようなサービスの必要性 これ考えてみましょう。開発では、これが意外と理由あるのです。\nまず、通常使用している自分のメールアカウントに開発サイトから送信されるメールを一緒にしたくありません。しかも、テストのために何回も何回もプログラムからメールを送信して、いつもそれらを読んだ後削除するから、Gmailなどは学習して勝手にスパムあるいはゴミ箱に入れてしまいます。いちいちゴミ箱を見るのも面倒です。\nそれなら、メールフィルターで送信元で仕分ければとか、違うGmailアカウントを作成すれば、とか言われそうです。\nしかし、開発では、１つのプロジェクトで複数の届け先のテストが必要なときがあります。例えば、違う会員で違う内容の送信となるときでは、違うメールアドレスでのテストが必要です。そうなら、いくつものフィルターの設定やメールアカウントが必要となり、いちいち設定が面倒なのです。\nまた、HTMLメールだと、特にLaravelでは最近マークダウンとかコンポーネントとか使用していて、最終的に生成されるメールのHTMLがどうなるかをチェックしたいときがあります。しかし、HTMLだとQuoted-printableのフォーマットであるし、Gmailでオリジナルのデータを見ても通常のHTMLとは違うのでわけがわからないのです。こんな感じです。\nということで、これらの開発者が直面する問題を解決すべく、このようなサービスがあるわけです。\nサインアップ 英語版しかないのですが、まずサインアップ。アカウントは、メールアドレスとパスワードだけで作成可能です。GoogleやGithubなどのアカウントがすでにあるならそれを通してログインしてもOKです。\n無料版なのでチャージされることないので心配ないです。\nLaravelの環境変数の設定 以下の画面で、Laravelを選択したら、.envにそのままコピーできる環境設定を表示してくれます。\nMailtrapのインボックス さっそく、メールを送信してみます。Laravelで会員登録完了時に送信されるメールアドレス確認のメールです。HTMLのメールです。\n見れましたね。どのメールアドレスに送信してもmailtrapのInboxに入ります。\n中身のHTMLソースを見てみましょう。\nこれなら、通常のブラウザで見るHTMLソースと同じようです。つまり読めます。並んでいるタブの「Raw」をクリックして比べてみてください。\n無料はいつまで？ この時点においては、無料版は1ヶ月500通までの受信OKで、3ヶ月まで永遠に無料です。 有料のプランとしては、月9ドルから始まります。\n","date":"2021-01-03T03:20:18+09:00","permalink":"https://www.larajapan.com/2021/01/03/%E3%83%A1%E3%83%BC%E3%83%AB%E3%81%AE%E9%80%81%E4%BF%A1%EF%BC%88%EF%BC%92%EF%BC%89%E3%83%A1%E3%83%BC%E3%83%AB%E5%8F%97%E4%BF%A1%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E3%80%80mailtrap/","title":"メールの送信（２）メール受信サービス　Mailtrap"},{"content":"先日、Laravelのメール送信の設定、つまり、.envでメールに関わる環境変数の設定をいじっていて、設定が正しいかどうかをテストすることで学んだことを共有です。\nメール送信のテストは、メールサーバーが走っている環境下なら以下のようなシェルのコマンドラインで実行できます。\n$ echo Hello | mail -s hello test@examle.com 届け先は、test@example.com、件名は、hello、内容は、Helloのメールが届きます。\nしかし、Laravelのプロジェクトを通して同様にメールを送るには、Mailのファサードの登場です。tinkerで実行してみましょう。\nPsy Shell v0.10.4 (PHP 7.2.16 — cli) by Justin Hileman \u0026gt;\u0026gt;\u0026gt; Mail::raw(\u0026#39;Hello\u0026#39;, function($m) { $m-\u0026gt;subject(\u0026#39;hello\u0026#39;)-\u0026gt;to(\u0026#39;test@example.com\u0026#39;); }); =\u0026gt; null これでメールが届けば、実際使用するプロジェクトのメール関連の環境設定が正しいということです。便利です。\n先ほどのは、テキストメールの送信の確認ですが、HTMLのメールの送信も確認したいです。\n以下のように、今度は Mail::htmlのコールとなりました。\nPsy Shell v0.10.4 (PHP 7.2.16 — cli) by Justin Hileman \u0026gt;\u0026gt;\u0026gt; Mail::html(\u0026#39;\u0026lt;h1\u0026gt;Hello\u0026lt;/h1\u0026gt;\u0026#39;, function($m) { $m-\u0026gt;subject(\u0026#39;hello\u0026#39;)-\u0026gt;to(\u0026#39;test@example.com\u0026#39;); }); =\u0026gt; null ","date":"2020-12-25T01:22:52+09:00","permalink":"https://www.larajapan.com/2020/12/25/%E3%83%A1%E3%83%BC%E3%83%AB%E3%81%AE%E9%80%81%E4%BF%A1%EF%BC%88%EF%BC%91%EF%BC%89%E3%83%A1%E3%83%BC%E3%83%AB%E9%80%81%E4%BF%A1%E3%81%AE%E7%A2%BA%E8%AA%8D/","title":"メールの送信（１）メール送信の確認"},{"content":"入力画面のバリデーションでちょいと頭をひねったケースです。２つの入力項目があり、どちらかが必須なのだけれど両方の入力は禁止という場合です。まず、具体的な例を見てみましょう。\n簡単化しましたが、要はお客さんに配るクーポンのレコードの作成です。\n注目してもらいたのは「割引」の項目で、２つの入力場所があります。ひとつは割引額で円で入力、もうひとつは割引率で%で入力です。\nコントローラのコードはこんな感じです。新規のDBレコード作成の部分だけですけれど。\nnamespace App\\Http\\Controllers; use Illuminate\\Http\\Request; class CouponController extends Controller { /** * Show the form for creating a new resource. * * @return \\Illuminate\\Http\\Response */ public function create() { return view(\u0026#39;coupon.create\u0026#39;); } /** * Store a newly created resource in storage. * * @param \\Illuminate\\Http\\Request $request * @return \\Illuminate\\Http\\Response */ public function store(Request $request) { $rules = [ \u0026#39;code\u0026#39; =\u0026gt; \u0026#39;required|alpha_numeric\u0026#39;, \u0026#39;discount_amount\u0026#39; =\u0026gt; \u0026#39;nullable|required_without:discount_rate|integer|gt:0\u0026#39;, \u0026#39;discount_rate\u0026#39; =\u0026gt; \u0026#39;nullable|required_without:discount_amount|integer|gt:0|lte:100\u0026#39;, ]; $validated = $request-\u0026gt;validate($rules); // DBに保存のコードがここに入る } ... discount_amount（割引額）と discount_rate（割引率）のバリデーションに注目してください。nullableは両方とも空になるかもしれないゆえに必要です。\n次の、required_withoutは指定する項目が空のとき（つまりnull）はバリデーションの対象の項目が必須となる、ということです。両方の項目でお互いの項目を指定しているのは、両方が空となっては困るからです。つまり、どちらかの入力が必須となります。\nよさそうですね。\nと思いきや、この問題は両方に値を入れてもパスしてしまうことです。\nその問題に対応するために思いついたのは、以下のようにバリデーションに匿名の関数を使用します。\n... $rules = [ \u0026#39;code\u0026#39; =\u0026gt; \u0026#39;required|alpha_num\u0026#39;, \u0026#39;discount_amount\u0026#39; =\u0026gt; [ \u0026#39;nullable\u0026#39;, \u0026#39;required_without:discount_rate\u0026#39;, function ($attribute, $value, $fail) { if (request(\u0026#39;discount_rate\u0026#39;)) { $fail(\u0026#34;割引額あるいは割引率どちらかだけ入力してください\u0026#34;); } }, \u0026#39;integer\u0026#39;, \u0026#39;gt:0\u0026#39;, ], \u0026#39;discount_rate\u0026#39; =\u0026gt; \u0026#39;nullable|required_without:discount_amount|integer|gt:0|lte:100\u0026#39;, ]; ... request()のヘルパーがここで役に立ちます。バリデーション対象以外の項目の値を簡単に取得できます。\n最後に、今回の「どちらかに入力してください」のケースはそう起こることではないと思いますが、UIを変えてクリエイティブな対応も可能と思います。例えば、以下のように割引額と割引率をラジオボタンとすると、それぞれに対応する値の入力項目は２つでなく１つになり常に必須とすることできます。もちろん、それはそれでバリデーションのルールが簡単になるかどうかは保証なしですけれど。\n","date":"2020-12-13T01:58:11+09:00","permalink":"https://www.larajapan.com/2020/12/13/%E3%80%8C%E3%81%A9%E3%81%A1%E3%82%89%E3%81%8B%E3%81%AB%E5%85%A5%E5%8A%9B%E3%81%97%E3%81%A6%E3%81%8F%E3%81%A0%E3%81%95%E3%81%84%E3%80%8D%E3%81%AE%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7/","title":"「どちらかに入力してください」のバリデーション"},{"content":"一緒に働くプログラマが増えると、まず気づくのは他のプログラマのコードのスタイルが自分とは違うことです。みなそれぞれ独自のスタイルとなる理由付けがあるのですが、ある程度統一されないと他者にとってコードを理解するのに時間がかかるようになります。しかし個人の慣習を変えてもらうのはいつも難しい。そこで登場するのが、以前にも紹介したコードスタイル修正のツール、php-cs-fixer。最近そのLaravel用のルールセットを見つけたので、それを合わせて再紹介です。\nインストール Laravelのプロジェクトのルートのディレクトリにおいて、以下を実行します。\n$ composer require friendsofphp/php-cs-fixer --dev 開発環境でしか必要ないので、最後の引数\u0026ndash;devは忘れないように。\n次に、ルールセットの設定です。先に「Laravel用のルールセットを見つけた」と書きましたが、それは、私がよく使うLaravelアップグレードの有料サービス、Laravel Shiftのクリエイターが作成したものです。以下のgistで取得できます。彼のサービスでもこれが使われています。\nhttps://gist.github.com/laravel-shift/cab527923ed2a109dda047b97d53c200\nこれをコピペして、.php_csのファイルを作成します。\n実行 実行はいたって簡単です。\n$ vendor/bin/php-cs-fixer fix Loaded config default from \u0026#34;/vol1/usr/www/repos/repos/l7x/.php_cs\u0026#34;. Using cache file \u0026#34;.php_cs.cache\u0026#34;. 1) app/Http/Controllers/Auth/RegisterController.php 2) app/Http/Middleware/CheckForMaintenanceMode.php 3) routes/web.php Fixed all files in 0.108 seconds, 14.000 MB memory used fixの引数は、修正の実行を指示します。そして修正されたファイルが修正後にリストされます。\nどんな修正が行われるかの一例として、以下のようなファイルを作成してみました。\nnamespace App; use Log; use Illuminate\\Database\\Eloquent\\Model; use DB; class Ugly extends Model { public function test ($x, $y) { if ($x == 1) { // } else if ($y == 2) { /* */ } } } 思い切って醜いコードにしました。\nまず、修正を実行するまえに\u0026ndash;dry-runでどの修正ルールが適用されるかをチェックします。\n$ vendor/bin/php-cs-fixer fix --dry-run -vv Loaded config default from \u0026#34;/vol1/usr/www/repos/repos/l7x/.php_cs\u0026#34;. Runtime: PHP 7.2.16 Using cache file \u0026#34;.php_cs.cache\u0026#34;. SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSF Legend: ?-unknown, I-invalid file syntax (file ignored), S-skipped (cached or empty file), .-no changes, F-fixed, E-error 1) app/Ugly.php (indentation_type, blank_line_after_opening_tag, elseif, function_declaration, braces, ordered_imports) Checked all files in 0.050 seconds, 14.000 MB memory used 上で指摘されているのは、修正時に適用するルールセットのことです。説明すると、\nindentation_type：表示では見えないですが、タブ文字が使われてるところあります。タブを空白文字に変換必要。 blank_line_after_opening_tag：namespaceの宣言の前には改行が必要。 elseif：else ifでなくelseifを使用に変換必要。 function_declaration：関数名と引数名の間のスペースの削除が必要。 braces：ブレース（波括弧）の場所がおかしいので修正必要。 ordered_imports：インポート(use)の宣言がABC順でないので揃える必要あり。 これらのルールのすべては以下で説明あります。\nhttps://github.com/FriendsOfPHP/PHP-CS-Fixer/blob/2.16/doc/rules/index.rst\nたくさんありますね。使用したいルールは、先の.php_csで設定するのですが、たくさんあるゆえに、Laravel Shiftのクリエーターから拝借というわけです。\nさて、修正した結果は以下のようになりました。\nnamespace App; use DB; use Illuminate\\Database\\Eloquent\\Model; use Log; class Ugly extends Model { public function test($x, $y) { if ($x == 1) { // } elseif ($y == 2) { /* */ } } } 読みやすくなりました。\n最後に、拝借した.php_csの設定ですが、私が気に入らない部分がひとつありました。\nそれは、phpdoc_summaryのルールです。\n修正前：\n/** * これはテスト */ class Ugly extends Model { ... 修正後：\n/** * これはテスト. */ class Ugly extends Model { ... ようく見ると、修正後には「これはテスト.」とコメントの最後にピリオドが入ります。つまり句点がないので親切にも追加されます。ちなみに、句点(。)だとピリオドはつきません。\nこれが余計なお世話と思うなら、.php_csのファイルにおいて、以下のように無効とできます。\n... \u0026#39;phpdoc_summary\u0026#39; =\u0026gt; false, ... 最後に、.php_cs_cacheが実行時に作成されるので、これは.gitignoreに含むことをお忘れずに。\n","date":"2020-12-05T01:09:45+09:00","permalink":"https://www.larajapan.com/2020/12/05/%E3%82%B3%E3%83%BC%E3%83%89%E3%82%B9%E3%82%BF%E3%82%A4%E3%83%AB%E3%82%92%E7%B5%B1%E4%B8%80%E3%81%99%E3%82%8B%E3%83%84%E3%83%BC%E3%83%AB/","title":"コードスタイルを統一するツール"},{"content":"今回は、またしても新規のLaravelのバージョン対しての私のキャッチアップの話です。もちろん、存在は知っていたのですが、最近まで使用する機会がありませんでした。ブレードで使われる@errorの話なのですが、調べたら登場したのはなんとLaravelバージョン5.8、現在のバージョンが8.xなので遠い昔のように思えます。しかし、5.8がリリースされたのはつい去年のことです。Laravelのバージョン形態が変わってから、どんどん更新が早まっていくように感じられます。\n@if vs @error まず、バリデーションエラー表示において、どう@ifから@errorへ進化したかを比較してみます。 @ifを使用すると、\n\u0026lt;label for=\u0026#34;title\u0026#34;\u0026gt;投稿のタイトル\u0026lt;/label\u0026gt; \u0026lt;input id=\u0026#34;title\u0026#34; type=\u0026#34;text\u0026#34; class=\u0026#34;@if ($errors-\u0026gt;has(\u0026#39;title\u0026#39;) is-invalid @endif\u0026#34;\u0026gt; @if ($errors-\u0026gt;has(\u0026#39;title\u0026#39;) \u0026lt;div class=\u0026#34;alert alert-danger\u0026#34;\u0026gt;{{ $errors-\u0026gt;first(\u0026#39;title\u0026#39;) }}\u0026lt;/div\u0026gt; @endif これが、@errorを使用すると、\n\u0026lt;label for=\u0026#34;title\u0026#34;\u0026gt;投稿のタイトル\u0026lt;/label\u0026gt; \u0026lt;input id=\u0026#34;title\u0026#34; type=\u0026#34;text\u0026#34; class=\u0026#34;@error(\u0026#39;title\u0026#39;) is-invalid @enderror\u0026#34;\u0026gt; @error(\u0026#39;title\u0026#39;) \u0026lt;div class=\u0026#34;alert alert-danger\u0026#34;\u0026gt;{{ $message }}\u0026lt;/div\u0026gt; @enderror 両者とも意味は同じですが、@errorの方がすっきりして意味がわかりやすいですね。\n配列の入力があるフォームで@error さて、先の例と違って、以下のような配列フォームではどう@errorを使うことができるでしょう？\nまず、エラー表示のもととなるコントローラのバリデーションは、\nnamespace App\\Http\\Controllers; use Illuminate\\Http\\Request; class FormController extends Controller { public function create() { return view(\u0026#39;form.create\u0026#39;); } public function store(Request $request) { $rules = [ \u0026#39;name.*\u0026#39; =\u0026gt; \u0026#39;nullable\u0026#39;, \u0026#39;price.*\u0026#39; =\u0026gt; \u0026#39;nullable|required_with:name.*|integer|gt:0\u0026#39;, ]; $messages = [ \u0026#39;price.*.required_with\u0026#39; =\u0026gt; \u0026#39;入力してください\u0026#39;, \u0026#39;price.*.integer\u0026#39; =\u0026gt; \u0026#39;整数で入力してください\u0026#39;, \u0026#39;price.*.gt\u0026#39; =\u0026gt; \u0026#39;０超の整数で入力してください\u0026#39;, ]; $validated = $request-\u0026gt;validate($rules, $messages); // DBに保存... } ... 配列だから、name.のように、項目名にが使用されていることに注意してください。 nullableは、空(null)の入力でもOKという意味です。\n使用されるブレードは、\n@extends(\u0026#39;layouts.app\u0026#39;) @section(\u0026#39;content\u0026#39;) \u0026lt;div class=\u0026#34;container\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;row justify-content-center\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;col-md-8\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;card\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;card-header\u0026#34;\u0026gt;出費\u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;card-body\u0026#34;\u0026gt; \u0026lt;form method=\u0026#34;POST\u0026#34; action=\u0026#34;{{ route(\u0026#39;form.store\u0026#39;) }}\u0026#34;\u0026gt; @csrf \u0026lt;div class=\u0026#34;table-responsive\u0026#34;\u0026gt; \u0026lt;table class=\u0026#34;table table-bordered table-hover table-sm\u0026#34;\u0026gt; \u0026lt;thead\u0026gt; \u0026lt;tr\u0026gt; \u0026lt;th class=\u0026#34;\u0026#34;\u0026gt;アイテム名\u0026lt;/th\u0026gt; \u0026lt;th class=\u0026#34;\u0026#34;\u0026gt;金額\u0026lt;/th\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;/thead\u0026gt; \u0026lt;tbody\u0026gt; @for($i = 0; $i \u0026lt; 5; $i++) \u0026lt;tr\u0026gt; \u0026lt;td\u0026gt; \u0026lt;input type=\u0026#34;text\u0026#34; class=\u0026#34;form-control @error(\u0026#39;name.\u0026#39;.$i) is-invalid @enderror\u0026#34; name=\u0026#34;name[]\u0026#34; value=\u0026#34;{{ old(\u0026#39;name.\u0026#39;.$i) }}\u0026#34;\u0026gt; @error(\u0026#39;name.\u0026#39;.$i) \u0026lt;span class=\u0026#34;invalid-feedback\u0026#34; role=\u0026#34;alert\u0026#34;\u0026gt; \u0026lt;strong\u0026gt;{{ $message }}\u0026lt;/strong\u0026gt; \u0026lt;/span\u0026gt; @enderror \u0026lt;/td\u0026gt; \u0026lt;td\u0026gt; \u0026lt;input type=\u0026#34;text\u0026#34; class=\u0026#34;form-control @error(\u0026#39;price.\u0026#39;.$i) is-invalid @enderror\u0026#34; name=\u0026#34;price[]\u0026#34; value=\u0026#34;{{ old(\u0026#39;price.\u0026#39;.$i) }}\u0026#34;\u0026gt; @error(\u0026#39;price.\u0026#39;.$i) \u0026lt;span class=\u0026#34;invalid-feedback\u0026#34; role=\u0026#34;alert\u0026#34;\u0026gt; \u0026lt;strong\u0026gt;{{ $message }}\u0026lt;/strong\u0026gt; \u0026lt;/span\u0026gt; @enderror \u0026lt;/td\u0026gt; \u0026lt;/tr\u0026gt; @endfor \u0026lt;/tbody\u0026gt; \u0026lt;/table\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;row justify-content-center\u0026#34;\u0026gt; \u0026lt;button type=\u0026#34;submit\u0026#34; class=\u0026#34;btn btn-primary\u0026#34;\u0026gt; 保存 \u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/form\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; @endsection @error(\u0026lsquo;price.\u0026rsquo;.$i)と、price1でなくprice.1となっていることに注意してください。Laravelでは、price.1は指定の配列値で、price[1]と同じことです。同様に、old()でもold(\u0026lsquo;price.\u0026rsquo;.$i)とprice.1となっています。\n","date":"2020-11-22T11:03:46+09:00","permalink":"https://www.larajapan.com/2020/11/22/%E5%85%A5%E5%8A%9B%E3%83%95%E3%82%A9%E3%83%BC%E3%83%A0%E3%81%A7error/","title":"入力フォームで@error"},{"content":"１つのフォームに複数の投稿ボタンがあるのは稀なことではありません。例えば、「保存」と「キャンセル」ボタンとか、ウィザードなら確認画面において「入力に戻る」と「確定」ボタンとか、結構２つのボタンが存在することあります。さらに、編集画面では「保存」、「削除」、「キャンセル」と３つのボタンのケースも。今回はそれらの対応に関しての話です。\n「戻る」ボタン（投稿ボタンではない） 例えば、会員登録において、入力画面、確認画面、完了画面の３つの画面からなるウィザードであるとき、真ん中の確認画面では、最初の入力画面へ「戻る」ボタンと、次の完了画面に行く「確定」ボタンが必要です。\n「戻る」ボタンの対応で、一番簡単なのは以下のようなjavascriptでの対応です。\n\u0026lt;button onclick=\u0026#34;window.history.back()\u0026#34;\u0026gt;入力に戻る\u0026lt;/button\u0026gt; onclickの部分は、以下でも同じことです。\n\u0026lt;button onclick=\u0026#34;window.history.go(-1)\u0026#34;\u0026gt;Go Back\u0026lt;/button\u0026gt; \u0026lt;button onclick=\u0026#34;history.back()\u0026#34;\u0026gt;Go Back\u0026lt;/button\u0026gt; しかし、このボタンはフォームの中にあっても、フォームに投稿（POST）するわけではないので投稿ボタンとは言えないですね。\nリダイレクトのボタン（これも投稿ボタンではない） 例えば、以下のように「保存」と「キャンセル」ボタンがあるとき、「キャンセル」ボタンではリダイレクトを使用できます。\nこれもjavascriptを使用するなら、\n\u0026lt;button onclick=\u0026#34;location.href=\u0026#39;{{ route(\u0026#39;home\u0026#39;) }}\u0026#39;\u0026#34; type=\u0026#34;button\u0026#34;\u0026gt;キャンセル\u0026lt;/button\u0026gt; Bootstrapを利用するなら、cssでリンクをボタンにもできます。\n\u0026lt;a class=\u0026#34;btn\u0026#34; href=\u0026#34;{{ route(\u0026#39;home\u0026#39;) }}\u0026#34;\u0026gt;キャンセル\u0026lt;/a\u0026gt; これもフォームの投稿ボタンとは言えないですね。\n「保存して新規」のボタン（これは投稿ボタン） このボタンは、例えば会員において複数の届け先を連続して入力してもらう、とかでの使用ケースあります。つまり、１つの届け先を登録してもらうなら「保存」ボタン１つでいいところ、複数の届け先を連続して登録したいときに、「保存」と「保存して新規」の２つのボタンを装着します。\n「保存して新規」のボタンを押したら現在の入力を保存してから、空の新規の画面を表示します。\nこの場合は、今までのケースとは違って、「保存」あるいは「保存して新規」のボタンのクリックは、両方とも現在入力されたデータの保存のためにPOSTの実行、つまりフォームの投稿が必要となります。それゆえに、コントローラで投稿を受けるメソッドではどちらのボタンがクリックされたかの区別が必要となります。\nまずは、ブレードではこんな感じです。\n... \u0026lt;form method=\u0026#34;POST\u0026#34; action=\u0026#34;{{ route(\u0026#39;register\u0026#39;) }}\u0026#34;\u0026gt; @csrf \u0026lt;div class=\u0026#34;form-group row\u0026#34;\u0026gt; \u0026lt;label for=\u0026#34;name\u0026#34; class=\u0026#34;col-md-4 col-form-label text-md-right\u0026#34;\u0026gt;名前\u0026lt;/label\u0026gt; \u0026lt;div class=\u0026#34;col-md-6\u0026#34;\u0026gt; {{ $name }} \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; ... \u0026lt;div class=\u0026#34;form-group row mb-0\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;col-md-6 offset-md-4\u0026#34;\u0026gt; \u0026lt;input type=\u0026#34;submit\u0026#34; class=\u0026#34;btn btn-primary\u0026#34; name=\u0026#34;submit\u0026#34; value=\u0026#34;保存\u0026#34;\u0026gt; \u0026lt;input type=\u0026#34;submit\u0026#34; class=\u0026#34;btn btn-primary\u0026#34; name=\u0026#34;submit_new\u0026#34; value=\u0026#34;保存して新規\u0026#34;\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/form\u0026gt; ２つのボタンはともに投稿ボタンです。どちらかがクリックされたかをコントローラで区別するには、\n... class RegisterController extends Controller { public function store(Request $request) { // 新規の届け先のレコードを保存する処理を行う // 新規画面に戻る if ($request-\u0026gt;has(\u0026#39;submit_new\u0026#39;)) { return redirect()-\u0026gt;route(\u0026#39;register\u0026#39;); } // ホームに戻る return redirect()-\u0026gt;route(\u0026#39;home\u0026#39;); } ... というように、ボタンのnameがリクエストに入ってくるので、それで区別できます。\nこれで解決と思いきや、巷では、のタグでなくのタグの方がデザイナーに好まれます。なぜなら、以下のようにボタンにアイコンを埋め込むことが容易にできるからです。\n\u0026lt;button type=\u0026#34;submit\u0026#34;\u0026gt; \u0026lt;i class=\u0026#34;fas fa-search\u0026#34;\u0026gt;\u0026lt;/i\u0026gt; 検索 \u0026lt;/button\u0026gt; それでは、をに変えてみます。\n... \u0026lt;button type=\u0026#34;submit\u0026#34; class=\u0026#34;btn btn-primary\u0026#34; name=\u0026#34;submit\u0026#34; value=\u0026#34;submit\u0026#34;\u0026gt;保存\u0026lt;/button\u0026gt; \u0026lt;button type=\u0026#34;submit\u0026#34; class=\u0026#34;btn btn-primary\u0026#34; name=\u0026#34;submit_new\u0026#34; value=\u0026#34;submit_new\u0026#34;\u0026gt;保存して新規\u0026lt;/button\u0026gt; ... あるいは、以下のようにformactionの属性を使用することも可能です。\n... \u0026lt;button type=\u0026#34;submit\u0026#34; class=\u0026#34;btn btn-primary\u0026#34;\u0026gt;保存\u0026lt;/button\u0026gt; \u0026lt;button type=\u0026#34;submit\u0026#34; class=\u0026#34;btn btn-primary\u0026#34; formaction=\u0026#34;{!! route(\u0026#39;register\u0026#39;, \u0026#39;edit\u0026#39;) !!}\u0026#34;\u0026gt;保存して新規\u0026lt;/button\u0026gt; ... formactionで指定するURLに（ここでは同じフォーム）に、?editの引数を追加して投稿してくれるので、\n... // 新規画面に戻る if ($request-\u0026gt;has(\u0026#39;edit\u0026#39;)) { return redirect()-\u0026gt;route(\u0026#39;register\u0026#39;); } とコントローラで区別して対応することが可能となります。\n","date":"2020-11-15T03:04:41+09:00","permalink":"https://www.larajapan.com/2020/11/15/%E3%83%95%E3%82%A9%E3%83%BC%E3%83%A0%E3%81%AB%E8%A4%87%E6%95%B0%E3%81%AE%E6%8A%95%E7%A8%BF%E3%83%9C%E3%82%BF%E3%83%B3/","title":"フォームに複数の投稿ボタン"},{"content":"Laravelの経験が長くなると、つまり初期のバージョンから使用していると、昔のやり方しか知らずに過ごしていることがあります。以前にメンテナンス画面の裏口のポストがありますが、それはバージョン5.1の話で、大昔です。ということでやり方学びなおします。\nメンテンナンス画面 プログラムの更新をインストールしたり、データベースに変更をしたりするときは、誰にも作業中にアクセスしてもらいたくありません。そのときに、以下を実行します。\n$ php artisan down これを実行すると、アクセスしてきたら、以下のような画面が表示されます。それで安心して、プログラムを更新したり、データベースを変更したり、など好き勝手なことできます。\nメンテナンスが完了したら、\n$ php artisan up を実行して、皆にアクセスできるようにします。\nメンテンナンス画面にメッセージを表示したいなら、\n$ php artisan down --message=\u0026#34;メンテナンス中です。午前３時に終了します。\u0026#34; と実行すると以下のように、メッセージが表示されます。\n便利ですね。\n私がアクセスしたい メンテナンスの間、好き勝手なことをするのはいいのですが、私自身がサイトにアクセスできないと困ります。プログラムの更新をテストすることなどができません。\n特定のIPアドレスからアクセスできるようにする、つまり裏口を作る必要あります。\nここが以前（古いLaravelのバージョン）と最近のバージョンが違うところです。\n$ php artisan down --message=\u0026#34;メンテナンス中です。午前３時に終了します。\u0026#34; --allow=192.168.56.1 のように、\u0026ndash;allowの引数で自分のIPアドレスを指定するだけで、私にとってはアクセス可能となります。\nさらに、毎回毎回コマンドラインで指定するのが面倒なら、以前のように\napp/Http/Middleware/CheckForMaintenanceMode.php namespace App\\Http\\Middleware; use Closure; use Symfony\\Component\\HttpFoundation\\IpUtils; use Illuminate\\Foundation\\Http\\Middleware\\CheckForMaintenanceMode as Middleware; class CheckForMaintenanceMode extends Middleware { /** * The URIs that should be reachable while maintenance mode is enabled. * * @var array */ protected $except = [ // ]; protected $allowed = [ \u0026#39;192.168.56.1\u0026#39;　// ここで許可するIPを登録 ]; /** * Handle an incoming request. * * @param \\Illuminate\\Http\\Request $request * @param \\Closure $next * @return mixed * * @throws \\Symfony\\Component\\HttpKernel\\Exception\\HttpException * @throws \\Illuminate\\Foundation\\Http\\Exceptions\\MaintenanceModeException */ public function handle($request, Closure $next) { if ($this-\u0026gt;app-\u0026gt;isDownForMaintenance()) { if (IpUtils::checkIp($request-\u0026gt;ip(), (array) $this-\u0026gt;allowed)) { return $next($request); } } return parent::handle($request, $next);　// ここで親の関数を呼ぶ } } と編集します。\nメンテナンス画面をカスタマイズしたいなら メンテナンス画面をカスタマイズしたいなら、まず、以下を実行してブレードを作成します。\n$ php artisan vendor:publish --tag=laravel-errors resources/viewsのディレクトリにerrorsサブディレクトリが作成されて以下のファイルがコピーされます。\nerrors ├── 401.blade.php ├── 403.blade.php ├── 404.blade.php ├── 419.blade.php ├── 429.blade.php ├── 500.blade.php ├── 503.blade.php ├── illustrated-layout.blade.php ├── layout.blade.php └── minimal.blade.php メンテナンス画面のカスタマイズのために編集するファイル、503.blade.phpです。503は、Service Unavailable、つまり「サービスは利用できません」のウェブステータスです。\nartisanのコマンドは実行できる？ 最後に、メンテナンス中に、クロンで設定されているphp artisanのコマンドは実行されるかどうかですが、php aritsan upが実行できるのだからもちろん実行可能です。ということは、メンテナンス中においてクロンのサービスをいったん止めることも必要かもしれません。ご注意を。\n","date":"2020-11-08T05:10:45+09:00","permalink":"https://www.larajapan.com/2020/11/08/%E3%83%A1%E3%83%B3%E3%83%86%E3%83%8A%E3%83%B3%E3%82%B9%E7%94%BB%E9%9D%A2%EF%BC%9Aphp-artisan-up/","title":"メンテナンス画面：php artisan up \u0026 down"},{"content":"以前から紹介したいと思っていたパンくずのパッケージがありました。人気ものなのですが、作者が忙しいという理由で管理を辞めてしまいました。幸い、Laravelのバージョンが上がっても問題なく動作していますが、いつダメになるわかりません。小さなパッケージはこれがあるから嫌だなと思うところですが、オープンソースゆえに他の開発者が後継してくれました。しかも、「オフィシャル」となっているので長期間管理されるようです。今回はこのパッケージの紹介です。\nまず、以下の画面見てください。\nパンくずとは、矢印の部分に表示される階層のナビゲーションのことです。\n現在の画面のポジション、ここなら管理ユーザーのtesttestの編集画面ということがわかるし、その前の「管理ユーザーリスト」、「ホーム」にもリンクがついてここから直接リンク先に飛ばすことができます。\nさて、これを作成するためには、まずパッケージのインストールから、\n$ composer require diglactic/laravel-breadcrumbs 実行が完了したら、次は設定です。\nroutes/breadcrumbs.php\nのファイルを作成して、以下のように定義します。、\nroutes/breadcrumbs.php Breadcrumbs::for(\u0026#39;admin.home\u0026#39;, function ($trail) { $trail-\u0026gt;push(\u0026#39;ホーム\u0026#39;, route(\u0026#39;admin.home\u0026#39;)); }) Breadcrumbs::for(\u0026#39;admin.user.index\u0026#39;, function ($trail) { $trail-\u0026gt;parent(\u0026#39;admin.home\u0026#39;); $trail-\u0026gt;push(\u0026#39;管理ユーザー\u0026#39;, route(\u0026#39;admin.user.index\u0026#39;)); }); Breadcrumbs::for(\u0026#39;admin.user.create\u0026#39;, function ($trail) { $trail-\u0026gt;parent(\u0026#39;admin.user.index\u0026#39;); $trail-\u0026gt;push(\u0026#39;新規\u0026#39;); }); Breadcrumbs::for(\u0026#39;admin.user.show\u0026#39;, function ($trail, $user) { $trail-\u0026gt;parent(\u0026#39;admin.user.index\u0026#39;); $trail-\u0026gt;push($user-\u0026gt;login); }); // 以下が先の画面のパンくず Breadcrumbs::for(\u0026#39;admin.user.edit\u0026#39;, function ($trail, $user) { $trail-\u0026gt;parent(\u0026#39;admin.user.show\u0026#39;, $user); $trail-\u0026gt;push(\u0026#39;編集\u0026#39;); }); パンくずの名前、つまりfor()の最初の引数、がユニークである必要があります。名前つきのルートを使用しているなら、それと同じとするのが良いでしょう。\nparent()の関数を使用するなら、その前に定義したパンくずとチェーンできます。例えば、上の、admin.user.editの定義なら、以下のようにチェーンされます。\nadmin.user.edit →　admin.user.show → admin.user.index → admin.home\n設定が完了したら、ブレイドのレイアウトのテンプレートファイルに、\n... {{ Breadcrumbs::render(\u0026#39;admin.user.edit\u0026#39;, $user) }} .. と埋め込むと、最初の画面のようなHTMLコードが自動生成されます。\nもちろん、このためのテンプレートがあるわけですが、デフォルトではbootstrap ver.4が使われます。\nこちらが今回紹介したパッケージ：\nhttps://github.com/diglactic/laravel-breadcrumbs\n長い間、大変お世話になっていた旧パッケージ：\nhttps://github.com/davejamesmiller/laravel-breadcrumbs\n","date":"2020-10-29T23:19:59+09:00","permalink":"https://www.larajapan.com/2020/10/29/%E3%83%91%E3%83%B3%E3%81%8F%E3%81%9A%E3%83%91%E3%83%83%E3%82%B1%E3%83%BC%E3%82%B8%E3%80%81%E5%BE%A9%E6%B4%BB%EF%BC%81/","title":"パンくずパッケージ、復活！"},{"content":"募集は終了しました。 私のお客さん食文化のビジネス拡大のために、Laravelのプログラマーを募集しています。もちろん欲しいスキルはいくつか（以下）あるのですが、私のように独立して、フリーランス系の人が欲しいです。社員となるのではなく、お互いにプロジェクトや知識を、将来共有できる人が良いです。副業として（半日など）も考慮します。\n給与や勤務条件 こちら 仕事内容 ●　ウェブアプリ開発・管理 ●　システム管理（システムを熟知してから） 必須のスキル！ ●　Laravel 5.5以上のアプリ開発経験 ●　gitを毎日使用している ●　Linuxを毎日使用している ●　jqueryなどのjavascriptの開発経験 ●　AWSのEC2、S3などのクラウドサービスの経験 これがあればもっと良い ●　phpunitでユニットテストを書ける ●　オンラインビジネスの仕組みの知識と理解がある（Eコマースのプロジェクトが多いので） ●　技術書の英語が問題なく読める（最近は自動翻訳が優れているからそうでもないかな） ●　ブログを書いている 最後に このブログを読んでいるあなたは、もう資格十分かも。こちらにご連絡を。資格足りないかも？と思っていてもとりあえず声をかけてください！\n","date":"2020-10-24T03:27:40+09:00","permalink":"https://www.larajapan.com/2020/10/24/laravel%E3%81%AE%E3%82%A8%E3%83%B3%E3%82%B8%E3%83%8B%E3%82%A2%E5%8B%9F%E9%9B%86%E4%B8%AD%EF%BC%81-2/","title":"またまた、Laravelのエンジニア募集中！"},{"content":"前回のFormRequestの入力補正を例にして、そのユニットテストを作成してみます。\nルールの部分だけのテスト まず、テスト作成のコマンドを実行します。 $ php artisan make:test App/Http/Requests/EventRequestTest 実行すると、テストのファイルはLaravelのディレクトリ構造を真似て、tests/Feature/App/Http/Requestsのディレクトリに作成されます。\n作成されたテストを以下のように編集します。\napp/tests/Feature/App/Http/Requests/EventRequestTest.php namespace Tests\\Feature\\App\\Http\\Requests; use App\\Http\\Requests\\EventRequest; use Illuminate\\Foundation\\Testing\\RefreshDatabase; use Illuminate\\Foundation\\Testing\\WithFaker; use Tests\\TestCase; class EventRequestTest extends TestCase { /** * @test * @dataProvider validationProvider */ public function validation($expected, $data) { $rules = (new EventRequest())-\u0026gt;rules(); // rules()だけを抜き出す $validator = validator($data, $rules); $this-\u0026gt;assertEquals($expected, $validator-\u0026gt;passes()); } public function validationProvider() { return [ [ true, [ \u0026#39;start_at\u0026#39; =\u0026gt; \u0026#39;2020-01-01 00:00:00\u0026#39;, \u0026#39;end_at\u0026#39; =\u0026gt; \u0026#39;2020-01-01 01:00:00\u0026#39;, ], ], [ false, [ \u0026#39;start_at\u0026#39; =\u0026gt; \u0026#39;2020-01-01 00:00:00\u0026#39;, \u0026#39;end_at\u0026#39; =\u0026gt; \u0026#39;2020-01-01 24:00:00\u0026#39;, // 24時はない ], ] ]; } } 実行すれば、こんな感じでパスです。あとはvalidationProvider()のテストケースを増やしていくだけです。\nPHPUnit 8.5.8 by Sebastian Bergmann and contributors. .. 2 / 2 (100%) Time: 152 ms, Memory: 22.00 MB OK (2 tests, 2 assertions) しかし、ちょっと待ってください!\nEventRequestでは以下のようにprepareForValidation()が使用されています。 つまり、実際の入力の項目（date, start_hr, end_hr）を合わせて新規の項目（start_at, end_at）を作成しています。\n... protected function prepareForValidation() { // ここでDBに収納する項目を作成 $this-\u0026gt;merge([ \u0026#39;start_at\u0026#39; =\u0026gt; sprintf(\u0026#39;%s %02d:00:00\u0026#39;, $this-\u0026gt;date, $this-\u0026gt;start_hr), \u0026#39;end_at\u0026#39; =\u0026gt; sprintf(\u0026#39;%s %02d:59:59\u0026#39;, $this-\u0026gt;date, $this-\u0026gt;end_hr), ]); } ... 先のテストでは、ルールだけのテストなのでこの部分のテストをスキップしてしまっているのです。\nベターなテスト やはり、直接画面から入力されるテストをしたいということで、$this-\u0026gt;post(\u0026rsquo;/event\u0026rsquo;)とかの使用も考えましたが、DBデータを用意する必要や認証も考えてやらないやら、ちょいと面倒です。\nということで、以下の記事を見つけました。\nhttps://dev.to/dsazup/testing-laravel-form-requests-853\nこれを参照して先のテストを変更しました。今度は、dateとstart_hrとend_hrがテストデータとなっていることに注意してください。\nnamespace Tests\\Feature\\App\\Http\\Requests; use App\\Http\\Requests\\EventRequest; use Illuminate\\Foundation\\Testing\\RefreshDatabase; use Illuminate\\Foundation\\Testing\\WithFaker; use Illuminate\\Validation\\ValidationException; use Tests\\TestCase; class EventRequestTest extends TestCase { /** * @test * @dataProvider validationProvider */ public function validation($expected, $data) { $this-\u0026gt;assertEquals($expected, $this-\u0026gt;validate($data)); } // ここが先の記事からとってきたところ、LaravelのFormRequestServiceProvider::boot()を参照したらしい。 protected function validate($data) { $this-\u0026gt;app-\u0026gt;resolving(EventRequest::class, function ($resolved) use ($data){ $resolved-\u0026gt;merge($data); }); try { app(EventRequest::class); return true; } catch (ValidationException $e) { return false; } } public function validationProvider() { return [ [ true, [ \u0026#39;date\u0026#39; =\u0026gt; \u0026#39;2020-01-01\u0026#39;, \u0026#39;start_hr\u0026#39; =\u0026gt; \u0026#39;0\u0026#39;, \u0026#39;end_hr\u0026#39; =\u0026gt; \u0026#39;1\u0026#39;, ], ], [ false, [ \u0026#39;date\u0026#39; =\u0026gt; \u0026#39;2020-01-01\u0026#39;, \u0026#39;start_hr\u0026#39; =\u0026gt; \u0026#39;0\u0026#39;, \u0026#39;end_hr\u0026#39; =\u0026gt; \u0026#39;24\u0026#39;, ], ] ]; } } 実行してみると、\nPHPUnit 8.5.8 by Sebastian Bergmann and contributors. .. 2 / 2 (100%) Time: 145 ms, Memory: 22.00 MB OK (2 tests, 2 assertions) うまいこと成功です！\nテストにより問題発見！ テストケースを増やしてみます。\n... public function validationProvider() { return [ ... [ true, [ // 同じ時間は、00:00:00と00:59:59となるのでOK \u0026#39;date\u0026#39; =\u0026gt; \u0026#39;2020-01-01\u0026#39;, \u0026#39;start_hr\u0026#39; =\u0026gt; \u0026#39;0\u0026#39;, \u0026#39;end_hr\u0026#39; =\u0026gt; \u0026#39;0\u0026#39;, ], ], [ false, [ \u0026#39;date\u0026#39; =\u0026gt; \u0026#39;2020-01-01\u0026#39;, \u0026#39;start_hr\u0026#39; =\u0026gt; \u0026#39;a\u0026#39;, \u0026#39;end_hr\u0026#39; =\u0026gt; \u0026#39;b\u0026#39;, ], ] ]; } } PHPUnit 8.5.8 by Sebastian Bergmann and contributors. ...F 4 / 4 (100%) Time: 268 ms, Memory: 22.00 MB There was 1 failure: 1) Tests\\Feature\\App\\Http\\Requests\\EventRequestTest::validation with data set #3 (false, array(\u0026#39;2020-01-01\u0026#39;, \u0026#39;a\u0026#39;, \u0026#39;b\u0026#39;)) Failed asserting that true matches expected false. あれれ、エラーになりました。　これでは、\u0026lsquo;2020-01-01\u0026rsquo;, \u0026lsquo;a\u0026rsquo;, \u0026lsquo;b\u0026rsquo;の入力でも正しいということになってしまいます。 これはおかしい。\n問題は、EventRequest::prepareForValidation()の部分で、以下のようにsprintf()で文字列が０に変換されることです。tinkerを使って検証してみると、\n\u0026gt;\u0026gt;\u0026gt; $data = (object)[\u0026#39;date\u0026#39; =\u0026gt; \u0026#39;2020-01-01\u0026#39;, \u0026#39;start_hr\u0026#39; =\u0026gt; \u0026#39;a\u0026#39;, \u0026#39;end_hr\u0026#39; =\u0026gt; \u0026#39;b\u0026#39;]; =\u0026gt; {#4452 +\u0026#34;date\u0026#34;: \u0026#34;2020-01-01\u0026#34;, +\u0026#34;start_hr\u0026#34;: \u0026#34;a\u0026#34;, +\u0026#34;end_hr\u0026#34;: \u0026#34;b\u0026#34;, } \u0026gt;\u0026gt;\u0026gt; sprintf(\u0026#39;%s %02d:00:00\u0026#39;, $data-\u0026gt;date, $data-\u0026gt;start_hr, $data-\u0026gt;end_hr); =\u0026gt; \u0026#34;2020-01-01 00:00:00\u0026#34; ゼロになっていましたね。\nさて、この修正は以下となりました。start_atを作成するFormRequestの関数で、start_hr, end_hrを先にチェックして違法なら、invalidという文字列を与えることにしました。\n... protected function prepareForValidation() { if (! preg_match(\u0026#39;/^[0-9]{1,2}$/\u0026#39;, $this-\u0026gt;start_hr)) { // ２桁の数字でないときは、\u0026#39;invalid\u0026#39;とする $start_at = \u0026#39;invalid\u0026#39;; } else { $start_at = sprintf(\u0026#39;%s %02d:00:00\u0026#39;, $this-\u0026gt;date, $this-\u0026gt;start_hr); } if (! preg_match(\u0026#39;/^[0-9]{1,2}$/\u0026#39;, $this-\u0026gt;end_hr)) { $end_at = \u0026#39;invalid\u0026#39;; } else { $end_at = sprintf(\u0026#39;%s %02d:59:59\u0026#39;, $this-\u0026gt;date, $this-\u0026gt;end_hr); } $this-\u0026gt;merge([ \u0026#39;start_at\u0026#39; =\u0026gt; $start_at, \u0026#39;end_at\u0026#39; =\u0026gt; $end_at, ]); } ... 実行すると、今度はパスです。\nPHPUnit 8.5.8 by Sebastian Bergmann and contributors. .... 4 / 4 (100%) Time: 203 ms, Memory: 22.00 MB OK (4 tests, 4 assertions) ","date":"2020-10-18T01:17:02+09:00","permalink":"https://www.larajapan.com/2020/10/18/formrequest%E3%81%AE%E3%83%A6%E3%83%8B%E3%83%83%E3%83%88%E3%83%86%E3%82%B9%E3%83%88/","title":"FormRequestのユニットテスト"},{"content":"FormRequestで入力値を補正を以前紹介しましたが、今度はちょっと違う補正の紹介です。何が違うというと、今度はバリデーションの後でなくバリデーション前に入力値を変えます。入力項目とDBに保存する項目の形態が違うときにとても便利です。とりあえず、それが必要なケースの説明から。\nまず、DBに保存するのは、start_at（開始日時）とend_at（終了日時）の日時タイプ（mysqlならDATETIME）の２つ項目とします。しかし、両方の項目ともに日付は同じという仮定。いくつかのUIのバリエーションが考えられますが、私が考えたのは以下。\nつまり、入力項目は、日付、開始時刻（数字）、終了時刻（数字）の３つの入力項目です。\n日付はjquery-uiでカレンダーからの選択も可能（以下）であるし、時刻もドロップダウン、とすることもできますね。\n画面から入ってくるのは、date, start_hr, end_hrの３つの値なのですが、さて、これらの入力値をバリデーションにかけて、DBには、start_atとend_atの２つの項目に保存するには、どうしたらよいでしょうか？\nそこで登場するのが、FormRequestです。\nまず、以下のコマンドでFormRequestファイルを、app/Http/Requestsのディレクトリに作成します。\n$ php artisan make:request EventRequest 作成されたファイルを以下のように編集します。\napp/Http/Requests/EventRequest.php namespace App\\Http\\Requests; use Illuminate\\Foundation\\Http\\FormRequest; class EventRequest extends FormRequest { /** * Get the validation rules that apply to the request. * * @return array */ public function rules() { return [ \u0026#39;start_at\u0026#39; =\u0026gt; \u0026#39;required|date|date_format:Y-m-d H:i:s\u0026#39;, \u0026#39;end_at\u0026#39; =\u0026gt; \u0026#39;required|date|date_format:Y-m-d H:i:s|after:start_at\u0026#39;, ]; } public function messages() { return [ \u0026#39;date_format\u0026#39; =\u0026gt; \u0026#39;不正な日時のフォーマットです\u0026#39;, ]; } public function attributes() { return [ \u0026#39;start_at\u0026#39; =\u0026gt; \u0026#39;開始日時\u0026#39;, \u0026#39;end_at\u0026#39; =\u0026gt; \u0026#39;終了日時\u0026#39;, ]; } /** * Prepare the data for validation. * * @return void */ protected function prepareForValidation() { // ここでDBに収納する項目を作成 $this-\u0026gt;merge([ \u0026#39;start_at\u0026#39; =\u0026gt; sprintf(\u0026#39;%s %02d:00:00\u0026#39;, $this-\u0026gt;date, $this-\u0026gt;start_hr), \u0026#39;end_at\u0026#39; =\u0026gt; sprintf(\u0026#39;%s %02d:59:59\u0026#39;, $this-\u0026gt;date, $this-\u0026gt;end_hr), ]); } } ここ、preparedForValidation()の関数において、入力値を、start_atとend_atの入力値に変換しています。DBではそれらの項目はDateTimeのタイプなので、時間だけでなく分や秒も足すこと必要です。\nコントローラのメソッドでは、以下のようにコールします。store()の引数が、通常のRequestでなくEventRequestとなっていることに注意を。\napp/Http/Controllers/EventController.php namespace App\\Http\\Controllers; use App\\Http\\Controllers\\Controller; use App\\Http\\Requests\\EventRequest; class EventController extends Controller { ... /** * Store * * @param \\App\\Http\\Requests\\EventRequest $request * @return \\Illuminate\\Http\\RedirectResponse */ public function store(EventRequest $request) { dd($request-\u0026gt;all()); ... 実行すると、dd()の出力は、\narray:6 [▼ \u0026#34;_token\u0026#34; =\u0026gt; \u0026#34;npq1EPLTVvthufpgQiIpXIkxpwMPEsEEIBIzYf5A\u0026#34; \u0026#34;date\u0026#34; =\u0026gt; \u0026#34;2020-10-08\u0026#34; \u0026#34;start_hr\u0026#34; =\u0026gt; \u0026#34;8\u0026#34; \u0026#34;end_hr\u0026#34; =\u0026gt; \u0026#34;17\u0026#34; \u0026#34;start_at\u0026#34; =\u0026gt; \u0026#34;2020-10-08 08:00:00\u0026#34; \u0026#34;end_at\u0026#34; =\u0026gt; \u0026#34;2020-10-08 17:59:59\u0026#34; ] ","date":"2020-10-10T01:20:11+09:00","permalink":"https://www.larajapan.com/2020/10/10/formrequest%E3%81%A7%E5%85%A5%E5%8A%9B%E5%80%A4%E3%82%92%E8%A3%9C%E6%AD%A3%EF%BC%88%EF%BC%92%EF%BC%89/","title":"FormRequestで入力値を補正（２）"},{"content":"前回では、入力画面で投稿した入力値やバリデーションエラーを画面に表示するために、セッションを利用してそれらの値がキープされることを知りました。今回はそれらがどのようにコントローラで指示されるかを見てみます。\nLaravelの認証パッケージで提供されるものは、より複雑なので簡単な会員登録のコントローラをここでは使います。\nRegisterController.php namespace App\\Http\\Controllers; use App\\User; use Illuminate\\Http\\Request; class RegisterController extends Controller { /** * Create * * @return \\Illuminate\\View\\View */ public function create() { return view(\u0026#39;auth.register\u0026#39;); } /** * Store * * @param \\Illuminate\\Http\\Request $request * @return \\Illuminate\\Http\\RedirectResponse */ public function store(Request $request) { $rules = [ \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;required|email\u0026#39;, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;required|confirmed\u0026#39;, \u0026#39;password_confirmation\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, ]; $request-\u0026gt;validate($rules); User::create($request-\u0026gt;all); return redirect() -\u0026gt;route(\u0026#39;login\u0026#39;) -\u0026gt;with(\u0026#39;status\u0026#39;, \u0026#39;新規作成完了\u0026#39;); } } ３４行目の$request-\u0026gt;validate($rules)のコードが、バリデーションエラーが起こったときに、入力値やエラーをセッションに一時的に保存する作業をすべてやってくれるのですが、これではシンプルすぎ（もちろん良いことですが）で説明のしようがないので、以下のように拡張したコードに変えてみます。\n... $validator = validator($request-\u0026gt;all(), $rules); // バリデーションを実行 if ($validator-\u0026gt;fails()) { // バリデーションエラーがあるなら、 return back() // 前の画面にリダイレクト -\u0026gt;withInput() // セッション(_old_input)に入力値すべてを入れる -\u0026gt;withErrors($validator); // セッション(errors)にエラーの情報を入れる } ... 上でコメントを入れましたが、back()にチェーンされた、withInput()とwithErrors()の関数が入力値とエラーをセッションに保存してくれます。\nwithInput()の引数として、配列関数を指定することもできます。例えば、\n... return back() -\u0026gt;withInput($request-\u0026gt;except(\u0026#39;email\u0026#39;)) // email以外の入力値を保存 -\u0026gt;withErrors($validator); ... さらに、入力値やエラーと関係ない値も、with()でセッションに保存して、次の画面で使用も可能です。\n... return back() -\u0026gt;withInput($request-\u0026gt;except(\u0026#39;email\u0026#39;)) -\u0026gt;with(\u0026#39;error\u0026#39;, \u0026#39;入力エラーがあります\u0026#39;) -\u0026gt;with([\u0026#39;more\u0026#39; =\u0026gt; \u0026#39;メッセージ\u0026#39;, \u0026#39;more2\u0026#39; =\u0026gt; \u0026#39;メッセージ2\u0026#39;]); ... デバッグバーで見ると、セッションに保存されていることがわかります。\nwith()で渡した値は、ブレードでは、session()ヘルパーを利用して、\n@if (session(\u0026#39;error\u0026#39;)) \u0026lt;div class=\u0026#34;alert alert-danger\u0026#34;\u0026gt;{{ session(\u0026#39;error\u0026#39;) }}\u0026lt;/div\u0026gt; @endif のように使えます。\n","date":"2020-10-03T02:10:22+09:00","permalink":"https://www.larajapan.com/2020/10/03/withinput-witherrors-with/","title":"withInput(), withErrors(), with()"},{"content":"Laravelのブレードファイルでよく見かける、old()ヘルパー。エラーが起こった時に入力した値をキープしてエラーとともに入力項目に残してくれる優れものです。今回はこの仕組みを見てみます。\nブレードファイル old()ヘルパーはブレードファイルに使われます。以下は、Laravelのデフォルトの認証に使用されるブレードのファイルの一部をとってきました。 ... \u0026lt;div class=\u0026#34;form-group row\u0026#34;\u0026gt; \u0026lt;label for=\u0026#34;email\u0026#34; class=\u0026#34;col-md-4 col-form-label text-md-right\u0026#34;\u0026gt;{{ __(\u0026#39;E-Mail Address\u0026#39;) }}\u0026lt;/label\u0026gt; \u0026lt;div class=\u0026#34;col-md-6\u0026#34;\u0026gt; \u0026lt;input id=\u0026#34;email\u0026#34; type=\u0026#34;text\u0026#34; class=\u0026#34;form-control @error(\u0026#39;email\u0026#39;) is-invalid @enderror\u0026#34; name=\u0026#34;email\u0026#34; value=\u0026#34;{{ old(\u0026#39;email\u0026#39;) }}\u0026#34; required autocomplete=\u0026#34;email\u0026#34;\u0026gt; @error(\u0026#39;email\u0026#39;) \u0026lt;span class=\u0026#34;invalid-feedback\u0026#34; role=\u0026#34;alert\u0026#34;\u0026gt; \u0026lt;strong\u0026gt;{{ $message }}\u0026lt;/strong\u0026gt; \u0026lt;/span\u0026gt; @enderror \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; ... inputのHTML文で、value=\u0026quot;{{ old(\u0026rsquo;email\u0026rsquo;) }}\u0026quot;にヘルパーが使われていますね。\nこのために、以下のようにエラーが起こると、入力した値が再表示されます（ここでは、デモののためにtype=\u0026ldquo;email\u0026quot;ではなく、type=\u0026ldquo;text\u0026quot;としています）。\nリクエストの流れ エラーが出力するまでのリクエストの流れをたどってみると、以下の３つのリクエストとリスポンスからなります。 GETのリクエストで空の入力画面が表示されます。 次に、不正なEmailの値を入れて、「Register」のボタンを押すとPOSTのリクエストでサーバー、つまりLaravelのコントローラに送信されます。 そこのバリデーションでエラーが起こると（つまりメールアドレスのフォーマットではない）、入力した値とエラーを表示するためにGETのリクエストで以下の画面となります。 さて、どうやってLaravelはPOSTした入力値を次のGETで表示される画面に出力しているのでしょう？\nLaravelはこの目的のためにセッションを利用しています。上の２番目のPOSTのリクエストでセッションに入力値を保存して、次のGETのリクエストのときに、セッションから取り出します。このセッションの保存は特別で、使用は次のリクエストのみです。それゆえに同じ画面を手動でリフレッシュすると上の１の空の入力の画面となります。\nデバッグバー ここ実際のセッションの状態がどう遷移するか見てみたいですね。 そこで登場するのがデバッグバーのパッケージです。\n以下でインストールできます。\n$ composer require barryvdh/laravel-debugbar --dev デバッグバーをインストールして、同じ画面でエラーを発生させると、以下のように画面下で実行したDBクエリーやセッションなどの情報を見ることできます。\n赤の矢印の部分をクリックすると、セッションの情報が表示されます。あれれ、old()の値となるセッション項目は空ですね。おかしいかなと思いきや、この画面は、流れの３番目の画面を表示後の段階で、もう保存した値はセッションにはないのです。その前のPOSTのの直後のセッションの値を見る必要あります。\nPOSTへ戻るには、青箱のフォルダーアイコンをクリックします。そこでポップアップ表示される以下のダイアログにおいて、POSTの行をクリックします。\n以下がPOSTの直後でのセッションの値です。見ての通りに、_old_inputには入力された値、errorsにはエラーのデータが保存されています。さらに、_flashの配列には、oldとして_old_inputとerrorsがあり、これらは次のリクエストで利用できることを示しています。\n","date":"2020-09-25T23:42:41+09:00","permalink":"https://www.larajapan.com/2020/09/25/old%E3%83%98%E3%83%AB%E3%83%91%E3%83%BC/","title":"old()ヘルパー"},{"content":"Laravelのヘルパーの関数は本当に便利なもので、日常よく使うものです。しかし、こんな使い方もあったのか、と知らない使い方を知って嬉しくなることあります。今回は私を嬉しくさせる、route()のヘルパーの紹介です。\n私はrouteに名前を付けるのが好きで、プログラムのrouteにはすべて名前がついています。それゆえに、route()のヘルパーは欠かせないものです。\n以下は、Laravel 7.xバージョンの新規プロジェクトを作成してユーザー認証もインストールしたプロジェクトのrouteです。\n+----------+------------------------+------------------+------------------------------------------------------------------------+ | Method | URI | Name | Action | +----------+------------------------+------------------+------------------------------------------------------------------------+ | GET|HEAD | / | | Closure | | GET|HEAD | api/user | | Closure | | GET|HEAD | home | home | App\\Http\\Controllers\\HomeController@index | | GET|HEAD | login | login | App\\Http\\Controllers\\Auth\\LoginController@showLoginForm | | POST | login | | App\\Http\\Controllers\\Auth\\LoginController@login | | POST | logout | logout | App\\Http\\Controllers\\Auth\\LoginController@logout | | GET|HEAD | password/confirm | password.confirm | App\\Http\\Controllers\\Auth\\ConfirmPasswordController@showConfirmForm | | POST | password/confirm | | App\\Http\\Controllers\\Auth\\ConfirmPasswordController@confirm | | POST | password/email | password.email | App\\Http\\Controllers\\Auth\\ForgotPasswordController@sendResetLinkEmail | | GET|HEAD | password/reset | password.request | App\\Http\\Controllers\\Auth\\ForgotPasswordController@showLinkRequestForm | | POST | password/reset | password.update | App\\Http\\Controllers\\Auth\\ResetPasswordController@reset | | GET|HEAD | password/reset/{token} | password.reset | App\\Http\\Controllers\\Auth\\ResetPasswordController@showResetForm | | GET|HEAD | register | register | App\\Http\\Controllers\\Auth\\RegisterController@showRegistrationForm | | POST | register | | App\\Http\\Controllers\\Auth\\RegisterController@register | +----------+------------------------+------------------+------------------------------------------------------------------------+ 第３番目のNameの列がrouteの名前です。この値をもとにtinkerで、route()を使っていろいろなurlを作成してみます。 まずは基本からで、route()の最初の引数に、route名を渡します。\n\u0026gt;\u0026gt;\u0026gt; route(\u0026#39;home\u0026#39;); =\u0026gt; \u0026#34;http://localhost/home\u0026#34; 作成されたurlの、http://localhostの部分は、.envのAPP_URLの定義から来ています。 .envにおいて、\n.env ... APP_URL=http://www.example.com ... と定義すると、\n\u0026gt;\u0026gt;\u0026gt; route(\u0026#39;home\u0026#39;); =\u0026gt; \u0026#34;http://www.example.com/home\u0026#34; となります。\n今度は、route()に２番目の引数を与えます。\n\u0026gt;\u0026gt;\u0026gt; route(\u0026#39;password.reset\u0026#39;, [\u0026#39;token\u0026#39; =\u0026gt; \u0026#39;1232457890\u0026#39;]); =\u0026gt; \u0026#34;http://www.example.com/password/reset/1232457890\u0026#34; このtokenは上のrouteの表のURIで password/reset/{token}と定義されているので、与えた値に置き換わります。\nしかし、定義されていない変数も渡すこと可能です。\n\u0026gt;\u0026gt;\u0026gt; route(\u0026#39;password.reset\u0026#39;, [\u0026#39;token\u0026#39; =\u0026gt; \u0026#39;1232457890\u0026#39;, \u0026#39;type\u0026#39; =\u0026gt; \u0026#39;user\u0026#39;]); =\u0026gt; \u0026#34;http://www.example.com/password/reset/1232457890?type=user\u0026#34; この値は、コントローラの引数の$requestを使って、$request-\u0026gt;typeとかで取得できますね。\nさらに、\u0026lsquo;type\u0026rsquo; =\u0026gt; \u0026lsquo;user\u0026rsquo;の配列の形でなくても、単に、\u0026lsquo;user\u0026rsquo;とかでも与えることできます。\n\u0026gt;\u0026gt;\u0026gt; route(\u0026#39;password.reset\u0026#39;, [\u0026#39;token\u0026#39; =\u0026gt; \u0026#39;1232457890\u0026#39;, \u0026#39;user\u0026#39;]); =\u0026gt; \u0026#34;http://www.example.com/password/reset/1232457890?user\u0026#34; この場合は、コントローラ側では、$request-\u0026gt;has(\u0026lsquo;user\u0026rsquo;)とかでブーリアン値で取得できます。\n今度は、route()の第３の引数です。これはブーリアン値で、デフォルトがtrueでfalseを与えるとドメイン名がついた絶対URLではなくパス名だけを生成してくれます。\n\u0026gt;\u0026gt;\u0026gt; route(\u0026#39;home\u0026#39;, null, false); =\u0026gt; \u0026#34;/home\u0026#34; 最後に、redirect()にもroute()を使えます。\nSomeController.php ] ... return redirect(route(\u0026#39;home\u0026#39;)); でもOKですが、\n... return redirect()-\u0026gt;route(\u0026#39;home\u0026#39;); とチェーンできます。\n","date":"2020-09-19T06:44:13+09:00","permalink":"https://www.larajapan.com/2020/09/19/route%E3%83%98%E3%83%AB%E3%83%91%E3%83%BC/","title":"route()ヘルパー"},{"content":"前回に引き続いて、Laravelを使用したBootstrapの管理画面のスターターパッケージの紹介です。いかに新規のプロジェクトの管理画面を速く立ち上げるかが目的です。前回は、Core UIをベースにしたパッケージでしたが、今度は、Admin LTEをベースにしたものです。両方ともBootstrapのバージョン４をもとにしています。\nインストール Laravel Boilerplateでは、Laravelそのものも含むzipファイルをダウンロードしてインストールという形式でしたが、Laravel-AdminLTEでは、まずLaravelのプロジェクトをコマンドラインで作成してから、Laravel-AdminLTEのパッケージjeroennoten/laravel-adminlteをcomposerでプロジェクトにインストールします。その際には、依存するパッケージalmasaeed2010/adminlteもインストールされます。 almasaeed2010/adminlteのパッケージがAdmin LTEの無料版のCSSフレームワークで、jeroennoten/laravel-adminlteのパッケージがLaravelのプロジェクトにおいて、管理画面のbladeファイル、レイアウト設定やメニューなどの機能を提供します。\nルック 残念ながらLaravel-Admin LTEのデモサイトがないのですが、以下のような感じです。\nログイン画像\n管理画面画像\nconfig/app.phpで設定が必要ですが、日本語も対応しています。\nAdmin LTEに関しては、こちらでいろいろなUIを閲覧できます。\n設定 Laravel Boilerplateと比べて、デフォルトでインストールされている機能は少ないですが、以下のベーシックなレイアウトの設定が独自のconfig/adminlte.phpのファイルから簡単にできます。 タイトル ファビコン ロゴ画像 ユーザーメニュー これは、画面右上のログインしたユーザー名（ここではkenji）をクリックしたときの表示。 レイアウト 例えば、上の画面の設定では、固定のナビバーとフッターを指定しています。 \u0026#39;layout_topnav\u0026#39; =\u0026gt; null, \u0026#39;layout_boxed\u0026#39; =\u0026gt; null, \u0026#39;layout_fixed_sidebar\u0026#39; =\u0026gt; false, \u0026#39;layout_fixed_navbar\u0026#39; =\u0026gt; true, \u0026#39;layout_fixed_footer\u0026#39; =\u0026gt; true, 認証の画面表示のCSSクラス body、content、sidebar、topnavなどのcssクラス サイドバーのルック ユーザーのプロフィールやダッシュボードやログアウトなどのURL Laravel MIXの使用 サイドバーのメニューの指定 配列で指定できます。階層もOKです。以下は上のメニューの設定の一部。 \u0026#39;menu\u0026#39; =\u0026gt; [ [ \u0026#39;text\u0026#39; =\u0026gt; \u0026#39;search\u0026#39;, \u0026#39;search\u0026#39; =\u0026gt; true, \u0026#39;topnav\u0026#39; =\u0026gt; true, ], [ \u0026#39;text\u0026#39; =\u0026gt; \u0026#39;blog\u0026#39;, \u0026#39;url\u0026#39; =\u0026gt; \u0026#39;admin/blog\u0026#39;, \u0026#39;can\u0026#39; =\u0026gt; \u0026#39;manage-blog\u0026#39;, ], [ \u0026#39;text\u0026#39; =\u0026gt; \u0026#39;pages\u0026#39;, \u0026#39;url\u0026#39; =\u0026gt; \u0026#39;admin/pages\u0026#39;, \u0026#39;icon\u0026#39; =\u0026gt; \u0026#39;far fa-fw fa-file\u0026#39;, \u0026#39;label\u0026#39; =\u0026gt; 4, \u0026#39;label_color\u0026#39; =\u0026gt; \u0026#39;success\u0026#39;, ], [\u0026#39;header\u0026#39; =\u0026gt; \u0026#39;account_settings\u0026#39;], [ \u0026#39;text\u0026#39; =\u0026gt; \u0026#39;profile\u0026#39;, \u0026#39;url\u0026#39; =\u0026gt; \u0026#39;admin/settings\u0026#39;, \u0026#39;icon\u0026#39; =\u0026gt; \u0026#39;fas fa-fw fa-user\u0026#39;, ], [ \u0026#39;text\u0026#39; =\u0026gt; \u0026#39;change_password\u0026#39;, \u0026#39;url\u0026#39; =\u0026gt; \u0026#39;admin/settings\u0026#39;, \u0026#39;icon\u0026#39; =\u0026gt; \u0026#39;fas fa-fw fa-lock\u0026#39;, ], [ \u0026#39;text\u0026#39; =\u0026gt; \u0026#39;multilevel\u0026#39;, \u0026#39;icon\u0026#39; =\u0026gt; \u0026#39;fas fa-fw fa-share\u0026#39;, \u0026#39;submenu\u0026#39; =\u0026gt; [ [ \u0026#39;text\u0026#39; =\u0026gt; \u0026#39;level_one\u0026#39;, \u0026#39;url\u0026#39; =\u0026gt; \u0026#39;#\u0026#39;, ], ... サイドバーのメニューのフィルター。権限によりメニューのオン・オフが可能となります。 プラグイン。datatablesやselect2などのjsの指定。 これら設定は、以下の場所で詳細がドキュメントされています。\nhttps://github.com/jeroennoten/Laravel-AdminLTE#6-configuration\n採用できそう？ 気に入ったのは、基本的な設定が簡単にconfigファイルで行えることです。また、Laravel Boilerplateと違って、独立したパッケージとして提供されているので、将来のパッケージの更新の面では管理しやすいと思います。\n","date":"2020-09-06T01:17:16+09:00","permalink":"https://www.larajapan.com/2020/09/06/laravel%E3%81%B8%E3%81%AE%E7%A7%BB%E8%A1%8C%EF%BC%88%EF%BC%93%EF%BC%89%E7%AE%A1%E7%90%86%E7%94%BB%E9%9D%A2%E3%81%AE%E3%82%B9%E3%82%BF%E3%83%BC%E3%82%BF%E3%83%BC%E3%83%91%E3%83%83%E3%82%B1%E3%83%BC/","title":"Laravelへの移行（３）管理画面のスターターパッケージ　Laravel-AdminLTE"},{"content":"前回ではセッションを共有することで、現行のPHPのプログラムと新規のLaravelのプログラムの行き来が可能となりました。もう現行に縛られることはないので、新規の画面のルックも最新のレスポンシブ対応のBootstrapを取り入れてみます。管理画面の移行を前提に、Laravelを使用したBootstrapの管理画面のスターターパッケージをいくつか紹介します。今回はLaravel Boilerplateをご紹介します。\nデモ Laravel Boilerplateは、現在Laravel6.xと7.xの２つのバージョンがあります。 ウェブサイトは、https://laravel-boilerplate.com/index.html\nデモサイトがあるので早速見てみましょう。\nhttps://demo.laravel-boilerplate.com/login\n以下の情報でログインできます。\n管理者: User: admin@admin.com Password: secret\nユーザー: User: user@user.com Password: secret\nログイン画面は、\n管理者としてログインすると管理画面となりますが、以下はそのユーザー管理画面、\nとちょっと日本語変だけれど日本語にも対応しています。\nルックは、以下のBoostrapバージョン４対応のCore UIを使用しています。これを見るといろいろなことができそうでワクワクしますね。\n追加されたcomposerのパッケージ インストールして標準装着されている機能をチェックしたいなら、composer.jsonを見るのが手っ取り早いです。\n以下は、Laravel 7.xのデフォルトのcomposerのrequireのセクションです。\n... \u0026#34;require\u0026#34;: { \u0026#34;php\u0026#34;: \u0026#34;^7.2.5\u0026#34;, \u0026#34;fideloper/proxy\u0026#34;: \u0026#34;^4.2\u0026#34;, \u0026#34;fruitcake/laravel-cors\u0026#34;: \u0026#34;^1.0\u0026#34;, \u0026#34;guzzlehttp/guzzle\u0026#34;: \u0026#34;^6.3\u0026#34;, \u0026#34;laravel/framework\u0026#34;: \u0026#34;^7.0\u0026#34;, \u0026#34;laravel/tinker\u0026#34;: \u0026#34;^2.0\u0026#34;, \u0026#34;laravel/ui\u0026#34;: \u0026#34;^2.1\u0026#34; }, ... 以下は、Laravel boilerplateの同様なセクションです。\n... \u0026#34;require\u0026#34;: { \u0026#34;php\u0026#34;: \u0026#34;^7.2.5\u0026#34;, \u0026#34;albertcht/invisible-recaptcha\u0026#34;: \u0026#34;^1.9\u0026#34;, \u0026#34;arcanedev/log-viewer\u0026#34;: \u0026#34;7.x\u0026#34;, \u0026#34;darkghosthunter/laraguard\u0026#34;: \u0026#34;^2.0\u0026#34;, \u0026#34;fideloper/proxy\u0026#34;: \u0026#34;^4.2\u0026#34;, \u0026#34;fruitcake/laravel-cors\u0026#34;: \u0026#34;^2.0\u0026#34;, \u0026#34;guzzlehttp/guzzle\u0026#34;: \u0026#34;^6.3\u0026#34;, \u0026#34;jamesmills/laravel-timezone\u0026#34;: \u0026#34;^1.9\u0026#34;, \u0026#34;lab404/laravel-impersonate\u0026#34;: \u0026#34;^1.6\u0026#34;, \u0026#34;langleyfoxall/laravel-nist-password-rules\u0026#34;: \u0026#34;^4.1\u0026#34;, \u0026#34;laravel/framework\u0026#34;: \u0026#34;^7.22.4\u0026#34;, \u0026#34;laravel/socialite\u0026#34;: \u0026#34;^4.3\u0026#34;, \u0026#34;laravel/tinker\u0026#34;: \u0026#34;^2.0\u0026#34;, \u0026#34;laravel/ui\u0026#34;: \u0026#34;^2.0\u0026#34;, \u0026#34;livewire/livewire\u0026#34;: \u0026#34;^1.0\u0026#34;, \u0026#34;rappasoft/laravel-livewire-tables\u0026#34;: \u0026#34;^0.1\u0026#34;, \u0026#34;rappasoft/lockout\u0026#34;: \u0026#34;^2.1\u0026#34;, \u0026#34;spatie/laravel-activitylog\u0026#34;: \u0026#34;^3.14\u0026#34;, \u0026#34;spatie/laravel-permission\u0026#34;: \u0026#34;^3.11\u0026#34;, \u0026#34;tabuna/breadcrumbs\u0026#34;: \u0026#34;^1.0\u0026#34; }, ... いくつか追加されたパッケージを紹介すると、\narcanedev/log-viewer Laravelのログも含めて、作成されたログファイルを管理画面でログのメッセージのレベルにマッチした色付きで閲覧できるようです。 darkghosthunter/laraguard 「2ファクタ認証」のパッケージです。最初から必要でないかもしれないですが、オプションとしてあるのはいいですね。 lab404/laravel-impersonate これは管理者があたかもユーザーになりすますためのパッケージです。すでにLaravelにその機能あったような。 rappasoft/laravel-livewire-tables これは以前紹介したjqueryのdatatablesのlivewire版です。 spatie/laravel-activitylog こちらは以前紹介した、DBの変更履歴記録と同様なパッケージです。 spatie/laravel-permission これは上に掲載したユーザー管理の画面において、それぞれのユーザーにロール（役割）を割り当てるためのパッケージです。それにより、ログインした管理者がどの画面にアクセスが可能かを操作します。結構なパッケージです。 tabuna/breadcrumbs このブログでいつか紹介しようと思っていたのですが、パンくず表示のための、とても有名な、https://github.com/davejamesmiller/laravel-breadcrumbsパッケージがありました。しかし、残念なことに作者の都合で今年更新ストップとなりました。しかし、嬉しいことに、違うプログラマがそれをもとに新たなパッケージを作成したのがこれです。まだ使用したことありませんが、いつか紹介したいです。 追加されたnode-jsのパッケージ 今度は、jsのパッケージの方を見てみます。\ncomposerと同様に、laravel 7.xでは、\n... \u0026#34;devDependencies\u0026#34;: { \u0026#34;axios\u0026#34;: \u0026#34;^0.19\u0026#34;, \u0026#34;bootstrap\u0026#34;: \u0026#34;^4.0.0\u0026#34;, \u0026#34;cross-env\u0026#34;: \u0026#34;^7.0\u0026#34;, \u0026#34;jquery\u0026#34;: \u0026#34;^3.2\u0026#34;, \u0026#34;laravel-mix\u0026#34;: \u0026#34;^5.0.1\u0026#34;, \u0026#34;lodash\u0026#34;: \u0026#34;^4.17.13\u0026#34;, \u0026#34;popper.js\u0026#34;: \u0026#34;^1.12\u0026#34;, \u0026#34;resolve-url-loader\u0026#34;: \u0026#34;^3.1.0\u0026#34;, \u0026#34;sass\u0026#34;: \u0026#34;^1.15.2\u0026#34;, \u0026#34;sass-loader\u0026#34;: \u0026#34;^8.0.0\u0026#34;, \u0026#34;vue-template-compiler\u0026#34;: \u0026#34;^2.6.11\u0026#34; } ... Laravel Boilerplateでは、\n... \u0026#34;devDependencies\u0026#34;: { \u0026#34;@coreui/coreui\u0026#34;: \u0026#34;^3.0.0\u0026#34;, \u0026#34;@coreui/icons\u0026#34;: \u0026#34;^1.0.1\u0026#34;, \u0026#34;@fortawesome/fontawesome-free\u0026#34;: \u0026#34;^5.12.1\u0026#34;, \u0026#34;@popperjs/core\u0026#34;: \u0026#34;^2.0.6\u0026#34;, \u0026#34;alpinejs\u0026#34;: \u0026#34;^2.3.5\u0026#34;, \u0026#34;axios\u0026#34;: \u0026#34;^0.19\u0026#34;, \u0026#34;bootstrap\u0026#34;: \u0026#34;^4.5.0\u0026#34;, \u0026#34;cross-env\u0026#34;: \u0026#34;^7.0\u0026#34;, \u0026#34;jquery\u0026#34;: \u0026#34;^3.5.1\u0026#34;, \u0026#34;laravel-mix\u0026#34;: \u0026#34;^5.0.1\u0026#34;, \u0026#34;lodash\u0026#34;: \u0026#34;^4.17.19\u0026#34;, \u0026#34;perfect-scrollbar\u0026#34;: \u0026#34;^1.5.0\u0026#34;, \u0026#34;popper.js\u0026#34;: \u0026#34;^1.16.1\u0026#34;, \u0026#34;resolve-url-loader\u0026#34;: \u0026#34;^2.3.1\u0026#34;, \u0026#34;sass\u0026#34;: \u0026#34;^1.20.1\u0026#34;, \u0026#34;sass-loader\u0026#34;: \u0026#34;^8.0.0\u0026#34;, \u0026#34;sweetalert2\u0026#34;: \u0026#34;^9.8.2\u0026#34;, \u0026#34;vue\u0026#34;: \u0026#34;^2.5.17\u0026#34;, \u0026#34;vue-template-compiler\u0026#34;: \u0026#34;^2.6.10\u0026#34; } ... こちらも追加されたパッケージを見てみましょう。\n@coreui/coreui、@coreui/icons テンプレートとして使用しているCore UIのパッケージです。なるほどnpmで管理するのですね。アイコンも1500以上あるようです。ちなみに、Core UIを使用しているのは、管理者画面の部分のみです。 @fortawesome/fontawesome-free これは非常に有名なアイコンのライブラリです。有料版もありますが、ここでは無料版を使用です。先の、Core UIのアイコンと合わせて膨大な数のアイコンから選択可能となります。 alpinejs こちらはLivewireの作者のjsフレームワークです。日本語のマニュアルもありますね。\nsweetalert2 こちらは、jsのalert()のベターバージョンのようですね。興味あります。 さて、これ採用できそう？ 機能が盛りだくさん、というのが印象です。しかし使いこなすには、彼ら独自のディレクトリの構造（Domainなど）があり、すべて理解する必要あり時間がかかりそうです。かと言って、要らない機能のパッケージを外して簡単化するのは、影響する部分を考えると難しそうだし。 また、resources/viewsのディレクトリの構造やその中のbladeファイルを見ると、コンポーネントとか使用してとてもフレキシブルにしていますが、細分化しすぎの感がありこれまた理解に時間がかかりそうです。また、多言語対応なので、国際的なプロジェクトの作成には良いかもしれませんが、日本語だけしか必要ないとなると複雑すぎです。\nあと、管理画面のメニューを管理する機能があればよいのですが、残念ながらありません。\nこれを採用するかどうかは別として、知らないパッケージがどう使われているかはとても参考になりそうで、時間かけて理解するのは価値はあります。ということで次回は違うLaravelのスターターパッケージ（？）を見てみます。\n","date":"2020-08-30T09:32:09+09:00","permalink":"https://www.larajapan.com/2020/08/30/laravel%E3%81%B8%E3%81%AE%E7%A7%BB%E8%A1%8C%EF%BC%88%EF%BC%92%EF%BC%89%E7%AE%A1%E7%90%86%E7%94%BB%E9%9D%A2%E3%81%AE%E3%82%B9%E3%82%BF%E3%83%BC%E3%82%BF%E3%83%BC%E3%83%91%E3%83%83%E3%82%B1%E3%83%BC/","title":"Laravelへの移行（２）管理画面のスターターパッケージ　Laravel Boilerplate"},{"content":"先日、Laravel-boilerplateというオープンソースのプロジェクトのコードを見ていて、composerでプロジェクトのキャッシュのクリアー、ユニットテストの実行やコードの整形などいろいろなタスクを実行できることに気づきました。今回はその話です。\nまっさらでインストールしたLaravelのプロジェクトでは、composer.jsonのscriptsのセクションは、以下のようになります。\ncomposer.json ... \u0026#34;scripts\u0026#34;: { \u0026#34;post-autoload-dump\u0026#34;: [ \u0026#34;Illuminate\\\\Foundation\\\\ComposerScripts::postAutoloadDump\u0026#34;, \u0026#34;@php artisan package:discover --ansi\u0026#34; ], \u0026#34;post-root-package-install\u0026#34;: [ \u0026#34;@php -r \\\u0026#34;file_exists(\u0026#39;.env\u0026#39;) || copy(\u0026#39;.env.example\u0026#39;, \u0026#39;.env\u0026#39;);\\\u0026#34;\u0026#34; ], \u0026#34;post-create-project-cmd\u0026#34;: [ \u0026#34;@php artisan key:generate --ansi\u0026#34; ] } } 上記のコマンドを説明すると、\npost-autoload-dumpのタスクは、以下のようにdump-autoloadを実行したときに自動的に実行され、composer.jsonで指定されているパッケージの自動ディスカバーをしてくれます。昔のLaravelではこれいちいちconfig/app.phpファイルでのパッケージの指定していました。\n$ composer dump-autoload Generating optimized autoload files\u0026gt; Illuminate\\Foundation\\ComposerScripts::postAutoloadDump \u0026gt; @php artisan package:discover --ansi Discovered Package: facade/ignition Discovered Package: fideloper/proxy Discovered Package: laravel/tinker Discovered Package: nesbot/carbon Discovered Package: nunomaduro/collision Package manifest generated successfully. Generated optimized autoload files containing 4218 classes composer create-projectの実行でもこのスクリプトは実行されます。\npost-root-package-installのタスクは、プロジェクト作成時において.envファイルがないときに実行され、.env.exampleから.envのファイルの作成します。\n最後の、post-create-project-cmdのタスクは、これまたプロジェクト作成時に実行され、.envで使用される暗号キーを自動作成して.envに保存します。\nさて、今度はlaravel-boilerplateのcomposer.jsonを見てみます。scriptsのセクションでは、上のデフォルトのスクリプトに付け加えて、いくつか独自のタスクのためのスクリプトを追加しています。以下では私が説明のためにコメントしていますが、jsonファイルはコメント指定不可なので注意を。\ncomposer.json ... \u0026#34;scripts\u0026#34;: { \u0026#34;post-autoload-dump\u0026#34;: [ \u0026#34;Illuminate\\\\Foundation\\\\ComposerScripts::postAutoloadDump\u0026#34;, \u0026#34;@php artisan package:discover --ansi\u0026#34; ], \u0026#34;post-root-package-install\u0026#34;: [ \u0026#34;@php -r \\\u0026#34;file_exists(\u0026#39;.env\u0026#39;) || copy(\u0026#39;.env.example\u0026#39;, \u0026#39;.env\u0026#39;);\\\u0026#34;\u0026#34; ], \u0026#34;post-create-project-cmd\u0026#34;: [ \u0026#34;@php artisan key:generate --ansi\u0026#34; ], \u0026#34;post-update-cmd\u0026#34;: [　// php stormなどのIDEエディターのヘルパーのため。 \u0026#34;@php artisan ide-helper:generate\u0026#34;, \u0026#34;@php artisan ide-helper:meta\u0026#34; ], \u0026#34;clear-all\u0026#34;: [　// bootstrap/cacheやstorage/frameworkなどに含まれるキャッシュファイルを削除して、vendor/composer/autoload_real.phpを再作成 \u0026#34;@php artisan clear-compiled\u0026#34;, \u0026#34;@php artisan cache:clear\u0026#34;, \u0026#34;@php artisan route:clear\u0026#34;, \u0026#34;@php artisan view:clear\u0026#34;, \u0026#34;@php artisan config:clear\u0026#34;, \u0026#34;@php artisan permission:cache-reset\u0026#34;, \u0026#34;composer dumpautoload -o\u0026#34; ], \u0026#34;cache-all\u0026#34;: [ // 設定データやルートのデータをキャッシュ。bootstrap/cacheにキャッシュファイルを作成 \u0026#34;@php artisan config:cache\u0026#34;, \u0026#34;@php artisan route:cache\u0026#34; ], \u0026#34;reset\u0026#34;: [ // composer関連のファイルをクリア \u0026#34;composer clear-all\u0026#34;, \u0026#34;composer cache-all\u0026#34; ], \u0026#34;test\u0026#34;: \u0026#34;vendor/bin/phpunit\u0026#34;,　// ユニットテストの実行 \u0026#34;test-coverage\u0026#34;: \u0026#34;vendor/bin/phpunit --coverage-html coverage\u0026#34;,　// ユニットテストのカバレージの作成 \u0026#34;format\u0026#34;: \u0026#34;vendor/bin/php-cs-fixer fix --allow-risky=yes\u0026#34;　// コードの整形 } } 最後に、追加されたスクリプトは、以下のように実行できます。\n$ composer run clear-all 便利ですね。\n紹介しておきながらなんですが、私はこれらのタスクは、Makefileに入れてmakeで実行します。それに関してはいつか紹介します。\n","date":"2020-08-21T00:19:34+09:00","permalink":"https://www.larajapan.com/2020/08/21/composer%E3%81%A7%E3%82%BF%E3%82%B9%E3%82%AF%E3%82%92%E5%AE%9F%E8%A1%8C/","title":"composerでタスクを実行"},{"content":"phpunitは私にはもう欠かせないツールなのですが、その実行をよりビジュアルに表示してくれるツールを見つけました。その紹介です。\n通常は、以下のようにコマンドラインで実行すると、\n$ vendor/bin/phpunit 赤字のFはFailureの失敗です。テスト数が多いと少ない表示で便利なのですが、どこでFなのかは最後まで待たないと判りません。\n$ vendor/bin/phpunit --stop-on-failure としてエラーがあったらそこで止まるというオプションもあります。しかし他のエラーもあるかもしれないし、１個のエラーを修正してまた再度テスト実行では、テストの数が多いとまどろっこしい。\n$ vendor/bin/phpunit --testdox このオプションでは以下のように実行したテストのそれぞれの詳細を出力してくれます。\nいい感じですが、出力が多くてテストの数が多いと画面をスクロールするのが面倒になります。\n中間というものはないかな、と調べたら、ありました。\nCodeDungeon PHPUnit Pretty Result Printer\nという名前のパッケージで、以下のgithubにレポジトリがあります。\nhttps://github.com/mikeerickson/phpunit-pretty-result-printer\nインストールには、以下を実行します。開発だけで使用なので、\u0026ndash;devのオプションを忘れずに。\n$ composer require --dev codedungeon/phpunit-result-printer 次に、以下を実行すると、\n$ php ./vendor/codedungeon/phpunit-result-printer/src/init.php phpunit.xmlに、printerClass=\u0026ldquo;Codedungeon\\PHPUnitPrettyResultPrinter\\Printer\u0026rdquo;を追加して、設定ファイル、phpunit-printer.ymlを作成します。\nさて、これで再度phpunitを実行すると、\nなかなか見やすいですね。\n","date":"2020-08-03T23:40:07+09:00","permalink":"https://www.larajapan.com/2020/08/03/phpunit%E3%81%AE%E5%AE%9F%E8%A1%8C%E7%8A%B6%E6%B3%81%E8%A1%A8%E7%A4%BA/","title":"phpunitの実行状況表示"},{"content":"Laravelのコントローラにおいて、abort(404)のヘルパーをコールすることで簡単に予期しないアクセスを、Not Foundのエラーページの表示ができます。しかし、Not Foundページの代わりにリダイレクトして違うページに飛ばしたいときには、redirect()のヘルパーをabort()同様に好きな場所で使うことはできません。\nそう書いても何を言っているかわからないと思いますので、まず、商品（Product）の情報を閲覧するコントローラを使って、abort()とredirect()の紹介から。\nabort()は以下のように単独でコードに配置します。\nnamespace App\\Http\\Controllers; use App\\Product; use App\\Http\\Controllers\\Controller; class ProductController extends Controller { public function show(Product $product) { if ($product-\u0026gt;active_flag == \u0026#39;N\u0026#39;) { // 無効な商品なら、 abort(404); // Not Foundページを表示 } return view(\u0026#39;user.product.show\u0026#39;)-\u0026gt;with(compact(\u0026#39;product\u0026#39;)); // 商品のページを表示 } } しかし、redirect()は、以下のようにreturnが必要です。\n... class ProductController extends Controller { public function show(Product $product) { if ($product-\u0026gt;active_flag == \u0026#39;N\u0026#39;) { // 無効な商品なら、 return redirect(\u0026#39;home\u0026#39;); // ホームページにリダイレクト } return view(\u0026#39;user.product.show\u0026#39;)-\u0026gt;with(compact(\u0026#39;product\u0026#39;)); // 商品のページを表示 } } この違いは、abort()がHttp Exceptionをthrowするのに対して、redirect()は、\\Illuminate\\Http\\RedirectResponseのオブジェクトを返すからです。\nさて、上のコントローラを変更して、以下のように条件文を新規のメソッドに移します。現在の条件は簡単だけれど将来はたいそう複雑になるぞ、という仮定です。\n... class ProductController extends Controller { public function show(Product $product) { $this-\u0026gt;checkProduct($product); // 条件文が複雑になるので新規の関数を作成 return view(\u0026#39;user.product.show\u0026#39;)-\u0026gt;with(compact(\u0026#39;product\u0026#39;)); // 商品のページを表示 } private function checkProduct($product) { if ($product-\u0026gt;active_flag == \u0026#39;N\u0026#39;) { // 無効な商品なら、 return redirect(\u0026#39;home\u0026#39;); // ホームページにリダイレクト } } } 一見たわいもない変更ですが、これを実行すると、なんと無効である商品の表示ページが表示されます。ホームページにリダイレクトはされません。問題は、checkProduct()がRedirectResponseを返すのに、コールした側でreturnが使用されていないからなのですね。\nしかし、\n... class ProductController extends Controller { public function show(Product $product) { return $this-\u0026gt;checkProduct($product); return view(\u0026#39;user.product.show\u0026#39;)-\u0026gt;with(compact(\u0026#39;product\u0026#39;)); // 商品のページを表示 ... こうすると、商品が有効なときもそこでリターンするために商品のページが表示されずに問題です。さてさて、どうしたらよいでしょう？\nということで、あれこれ調べていたら、abort()を使用してリダイレクトが可能なことがわかりました。以下のように、abort()の引数に404のようなHttpステータスのコードの代わりにredirect()を渡すことが可能なのです。以下が変更後のコードです。素晴らしい！\n... class ProductController extends Controller { public function show(Product $product) { $this-\u0026gt;checkProduct($product); // 条件文が複雑になるので新規の関数を作成 return view(\u0026#39;user.product.show\u0026#39;)-\u0026gt;with(compact(\u0026#39;product\u0026#39;)); // 商品のページを表示 } private function checkProduct($product) { if ($product-\u0026gt;active_flag == \u0026#39;N\u0026#39;) { // 無効な商品なら、 abort(redirect(\u0026#39;home\u0026#39;)); // ホームページにリダイレクト } } } ","date":"2020-07-25T00:01:46+09:00","permalink":"https://www.larajapan.com/2020/07/25/abort%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E3%83%AA%E3%83%80%E3%82%A4%E3%83%AC%E3%82%AF%E3%83%88/","title":"abort()を使ってリダイレクト"},{"content":"最新のLaravelはバージョン7.xですが、バージョン6.xは以下の表に見られるように２年間のサポート(LTS)があります。管理を頻繁に行う必要がないなら、新規のプロジェクトでは6.xバージョンをインストールするのが良いです。ということで、久しぶりに初心に戻ってLaravelのプロジェクトの作成手順を書きます。\n新規プロジェクトの作成 composerはすでにシステムにインストールされていると仮定して、以下のコマンドがl6xのディレクトリ名でLaravelをインストールします。\n$ composer create-project --prefer-dist laravel/laravel l6x \u0026#34;6.*\u0026#34; ウェブサーバーなどが書き込みを行うディレクトリにはパーミッションが必要です。\n$ cd l6x $ chmod -R a+w storage 以下でページを見ることができるか確認です。\n$ php artisan serve l6x/publicのディレクトリがウェブルートと仮定して、ブラウザで、http://local:8000にアクセスすると、以下のページが表示されます。\n必要なら、この時点でパージョン管理をするのも良いアイデアです。\n$ cd l6x $ git init $ git add . $ git commit -am \u0026#34;init\u0026#34; 認証のパッケージ 6.x以前のバージョンでは、ユーザー認証のデモに必要なファイルは以下のコマンドで作成できました。\n$ php artisan make:auth もうそのコマンドは存在しません。その代わりに以下のパッケージをインストールします。\n$ composer require laravel/ui \u0026#34;^1.0\u0026#34; 注意：ここ間違いありで編集されています。--devなしで実行が必要です。 $ composer require laravel/ui \"^1.0\" --dev 次に、以下を実行して会員登録画面やログイン画面などを作成します。boostrapだけでなく、vueやreactの指定も可能です。\n$ php artisan ui bootstrap --auth 今度は、javascriptやcssのトランスパイルのために、nodeのパッケージのインストールとwebpackの実行です。\n$ npm install \u0026amp;\u0026amp; npm run dev 次はDBの設定です。mysqlのコマンドを実行して、l6xのデータベースを作成します。\n$ mysql -u root -p mysql\u0026gt; create database l6x; .envファイルを次のように編集してDB情報を入れます。\nDB_DATABASE=l6x DB_USERNAME=DBユーザー名 DB_PASSWORD=DBパスワード\n以下のコマンドを実行してDBテーブルを作成します。\n$ php artisan migrate 以下のように、４つのテーブルが作成されます。\nmysql\u0026gt; show tables; +-----------------+ | Tables_in_l6x | +-----------------+ | failed_jobs | | migrations | | password_resets | | users | +-----------------+ 4 rows in set (0.00 sec) 再度、ブラウザでhttp://localhost:8000/registerにアクセスします。\nと入力して、「Register」ボタンをクリックすると、\n新規会員登録成功で、動作確認完了です。\n","date":"2020-07-18T06:23:09+09:00","permalink":"https://www.larajapan.com/2020/07/18/laravel-6-x%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB/","title":"Laravel 6.xのインストール"},{"content":"長いこと開発者をやっていると自分が開発した古いphpのプログラムの管理が悩みの種になります。なぜならLaravelのフレームワークが使われていないからです。年々新しい機能を追加したりしていると複雑になってくるし、ある昔のフレームワークで書かれたプロジェクトではphpunitも使えなく検証も容易にできません。このままで進んでいくにはますます管理において頭が痛くなります。しかし、すべて書き直してLaravelへの移行となると、日数とコストは大変なものです。さらに、外見がほとんど変わらずに中身だけの変更なので、お客さんを説得するのも難しい。\nしかし、機会は訪れるもので、その悩みのプロジェクトのひとつにおいて、お客さんから大きな新規のリクエストが来ました。その新規のリクエストは現在の機能とは独立して扱える部分が多いので、こちらを独立させてLaravelで開発することにして、完成したら現行の機能を徐々にLaravelに移行していく、ということになりました。\n嬉しい前進ですが、このプロジェクトに関して１つの必須条件は、現行のプログラムの認証を使用して（まだ使われるので）、新規のLaravelのプログラムの方では再度ログインを必要としてはいけない、というものです。\n現行のプログラムの認証セッション 現行のプログラムもLaravelと同様に、ユーザー認証後ブラウザのクッキーを使用してそのユーザーのためだけの認証セッションを作成します。\n以下は、現行のプログラムの認証セッションをシミュレートするプログラムですが、ログインしたユーザーの情報（ここでは、user_id）をセッションの情報に保存します。\nstart.php ini_set(\u0026#39;session.save_path\u0026#39;, \u0026#34;/usr/www/repos/session\u0026#34;); ini_set(\u0026#39;session.cookie_secure\u0026#39;, \u0026#39;1\u0026#39;); // only secure ini_set(\u0026#39;session.name\u0026#39;, \u0026#39;admin\u0026#39;); session_start(); $_SESSION = [\u0026#39;user_id\u0026#39; =\u0026gt; 1]; このプログラムをウェブでアクセスすると、アクセスしたクライアント、つまりブラウザでは、そのドメインで、adminという名前のクッキーが作成され、t20fa2tc05dd5gda1sfs1tsrofのような値が保存されます。そして、サーバー側では、指定した場所、つまり、/usr/www/repos/sessionのディレクトリに、sess_t20fa2tc05dd5gda1sfs1tsrofのセッションファイルが作成されます。その中身は以下のようになります。phpの$_SESSION変数の中身がそこで保存されています。\n$ sudo cat /usr/www/repos/session/sess_t20fa2tc05dd5gda1sfs1tsrof user_id|i:1; 認証セッションの認識 さて、問題は先の他のphpプログラムでの認証セッションをどうやってLaravelで認識して、あたかも自分の認証とするか、です。しかし、phpのセッションの構造は、Laravelの認証後に作成されるセッションとは違い、Laravelで簡単に共有というわけにはいきません。\nそこで、私が考えたのは、まず、新規のLaravelのプログラムが認証を必要するときに、現行のプログラムですでに作成された認証セッションを認識して、それが正しいものなら、改めてLaravelの認証を実行することです。幸い、Auth::login($user);のようにログイン画面を通さずに認証する関数もあります。\nまず、routeの設定部分を見てみましょう。\nroutes/web.php Route::get(\u0026#39;/\u0026#39;, function () { return view(\u0026#39;welcome\u0026#39;); }); Auth::routes(); Route::get(\u0026#39;/home\u0026#39;, \u0026#39;HomeController@index\u0026#39;)-\u0026gt;name(\u0026#39;home\u0026#39;); 上では、/homeにアクセスすると、ログイン画面、/loginにリダイレクトされますが、それはHomeControllerのコンストラクタで以下のようにガードされているからです。\napp/Http/Controlers/HomeController.php namespace App\\Http\\Controllers; use Illuminate\\Http\\Request; class HomeController extends Controller { /** * Create a new controller instance. * * @return void */ public function __construct() { $this-\u0026gt;middleware(\u0026#39;auth:admin\u0026#39;);　// ここで保護 } /** * Show the application dashboard. * * @return \\Illuminate\\Contracts\\Support\\Renderable */ public function index() { return view(\u0026#39;home\u0026#39;); } } しかし、以下のようにAuthenticateのミドルウェアのauthenticate()を上書き定義すると、\napp/Http/Middleware/Authenticate.php namespace App\\Http\\Middleware; use Illuminate\\Auth\\Middleware\\Authenticate as Middleware; use Auth; use App\\User; class Authenticate extends Middleware { /** * Determine if the user is logged in to any of the given guards. * * @param \\Illuminate\\Http\\Request $request * @param array $guards * @return void * * @throws \\Illuminate\\Auth\\AuthenticationException */ protected function authenticate($request, array $guards) { if (empty($guards)) { $guards = [null]; } if (in_array(\u0026#39;admin\u0026#39;, $guards)) { // すでにLaravel側で認証されているなら、OK if ($this-\u0026gt;auth-\u0026gt;guard(\u0026#39;admin\u0026#39;)-\u0026gt;check()) { return $this-\u0026gt;auth-\u0026gt;shouldUse(\u0026#39;admin\u0026#39;); } // 現行のプログラムの認証セッションから情報を取得 ini_set(\u0026#39;session.save_path\u0026#39;, \u0026#34;/usr/www/repos/session\u0026#34;); ini_set(\u0026#39;session.cookie_secure\u0026#39;, \u0026#39;1\u0026#39;); // only secure ini_set(\u0026#39;session.name\u0026#39;, \u0026#39;admin\u0026#39;); session_start(); if (isset($_SESSION[\u0026#39;user_id\u0026#39;])) { if ($user = User::find($_SESSION[\u0026#39;user_id\u0026#39;])) { // ユーザーが存在するなら、Laravelで認証 Auth::guard(\u0026#39;admin\u0026#39;)-\u0026gt;login($user); } } } foreach ($guards as $guard) { if ($this-\u0026gt;auth-\u0026gt;guard($guard)-\u0026gt;check()) { return $this-\u0026gt;auth-\u0026gt;shouldUse($guard); } } $this-\u0026gt;unauthenticated($request, $guards); } ... すでに、現行のプログラムで認証されているなら、そのセッションの値を取得してLaravel側でも認証とします。\n注意点としては、現行のプログラムと新規のLaravelのプログラムとで２つの認証セッションが存在することになります。両方とも違うセッションでお互いの存在を知らないので、セッションの期間が異なることになります。現行の方の認証が期限切れとなってもLaravelの方ではまだ期限内かもしれません。しかし、逆にLaravelのセッションが期限切れとなっても現行のプログラムのセッションが期限内なら、再度Laravelでセッションを作成するので問題はありません。\nそれから、現行のプログラムもLaravel側でも認証されていなときに、Laravel側にアクセスされたら、現行のプログラムのログイン画面にリダイレクトすることが必要です。それは以下のように、先のAuthenticateのクラスで、unauthenticated()を定義することにより可能です。\napp/Http/Middleware/Authenticate.php ... /** * Handle an unauthenticated user. * * @param \\Illuminate\\Http\\Request $request * @param array $guards * @return void * * @throws \\Illuminate\\Auth\\AuthenticationException */ protected function unauthenticated($request, array $guards) { if (in_array(\u0026#39;admin\u0026#39;, $guards)) { throw new AuthenticationException( \u0026#39;Unauthenticated.\u0026#39;, $guards, \u0026#39;http://localhost/app/admin/login\u0026#39; //現行のログイン画面 ); } } ... 最後に、現行のプログラムも新規のLaravelでも、セッションのクッキーパスが同じとすることをお忘れずに。そうでないと、新規のLaravelでは現行のプログラムのセッションにアクセスできません。上では両方ともクッキーパスはデフォルトの/となっています。\n","date":"2020-07-12T07:52:38+09:00","permalink":"https://www.larajapan.com/2020/07/12/laravel%E3%81%B8%E3%81%AE%E7%A7%BB%E8%A1%8C%EF%BC%88%EF%BC%91%EF%BC%89%E4%BB%96%E3%81%AE%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%A0%E3%81%AE%E8%AA%8D%E8%A8%BC%E3%82%92%E8%AA%8D%E8%AD%98/","title":"Laravelへの移行（１）他のプログラムの認証セッションを認識"},{"content":"このブログサイトのように、自分でサーバー(AWS EC2）を持ち、サイトをセットアップしていると、いろいろと出費があります。AWSのサービスに対してのお金とともに、昨今のウェブセキュリティ強化ゆえに、セキュア認証の取得にも年に１回出費があります。\n今年はなぜかその認証の日までに業者からの催促のEメールも来ずに、リマインダーのためのカレンダーにも入れていなかったおかげで、認証が期限切れとなっていると知らず。次の日に朝にサイトにアクセスすると「安全な接続ではありません！」の警告画面となってしまいました。\nこれはいかん、と慌てたのですが、これはいいチャンス、巷で有名な無料のLet\u0026rsquo;s Encryptやらを試して見ようではないか、となりました。インストールのツールもあり便利そうです。\nLet's Encrypt Let\u0026rsquo;s Encryptは、もとMozzila（Firefoxブラウザで有名な）の社員が2014年から始めた事業なのですが、Let\u0026rsquo;s Encryptがどれだけ有名かというと、今年の２月の時点で、なんと10億サイトにセキュア認証がインストールされています。\nウェブサイトにセキュア認証をインストールするには、通常、プライベートキーやCSRをコマンドで作成して、Digicertのような会社に認証してもらい、SSL証明書が発行され、それをウェブサーバーにインストールします。面倒なうえに認証まで待つ必要もあり時間がかかり、しかも有料です。しかし、Let\u0026rsquo;s Encryptでは、それらの作業をすべて自動化したCertbotという便利なツールがあり、意外と簡単です。\nLet\u0026rsquo;s Encryptのセキュア認証のインストールをこれから説明しますが、ここでの話は私の環境、つまりAmazon Linux（Amazon Linux 2ではない）で、ウェブサーバーにはapacheを使用という限定された環境での話なので、注意してください。\nCertbotをインストール Certbotのサイトへ行くと、どのウェブサーバー、どのSystemという選択があり、それぞれにおいて適切な指示をリストした画面に飛ばしてくれます。しかし、残念なことに私が使用しているAmazon LinuxがSystemの選択がありません。\n近いのは、Cent OSかな、ということで、私は、https://certbot.eff.org/lets-encrypt/centos6-apacheのインストラクションを選びました。\nそれによると、まず以下のシェルスクリプトをダウンロードしてコマンドラインで実行可能とします。\n$ wget https://dl.eff.org/certbot-auto $ sudo mv certbot-auto /usr/local/bin/certbot-auto $ sudo chown root /usr/local/bin/certbot-auto $ sudo chmod 0755 /usr/local/bin/certbot-auto Certbotの実行 通常は、この実行だけで済むのですが、残念、早速エラーとなりました。\n$ sudo /usr/local/bin/certbot-auto certonly --apache FATAL: Amazon Linux support is very experimental at present... if you would like to work on improving it, please ensure you have backups and then run this script again with the --debug flag! Alternatively, you can install OS dependencies yourself and run this script again with --no-bootstrap. Amazon Linuxはまだ実験段階ということで、デバッグオプションを追加して再度実行します。\n$ sudo /usr/local/bin/certbot-auto certonly --apache --debug\tBootstrapping dependencies for Amazon... (you can skip this with --no-bootstrap) ... Complete! Creating virtual environment... Traceback (most recent call last): File \u0026#34;\u0026lt;stdin\u0026gt;\u0026#34;, line 27, in \u0026lt;module\u0026gt; File \u0026#34;\u0026lt;stdin\u0026gt;\u0026#34;, line 19, in create_venv File \u0026#34;/usr/lib64/python2.7/subprocess.py\u0026#34;, line 185, in check_call retcode = call(*popenargs, **kwargs) File \u0026#34;/usr/lib64/python2.7/subprocess.py\u0026#34;, line 172, in call return Popen(*popenargs, **kwargs).wait() File \u0026#34;/usr/lib64/python2.7/subprocess.py\u0026#34;, line 394, in __init__ errread, errwrite) File \u0026#34;/usr/lib64/python2.7/subprocess.py\u0026#34;, line 1047, in _execute_child raise child_exception OSError: [Errno 2] No such file or directory なんだか進んだな、と思ったら最後にエラーです。\n調査してみると、私の環境のPythonのバージョンが古いようです。\nということで、前段階でインストールされたバージョン2.7に切り替えます。\n$ sudo alternatives --config python There are 2 programs which provide \u0026#39;python\u0026#39;. Selection Command ----------------------------------------------- + 1 /usr/bin/python2.6 * 2 /usr/bin/python2.7 Enter to keep the current selection[+], or type selection number: 2 そして、すでに完了した段階をスキップするために、\u0026ndash;no-bootstrapを追加して再度実行します。\n$ sudo /usr/local/bin/certbot-auto certonly --apache --debug --no-bootstrap Creating virtual environment... Installing Python packages... Installation succeeded. Saving debug log to /var/log/letsencrypt/letsencrypt.log Plugins selected: Authenticator apache, Installer apache Enter email address (used for urgent renewal and security notices) (Enter \u0026#39;c\u0026#39; to cancel): kenji@lotsofbytes.com - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Please read the Terms of Service at https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf. You must agree in order to register with the ACME server at https://acme-v02.api.letsencrypt.org/directory - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - (A)gree/(C)ancel: A - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Would you be willing to share your email address with the Electronic Frontier Foundation, a founding partner of the Let\u0026#39;s Encrypt project and the non-profit organization that develops Certbot? We\u0026#39;d like to send you email about our work encrypting the web, EFF news, campaigns, and ways to support digital freedom. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - (Y)es/(N)o: N Which names would you like to activate HTTPS for? - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1: larajapan.com 2: www.larajapan.com - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Select the appropriate numbers separated by commas and/or spaces, or leave input blank to select all options shown (Enter \u0026#39;c\u0026#39; to cancel): Obtaining a new certificate Performing the following challenges: http-01 challenge for larajapan.com http-01 challenge for www.larajapan.com Waiting for verification... Cleaning up challenges IMPORTANT NOTES: - Congratulations! Your certificate and chain have been saved at: /etc/letsencrypt/live/larajapan.com/fullchain.pem Your key file has been saved at: /etc/letsencrypt/live/larajapan.com/privkey.pem Your cert will expire on 2020-09-30. To obtain a new or tweaked version of this certificate in the future, simply run certbot-auto again. To non-interactively renew *all* of your certificates, run \u0026#34;certbot-auto renew\u0026#34; - Your account credentials have been saved in your Certbot configuration directory at /etc/letsencrypt. You should make a secure backup of this folder now. This configuration directory will also contain certificates and private keys obtained by Certbot so making regular backups of this folder is ideal. - If you like Certbot, please consider supporting our work by: Donating to ISRG / Let\u0026#39;s Encrypt: https://letsencrypt.org/donate Donating to EFF: https://eff.org/donate-le セキュア認証の作成、成功です！\nさて、上で何が起こっているかを説明すると、まず、必要な環境を作成して必要なライブラリのインストールを行った後、以下が問われます。\n緊急の場合や更新のお知らせのためにEメールアドレス 同意するか。Aをタイプしてください。 先のEメールを共有してよいか。YあるいはNのタイプです。 どのサイトのHttpsが必要か。既存のapacheの設定からリスト作成してれます。指定するならコンマ区切りで番号を、すべてなら単にリターンします。 これらを回答すると、/etc/letsencryptのディレクトリに以下のようにSSL認証関連のいろいろなファイルが作成されます。\n$ cd /etc/letsencrypt $ tree . ├── accounts │ └── acme-v02.api.letsencrypt.org │ └── directory │ └── 20adce2f0bc35e942a75b71a4716e1c3 │ ├── meta.json │ ├── private_key.json │ └── regr.json ├── archive │ └── larajapan.com │ ├── cert1.pem │ ├── chain1.pem │ ├── fullchain1.pem │ └── privkey1.pem ├── csr │ └── 0000_csr-certbot.pem ├── keys │ └── 0000_key-certbot.pem ├── live │ ├── larajapan.com │ │ ├── cert.pem -\u0026gt; ../../archive/larajapan.com/cert1.pem │ │ ├── chain.pem -\u0026gt; ../../archive/larajapan.com/chain1.pem │ │ ├── fullchain.pem -\u0026gt; ../../archive/larajapan.com/fullchain1.pem │ │ ├── privkey.pem -\u0026gt; ../../archive/larajapan.com/privkey1.pem │ │ └── README │ └── README ├── options-ssl-apache.conf ├── renewal │ └── larajapan.com.conf └── renewal-hooks ├── deploy ├── post └── pre apacheの設定 作成された認証関連のファイルを使用するために、apacheの設定ファイル、/etc/httpd/conf/httpd.conf、あるいは、httpd-vhosts.confを以下のように編集します。\n\u0026lt;VirtualHost *:443\u0026gt; Include /etc/letsencrypt/options-ssl-apache.conf SSLCertificateFile /etc/letsencrypt/live/larajapan.com/fullchain.pem SSLCertificateKeyFile /etc/letsencrypt/live/larajapan.com/privkey.pem .. 設定後、apacheを再起動します。\n$ sudo service httpd restart セキュア認証の確認 さて、セキュア認証が問題なくインストールされているか、確認してみましょう。\n以下のサイトで検証できます。\nhttps://www.ssllabs.com/ssltest\n私のサイトのURLを入れて、\nAになりました！\nセキュア認証の更新 巷の有料なセキュア認証は、たいて１年か２年間更新しなくて済むのですが、、実はこの認証は９０日ごとに更新される必要あります。\nそれも自動化するために、以下でのクロンの設定が必要です。\n$ echo \u0026#34;0 0,12 * * * root python -c \u0026#39;import random; import time; time.sleep(random.random() * 3600)\u0026#39; \u0026amp;\u0026amp; /usr/local/bin/certbot-auto renew -q\u0026#34; | sudo tee -a /etc/crontab \u0026gt; /dev/null ９０日後に実際、うまく更新されるかどうか報告しますね。\n","date":"2020-07-05T02:00:43+09:00","permalink":"https://www.larajapan.com/2020/07/05/lets-encrypt%E3%80%80%E7%84%A1%E6%96%99%E3%81%AE%E3%82%BB%E3%82%AD%E3%83%A5%E3%82%A2%E8%AA%8D%E8%A8%BC/","title":"Let's Encrypt　無料のセキュア認証"},{"content":"商品のページのURLが例えばhttps://localhost/product/5734とかだと、5734はproductsテーブルのidの値だな、とわかってしまいます。しかしこれが、商品のIDではなく会員のIDや注文のIDなら大きな問題です。もちろん通常は認証で保護してログインした人のみしか閲覧できないようにしますが、プログラムのバグで認証が外れていたら。。。そんなときにIDがシーケンスの数字でなかったら、例えば、043a9c43-26cd-432f-bb25-a0eb045815b7とかのわけのわからない数字だったら安心です。今回はidの難読化のためのuuidを紹介です。\nUUIDとは wikiによると、 UUID（Universally Unique Identifier）とは、ソフトウェア上でオブジェクトを一意に識別するための識別子である。UUIDは128ビットの数値だが、16進法による550e8400-e29b-41d4-a716-446655440000というような文字列による表現が使われることが多い。元来は分散システム上で統制なしに作成できる識別子として設計されており、したがって将来にわたって重複や偶然の一致が起こらないという前提で用いることができる。\nとあります。「重複が起こらないという前提で用いる」、これがidの代わりとして使用できる理由です。\nLaravelで使用するには phpにはuuidを作成する関数はないのですが、Laravelではもちろんパッケージを通して対応しています。\nまず、migrationを使用してproductsのDBテーブルの項目として追加してみます。 プライマリキーのidはキープして、ユニークなキーを持つuuidとして別に定義します。そうでないと、joinする他のDBテーブルのjoin keyも36文字の長い値を保存することになり効率的ではありません。\ndatabase/migrations/2020_06_25_172420_create_products.php use Illuminate\\Database\\Migrations\\Migration; use Illuminate\\Database\\Schema\\Blueprint; use Illuminate\\Support\\Facades\\Schema; class CreateProducts extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create(\u0026#39;products\u0026#39;, function (Blueprint $table) { $table-\u0026gt;bigIncrements(\u0026#39;id\u0026#39;); $table-\u0026gt;uuid(\u0026#39;uuid\u0026#39;)-\u0026gt;unique(); $table-\u0026gt;char(\u0026#39;name\u0026#39;, 100); $table-\u0026gt;decimal(\u0026#39;price\u0026#39;, 10, 0); $table-\u0026gt;timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists(\u0026#39;products\u0026#39;); } } これをmigrateして、\n$ php artisan migrate mysqlで作成されたテーブルの定義を見ると、\nmysql\u0026gt; describe products; +------------+---------------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +------------+---------------------+------+-----+---------+----------------+ | id | bigint(20) unsigned | NO | PRI | NULL | auto_increment | | uuid | char(36) | NO | UNI | NULL | | | name | char(100) | NO | | NULL | | | price | decimal(10,0) | NO | | NULL | | | created_at | timestamp | YES | | NULL | | | updated_at | timestamp | YES | | NULL | | +------------+---------------------+------+-----+---------+----------------+ 36文字長のユニークなキーを持つuuidの項目が作成されています。\n早速、DBレコードを作成してみます。\nまず、Productのモデルを作成して、\napp/Product.php namespace App; use Illuminate\\Database\\Eloquent\\Model; class Product extends Model { protected $fillable = [ \u0026#39;uuid\u0026#39;, \u0026#39;name\u0026#39;, \u0026#39;price\u0026#39;, ]; } そして、tinkerでレコード作成してみます。\n\u0026gt;\u0026gt;\u0026gt; Product::create([\u0026#39;uuid\u0026#39; =\u0026gt; (string) Str::uuid(), \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;商品名\u0026#39;, \u0026#39;price\u0026#39; =\u0026gt; 1000]); =\u0026gt; App\\Product {#3986 uuid: \u0026#34;043a9c43-26cd-432f-bb25-a0eb045815b7\u0026#34;, name: \u0026#34;商品名\u0026#34;, price: 1000, updated_at: \u0026#34;2020-06-25 18:03:45\u0026#34;, created_at: \u0026#34;2020-06-25 18:03:45\u0026#34;, id: 1, } Str::uuid()はLaravelのヘルパーです。ヘルパーはLaravelが使用しているパッケージのオブジェクトを返すので、stringで文字列にキャストしています。\nidを隠す せっかく、uuidの項目があるのだから、プライマリキーのidが表に出ないように隠しましょう。\nモデルにおいて、hiddenの属性にidを入れます。\napp/Product.php ... class Product extends Model { protected $fillable = [ \u0026#39;uuid\u0026#39;, \u0026#39;name\u0026#39;, \u0026#39;price\u0026#39;, ]; protected $hidden = [ \u0026#39;id\u0026#39; ]; } idが隠れているか、tinkerで確認です。\n\u0026gt;\u0026gt;\u0026gt; Product::find(1); =\u0026gt; App\\Product {#3996 uuid: \u0026#34;043a9c43-26cd-432f-bb25-a0eb045815b7\u0026#34;, name: \u0026#34;商品名\u0026#34;, price: \u0026#34;1000\u0026#34;, created_at: \u0026#34;2020-06-25 18:03:45\u0026#34;, updated_at: \u0026#34;2020-06-25 18:03:45\u0026#34;, } idの項目表示されないですね。\nしかし、直接ならアクセスできます。\n\u0026gt;\u0026gt;\u0026gt; Product::find(1)-\u0026gt;id; =\u0026gt; 1 しかし、配列として変換などするとidの値は入ってきません。\n\u0026gt;\u0026gt;\u0026gt; Product::find(1)-\u0026gt;toArray(); =\u0026gt; [ \u0026#34;uuid\u0026#34; =\u0026gt; \u0026#34;043a9c43-26cd-432f-bb25-a0eb045815b7\u0026#34;, \u0026#34;name\u0026#34; =\u0026gt; \u0026#34;商品名\u0026#34;, \u0026#34;price\u0026#34; =\u0026gt; \u0026#34;1000\u0026#34;, \u0026#34;created_at\u0026#34; =\u0026gt; \u0026#34;2020-06-25 18:03:45\u0026#34;, \u0026#34;updated_at\u0026#34; =\u0026gt; \u0026#34;2020-06-25 18:03:45\u0026#34;, ] コントローラからuuidでアクセス 以下のように、getRouteKeyNane()で、uuidを指定すると、コントローラのURLでuuidを指定してレコードの取得可能です。\napp/Product.php ... class Product extends Model { protected $fillable = [ \u0026#39;uuid\u0026#39;, \u0026#39;name\u0026#39;, \u0026#39;price\u0026#39;, ]; protected $hidden = [ \u0026#39;id\u0026#39; ]; public function getRouteKeyName() { return \u0026#39;uuid\u0026#39;; } } コントローラを作成して、\napp/Http/Controllers/ProductController.php namespace App\\Http\\Controllers; use App\\Product; use App\\Http\\Controllers\\Controller; class ProductController extends Controller { public function show(Product $product) { return $product; } } ルートを設定します。\nroutes/web.php ... Route::resource(\u0026#39;/product\u0026#39;, \u0026#39;ProductController\u0026#39;)-\u0026gt;only(\u0026#39;show\u0026#39;); ... 通常のように、uuidでなくidの値でhttps::/local/product/1としてアクセスすると、\n\u0026gt;\u0026gt;\u0026gt; $option = [ \u0026#39;ssl\u0026#39; =\u0026gt; [\u0026#39;verify_peer\u0026#39; =\u0026gt; false, \u0026#39;verify_peer_name\u0026#39; =\u0026gt; false]];　//これはSSL認証のエラーを抑えるために必要。 =\u0026gt; [ \u0026#34;ssl\u0026#34; =\u0026gt; [ \u0026#34;verify_peer\u0026#34; =\u0026gt; false, \u0026#34;verify_peer_name\u0026#34; =\u0026gt; false, ], ] \u0026gt;\u0026gt;\u0026gt; file_get_contents(\u0026#39;https://localhost/product/1\u0026#39;, false, stream_context_create($option)); PHP Warning: file_get_contents(https://localhost/product/1): failed to open stream: HTTP request failed! HTTP/1.1 404 Not Found in Psy Shell code on line 1 =\u0026gt; false 404のページが見つからない、のエラーです。\nしかし、https://localhost/product/043a9c43-26cd-432f-bb25-a0eb045815b7のようにuuidでアクセスすると、以下のようにレコードを取得してくれます。\n\u0026gt;\u0026gt;\u0026gt; file_get_contents(\u0026#39;https://localhost/product/043a9c43-26cd-432f-bb25-a0eb045815b7\u0026#39;, false, stream_context_create($option)); =\u0026gt; \u0026#34;{\u0026#34;uuid\u0026#34;:\u0026#34;043a9c43-26cd-432f-bb25-a0eb045815b7\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;\\u5546\\u54c1\\u540d\u0026#34;,\u0026#34;price\u0026#34;:\u0026#34;1000\u0026#34;,\u0026#34;created_at\u0026#34;:\u0026#34;2020-06-25 18:03:45\u0026#34;,\u0026#34;updated_at\u0026#34;:\u0026#34;2020-06-25 18:03:45\u0026#34;}\u0026#34; \u0026gt;\u0026gt;\u0026gt; ","date":"2020-06-28T07:29:45+09:00","permalink":"https://www.larajapan.com/2020/06/28/uuid/","title":"UUID　ユニークなID"},{"content":"Laravelのプロジェクトでは、環境変数の設定は.envのファイルで行います。インストールするプロジェクトの環境（開発、ステージング、プロダクション）ごとで内容を変更できるし、.gitignoreに入れておけばバージョン管理のレポジトリでプライベートなデータ（パスワードやapiのトークンなど）を共有する必要もなく安全です。この機能、私の昔のphpのプロジェクト、つまりLaravel生誕以前の時代でのプロジェクトで現在もお客さんに使用されているプロジェクトでも取り入れたいです。\nまず、この機能を提供するのは、phpdotenvというパッケージです。\nhttps://github.com/vlucas/phpdotenv\nLaravelのプロジェクトでは、以下のコマンドを実行すると、\n$ composer show | grep dotenv vlucas/phpdotenv v3.6.4 Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically. とパッケージが使用されていることが確認されます。\nさて、Laravelでないphpのプロジェクトでこのパッケージを使うには、以下のように、プロジェクトのディレクトリを作成してから、パッケージをインストールします。\n$ mkdir dotenv $ composer require vlucas/phpdotenv チェックのためにテストのプログラムを作成します。\ntest.php \u0026lt;?php require_once \u0026#39;vendor/autoload.php\u0026#39;; $dotenv = Dotenv\\Dotenv::createImmutable(__DIR__); $dotenv-\u0026gt;load(); echo $_ENV[\u0026#39;BASE_DIR\u0026#39;].\u0026#34;\\n\u0026#34;; .envのファイルも作成します。\n.env BASE_DIR=/var/www/example この時点で、このプロジェクトのディレクトリ構造は、以下のようになります。\n. ├── composer.json ├── composer.lock ├── .env ├── test.php └── vendor ├── autoload.php ├── composer ├── graham-campbell ├── phpoption ├── symfony └── vlucas テストを実行してみます。\n$ php test.php /var/www/example .envから値を取得できましたね。以下のような設定も可能です。\n.env BASE_DIR=/var/www/example IMAGE_DIR=${BASE_DIR}/images すでに設定した環境変数を.envの中で変数として使用できます。つまり、Laravelでも同様なことができるのですね、\n","date":"2020-06-21T01:14:36+09:00","permalink":"https://www.larajapan.com/2020/06/21/phpdotenv/","title":"phpdotenv"},{"content":"Windows 10では、ファイル名として使用してはいけない文字がいくつかあります。入力しようとすると、「ファイル名に次の文字は使えません」とエラーになります。さて、ダウンロードのファイルにこれらの禁止文字が使用されていたらどうしましょう？\n例えば、サーバーにアップされているファイル名は「567.jpg」と商品ID.jpgのフォーマットとします。しかし、ユーザーがダウンロードする時には「世界一おいしい? りんご、4/1から販売.jpg」と商品名.jpgにしたいときです。しかし、商品名に含まれる、?（半角疑問符）や/（半角バックスラッシュ）が禁止文字なのです。\n手っ取り早いのは、半角の禁止文字を皆全角に置き換えてしまえばよいです。\nということでヘルパーの関数を作成します。\napp/Helpers/MyHelper.php namespace App\\Helpers; class MyHelper { /** * 禁止半角文字を全角に * @param string $value * @return string */ public static function convertToValidFilename($value) { $from = [\u0026#39;\\\\\u0026#39;, \u0026#39;/\u0026#39;, \u0026#39;:\u0026#39;, \u0026#39;*\u0026#39;, \u0026#39;?\u0026#39;, \u0026#39;\u0026#34;\u0026#39;, \u0026#39;\u0026gt;\u0026#39;, \u0026#39;\u0026lt;\u0026#39;, \u0026#39;|\u0026#39;]; $to = [\u0026#39;￥\u0026#39;, \u0026#39;／\u0026#39;, \u0026#39;：\u0026#39;, \u0026#39;＊\u0026#39;, \u0026#39;？\u0026#39;, \u0026#39;”\u0026#39;, \u0026#39;＞\u0026#39;, \u0026#39;＜\u0026#39;, \u0026#39;｜\u0026#39;]; $patterns = array_map(function($val) { return \u0026#39;#\\\\\u0026#39;.$val.\u0026#39;#\u0026#39;; }, $from); return preg_replace($patterns, $to, $value); } } preg_replace()の最初の引数に、その前の行のarray_map()で作成された配列を与えているところに注意してください。\n通常は、\n\u0026gt;\u0026gt;\u0026gt; preg_replace(\u0026#39;/123/\u0026#39;, \u0026#39;xyz\u0026#39;, \u0026#39;01234\u0026#39;); =\u0026gt; \u0026#34;0xyz4\u0026#34; のように、パターンには/（半角スラッシュ）で囲んだ文字列を与えますが、配列を与えて複数の置き換えをすべて実行することもできます。\nconvertToValidFilename()では、$patternsが以下のような配列となり、\nArray ( [0] =\u0026gt; #\\\\# [1] =\u0026gt; #\\/# [2] =\u0026gt; #\\:# [3] =\u0026gt; #\\*# [4] =\u0026gt; #\\?# [5] =\u0026gt; #\\\u0026#34;# [6] =\u0026gt; #\\\u0026gt;# [7] =\u0026gt; #\\\u0026lt;# [8] =\u0026gt; #\\|# ) これらは、以下の対応する$toに、\nArray ( [0] =\u0026gt; \\ [1] =\u0026gt; / [2] =\u0026gt; : [3] =\u0026gt; * [4] =\u0026gt; ? [5] =\u0026gt; \u0026#34; [6] =\u0026gt; \u0026gt; [7] =\u0026gt; \u0026lt; [8] =\u0026gt; | ) に置き換えます。\nパターンにおいて、通常使われる/（半角バックスラッシュ）の代わりに、#（半角シャープ）で囲まれているのは、/（半角バックスラッシュ）がパターン内の文字としてありエスケープ文字となってしまうからです。preg_replace()のパターンの境界文字は、そのように必要に応じて変えることができるのです。\n早速、tinkerを使用して試してみましょう。\n\u0026gt;\u0026gt;\u0026gt; use App\\Helpers\\MyHelper; \u0026gt;\u0026gt;\u0026gt; MyHelper::convertToValidFileName(\u0026#39;世界一おいしい? りんご、4/1から販売.jpg\u0026#39;); =\u0026gt; \u0026#34;世界一おいしい？ りんご、4／1から販売.jpg\u0026#34; 禁止文字が半角から全角に置き換わりましたね。\n最後に、これを用いたコントローラのダウンロードメソッドは、こんな感じとなります。\npublic function download(Product $product) { Storage::download(storage_path().\u0026#39;/\u0026#39;.$product-\u0026gt;product-id.\u0026#39;.jpg\u0026#39;, MyHelper::convertToValidFileName($product-\u0026gt;name).\u0026#39;jpg\u0026#39;); } ","date":"2020-06-14T08:58:25+09:00","permalink":"https://www.larajapan.com/2020/06/14/%E3%83%80%E3%82%A6%E3%83%B3%E3%83%AD%E3%83%BC%E3%83%89%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E5%90%8D%E7%A6%81%E6%AD%A2%E6%96%87%E5%AD%97%E3%81%AB%E5%AF%BE%E5%BF%9C/","title":"ダウンロードファイル名禁止文字に対応"},{"content":"DB変更の履歴の作成をトレイトで簡単にEloquentのモデルに装着できるとしたころまでが前回ですが、巷では似たような仕組みでより良いパッケージがすでにあります。今回はそれを紹介します。\nLaravel Auditing パッケージは、Laravel Auditingという名でララベル専用です。\n独自のサイトもあります。\nhttp://www.laravel-auditing.com/\nまずはインストール、\n$ composer require owen-it/laravel-auditing 次に設定ファイルのパブリッシュ。\n$ php artisan vendor:publish --provider \u0026#34;OwenIt\\Auditing\\AuditingServiceProvider\u0026#34; --tag=\u0026#34;config\u0026#34; config/audit.phpが作成されます。\nDBテーブルの作成が必要なので、以下を実行してmigrationファイルを作成します。\n$ php artisan vendor:publish --provider \u0026#34;OwenIt\\Auditing\\AuditingServiceProvider\u0026#34; --tag=\u0026#34;migrations\u0026#34; 作成されたファイルを見てみましょう。\ndatabase/migrations/2020_06_05_152546_create_audits_table.php use Illuminate\\Database\\Migrations\\Migration; use Illuminate\\Database\\Schema\\Blueprint; use Illuminate\\Support\\Facades\\Schema; class CreateAuditsTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create(\u0026#39;audits\u0026#39;, function (Blueprint $table) { $table-\u0026gt;bigIncrements(\u0026#39;id\u0026#39;); $table-\u0026gt;string(\u0026#39;user_type\u0026#39;)-\u0026gt;nullable(); $table-\u0026gt;unsignedBigInteger(\u0026#39;user_id\u0026#39;)-\u0026gt;nullable(); $table-\u0026gt;string(\u0026#39;event\u0026#39;); $table-\u0026gt;morphs(\u0026#39;auditable\u0026#39;); $table-\u0026gt;text(\u0026#39;old_values\u0026#39;)-\u0026gt;nullable(); $table-\u0026gt;text(\u0026#39;new_values\u0026#39;)-\u0026gt;nullable(); $table-\u0026gt;text(\u0026#39;url\u0026#39;)-\u0026gt;nullable(); $table-\u0026gt;ipAddress(\u0026#39;ip_address\u0026#39;)-\u0026gt;nullable(); $table-\u0026gt;string(\u0026#39;user_agent\u0026#39;, 1023)-\u0026gt;nullable(); $table-\u0026gt;string(\u0026#39;tags\u0026#39;)-\u0026gt;nullable(); $table-\u0026gt;timestamps(); $table-\u0026gt;index([\u0026#39;user_id\u0026#39;, \u0026#39;user_type\u0026#39;]); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::drop(\u0026#39;audits\u0026#39;); } } これを見ると、作成するDBテーブルの名前はauditsで、 user_type, user_idは、アクセスしたユーザーの情報、 old_valuesとnew_valuesはモデルの古い値と新規の値が入る、 url、ip_address, user_agentはウェブのどこからアクセスされたかの情報、などと検討できますが、その他の項目はどう使われるが興味あるところです。特に、morphs(\u0026lsquo;auditable\u0026rsquo;)の部分はなんでしょう？見たことないですね。\nとりあえず、migrateしてテーブルを作成します。\n$ php artisan migrate Migrating: 2020_06_05_152546_create_audits_table Migrated: 2020_06_05_152546_create_audits_table (0.04 seconds) 作成されたテーブルの構造を見ると、\nmysql\u0026gt; describe audits; +----------------+---------------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +----------------+---------------------+------+-----+---------+----------------+ | id | bigint(20) unsigned | NO | PRI | NULL | auto_increment | | user_type | varchar(255) | YES | | NULL | | | user_id | bigint(20) unsigned | YES | MUL | NULL | | | event | varchar(255) | NO | | NULL | | | auditable_type | varchar(255) | NO | MUL | NULL | | | auditable_id | bigint(20) unsigned | NO | | NULL | | | old_values | text | YES | | NULL | | | new_values | text | YES | | NULL | | | url | text | YES | | NULL | | | ip_address | varchar(45) | YES | | NULL | | | user_agent | varchar(1023) | YES | | NULL | | | tags | varchar(255) | YES | | NULL | | | created_at | timestamp | YES | | NULL | | | updated_at | timestamp | YES | | NULL | | +----------------+---------------------+------+-----+---------+----------------+ $table-\u0026gt;morphs(\u0026lsquo;auditable\u0026rsquo;);は、auditable_typeとauditable_idの２つの項目が作成される結果となっていますが、何の値が入るのでしょうね。\nモデルでの設定 さて、実際にモデルに設定してみましょう。私の仕組みと同様にトレイト(Auditable)を使用しますが、同じ名前のインターフェース(Auditable)も使われています。以下の「これが必要！」の部分です。\napp/User.php namespace App; use Illuminate\\Contracts\\Auth\\MustVerifyEmail; use Illuminate\\Foundation\\Auth\\User as Authenticatable; use Illuminate\\Notifications\\Notifiable; use Request; use OwenIt\\Auditing\\Contracts\\Auditable; // これが必要！ class User extends Authenticatable implements Auditable　// これが必要！ { use Notifiable; use \\OwenIt\\Auditing\\Auditable; // これが必要！ /** * The attributes that are mass assignable. * * @var array */ protected $fillable = [ \u0026#39;name\u0026#39;, \u0026#39;email\u0026#39;, \u0026#39;password\u0026#39;, ]; /** * The attributes that should be hidden for arrays. * * @var array */ protected $hidden = [ \u0026#39;password\u0026#39;, \u0026#39;remember_token\u0026#39;, ]; /** * The attributes that should be cast to native types. * * @var array */ protected $casts = [ \u0026#39;email_verified_at\u0026#39; =\u0026gt; \u0026#39;datetime\u0026#39;, ]; /** * ここで監査したい項目を指定！ * * @var array */ protected $auditInclude = [ \u0026#39;name\u0026#39;, \u0026#39;email\u0026#39;, ]; 早速、tinkerで実行してみましょう。 tinkerはコンソールの実行なので、設定ファイルを以下のように変更する必要あります。\nconfig/audit.php ... /* |-------------------------------------------------------------------------- | Audit Console |-------------------------------------------------------------------------- | | Whether console events should be audited (eg. php artisan db:seed). | */ \u0026#39;console\u0026#39; =\u0026gt; true, ]; Psy Shell v0.10.4 (PHP 7.2.16 — cli) by Justin Hileman \u0026gt;\u0026gt;\u0026gt; User::truncate(); //　リセットして空にします。 \u0026gt;\u0026gt;\u0026gt; $user = User::create([\u0026#39;name\u0026#39; =\u0026gt; \u0026#39;name1\u0026#39;, \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;test1@example.com\u0026#39;, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;test\u0026#39;]); [!] Aliasing \u0026#39;User\u0026#39; to \u0026#39;App\\User\u0026#39; for this Tinker session. =\u0026gt; App\\User {#3995 name: \u0026#34;name1\u0026#34;, email: \u0026#34;test1@example.com\u0026#34;, updated_at: \u0026#34;2020-06-05 17:01:38\u0026#34;, created_at: \u0026#34;2020-06-05 17:01:38\u0026#34;, id: 1, } \u0026gt;\u0026gt;\u0026gt; $user-\u0026gt;update([\u0026#39;email\u0026#39; =\u0026gt; \u0026#39;test2@example.com\u0026#39;]); =\u0026gt; true \u0026gt;\u0026gt;\u0026gt; $user-\u0026gt;audits; // 作成した監査レコードを取得 =\u0026gt; Illuminate\\Database\\Eloquent\\Collection {#4005 all: [ OwenIt\\Auditing\\Models\\Audit {#4021 id: 1, user_type: null, user_id: null, event: \u0026#34;created\u0026#34;, auditable_type: \u0026#34;App\\User\u0026#34;, auditable_id: 1, old_values: \u0026#34;[]\u0026#34;, new_values: \u0026#34;{\u0026#34;name\u0026#34;:\u0026#34;name1\u0026#34;,\u0026#34;email\u0026#34;:\u0026#34;test1@example.com\u0026#34;}\u0026#34;, url: \u0026#34;console\u0026#34;, ip_address: \u0026#34;127.0.0.1\u0026#34;, user_agent: \u0026#34;Symfony\u0026#34;, tags: null, created_at: \u0026#34;2020-06-05 17:01:38\u0026#34;, updated_at: \u0026#34;2020-06-05 17:01:38\u0026#34;, }, OwenIt\\Auditing\\Models\\Audit {#4026 id: 2, user_type: null, user_id: null, event: \u0026#34;updated\u0026#34;, auditable_type: \u0026#34;App\\User\u0026#34;, auditable_id: 1, old_values: \u0026#34;{\u0026#34;email\u0026#34;:\u0026#34;test1@example.com\u0026#34;}\u0026#34;, new_values: \u0026#34;{\u0026#34;email\u0026#34;:\u0026#34;test2@example.com\u0026#34;}\u0026#34;, url: \u0026#34;console\u0026#34;, ip_address: \u0026#34;127.0.0.1\u0026#34;, user_agent: \u0026#34;Symfony\u0026#34;, tags: null, created_at: \u0026#34;2020-06-05 17:01:43\u0026#34;, updated_at: \u0026#34;2020-06-05 17:01:43\u0026#34;, }, ], } なるほど、\nauditable_type: \u0026#34;App\\User\u0026#34;, auditable_id: 1, これらの項目には、何のモデルのどのレコードの履歴という情報を残しているのですね。\n引き続いて、レコード削除も試みると、\n\u0026gt;\u0026gt;\u0026gt; $user-\u0026gt;delete(); =\u0026gt; true \u0026gt;\u0026gt;\u0026gt; DB::table(\u0026#39;audits\u0026#39;)-\u0026gt;get(); =\u0026gt; Illuminate\\Support\\Collection {#3986 all: [ {#4019 +\u0026#34;id\u0026#34;: 1, +\u0026#34;user_type\u0026#34;: null, +\u0026#34;user_id\u0026#34;: null, +\u0026#34;event\u0026#34;: \u0026#34;created\u0026#34;, +\u0026#34;auditable_type\u0026#34;: \u0026#34;App\\User\u0026#34;, +\u0026#34;auditable_id\u0026#34;: 1, +\u0026#34;old_values\u0026#34;: \u0026#34;[]\u0026#34;, +\u0026#34;new_values\u0026#34;: \u0026#34;{\u0026#34;name\u0026#34;:\u0026#34;name1\u0026#34;,\u0026#34;email\u0026#34;:\u0026#34;test1@example.com\u0026#34;}\u0026#34;, +\u0026#34;url\u0026#34;: \u0026#34;console\u0026#34;, +\u0026#34;ip_address\u0026#34;: \u0026#34;127.0.0.1\u0026#34;, +\u0026#34;user_agent\u0026#34;: \u0026#34;Symfony\u0026#34;, +\u0026#34;tags\u0026#34;: null, +\u0026#34;created_at\u0026#34;: \u0026#34;2020-06-05 17:01:38\u0026#34;, +\u0026#34;updated_at\u0026#34;: \u0026#34;2020-06-05 17:01:38\u0026#34;, }, {#4025 +\u0026#34;id\u0026#34;: 2, +\u0026#34;user_type\u0026#34;: null, +\u0026#34;user_id\u0026#34;: null, +\u0026#34;event\u0026#34;: \u0026#34;updated\u0026#34;, +\u0026#34;auditable_type\u0026#34;: \u0026#34;App\\User\u0026#34;, +\u0026#34;auditable_id\u0026#34;: 1, +\u0026#34;old_values\u0026#34;: \u0026#34;{\u0026#34;email\u0026#34;:\u0026#34;test1@example.com\u0026#34;}\u0026#34;, +\u0026#34;new_values\u0026#34;: \u0026#34;{\u0026#34;email\u0026#34;:\u0026#34;test2@example.com\u0026#34;}\u0026#34;, +\u0026#34;url\u0026#34;: \u0026#34;console\u0026#34;, +\u0026#34;ip_address\u0026#34;: \u0026#34;127.0.0.1\u0026#34;, +\u0026#34;user_agent\u0026#34;: \u0026#34;Symfony\u0026#34;, +\u0026#34;tags\u0026#34;: null, +\u0026#34;created_at\u0026#34;: \u0026#34;2020-06-05 17:01:43\u0026#34;, +\u0026#34;updated_at\u0026#34;: \u0026#34;2020-06-05 17:01:43\u0026#34;, }, {#4032 +\u0026#34;id\u0026#34;: 3, +\u0026#34;user_type\u0026#34;: null, +\u0026#34;user_id\u0026#34;: null, +\u0026#34;event\u0026#34;: \u0026#34;deleted\u0026#34;, +\u0026#34;auditable_type\u0026#34;: \u0026#34;App\\User\u0026#34;, +\u0026#34;auditable_id\u0026#34;: 1, +\u0026#34;old_values\u0026#34;: \u0026#34;{\u0026#34;name\u0026#34;:\u0026#34;name1\u0026#34;,\u0026#34;email\u0026#34;:\u0026#34;test2@example.com\u0026#34;}\u0026#34;, +\u0026#34;new_values\u0026#34;: \u0026#34;[]\u0026#34;, +\u0026#34;url\u0026#34;: \u0026#34;console\u0026#34;, +\u0026#34;ip_address\u0026#34;: \u0026#34;127.0.0.1\u0026#34;, +\u0026#34;user_agent\u0026#34;: \u0026#34;Symfony\u0026#34;, +\u0026#34;tags\u0026#34;: null, +\u0026#34;created_at\u0026#34;: \u0026#34;2020-06-05 17:04:27\u0026#34;, +\u0026#34;updated_at\u0026#34;: \u0026#34;2020-06-05 17:04:27\u0026#34;, }, ], } 削除されたレコードゆえにUser::find()では取得できないので、クエリビルダーで取得しましたが、３番目のauditsのレコードが作成されていること確認できます。また、eventの項目に注目してもらうと、created, updated, deletedという値が入っていることも確認できます。\n今回はここまでですが、他にもタグを追加できる機能や、古いレコードを自動的に削除する機能など将来紹介したい機能がたくさんあります。また、クラス定義のインターフェイス、つまり、implementsや、よく知らないmorphsのトピックも将来扱いたいです。\n","date":"2020-06-07T04:55:17+09:00","permalink":"https://www.larajapan.com/2020/06/07/db%E5%A4%89%E6%9B%B4%E5%B1%A5%E6%AD%B4%E3%81%AE%E4%BD%9C%E6%88%90%EF%BC%88%EF%BC%94%EF%BC%89%E3%83%91%E3%83%83%E3%82%B1%E3%83%BC%E3%82%B8/","title":"DB変更履歴の作成（４）パッケージ"},{"content":"前回のDB変更履歴保存のメカニズムは、Userのモデルのためだけですが、どのモデルでも同様なことが簡単にできるようにトレイトを作成してみます。\nまず、audit_logsのテーブルを作成し直します。\nどのモデルかわかるように項目を増やしました。前回、Userモデルのキーという意味でuser_idとしたところは、使用した会員のidの値が入るように変更しました。そして、モデルのキーは、model_keyという項目となっています。\ndatabase/migrations/2020_05_28_171205_create_audit_logs_table.php use Illuminate\\Database\\Migrations\\Migration; use Illuminate\\Database\\Schema\\Blueprint; use Illuminate\\Support\\Facades\\Schema; class CreateAuditLogsTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create(\u0026#39;audit_logs\u0026#39;, function (Blueprint $table) { $table-\u0026gt;bigIncrements(\u0026#39;id\u0026#39;); $table-\u0026gt;string(\u0026#39;route\u0026#39;)-\u0026gt;nullable(); // 実行したプログラムのルート $table-\u0026gt;string(\u0026#39;path\u0026#39;)-\u0026gt;nullable(); // 実行したプログラムのパス名 $table-\u0026gt;string(\u0026#39;model\u0026#39;)-\u0026gt;nullable; // モデルのクラス名 $table-\u0026gt;unsignedBigInteger(\u0026#39;model_key\u0026#39;)-\u0026gt;nullable(); // モデルのプライマリキー $table-\u0026gt;unsignedBigInteger(\u0026#39;user_id\u0026#39;)-\u0026gt;nullable(); // ユーザーのid $table-\u0026gt;longText(\u0026#39;old_value\u0026#39;)-\u0026gt;nullable(); // 変更前の値 $table-\u0026gt;longText(\u0026#39;new_value\u0026#39;)-\u0026gt;nullable(); // 変更後の値 $table-\u0026gt;timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists(\u0026#39;audit_logs\u0026#39;); } } 次に、トレイトを作成します。\n前回、boot()の関数名が、bootAuditTrait()と、\u0026lsquo;boot\u0026rsquo; + \u0026lsquo;AuditTrait\u0026rsquo;（トレイト名）となっていることに注意してください。\napp/AuditTrait namespace App; use Request; use Route; trait AuditTrait { /** * Bootイベント * * 関数名は、boot()でなく\u0026#39;boot\u0026#39; + トレイト名 * * @return void */ public static function bootAuditTrait() { // parent::boot()はコールしない static::created(function ($model) { $model-\u0026gt;addAuditLog(\u0026#39;created\u0026#39;); }); static::updated(function ($model) { $model-\u0026gt;addAuditLog(\u0026#39;updated\u0026#39;); }); static::deleted(function ($model) { $model-\u0026gt;addAuditLog(\u0026#39;deleted\u0026#39;); }); } public function addAuditLog($event) { $route = optional(Route::getCurrentRoute())-\u0026gt;getName(); $path = Request::path(); $model = get_class($this); $model_key = $this-\u0026gt;id; $user_id = optional(auth()-\u0026gt;user())-\u0026gt;id; $old_value = $new_value = null; if (in_array($event, [\u0026#39;created\u0026#39;, \u0026#39;updated\u0026#39;])) { $new_value = serialize($this-\u0026gt;attributes); } if (in_array($event, [\u0026#39;updated\u0026#39;, \u0026#39;deleted\u0026#39;])) { $old_value = serialize($this-\u0026gt;original); } AuditLog::create([ \u0026#39;route\u0026#39; =\u0026gt; $route, \u0026#39;path\u0026#39; =\u0026gt; $path, \u0026#39;model\u0026#39; =\u0026gt; $model, \u0026#39;model_key\u0026#39; =\u0026gt; $model_key, \u0026#39;user_id\u0026#39; =\u0026gt; $user_id, \u0026#39;old_value\u0026#39; =\u0026gt; $old_value, \u0026#39;new_value\u0026#39; =\u0026gt; $new_value, ]); } } 上のトレイトにより、Userのクラスでは、use AuditTraittの１行の追加だけで済みます。\napp/User.php namespace App; use Illuminate\\Contracts\\Auth\\MustVerifyEmail; use Illuminate\\Foundation\\Auth\\User as Authenticatable; use Illuminate\\Notifications\\Notifiable; use Request; class User extends Authenticatable { use Notifiable; use AuditTrait; /** * The attributes that are mass assignable. * * @var array */ protected $fillable = [ \u0026#39;name\u0026#39;, \u0026#39;email\u0026#39;, \u0026#39;password\u0026#39;, ]; /** * The attributes that should be hidden for arrays. * * @var array */ protected $hidden = [ \u0026#39;password\u0026#39;, \u0026#39;remember_token\u0026#39;, ]; /** * The attributes that should be cast to native types. * * @var array */ protected $casts = [ \u0026#39;email_verified_at\u0026#39; =\u0026gt; \u0026#39;datetime\u0026#39;, ]; } さて、tinkerの出番です。変更をテストみましょう。\nPsy Shell v0.10.4 (PHP 7.2.16 — cli) by Justin Hileman \u0026gt;\u0026gt;\u0026gt; use App\\AuditLog; \u0026gt;\u0026gt;\u0026gt; $user = User::create([\u0026#39;name\u0026#39; =\u0026gt; \u0026#39;name1\u0026#39;, \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;test1@example.com\u0026#39;, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;test\u0026#39;]); [!] Aliasing \u0026#39;User\u0026#39; to \u0026#39;App\\User\u0026#39; for this Tinker session. =\u0026gt; App\\User {#3990 name: \u0026#34;name1\u0026#34;, email: \u0026#34;test1@example.com\u0026#34;, updated_at: \u0026#34;2020-05-28 18:36:06\u0026#34;, created_at: \u0026#34;2020-05-28 18:36:06\u0026#34;, id: 1, } \u0026gt;\u0026gt;\u0026gt; $user-\u0026gt;update([\u0026#39;email\u0026#39; =\u0026gt; \u0026#39;test2@example.com\u0026#39;]); =\u0026gt; true \u0026gt;\u0026gt;\u0026gt; $user-\u0026gt;delete(); =\u0026gt; true \u0026gt;\u0026gt;\u0026gt; AuditLog::all(); =\u0026gt; Illuminate\\Database\\Eloquent\\Collection {#4015 all: [ App\\AuditLog {#4016 id: 1, route: null, path: \u0026#34;/\u0026#34;, model: \u0026#34;App\\User\u0026#34;, model_key: 1, user_id: null, old_value: null, new_value: \u0026#34;a:6:{s:4:\u0026#34;name\u0026#34;;s:5:\u0026#34;name1\u0026#34;;s:5:\u0026#34;email\u0026#34;;s:17:\u0026#34;test1@example.com\u0026#34;;s:8:\u0026#34;password\u0026#34;;s:4:\u0026#34;test\u0026#34;;s:10:\u0026#34;updated_at\u0026#34;;s:19:\u0026#34;2020-05-28 18:36:06\u0026#34;;s:10:\u0026#34;created_at\u0026#34;;s:19:\u0026#34;2020-05-28 18:36:06\u0026#34;;s:2:\u0026#34;id\u0026#34;;i:1;}\u0026#34;, created_at: \u0026#34;2020-05-28 18:36:06\u0026#34;, updated_at: \u0026#34;2020-05-28 18:36:06\u0026#34;, }, App\\AuditLog {#4017 id: 2, route: null, path: \u0026#34;/\u0026#34;, model: \u0026#34;App\\User\u0026#34;, model_key: 1, user_id: null, old_value: \u0026#34;a:6:{s:4:\u0026#34;name\u0026#34;;s:5:\u0026#34;name1\u0026#34;;s:5:\u0026#34;email\u0026#34;;s:17:\u0026#34;test1@example.com\u0026#34;;s:8:\u0026#34;password\u0026#34;;s:4:\u0026#34;test\u0026#34;;s:10:\u0026#34;updated_at\u0026#34;;s:19:\u0026#34;2020-05-28 18:36:06\u0026#34;;s:10:\u0026#34;created_at\u0026#34;;s:19:\u0026#34;2020-05-28 18:36:06\u0026#34;;s:2:\u0026#34;id\u0026#34;;i:1;}\u0026#34;, new_value: \u0026#34;a:6:{s:4:\u0026#34;name\u0026#34;;s:5:\u0026#34;name1\u0026#34;;s:5:\u0026#34;email\u0026#34;;s:17:\u0026#34;test2@example.com\u0026#34;;s:8:\u0026#34;password\u0026#34;;s:4:\u0026#34;test\u0026#34;;s:10:\u0026#34;updated_at\u0026#34;;s:19:\u0026#34;2020-05-28 18:36:10\u0026#34;;s:10:\u0026#34;created_at\u0026#34;;s:19:\u0026#34;2020-05-28 18:36:06\u0026#34;;s:2:\u0026#34;id\u0026#34;;i:1;}\u0026#34;, created_at: \u0026#34;2020-05-28 18:36:10\u0026#34;, updated_at: \u0026#34;2020-05-28 18:36:10\u0026#34;, }, App\\AuditLog {#4018 id: 3, route: null, path: \u0026#34;/\u0026#34;, model: \u0026#34;App\\User\u0026#34;, model_key: 1, user_id: null, old_value: \u0026#34;a:6:{s:4:\u0026#34;name\u0026#34;;s:5:\u0026#34;name1\u0026#34;;s:5:\u0026#34;email\u0026#34;;s:17:\u0026#34;test2@example.com\u0026#34;;s:8:\u0026#34;password\u0026#34;;s:4:\u0026#34;test\u0026#34;;s:10:\u0026#34;updated_at\u0026#34;;s:19:\u0026#34;2020-05-28 18:36:10\u0026#34;;s:10:\u0026#34;created_at\u0026#34;;s:19:\u0026#34;2020-05-28 18:36:06\u0026#34;;s:2:\u0026#34;id\u0026#34;;i:1;}\u0026#34;, new_value: null, created_at: \u0026#34;2020-05-28 18:36:14\u0026#34;, updated_at: \u0026#34;2020-05-28 18:36:14\u0026#34;, }, ], } \u0026gt;\u0026gt;\u0026gt; 前回と同様に、レコードの作成、編集、削除のすべてが、audit_logsのレコードに記録されています。\n","date":"2020-05-31T01:34:51+09:00","permalink":"https://www.larajapan.com/2020/05/31/db%E5%A4%89%E6%9B%B4%E5%B1%A5%E6%AD%B4%E3%81%AE%E4%BD%9C%E6%88%90%EF%BC%88%EF%BC%93%EF%BC%89%E3%83%88%E3%83%AC%E3%82%A4%E3%83%88/","title":"DB変更履歴の作成（３）トレイト"},{"content":"前回においては、Eloquentのbootメソッドを利用してDB変更の情報（監査の情報）を取得することができました。今回は、これをDBに記録する部分を考えてみましょう。\n監査のデータを保存するテーブル 監査のデータを保存するテーブルを、audit_logsとしてmigrationファイルを作成します。 $ php artisan make:migration create_audit_logs_table Created Migration: 2020_05_20_024714_create_audit_logs_table 作成されたファイルを編集して、テーブルの項目定義を入れます。\ndatabase/migrations/2020_05_20_024714_create_audit_logs_table use Illuminate\\Database\\Migrations\\Migration; use Illuminate\\Database\\Schema\\Blueprint; use Illuminate\\Support\\Facades\\Schema; class CreateAuditLogsTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create(\u0026#39;audit_logs\u0026#39;, function (Blueprint $table) { $table-\u0026gt;bigIncrements(\u0026#39;id\u0026#39;); $table-\u0026gt;unsignedBigInteger(\u0026#39;user_id\u0026#39;)-\u0026gt;default(0)-\u0026gt;index(); // Userモデルのid $table-\u0026gt;string(\u0026#39;path\u0026#39;)-\u0026gt;default(\u0026#39;\u0026#39;); // 実行したプログラムのパス名 $table-\u0026gt;longText(\u0026#39;old_value\u0026#39;)-\u0026gt;nullable(); // 変更前の値 $table-\u0026gt;longText(\u0026#39;new_value\u0026#39;)-\u0026gt;nullable(); // 変更後の値 $table-\u0026gt;timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists(\u0026#39;audit_logs\u0026#39;); } old_valueとnew_valueには、レコードの項目のすべての値をserializeして入れるために大きいデータタイプとしています。\n以下を実行してテーブルを作成します。\n$ php artisan migrate Migrating: 2020_05_20_024714_create_audit_logs_table Migrated: 2020_05_20_024714_create_audit_logs_table (0.02 seconds) AuditLogのクラスも作成します。\napp/AuditLog namespace App; use Illuminate\\Database\\Eloquent\\Model; class AuditLog extends Model { protected $guarded = []; } そして、Userクラスのプログラムを書き替えます。\napp/User.php namespace App; use Illuminate\\Contracts\\Auth\\MustVerifyEmail; use Illuminate\\Foundation\\Auth\\User as Authenticatable; use Illuminate\\Notifications\\Notifiable; use Request; class User extends Authenticatable { use Notifiable; /** * The attributes that are mass assignable. * * @var array */ protected $fillable = [ \u0026#39;name\u0026#39;, \u0026#39;email\u0026#39;, \u0026#39;password\u0026#39;, ]; /** * The attributes that should be hidden for arrays. * * @var array */ protected $hidden = [ \u0026#39;password\u0026#39;, \u0026#39;remember_token\u0026#39;, ]; /** * The attributes that should be cast to native types. * * @var array */ protected $casts = [ \u0026#39;email_verified_at\u0026#39; =\u0026gt; \u0026#39;datetime\u0026#39;, ]; protected static function boot() { parent::boot(); static::created(function ($model) { AuditLog::create([ \u0026#39;user_id\u0026#39; =\u0026gt; $model-\u0026gt;id, \u0026#39;path\u0026#39; =\u0026gt; Request::path(), \u0026#39;old_value\u0026#39; =\u0026gt; null, \u0026#39;new_value\u0026#39; =\u0026gt; serialize($model-\u0026gt;attributes), ]); }); static::updated(function ($model) { AuditLog::create([ \u0026#39;user_id\u0026#39; =\u0026gt; $model-\u0026gt;id, \u0026#39;path\u0026#39; =\u0026gt; Request::path(), \u0026#39;old_value\u0026#39; =\u0026gt; serialize($model-\u0026gt;original), \u0026#39;new_value\u0026#39; =\u0026gt; serialize($model-\u0026gt;attributes), ]); }); static::deleted(function ($model) { AuditLog::create([ \u0026#39;user_id\u0026#39; =\u0026gt; $model-\u0026gt;id, \u0026#39;path\u0026#39; =\u0026gt; Request::path(), \u0026#39;old_value\u0026#39; =\u0026gt; serialize($model-\u0026gt;original), \u0026#39;new_value\u0026#39; =\u0026gt; null, ]); }); } } tinkerでレコードの作成、Eメールアドレスの変更、そしてレコードの削除を実行します。\nPsy Shell v0.10.2 (PHP 7.2.16 — cli) by Justin Hileman \u0026gt;\u0026gt;\u0026gt; use App\\AuditLog; \u0026gt;\u0026gt;\u0026gt; $user = User::create([\u0026#39;name\u0026#39; =\u0026gt; \u0026#39;name1\u0026#39;, \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;test1@example.com\u0026#39;, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;test\u0026#39;]); [!] Aliasing \u0026#39;User\u0026#39; to \u0026#39;App\\User\u0026#39; for this Tinker session. =\u0026gt; App\\User {#3058 name: \u0026#34;name1\u0026#34;, email: \u0026#34;test1@example.com\u0026#34;, updated_at: \u0026#34;2020-05-20 03:28:18\u0026#34;, created_at: \u0026#34;2020-05-20 03:28:18\u0026#34;, id: 1, } \u0026gt;\u0026gt;\u0026gt; $user-\u0026gt;update([\u0026#39;email\u0026#39; =\u0026gt; \u0026#39;test2@example.com\u0026#39;]); =\u0026gt; true \u0026gt;\u0026gt;\u0026gt; $user-\u0026gt;delete(); =\u0026gt; true AuditLogのレコードを取り出してみると、すべてのの変更履歴、監査の情報が保存されています。\n\u0026gt;\u0026gt;\u0026gt; AuditLog::all(); =\u0026gt; Illuminate\\Database\\Eloquent\\Collection {#3072 all: [ App\\AuditLog {#3073 id: 1, user_id: 1, path: \u0026#34;/\u0026#34;, old_value: null, new_value: \u0026#34;a:6:{s:4:\u0026#34;name\u0026#34;;s:5:\u0026#34;name1\u0026#34;;s:5:\u0026#34;email\u0026#34;;s:17:\u0026#34;test1@example.com\u0026#34;;s:8:\u0026#34;password\u0026#34;;s:4:\u0026#34;test\u0026#34;;s:10:\u0026#34;updated_at\u0026#34;;s:19:\u0026#34;2020-05-20 03:28:18\u0026#34;;s:10:\u0026#34;created_at\u0026#34;;s:19:\u0026#34;2020-05-20 03:28:18\u0026#34;;s:2:\u0026#34;id\u0026#34;;i:1;}\u0026#34;, created_at: \u0026#34;2020-05-20 03:28:18\u0026#34;, updated_at: \u0026#34;2020-05-20 03:28:18\u0026#34;, }, App\\AuditLog {#3074 id: 2, user_id: 1, path: \u0026#34;/\u0026#34;, old_value: \u0026#34;a:6:{s:4:\u0026#34;name\u0026#34;;s:5:\u0026#34;name1\u0026#34;;s:5:\u0026#34;email\u0026#34;;s:17:\u0026#34;test1@example.com\u0026#34;;s:8:\u0026#34;password\u0026#34;;s:4:\u0026#34;test\u0026#34;;s:10:\u0026#34;updated_at\u0026#34;;s:19:\u0026#34;2020-05-20 03:28:18\u0026#34;;s:10:\u0026#34;created_at\u0026#34;;s:19:\u0026#34;2020-05-20 03:28:18\u0026#34;;s:2:\u0026#34;id\u0026#34;;i:1;}\u0026#34;, new_value: \u0026#34;a:6:{s:4:\u0026#34;name\u0026#34;;s:5:\u0026#34;name1\u0026#34;;s:5:\u0026#34;email\u0026#34;;s:17:\u0026#34;test2@example.com\u0026#34;;s:8:\u0026#34;password\u0026#34;;s:4:\u0026#34;test\u0026#34;;s:10:\u0026#34;updated_at\u0026#34;;s:19:\u0026#34;2020-05-20 03:28:38\u0026#34;;s:10:\u0026#34;created_at\u0026#34;;s:19:\u0026#34;2020-05-20 03:28:18\u0026#34;;s:2:\u0026#34;id\u0026#34;;i:1;}\u0026#34;, created_at: \u0026#34;2020-05-20 03:28:38\u0026#34;, updated_at: \u0026#34;2020-05-20 03:28:38\u0026#34;, }, App\\AuditLog {#3075 id: 3, user_id: 1, path: \u0026#34;/\u0026#34;, old_value: \u0026#34;a:6:{s:4:\u0026#34;name\u0026#34;;s:5:\u0026#34;name1\u0026#34;;s:5:\u0026#34;email\u0026#34;;s:17:\u0026#34;test2@example.com\u0026#34;;s:8:\u0026#34;password\u0026#34;;s:4:\u0026#34;test\u0026#34;;s:10:\u0026#34;updated_at\u0026#34;;s:19:\u0026#34;2020-05-20 03:28:38\u0026#34;;s:10:\u0026#34;created_at\u0026#34;;s:19:\u0026#34;2020-05-20 03:28:18\u0026#34;;s:2:\u0026#34;id\u0026#34;;i:1;}\u0026#34;, new_value: null, created_at: \u0026#34;2020-05-20 03:28:47\u0026#34;, updated_at: \u0026#34;2020-05-20 03:28:47\u0026#34;, }, ], } ","date":"2020-05-24T03:33:06+09:00","permalink":"https://www.larajapan.com/2020/05/24/db%E5%A4%89%E6%9B%B4%E5%B1%A5%E6%AD%B4%E3%81%AE%E4%BD%9C%E6%88%90%EF%BC%88%EF%BC%92%EF%BC%89%E7%9B%A3%E6%9F%BB%E3%81%AE%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92%E4%BF%9D%E5%AD%98/","title":"DB変更履歴の作成（２）監査のデータを保存"},{"content":"データベースに保存されているデータはいつも現時点での値であり、過去の値は保存されていません。しかし、レコードのデータがどう変更したかという情報は重要です。どう変更したかだけではなく、誰がいつ、アプリのどの機能を使用して、さらにはどういう理由で、という情報も必要になってきます。例えば、カスタマサポートにおいて、会員の住所が変更されて注文した商品が届かなかったとき、それらの情報があれば、いついつにお客様が住所を変更しましたね、とか即答できます。さて、これらの変更イベント時の変更情報（監査あるいはAudit情報）、Laravelではどのように効率的にDBに保存することが可能でしょうか？\nEloquentのイベントメソッド LaravelのEloquentを利用すると、DBのイベント、つまりレコードの作成、変更、削除の３つのイベントに連結したメソッドが提供されています。そして、それらのイベントが発生する前あるいは後においてユーザー定義のコードをコールバックさせることが可能です。これらを利用したら、希望するAuditの情報をキャプチャすることが簡単にできます。 さてLaravelのデフォルトのプロジェクトを使って見てみましょう。 まず、UserのクラスにDBのレコード変更後のイベントのコールバックを作成します。以下のようにbootメソッドにおいて見慣れないクラスメソッドupdatedをコールすることで可能となります。\napp/User.php namespace App; use Illuminate\\Contracts\\Auth\\MustVerifyEmail; use Illuminate\\Foundation\\Auth\\User as Authenticatable; use Illuminate\\Notifications\\Notifiable; class User extends Authenticatable { use Notifiable; ... protected static function boot() { parent::boot(); static::updated(function () { echo \u0026#34;updated\\n\u0026#34;; }); } } ちなみに、updatedは更新後にコールされるものですが、新規作成後は、created、削除後はdeletedというメソッドになります。\ntinkerを使用して実行してみます。\n\u0026gt;\u0026gt;\u0026gt; $user = User::find(1); =\u0026gt; App\\User {#3048 id: 1, name: \u0026#34;近藤 篤司\u0026#34;, email: \u0026#34;manabu.takahashi@example.net\u0026#34;, email_verified_at: \u0026#34;2020-02-13 04:27:15\u0026#34;, created_at: \u0026#34;2020-02-13 04:27:15\u0026#34;, updated_at: \u0026#34;2020-05-17 18:31:32\u0026#34;, } \u0026gt;\u0026gt;\u0026gt; $user-\u0026gt;update([\u0026#39;email\u0026#39; =\u0026gt; \u0026#39;test@example.com\u0026#39;]); updated =\u0026gt; true コールバックで定義したupdatedが表示されましたね！\n変更前後の情報の取得 DBのレコード変更イベントでユーザー定義の関数をコールバックを確認できたら、次は変更前後でのレコードの情報をどうやって取得するかです。以下見てください。コールバックにそれらの情報を表示するコードを追加しました。\napp/User.php namespace App; use Illuminate\\Contracts\\Auth\\MustVerifyEmail; use Illuminate\\Foundation\\Auth\\User as Authenticatable; use Illuminate\\Notifications\\Notifiable; class User extends Authenticatable { use Notifiable; ... protected static function boot() { parent::boot(); static::updated(function ($model) { echo \u0026#34;updated\\n\u0026#34;; print_r($model-\u0026gt;original); print_r($model-\u0026gt;attributes); }); } } tinkerで実行してみます。\n\u0026gt;\u0026gt;\u0026gt; $user = User::find(1); [!] Aliasing \u0026#39;User\u0026#39; to \u0026#39;App\\User\u0026#39; for this Tinker session. =\u0026gt; App\\User {#3067 id: 1, name: \u0026#34;近藤 篤司\u0026#34;, email: \u0026#34;test@example.com\u0026#34;, email_verified_at: \u0026#34;2020-02-13 04:27:15\u0026#34;, created_at: \u0026#34;2020-02-13 04:27:15\u0026#34;, updated_at: \u0026#34;2020-05-17 18:59:31\u0026#34;, } \u0026gt;\u0026gt;\u0026gt; $user-\u0026gt;update([\u0026#39;email\u0026#39; =\u0026gt; \u0026#39;test2@example.com\u0026#39;]); updated Array ( [id] =\u0026gt; 1 [name] =\u0026gt; 近藤 篤司 [email] =\u0026gt; test@example.com [email_verified_at] =\u0026gt; 2020-02-13 04:27:15 [password] =\u0026gt; $2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi [remember_token] =\u0026gt; cDvZkS5nOA [created_at] =\u0026gt; 2020-02-13 04:27:15 [updated_at] =\u0026gt; 2020-05-17 18:59:31 ) Array ( [id] =\u0026gt; 1 [name] =\u0026gt; 近藤 篤司 [email] =\u0026gt; test2@example.com [email_verified_at] =\u0026gt; 2020-02-13 04:27:15 [password] =\u0026gt; $2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi [remember_token] =\u0026gt; cDvZkS5nOA [created_at] =\u0026gt; 2020-02-13 04:27:15 [updated_at] =\u0026gt; 2020-05-17 19:03:27 ) =\u0026gt; true 前後の値、キャプチャできましたね。これで変更履歴を作成するには十分な準備ができました。これらを利用すれば、変更前後の値あるいは差分を保存できますね。\n","date":"2020-05-18T11:58:18+09:00","permalink":"https://www.larajapan.com/2020/05/18/db%E3%81%AE%E5%A4%89%E6%9B%B4%E5%B1%A5%E6%AD%B4%E3%81%AE%E4%BD%9C%E6%88%90/","title":"DBの変更履歴の作成"},{"content":"マスターDBの変更が複製DBに反映されるまでの時間差を考えると、サイトの特定のページでは複製DBのアクセスを使用したくないかもしれません。例えば、会員のログインなど会員のアカウントの情報に関わるページとか、Eコマースのチェックアウトのページとか、あるいは管理者のみがアクセスする管理ページとか。そうなると、特定のコントローラだけに複製DBコネクションを限定するにはどのようにすればよいのでしょう？\nまず、マスターDBのみのDBコネクションをmysqlとして、複製DBを使用するDBコネクションをmysql_rwとして、config/database.phpで２つのコネクションを設定します。\nconfig/database.php \u0026#39;default\u0026#39; =\u0026gt; env(\u0026#39;DB_CONNECTION\u0026#39;, \u0026#39;mysql\u0026#39;), \u0026#39;connections\u0026#39; =\u0026gt; [ \u0026#39;mysql\u0026#39; =\u0026gt; [ \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;mysql\u0026#39;, \u0026#39;url\u0026#39; =\u0026gt; env(\u0026#39;DATABASE_URL\u0026#39;), \u0026#39;host\u0026#39; =\u0026gt; env(\u0026#39;DB_HOST\u0026#39;, \u0026#39;127.0.0.1\u0026#39;), \u0026#39;port\u0026#39; =\u0026gt; env(\u0026#39;DB_PORT\u0026#39;, \u0026#39;3306\u0026#39;), \u0026#39;database\u0026#39; =\u0026gt; env(\u0026#39;DB_DATABASE\u0026#39;, \u0026#39;forge\u0026#39;), \u0026#39;username\u0026#39; =\u0026gt; env(\u0026#39;DB_USERNAME\u0026#39;, \u0026#39;forge\u0026#39;), \u0026#39;password\u0026#39; =\u0026gt; env(\u0026#39;DB_PASSWORD\u0026#39;, \u0026#39;\u0026#39;), \u0026#39;unix_socket\u0026#39; =\u0026gt; env(\u0026#39;DB_SOCKET\u0026#39;, \u0026#39;\u0026#39;), \u0026#39;charset\u0026#39; =\u0026gt; \u0026#39;utf8\u0026#39;, \u0026#39;collation\u0026#39; =\u0026gt; \u0026#39;utf8_unicode_ci\u0026#39;, \u0026#39;prefix\u0026#39; =\u0026gt; \u0026#39;\u0026#39;, \u0026#39;strict\u0026#39; =\u0026gt; false, \u0026#39;engine\u0026#39; =\u0026gt; null, \u0026#39;timezone\u0026#39; =\u0026gt; \u0026#39;Asia/Tokyo\u0026#39;, ], \u0026#39;mysql_rw\u0026#39; =\u0026gt; [ \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;mysql\u0026#39;, \u0026#39;url\u0026#39; =\u0026gt; env(\u0026#39;DATABASE_URL\u0026#39;), \u0026#39;read\u0026#39; =\u0026gt; [ \u0026#39;host\u0026#39; =\u0026gt; env(\u0026#39;DB_HOST_RO\u0026#39;, env(\u0026#39;DB_HOST\u0026#39;, \u0026#39;127.0.0.1\u0026#39;)), \u0026#39;username\u0026#39; =\u0026gt; env(\u0026#39;DB_USERNAME_RO\u0026#39;, env(\u0026#39;DB_USERNAME\u0026#39;)), \u0026#39;password\u0026#39; =\u0026gt; env(\u0026#39;DB_PASSWORD_RO\u0026#39;, env(\u0026#39;DB_PASSWORD\u0026#39;)), ], \u0026#39;write\u0026#39; =\u0026gt; [ \u0026#39;host\u0026#39; =\u0026gt; env(\u0026#39;DB_HOST\u0026#39;, \u0026#39;127.0.0.1\u0026#39;), \u0026#39;username\u0026#39; =\u0026gt; env(\u0026#39;DB_USERNAME\u0026#39;, \u0026#39;forge\u0026#39;), \u0026#39;password\u0026#39; =\u0026gt; env(\u0026#39;DB_PASSWORD\u0026#39;, \u0026#39;\u0026#39;), ], \u0026#39;port\u0026#39; =\u0026gt; env(\u0026#39;DB_PORT\u0026#39;, \u0026#39;3306\u0026#39;), \u0026#39;database\u0026#39; =\u0026gt; env(\u0026#39;DB_DATABASE\u0026#39;), \u0026#39;unix_socket\u0026#39; =\u0026gt; env(\u0026#39;DB_SOCKET\u0026#39;, \u0026#39;\u0026#39;), \u0026#39;charset\u0026#39; =\u0026gt; \u0026#39;utf8\u0026#39;, \u0026#39;collation\u0026#39; =\u0026gt; \u0026#39;utf8_unicode_ci\u0026#39;, \u0026#39;prefix\u0026#39; =\u0026gt; \u0026#39;\u0026#39;, \u0026#39;strict\u0026#39; =\u0026gt; false, \u0026#39;engine\u0026#39; =\u0026gt; null, \u0026#39;timezone\u0026#39; =\u0026gt; \u0026#39;Asia/Tokyo\u0026#39;, ], .. .envのファイルでは、DB_HOST_RO, DB_USERNAME_RO, DB_PASSWORD_ROの設定項目の指定が必要となります。 ROはRead Onlyという意味で読み込み専用のユーザーが複製DBにアクセスします。\nまた、デフォルトは、マスターDBのコネクションであることに注意してください。\n次に、例えば、商品閲覧ページにおいてDBコネクションを変えるには、以下のように、コントローラのコンストラクタにおいて、コネクションのデフォルトを上書きしてDB::reconnect()で接続します。\napp/Http/Controllers/ProductController.php namespace App\\Http\\Controllers; class ProductController extends Controller { public function __construct() { parent::__construct(); config([\u0026#39;database.default\u0026#39; =\u0026gt; \u0026#39;mysql_rw\u0026#39;]); DB::reconnect(); .. 簡単ですね。\nいちいち、それぞれのコンストラクタでコネクションを変えるのが面倒なら、以下のよう親のコントローラにおいてまとめて指定することも可能です。試してはいないですが、ミドルウェアでの対応も可能なはずです。\napp/Http/Controllers/Controller.php public function __construct() { $route = Route::currentRouteName(); // 特定のページだけDBコネクションを変更する $routes = [ \u0026#39;user.home\u0026#39;, \u0026#39;user.page\u0026#39;, \u0026#39;user.shop\u0026#39;, \u0026#39;user.category\u0026#39;, \u0026#39;user.collection\u0026#39;, \u0026#39;user.product\u0026#39;, ]; if (in_array($route, $routes)) { config([\u0026#39;database.default\u0026#39; =\u0026gt; \u0026#39;mysql_rw\u0026#39;]); DB::reconnect(); } ... ","date":"2020-04-26T01:28:18+09:00","permalink":"https://www.larajapan.com/2020/04/26/db%E3%81%AE%E8%B2%A0%E8%8D%B7%E3%81%AE%E7%B7%A9%E5%92%8C%EF%BC%883%EF%BC%89%E3%82%B3%E3%83%B3%E3%83%88%E3%83%AD%E3%83%BC%E3%83%A9%E3%81%AB%E3%82%88%E3%82%8A%E6%8E%A5%E7%B6%9A%E3%82%92%E5%A4%89/","title":"DBの負荷の緩和（3）コントローラにより接続を変える"},{"content":"前回において、Laravelの読み込みと書き込みのDBを自動使い分け機能により、プログラムの設定だけで簡単にDB負荷を緩和できることを知りました。今回は、DBのトランザクションにおいてその機能の振る舞いをチェックしてみます。\n例えば、以前の個数管理での在庫数変更の以下の例を見てます。\n$product = Product::find(1);　// 複製のDBから読み込む $product-\u0026gt;update([\u0026#39;inventory\u0026#39; =\u0026gt; $product-\u0026gt;inventory - 1]); // マスターのDBで書き込む 上では、まず、商品の情報をDBから読み込み、そこから得る在庫数をもとに次の行で在庫数を変更します。 しかし、２つのDBを使用するとなると、最初の行は複製DBからの読み込みとなり、次の行ではマスターのDBにおいての書き込みとなります。\n問題は、複製のDBはマスターDBとの時間差があるので、もしかしたら最初の行で読み込んだのは最新の在庫数ではないかもしれないません。つまりデータの整合性の問題となる可能性です。\nそうなら、以下のようにトランザクションで囲んでみましょう。\nDB::beginTransaction(); $product = Product::find(1); $product-\u0026gt;update([\u0026#39;inventory\u0026#39; =\u0026gt; $product-\u0026gt;inventory - 1]); DB::commit(); しかし、トランザクションはマスターDB内だけでの作業の整合性を保証するものなので、同じ問題となるのでは？\nLaravelのコードを深く追ってみます。\nまず、LaravelのDB接続のクラスでは、マスターDBの$pdoと読み込み専用の$readPdoの２つのDB接続のための変数が宣言されています。\n/vendor/laravel/framework/src/Illuminate/Database/Connection.php ... class Connection implements ConnectionInterface { use DetectsDeadlocks, DetectsLostConnections, Concerns\\ManagesTransactions; /** * The active PDO connection. * * @var \\PDO|\\Closure */ protected $pdo; /** * The active PDO connection used for reads. * * @var \\PDO|\\Closure */ protected $readPdo; ... そして、同じクラス内で、読み込みDBの接続の取得は、以下のメソッドで定義されています。\n/** * Get the current PDO connection used for reading. * * @return \\PDO */ public function getReadPdo() { if ($this-\u0026gt;transactions \u0026gt; 0) { //トランザクションがあると、マスターのDB接続を使用 return $this-\u0026gt;getPdo(); } if ($this-\u0026gt;recordsModified \u0026amp;\u0026amp; $this-\u0026gt;getConfig(\u0026#39;sticky\u0026#39;)) { return $this-\u0026gt;getPdo(); } if ($this-\u0026gt;readPdo instanceof Closure) { return $this-\u0026gt;readPdo = call_user_func($this-\u0026gt;readPdo); } return $this-\u0026gt;readPdo ?: $this-\u0026gt;getPdo(); } ... トランザクションがあると、読み込み専用のDBではなく、マスターのDBが使われる条件文があります。つまり、トランザクションの中では、たとえDB実行文が読み込み（SELECT SQL文）であっても、読み込みのDB接続は使われずマスターDBしか使われないということです。\nちなみに、上のコードではstickyのときも、条件文で読み込みのDBの使用しないようになっています。これは以下のようにデータベースの設定で指定できます。そして、目的はDB更新後に、書き込みをしたマスターDBから値を読みたいときです。トランザクションと同様に、読み込み専用のDBからでは時間差の問題が生ずるからです。良く考えられていますね。\nconfig/database.php ... \u0026#39;mysql\u0026#39; =\u0026gt; [ \u0026#39;read\u0026#39; =\u0026gt; [ \u0026#39;host\u0026#39; =\u0026gt; [ \u0026#39;192.168.1.1\u0026#39;, \u0026#39;196.168.1.2\u0026#39;, ], ], \u0026#39;write\u0026#39; =\u0026gt; [ \u0026#39;host\u0026#39; =\u0026gt; [ \u0026#39;196.168.1.3\u0026#39;, ], ], \u0026#39;sticky\u0026#39; =\u0026gt; true, \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;mysql\u0026#39;, \u0026#39;database\u0026#39; =\u0026gt; \u0026#39;database\u0026#39;, \u0026#39;username\u0026#39; =\u0026gt; \u0026#39;root\u0026#39;, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;\u0026#39;, \u0026#39;charset\u0026#39; =\u0026gt; \u0026#39;utf8mb4\u0026#39;, \u0026#39;collation\u0026#39; =\u0026gt; \u0026#39;utf8mb4_unicode_ci\u0026#39;, \u0026#39;prefix\u0026#39; =\u0026gt; \u0026#39;\u0026#39;, ], ... ","date":"2020-04-20T02:31:25+09:00","permalink":"https://www.larajapan.com/2020/04/20/db%E3%81%AE%E8%B2%A0%E8%8D%B7%E3%81%AE%E7%B7%A9%E5%92%8C%EF%BC%88%EF%BC%92%EF%BC%89%E3%83%88%E3%83%A9%E3%83%B3%E3%82%B6%E3%82%AF%E3%82%B7%E3%83%A7%E3%83%B3/","title":"DBの負荷の緩和（２）トランザクション"},{"content":"お客さんのサイトのアクセスが最近10倍以上に増えてきました。ロードバランスもなく1台にウェブもDBもメールサーバーもあるAWSの仮想マシンなので、とりあえず、仮想マシンをアップグレードしました。\nしかし、アップグレードしてもDBのトランザクションでデッドロックが起きるなどの問題が起こります。これはDBやOSのカーネルの調整が必要となるか、それともやはり、ロードバランスが必要なときか、しかしそんな専門的知識もないしそれを学習や試験する時間も必要だし。。。と悩んでいるときに、DBの読み書きでDB接続を自動的に使い分ける機能がLaravelにあることを見つけました。\nDBの複製は、MySQLデータベースのレプリカにAWS RDSを利用で、すでに設定してあり、これをDBの読み込み専用に使います。書き込みは本マシンのマスターのDBを使います。\nさて、Laravel側で変更することは以下のDB設定の変更です。まず、新規にmysql_rwなる接続を定義します。\nconfig/database.php ... \u0026#39;mysql_rw\u0026#39; =\u0026gt; [ \u0026#39;write\u0026#39; =\u0026gt; [ // 書き込み \u0026#39;host\u0026#39; =\u0026gt; \u0026#39;127.0.0.1\u0026#39; ], \u0026#39;read\u0026#39; =\u0026gt; [ // 読み込み専用、複製DB \u0026#39;host\u0026#39; =\u0026gt; \u0026#39;127.0.0.2\u0026#39; // RDSのendpointのURLあるいはプライベートIPアドレス ], \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;mysql\u0026#39;, \u0026#39;url\u0026#39; =\u0026gt; env(\u0026#39;DATABASE_URL\u0026#39;), \u0026#39;port\u0026#39; =\u0026gt; env(\u0026#39;DB_PORT\u0026#39;, \u0026#39;3306\u0026#39;), \u0026#39;database\u0026#39; =\u0026gt; env(\u0026#39;DB_DATABASE\u0026#39;, \u0026#39;forge\u0026#39;), \u0026#39;username\u0026#39; =\u0026gt; env(\u0026#39;DB_USERNAME\u0026#39;, \u0026#39;forge\u0026#39;), \u0026#39;password\u0026#39; =\u0026gt; env(\u0026#39;DB_PASSWORD\u0026#39;, \u0026#39;\u0026#39;), \u0026#39;unix_socket\u0026#39; =\u0026gt; env(\u0026#39;DB_SOCKET\u0026#39;, \u0026#39;\u0026#39;), \u0026#39;charset\u0026#39; =\u0026gt; \u0026#39;utf8mb4\u0026#39;, \u0026#39;collation\u0026#39; =\u0026gt; \u0026#39;utf8mb4_unicode_ci\u0026#39;, \u0026#39;prefix\u0026#39; =\u0026gt; \u0026#39;\u0026#39;, \u0026#39;prefix_indexes\u0026#39; =\u0026gt; true, \u0026#39;strict\u0026#39; =\u0026gt; true, \u0026#39;engine\u0026#39; =\u0026gt; null, \u0026#39;options\u0026#39; =\u0026gt; extension_loaded(\u0026#39;pdo_mysql\u0026#39;) ? array_filter([ PDO::MYSQL_ATTR_SSL_CA =\u0026gt; env(\u0026#39;MYSQL_ATTR_SSL_CA\u0026#39;), ]) : [], ], ... 上の例では、複製のDBのユーザーとパスワードはマスターのDBのユーザーと同じという仮定ですが、違うなら以下のようにも指定できます。\n\u0026#39;write\u0026#39; =\u0026gt; [ \u0026#39;host\u0026#39; =\u0026gt; \u0026#39;127.0.0.1\u0026#39;, \u0026#39;username\u0026#39; =\u0026gt; env(\u0026#39;DB_USERNAME\u0026#39;, \u0026#39;forge\u0026#39;), \u0026#39;password\u0026#39; =\u0026gt; env(\u0026#39;DB_PASSWORD\u0026#39;, \u0026#39;\u0026#39;), ], \u0026#39;read\u0026#39; =\u0026gt; [ \u0026#39;host\u0026#39; =\u0026gt; \u0026#39;127.0.0.2\u0026#39;, \u0026#39;username\u0026#39; =\u0026gt; env(\u0026#39;DB_USERNAME_RO\u0026#39;, env(\u0026#39;DB_USERNAME\u0026#39;)), \u0026#39;password\u0026#39; =\u0026gt; env(\u0026#39;DB_PASSWORD_RO\u0026#39;, env(\u0026#39;DB_PASSWORD\u0026#39;)), ], さらに、複製DBが1つでは足りないときは、複数の設定も可能です。\n\u0026#39;read\u0026#39; =\u0026gt; [ \u0026#39;host\u0026#39; =\u0026gt; [\u0026#39;127.0.0.2\u0026#39;, \u0026#39;127.0.0.3\u0026#39;, \u0026#39;127.0.0.4\u0026#39;], \u0026#39;username\u0026#39; =\u0026gt; env(\u0026#39;DB_USERNAME_RO\u0026#39;, env(\u0026#39;DB_USERNAME\u0026#39;)), \u0026#39;password\u0026#39; =\u0026gt; env(\u0026#39;DB_PASSWORD_RO\u0026#39;, env(\u0026#39;DB_PASSWORD\u0026#39;)), ], 最後に、.envを編集して、\n... DB_CONNECTION=mysql_rw ... とDB接続を変更します。\n","date":"2020-04-13T04:47:24+09:00","permalink":"https://www.larajapan.com/2020/04/13/db%E3%81%AE%E8%B2%A0%E8%8D%B7%E3%81%AE%E7%B7%A9%E5%92%8C/","title":"DBの負荷の緩和"},{"content":"前回の「個数管理」のコードに少し肉付けをして実際に近いケースを考慮してみます。\n例えば、Ｅコマースにおいてチェックアウトの際にカートに追加したアイテムの在庫を確保するとします。しかし、カートには複数のアイテムが含まれるかもしれないので、すべてのアイテムにおいて在庫確保成功とはいかないかもしれません。在庫数の更新失敗のアイテムがあるときにはすでに在庫確保したアイテムも元に戻す必要あります。\nCheckoutというクラスを作成し、reserveInventoryというメソッドを作成します。 DBのトランザクションとカスタムExceptionに注目してください。\nApp/Checkout.php namespace App; use DB; use Exception; use App\\Product; use App\\Exceptions\\InventoryException; use Illuminate\\Database\\Eloquent\\Model; class Checkout extends Model { public static function reserveInventory($ids) { try { DB::beginTransaction(); foreach($ids as $id =\u0026gt; $quantity) { $count = Product::query() -\u0026gt;where(\u0026#39;id\u0026#39;, \u0026#39;=\u0026#39;, $id) -\u0026gt;whereRaw(\u0026#34;inventory - $quantity \u0026gt;= 0\u0026#34;) -\u0026gt;decrement(\u0026#39;inventory\u0026#39;, $quantity); if ($count == 0) { throw new InventoryException(); // 在庫がなくて在庫数を更新できなかったとき } } DB::commit(); } catch (InventoryException $e) { DB::rollBack(); return \u0026#39;購入しようとしている商品の在庫が十分にありません\u0026#39;; } catch (Exception $e) { DB::rollBack(); return \u0026#39;DBエラーが発生しました\u0026#39; . $e-\u0026gt;getMessage(); } return \u0026#39;在庫確保成功！\u0026#39;; } } reserveInventory()の引数は、product.idとquantityの値の配列関数です。例えば、product.id = 1で購入個数１個なら、[ 1 =\u0026gt; 2 ]。\n上で使用されている、InventoryExceptionの定義は、\nApp/ExceptionsInventoryException.php namespace App\\Exceptions; use Exception; class InventoryException extends Exception { public function __construct($message = \u0026#39;\u0026#39;) { parent::__construct($message); } } さて、tinkerで実行してみましょう。\nまず、商品を２つ用意します。商品１の在庫は２で、商品２の在庫は１とします。\n\u0026gt;\u0026gt;\u0026gt; App\\Product::all() =\u0026gt; Illuminate\\Database\\Eloquent\\Collection {#3059 all: [ App\\Product {#3083 id: 1, name: \u0026#34;商品1\u0026#34;, price: \u0026#34;1000\u0026#34;, inventory: 2, created_at: \u0026#34;2020-03-27 19:35:37\u0026#34;, updated_at: \u0026#34;2020-04-02 23:06:36\u0026#34;, }, App\\Product {#3071 id: 2, name: \u0026#34;商品2\u0026#34;, price: \u0026#34;2000\u0026#34;, inventory: 1, created_at: \u0026#34;2020-04-02 23:05:52\u0026#34;, updated_at: \u0026#34;2020-04-02 23:05:52\u0026#34;, }, ], } 次に、カートのアイテムを用意して在庫確保します。\n商品１を１個、商品２を１個購入です。\n\u0026gt;\u0026gt;\u0026gt; $products = [ 1 =\u0026gt; 1, 2 =\u0026gt; 1 ]; =\u0026gt; [ 1 =\u0026gt; 1, 2 =\u0026gt; 1, ] \u0026gt;\u0026gt;\u0026gt; Cart::reserveInventory($products); =\u0026gt; \u0026#34;在庫確保成功！\u0026#34; \u0026gt;\u0026gt;\u0026gt; App\\Product::all(); =\u0026gt; Illuminate\\Database\\Eloquent\\Collection {#3060 all: [ App\\Product {#3075 id: 1, name: \u0026#34;商品1\u0026#34;, price: \u0026#34;1000\u0026#34;, inventory: 1, created_at: \u0026#34;2020-03-27 19:35:37\u0026#34;, updated_at: \u0026#34;2020-04-02 23:08:53\u0026#34;, }, App\\Product {#3078 id: 2, name: \u0026#34;商品2\u0026#34;, price: \u0026#34;2000\u0026#34;, inventory: 0, created_at: \u0026#34;2020-04-02 23:05:52\u0026#34;, updated_at: \u0026#34;2020-04-02 23:08:53\u0026#34;, }, ], } 商品１の在庫は１で、商品２の在庫は０となりました。うまく在庫数に反映されています。\n更新失敗を見るために、もう１度在庫確保を実行してみます。\n\u0026gt;\u0026gt;\u0026gt; Cart::reserveInventory($products); =\u0026gt; \u0026#34;購入しようとしている商品の在庫が十分にありません\u0026#34; \u0026gt;\u0026gt;\u0026gt; App\\Product::all(); =\u0026gt; Illuminate\\Database\\Eloquent\\Collection {#3070 all: [ App\\Product {#3072 id: 1, name: \u0026#34;商品1\u0026#34;, price: \u0026#34;1000\u0026#34;, inventory: 1, created_at: \u0026#34;2020-03-27 19:35:37\u0026#34;, updated_at: \u0026#34;2020-04-02 23:08:53\u0026#34;, }, App\\Product {#3082 id: 2, name: \u0026#34;商品2\u0026#34;, price: \u0026#34;2000\u0026#34;, inventory: 0, created_at: \u0026#34;2020-04-02 23:05:52\u0026#34;, updated_at: \u0026#34;2020-04-02 23:08:53\u0026#34;, }, ], } 今度は、在庫エラーが出ました。商品１は更新されますが、商品２においては在庫がないのでInventoryExceptionがthrowされ、トランザクションのためにDBへの変更はロールバックされて商品１の変更ももとに戻ります。\n以上は、シンプル化された例なので、改良点はたくさんありそうです。エラー文に商品名を入れるとか、在庫エラーを管理者にメールで伝えるとか。\n","date":"2020-04-05T01:41:59+09:00","permalink":"https://www.larajapan.com/2020/04/05/%E5%80%8B%E6%95%B0%E7%AE%A1%E7%90%86%EF%BC%9A%E5%AE%9F%E7%94%A8%E3%82%92%E8%80%83%E3%81%88%E3%81%9F%E3%82%B1%E3%83%BC%E3%82%B9/","title":"個数管理：実用を考えたケース"},{"content":"商品の在庫数管理など一般的な個数管理に必要とされる、Eloquentのメソッドdecrementを紹介します。\n準備 まず、商品テーブルの作成からです。migrationを作成します。\n$ php artisan make:migration create_products_table --create=products 作成されたファイルを編集して、name, price, inventoryの項目を追加します。\nuse Illuminate\\Database\\Migrations\\Migration; use Illuminate\\Database\\Schema\\Blueprint; use Illuminate\\Support\\Facades\\Schema; class CreateProductsTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create(\u0026#39;products\u0026#39;, function (Blueprint $table) { $table-\u0026gt;bigIncrements(\u0026#39;id\u0026#39;); $table-\u0026gt;char(\u0026#39;name\u0026#39;, 100); // 商品名 $table-\u0026gt;decimal(\u0026#39;price\u0026#39;, 10, 0);　// 価格 $table-\u0026gt;smallInteger(\u0026#39;inventory\u0026#39;); // 在庫数 $table-\u0026gt;timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists(\u0026#39;products\u0026#39;); } } これをmigrateしてテーブルを作成します。\n$ php artisan migrate 次に、tinkerを立ち上げて商品レコードを作成します。\n$ php artisan tinker Psy Shell v0.9.12 (PHP 7.2.16 — cli) by Justin Hileman \u0026gt;\u0026gt;\u0026gt; use App\\Product; \u0026gt;\u0026gt;\u0026gt; Product::create([\u0026#39;name\u0026#39; =\u0026gt; \u0026#39;商品\u0026#39;, \u0026#39;price\u0026#39; =\u0026gt; 1000, \u0026#39;inventory\u0026#39; =\u0026gt; 10]); =\u0026gt; App\\Product {#3064 name: \u0026#34;商品\u0026#34;, price: 1000, inventory: 10, updated_at: \u0026#34;2020-03-27 19:35:37\u0026#34;, created_at: \u0026#34;2020-03-27 19:35:37\u0026#34;, id: 1, } トランザクションで整合性を保持 商品の在庫から購入数分だけ差し引く処理として、まず考えられるのは以下のようになります（引き続きtinkerを使用します）。\n\u0026gt;\u0026gt;\u0026gt; use App\\Product; \u0026gt;\u0026gt;\u0026gt; $product = Product::find(1); =\u0026gt; App\\Product {#3056 id: 1, name: \u0026#34;商品\u0026#34;, price: \u0026#34;1000\u0026#34;, inventory: 10, created_at: \u0026#34;2020-03-27 19:35:37\u0026#34;, updated_at: \u0026#34;2020-03-27 20:03:27\u0026#34;, } \u0026gt;\u0026gt;\u0026gt; $product-\u0026gt;update([\u0026#39;inventory\u0026#39; =\u0026gt; $product-\u0026gt;inventory - 1]); =\u0026gt; true \u0026gt;\u0026gt;\u0026gt; $product-\u0026gt;refresh(); =\u0026gt; App\\Product {#3056 id: 1, name: \u0026#34;商品\u0026#34;, price: \u0026#34;1000\u0026#34;, inventory: 9, created_at: \u0026#34;2020-03-27 19:35:37\u0026#34;, updated_at: \u0026#34;2020-03-27 20:04:56\u0026#34;, } \u0026gt;\u0026gt;\u0026gt; つまり、商品をオブジェクトに読み込んで、現在の在庫数の属性（inventory）の値から、必要な個数（ここでは１個）を引いた値でDBレコードを更新（update）します。\n上で実行されたSQL文を確認のために見てみましょう。レコード読み込み、更新、さらに読み込みと３つのSQL文となります。\n\u0026gt;\u0026gt;\u0026gt; sql(); =\u0026gt; [ [ \u0026#34;query\u0026#34; =\u0026gt; \u0026#34;select * from `products` where `products`.`id` = ? limit 1\u0026#34;, \u0026#34;bindings\u0026#34; =\u0026gt; [ 1, ], \u0026#34;time\u0026#34; =\u0026gt; 14.83, ], [ \u0026#34;query\u0026#34; =\u0026gt; \u0026#34;update `products` set `inventory` = ?, `products`.`updated_at` = ? where `id` = ?\u0026#34;, \u0026#34;bindings\u0026#34; =\u0026gt; [ 9, \u0026#34;2020-03-27 20:04:56\u0026#34;, 1, ], \u0026#34;time\u0026#34; =\u0026gt; 5.79, ], [ \u0026#34;query\u0026#34; =\u0026gt; \u0026#34;select * from `products` where `products`.`id` = ? limit 1\u0026#34;, \u0026#34;bindings\u0026#34; =\u0026gt; [ 1, ], \u0026#34;time\u0026#34; =\u0026gt; 8.23, ], ] このやり方、一見良さそうに見えますが、大きな問題があります。\nウェブのように同時に同じ処理が可能な環境において、レコードを読み込んだ時と書き込むときの間に在庫数がすでに更新されているかもしれないことです。その場合には在庫数は不正確になってしまいます。\nこれを防ぐには、上のプログラムをトランザクションで囲むことです。\nDB::beginTransaction(); $product = Product::find(1); $product-\u0026gt;update([\u0026#39;inventory\u0026#39; =\u0026gt; $product-\u0026gt;inventory - 1]); DB::commit(); トランザクションでは複数のSQL文の実行があたかも１つのように実行してくれるので、その間の作業の整合性を保持してれます。\nよりシンプルに 先のトランザクションでも問題ないですが、以下のようEloquentのdecrementのメソッドを使用して同様な処理も可能です。\n\u0026gt;\u0026gt;\u0026gt; Product::where(\u0026#39;id\u0026#39;, \u0026#39;=\u0026#39;, 1)-\u0026gt;decrement(\u0026#39;inventory\u0026#39;, 1); =\u0026gt; 1 \u0026gt;\u0026gt;\u0026gt; sql(); =\u0026gt; [ [ \u0026#34;query\u0026#34; =\u0026gt; \u0026#34;update `products` set `inventory` = `inventory` - 1, `products`.`updated_at` = ? where `id` = ?\u0026#34;, \u0026#34;bindings\u0026#34; =\u0026gt; [ \u0026#34;2020-03-27 20:18:48\u0026#34;, 1, ], \u0026#34;time\u0026#34; =\u0026gt; 9.34, ], ] こうなるとSQL文は１つなので、整合性保持のためのトランザクションの必要はなくなります。\nさて、注意する必要があるのは、上の例を見て、以下でも同じことができるのでは？と思われることです。\n\u0026gt;\u0026gt;\u0026gt; Product::find(1)-\u0026gt;decrement(\u0026#39;inventory\u0026#39;, 1); =\u0026gt; 1 しかし、このSQL文を見てみると、以下のように読み込みと更新の２つのSQL文になっています。これではトランザクションが必要となります。\n\u0026gt;\u0026gt;\u0026gt; sql(); =\u0026gt; [ [ \u0026#34;query\u0026#34; =\u0026gt; \u0026#34;select * from `products` where `products`.`id` = ? limit 1\u0026#34;, \u0026#34;bindings\u0026#34; =\u0026gt; [ 1, ], \u0026#34;time\u0026#34; =\u0026gt; 12.63, ], [ \u0026#34;query\u0026#34; =\u0026gt; \u0026#34;update `products` set `inventory` = `inventory` - 1, `products`.`updated_at` = ? where `id` = ?\u0026#34;, \u0026#34;bindings\u0026#34; =\u0026gt; [ \u0026#34;2020-03-27 20:22:48\u0026#34;, 1, ], \u0026#34;time\u0026#34; =\u0026gt; 2.58, ], ] 在庫がゼロにならないようにするには 在庫数を減らすときに、在庫数がゼロにならないようにするには、チェックが必要です。これも以下のように実行するなら１つのSQL文で済みます。\n\u0026gt;\u0026gt;\u0026gt; Product::where(\u0026#39;id\u0026#39;, \u0026#39;=\u0026#39;, 1)-\u0026gt;whereRaw(\u0026#39;inventory - 1 \u0026gt;= 0\u0026#39;)-\u0026gt;decrement(\u0026#39;inventory\u0026#39;, 1); =\u0026gt; 1 \u0026gt;\u0026gt;\u0026gt; sql(); =\u0026gt; [ [ \u0026#34;query\u0026#34; =\u0026gt; \u0026#34;update `products` set `inventory` = `inventory` - 1, `products`.`updated_at` = ? where `id` = ? and inventory - 1 \u0026gt;= 0\u0026#34;, \u0026#34;bindings\u0026#34; =\u0026gt; [ \u0026#34;2020-03-27 20:38:46\u0026#34;, 1, ], \u0026#34;time\u0026#34; =\u0026gt; 10.09, ], ] 最初の行の実行で返り値が１となっていること気づいたと思いますが、そこでは更新されたレコードの数が返っています。\nもし、実行の前の在庫数がすでにゼロなら、\n\u0026gt;\u0026gt;\u0026gt; Product::where(\u0026#39;id\u0026#39;, \u0026#39;=\u0026#39;, 1)-\u0026gt;update([\u0026#39;inventory\u0026#39; =\u0026gt; 0]); =\u0026gt; 1 \u0026gt;\u0026gt;\u0026gt; Product::where(\u0026#39;id\u0026#39;, \u0026#39;=\u0026#39;, 1)-\u0026gt;whereRaw(\u0026#39;inventory - 1 \u0026gt;= 0\u0026#39;)-\u0026gt;decrement(\u0026#39;inventory\u0026#39;, 1); =\u0026gt; 0 ０が返って更新はなかった、ということです。\n","date":"2020-03-28T05:46:15+09:00","permalink":"https://www.larajapan.com/2020/03/28/%E5%80%8B%E6%95%B0%E7%AE%A1%E7%90%86/","title":"個数管理"},{"content":"世界中大変なことになっていますね。私が住んでいるところも、子供たちの学校は４月いっぱい休み、レストランやバーはテイクアウトのみとなっています。それらのビジネスに携わっている人たちは本当に困っていると思います。逆に、私を含めてこのブログを読んでくれる人たちは、インターネットのおかげで忙しくなっているくらいと思います。その有難さを胸に、日々精進と行きましょう！\n今回は、またもやFormRequestの話で、１つのコントローラーにおいてあるいは複数のコントローラでどうバリデーションのルールを共有の仕方を考えてみます。\nルールを共有する必要性 ルールをコントローラ間で共有する必要があるのは、重複の定義をせずに一か所で管理をして間違いを防ぎたいからです。\n会員レコードを例にとると、\nユーザー画面での会員登録 ユーザー画面で名前や住所などの会員情報更新 ユーザー画面でパスワード変更 さらに、管理画面ではユーザー画面と同様に、新規、更新、パスワード更新の機能が必要となります。\nつまり、少なくともユーザー画面と管理画面で２つのコントローラーが必要で、それぞれの入力画面に異なるFormRequestを作成すれば６つ必要となります。そして、それぞれにルールの定義があるわけですが、いくつかは明らかに同じものを定義することになります。将来の管理性を考えるとどうかしたいですね。\nルールの共有の仕方 ルールを共有するにはいくつかのやり方ありますが、ここでは、以前書いた以下のブログを利用してみます。\nコントローラー中でのルールの共有\nそこでは、１つのコントローラ内でのルールの共有していますが、今度はFormRequest内でルールを共有します。そのために、先に書いた６つのFormRequestを作成する代わりに、１つのFormRequestを作成してそれを複数のコントローラのメソッドで共有とします。\nまず、以前のコードを以下に再掲載します。\nnamespace App\\Http\\Controllers; use Illuminate\\Http\\Request; use Illuminate\\Validation\\Rule; use App\\User; class UserController extends Controller { public function rule($action, User $user = null) { // 共有するルール　kana, mailcode, phone_with_dashはカスタムバリデーター $rules = [ \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;name_kana\u0026#39; =\u0026gt; \u0026#39;required|kana\u0026#39;, \u0026#39;mailcode\u0026#39; =\u0026gt; \u0026#39;required|mailcode\u0026#39;, \u0026#39;prefecture\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;city\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;address1\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;phone\u0026#39; =\u0026gt; \u0026#39;required|phone_with_dash\u0026#39;, ]; // 共有しないルール switch ($action) { case \u0026#39;store\u0026#39;:　// レコード作成のためのルールを追加 $rules[\u0026#39;password\u0026#39;] = \u0026#39;required|min:8|max:20|confirmed\u0026#39;; $rules[\u0026#39;email\u0026#39;] = \u0026#39;required|email|unique:users\u0026#39;; break; case \u0026#39;update\u0026#39;: // レコード編集のためのルールを追加 $rules[\u0026#39;email\u0026#39;] = [ \u0026#39;required\u0026#39;, \u0026#39;email\u0026#39;, Rule::unique(\u0026#39;users\u0026#39;)-\u0026gt;ignore($user-\u0026gt;id), //現在のレコード以外のレコードで重複がないかチェック ]; break; } return [ // rules $rules, // messages [ \u0026#39;password.min\u0026#39; =\u0026gt; \u0026#39;8から20文字長でお願いします\u0026#39;, \u0026#39;password.max\u0026#39; =\u0026gt; \u0026#39;8から20文字長でお願いします\u0026#39; ] // attributes ]; } ... public function store(Request $request) { $request-\u0026gt;validate(...$this-\u0026gt;rules(\u0026#39;store\u0026#39;)); ... } ... public function update(Request $request, User $user) { $request-\u0026gt;validate(...$this-\u0026gt;rules(\u0026#39;update\u0026#39;, $user)); ... } ... } FormRequestを使用すると、rules()の部分がUserFormRequestに移って、\n... public function store(UserFormRequest $request) { ... } ... public function update(UserFormRequest $request, User $user) { ... } ... } となりますが、問題は、上と違って$actionや$userなどのパラメータを渡すことができません。どうしたらよいでしょう？\nそこで登場するのが、$this-\u0026gt;route()のメソッドです。UserFormRequestの中で、以下のようにそれらの情報が取り出せます。名前付きのルートがわかれば、それで条件分岐できます。\n$route= $this-\u0026gt;route()-\u0026gt;getName(); // 現在のrouteの名前を取得 $user = $this-\u0026gt;route(\u0026#39;user\u0026#39;); // update(UserFormRequest $request, User $user)の$userを取得 以下が、UserFormRequestのコードです。\nnamespace App\\Http\\Requests; use Illuminate\\Foundation\\Http\\FormRequest; class UserFormRequest extends FormRequest { /** * Determine if the user is authorized to make this request. * * @return bool */ public function authorize() { return true; } /** * Get the validation rules that apply to the request. * * @return array */ public function rule() { $route= $this-\u0026gt;route()-\u0026gt;getName(); // 現在のrouteの名前を取得 $user = $this-\u0026gt;route(\u0026#39;user\u0026#39;); // update(UserFormRequest $request, User $user)の$userを取得 // 共有するルール　kana, mailcode, phone_with_dashはカスタムバリデーター $rules = [ \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;name_kana\u0026#39; =\u0026gt; \u0026#39;required|kana\u0026#39;, \u0026#39;mailcode\u0026#39; =\u0026gt; \u0026#39;required|mailcode\u0026#39;, \u0026#39;prefecture\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;city\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;address1\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;phone\u0026#39; =\u0026gt; \u0026#39;required|phone_with_dash\u0026#39;, ]; // 共有しないルール switch ($route) { case \u0026#39;user.store\u0026#39;:　// レコード作成のためのルールを追加 $rules[\u0026#39;password\u0026#39;] = \u0026#39;required|min:8|max:20|confirmed\u0026#39;; $rules[\u0026#39;email\u0026#39;] = \u0026#39;required|email|unique:users\u0026#39;; break; case \u0026#39;user.update\u0026#39;: // レコード編集のためのルールを追加 $rules[\u0026#39;email\u0026#39;] = [ \u0026#39;required\u0026#39;, \u0026#39;email\u0026#39;, Rule::unique(\u0026#39;users\u0026#39;)-\u0026gt;ignore($user-\u0026gt;id), //現在のレコード以外のレコードで重複がないかチェック ]; break; } return $rules; } public function messages() { return [ \u0026#39;password.min\u0026#39; =\u0026gt; \u0026#39;8から20文字長でお願いします\u0026#39;, \u0026#39;password.max\u0026#39; =\u0026gt; \u0026#39;8から20文字長でお願いします\u0026#39; ]; } } ","date":"2020-03-24T04:28:31+09:00","permalink":"https://www.larajapan.com/2020/03/24/formrequest%E3%81%A7%E3%83%AB%E3%83%BC%E3%83%AB%E3%82%92%E5%85%B1%E6%9C%89/","title":"FormRequestでルールを共有"},{"content":"FormRequestの続きです。FormRequestは、ユーザーの入力をチェックしてくれるだけでなく、入力値も変えてくれることができます。\n例えば、フォームにおいて以下のようなチェックボックスがあったとき、\n\u0026lt;div class=\u0026#34;form-group row\u0026#34;\u0026gt; \u0026lt;label for=\u0026#34;subscribe\u0026#34; class=\u0026#34;col-md-4 col-form-label text-md-right\u0026#34;\u0026gt;メルマガを購読？\u0026lt;/label\u0026gt; \u0026lt;div class=\u0026#34;col-md-6\u0026#34;\u0026gt; \u0026lt;input id=\u0026#34;subscribe\u0026#34; type=\u0026#34;checkbox\u0026#34; class=\u0026#34;form-control\u0026#34; name=\u0026#34;subscribe\u0026#34; value=\u0026#34;Y\u0026#34;\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; チェックをオンにすれば、リクエストにはY（Yesの意味）の値が返ってきますが、チェックがオフなら何も値が返ってきません。 それゆえに、例えばチェックなしをDBにN（Noの意味）として保存したいなら、どうしましょう？\n私のFormRequestの継承元のFormRequestのLaravelのコードを見ると、以下のようなメソッドがあります。\nvendor/laravel/framework/src/Illuminate/Foundation/Http/FormRequest.php ... /** * Get data to be validated from the request. * * @return array */ public function validationData() { return $this-\u0026gt;all(); } ... このメソッドを上書きして、\napp/Http/Requests/UserRequest.php namespace App\\Http\\Requests; use Illuminate\\Foundation\\Http\\FormRequest; class UserRequest extends FormRequest { /** * Determine if the user is authorized to make this request. * * @return bool */ public function authorize() { return true; } /** * Get the validation rules that apply to the request. * * @return array */ public function rules() { return [ \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;required|email\u0026#39;, \u0026#39;subscribe\u0026#39; =\u0026gt; \u0026#39;string\u0026#39;,　// これを忘れずに！ ]; } public function validationData() { $all = parent::validationData(); //値がないなら、\u0026#39;N\u0026#39;を入れる if (! $this-\u0026gt;has(\u0026#39;subscribe\u0026#39;)) { $all[\u0026#39;subscribe\u0026#39;] = \u0026#39;N\u0026#39;; } return $all; } } ルールにおいては、チェックボックスの項目（subscribe）のルールが必ず必要です。\nこうすれば、\nnamespace App\\Http\\Controllers; use App\\Http\\Requests\\UserRequest; class UserController extends Controller { public function __construct() { $this-\u0026gt;middleware(\u0026#39;auth\u0026#39;); } public function edit() { $user = auth()-\u0026gt;user(); return view(\u0026#39;user_edit\u0026#39;)-\u0026gt;with(compact(\u0026#39;user\u0026#39;)); } public function update(UserRequest $request) { dd($request-\u0026gt;validated()); // デバッグ文を入れる auth()-\u0026gt;user()-\u0026gt;update($request-\u0026gt;validated()); return redirect()-\u0026gt;route(\u0026#39;home\u0026#39;); } } フォームでsubmitすると、上で入れたデバッグ文の実行は、\narray:3 [▼ \u0026#34;name\u0026#34; =\u0026gt; \u0026#34;kenji\u0026#34; \u0026#34;email\u0026#34; =\u0026gt; \u0026#34;kenji@lotsofbytes.com\u0026#34; \u0026#34;subscribe\u0026#34; =\u0026gt; \u0026#34;N\u0026#34; ] と返してくれます。\n","date":"2020-03-13T23:59:58+09:00","permalink":"https://www.larajapan.com/2020/03/13/formrequest%E3%81%A7%E5%85%A5%E5%8A%9B%E5%80%A4%E3%82%92%E8%A3%9C%E6%AD%A3/","title":"FormRequestで入力値を補正"},{"content":"コントローラーのファイルが大きくなってきたな、と思ったら、FormRequestを使ってみようかと真剣に考え始めました。まずは、FormRequestとは何ものかの紹介からです。\nFormRequestを使わないコントローラ FormRequestを使わないコントローラというのは、例えば以下のコードに代表されます。\nnamespace App\\Http\\Controllers; use Illuminate\\Http\\Request; class UserController extends Controller { public function __construct() { $this-\u0026gt;middleware(\u0026#39;auth\u0026#39;); //認証したユーザーのみがアクセス可能 } public function edit() { $user = auth()-\u0026gt;user();　// 認証したユーザーの情報を取得 return view(\u0026#39;user_edit\u0026#39;)-\u0026gt;with(compact(\u0026#39;user\u0026#39;)); } public function update(Request $request) { // リクエストのバリデーション $validated = $request-\u0026gt;validate([ \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;required|email\u0026#39;, ]); // DBの情報を更新 auth()-\u0026gt;user()-\u0026gt;update($validated); // ホーム画面へリダイレクト return redirect()-\u0026gt;route(\u0026#39;home\u0026#39;); } } このコントローラを使用する、routeは以下のように定義されます。\nroutes/web.php Route::get(\u0026#39;/\u0026#39;, function () { return view(\u0026#39;welcome\u0026#39;); }); Auth::routes(); Route::get(\u0026#39;/home\u0026#39;, \u0026#39;HomeController@index\u0026#39;)-\u0026gt;name(\u0026#39;home\u0026#39;); Route::get(\u0026#39;/user/edit\u0026#39;, \u0026#39;UserController@edit\u0026#39;)-\u0026gt;name(\u0026#39;user.edit\u0026#39;); Route::put(\u0026#39;/user/edit\u0026#39;, \u0026#39;UserController@update\u0026#39;)-\u0026gt;name(\u0026#39;user.update\u0026#39;); よく見て欲しいのは、UserController::update()のメソッド。 リクエストの入力値をバリデートして、バリデーションをパスした入力値をそのまま、EloquestのUserオブジェクト(auth()-\u0026gt;user)のupdate()に渡して更新です。\nちなみに、このコントローラは、ログインしたユーザーが自分の情報を編集するためです。Laravelのデフォルトのプロジェクトにはユーザー認証のコードがついてきますが、この機能はありません。\nFormRequestを使用したら まず、コマンドラインから、FormRequestの作成です。\n$ php artisan make:request UserRequest これで、UserRequest.phpが作成されます。それを以下のように編集します。\napp/Http/Requests/UserRequest.php amespace App\\Http\\Requests; use Illuminate\\Foundation\\Http\\FormRequest; class UserRequest extends FormRequest { /** * Determine if the user is authorized to make this request. * * @return bool */ public function authorize() { return true; // すでにUserController::__construct()でチェックしてあるので、true } /** * Get the validation rules that apply to the request. * * @return array */ public function rules() { return [ \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;required|email\u0026#39;, ]; } } 見ての通り、コントローラにあったバリデーションルールが移動されています。\nコントローラの方は、\nnamespace App\\Http\\Controllers; use App\\Http\\Requests\\UserRequest; //ここで宣言 class UserController extends Controller { public function __construct() { $this-\u0026gt;middleware(\u0026#39;auth\u0026#39;); } public function edit() { $user = auth()-\u0026gt;user(); return view(\u0026#39;user_edit\u0026#39;)-\u0026gt;with(compact(\u0026#39;user\u0026#39;)); } public function update(UserRequest $request) { auth()-\u0026gt;user()-\u0026gt;update($request-\u0026gt;validated());　// １行になりました return redirect()-\u0026gt;route(\u0026#39;home\u0026#39;); } } FormRequestを使うことでコントローラのコードの行数は減りました。しかし、FormRequestの定義のファイルが増えて全体のコードの行数は増えたことも事実。今回のような、簡単なコードではあまり得したことにはならない感じですね。ない方がわかりやすいとも思えます。しかし、もしバリデーションルールがもっとたくさんあったら、FormRequestはベターなのは明らかです。コードが複雑になったらリファクターして新規の関数を作成するのと同じかな。\nしかし、FormRequestを使うことに関して他の利点はなんでしょう？次回からそれらの利点を探ってみます。\n編集のブレード 参考として、作成したブレードファイルも掲載します。resources/views/auth/register.blade.phpをもとにしましたが、Eメールの入力において、type=\u0026ldquo;email\u0026rdquo;のところ、type=\u0026ldquo;text\u0026rdquo;にしています。バリデーションエラーの表示をテストしたかったためです。\nresources/views/user_edit.blade.pohp @extends(\u0026#39;layouts.app\u0026#39;) @section(\u0026#39;content\u0026#39;) \u0026lt;div class=\u0026#34;container\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;row justify-content-center\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;col-md-8\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;card\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;card-header\u0026#34;\u0026gt;{{ __(\u0026#39;Register\u0026#39;) }}\u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;card-body\u0026#34;\u0026gt; \u0026lt;form method=\u0026#34;POST\u0026#34; action=\u0026#34;{{ route(\u0026#39;user.edit\u0026#39;) }}\u0026#34;\u0026gt; @csrf @method(\u0026#39;PUT\u0026#39;) \u0026lt;div class=\u0026#34;form-group row\u0026#34;\u0026gt; \u0026lt;label for=\u0026#34;name\u0026#34; class=\u0026#34;col-md-4 col-form-label text-md-right\u0026#34;\u0026gt;{{ __(\u0026#39;Name\u0026#39;) }}\u0026lt;/label\u0026gt; \u0026lt;div class=\u0026#34;col-md-6\u0026#34;\u0026gt; \u0026lt;input id=\u0026#34;name\u0026#34; type=\u0026#34;text\u0026#34; class=\u0026#34;form-control @error(\u0026#39;name\u0026#39;) is-invalid @enderror\u0026#34; name=\u0026#34;name\u0026#34; value=\u0026#34;{{ old(\u0026#39;name\u0026#39;, $user-\u0026gt;name) }}\u0026#34; required autocomplete=\u0026#34;name\u0026#34; autofocus\u0026gt; @error(\u0026#39;name\u0026#39;) \u0026lt;span class=\u0026#34;invalid-feedback\u0026#34; role=\u0026#34;alert\u0026#34;\u0026gt; \u0026lt;strong\u0026gt;{{ $message }}\u0026lt;/strong\u0026gt; \u0026lt;/span\u0026gt; @enderror \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;form-group row\u0026#34;\u0026gt; \u0026lt;label for=\u0026#34;email\u0026#34; class=\u0026#34;col-md-4 col-form-label text-md-right\u0026#34;\u0026gt;{{ __(\u0026#39;E-Mail Address\u0026#39;) }}\u0026lt;/label\u0026gt; \u0026lt;div class=\u0026#34;col-md-6\u0026#34;\u0026gt; \u0026lt;input id=\u0026#34;email\u0026#34; type=\u0026#34;text\u0026#34; class=\u0026#34;form-control @error(\u0026#39;email\u0026#39;) is-invalid @enderror\u0026#34; name=\u0026#34;email\u0026#34; value=\u0026#34;{{ old(\u0026#39;email\u0026#39;, $user-\u0026gt;email) }}\u0026#34; required autocomplete=\u0026#34;email\u0026#34;\u0026gt; @error(\u0026#39;email\u0026#39;) \u0026lt;span class=\u0026#34;invalid-feedback\u0026#34; role=\u0026#34;alert\u0026#34;\u0026gt; \u0026lt;strong\u0026gt;{{ $message }}\u0026lt;/strong\u0026gt; \u0026lt;/span\u0026gt; @enderror \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;form-group row mb-0\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;col-md-6 offset-md-4\u0026#34;\u0026gt; \u0026lt;button type=\u0026#34;submit\u0026#34; class=\u0026#34;btn btn-primary\u0026#34;\u0026gt; {{ __(\u0026#39;Register\u0026#39;) }} \u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/form\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; @endsection ","date":"2020-03-09T11:58:03+09:00","permalink":"https://www.larajapan.com/2020/03/09/formrequest%E3%82%92%E4%BD%BF%E3%81%86/","title":"FormRequestを使う"},{"content":"Laravelではdd()というヘルパー関数をデバッグでよく使用しますが、ddd()は初耳です。なぜなら、Laravel 6.xバージョンで新登場したデバッグの関数だからです。\ndd() 新登場のddd()の比較のためと、Laravel 6.xの以前のバージョンのために、まずdd()の使用を見てみましょう。 dd()を以下のように追加すると、\nroutes/web.php Route::get(\u0026#39;/\u0026#39;, function () { $user = App\\User::first(); dd($user); return view(\u0026#39;welcome\u0026#39;); }); 以下のように実行を止めて、引数で指定した変数の情報をブラウザに表示してくれます。 その時点で、実行した関数のスタックトレースをしたいなら、\nroutes/web.php Route::get(\u0026#39;/\u0026#39;, function () { $user = App\\User::first(); dd(print_r(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS),true));//面倒なら、debug_backtrace(2)でもＯＫ return view(\u0026#39;welcome\u0026#39;); }); とすると、\nArray ( [0] =\u0026gt; Array ( [file] =\u0026gt; /vol1/usr/www/repos/repos/l6x/vendor/laravel/framework/src/Illuminate/Routing/Route.php [line] =\u0026gt; 205 [function] =\u0026gt; {closure} [class] =\u0026gt; Illuminate\\Routing\\RouteFileRegistrar [type] =\u0026gt; -\u0026gt; ) [1] =\u0026gt; Array ( [file] =\u0026gt; /vol1/usr/www/repos/repos/l6x/vendor/laravel/framework/src/Illuminate/Routing/Route.php [line] =\u0026gt; 179 [function] =\u0026gt; runCallable [class] =\u0026gt; Illuminate\\Routing\\Route [type] =\u0026gt; -\u0026gt; ) [2] =\u0026gt; Array ( [file] =\u0026gt; /vol1/usr/www/repos/repos/l6x/vendor/laravel/framework/src/Illuminate/Routing/Router.php [line] =\u0026gt; 681 [function] =\u0026gt; run [class] =\u0026gt; Illuminate\\Routing\\Route [type] =\u0026gt; -\u0026gt; ) ... ddd() さて、Laravel 6.xのバージョンのddd()ヘルパーを使用すると、\nroutes/web.php Route::get(\u0026#39;/\u0026#39;, function () { $user = App\\User::first(); ddd($user); return view(\u0026#39;welcome\u0026#39;); }); 以下のように、とても見やすいＵＩで出力してくれます。\nそればかりでなく、関数スタックのトレースもメニューを選択して表示してくれます。しかも、Laravel内部での関数の実行を隠して（拡張して見ることも可能）開発した関数だけを見せてくれます。\nこういう便利な機能を追加してくれると、Laravelをアップグレードするモチベーションが上がりますね。\n","date":"2020-02-29T02:03:48+09:00","permalink":"https://www.larajapan.com/2020/02/29/ddd/","title":"ddd()"},{"content":"Laravel 5.8において、ユーザー認証のパスワードの長さの制限がそれまでの最低の６文字長から８文字長に変わりました。セキュリティ強化の目的です。しかし、この制限を変えたい、つまり昔の６文字長をキープしたい、あるいは８文字長でなくより強化して１０文字長としたいなら、どうするのでしょう？\n5.8のコードを見ると、会員登録とパスワードを忘れた時のパスワードリセットにおいてのみ、min:8のバリデーションが使われています（不思議とログインではパスワードの長さの制限なし）。しかもトレイトで定義されているバリデーションの関数rules()を上書きすればよいだけ、と思いきや、ハードコードされていて効かない箇所がありました。\nハードコードの箇所は、パスワードリセットの機能です。そこのコントローラーから、コードを追跡してみましょう。\napp/Http/Controller/Auth/ResetPasswordController namespace App\\Http\\Controllers\\Auth; use App\\Http\\Controllers\\Controller; use Illuminate\\Foundation\\Auth\\ResetsPasswords; class ResetPasswordController extends Controller { use ResetsPasswords; /** * Where to redirect users after resetting their password. * * @var string */ protected $redirectTo = \u0026#39;/home\u0026#39;; /** * Create a new controller instance. * * @return void */ public function __construct() { $this-\u0026gt;middleware(\u0026#39;guest\u0026#39;); } } そこで使用されている、ResetPasswordsのトレイトの定義は、\nvendor/laravel/framework/src/Illuminate/Foundation/Auth/ResetsPasswords.php namespace Illuminate\\Foundation\\Auth; use Illuminate\\Support\\Str; use Illuminate\\Http\\Request; use Illuminate\\Support\\Facades\\Auth; use Illuminate\\Support\\Facades\\Hash; use Illuminate\\Support\\Facades\\Password; use Illuminate\\Auth\\Events\\PasswordReset; trait ResetsPasswords { ... /** * Reset the given user\u0026#39;s password. * * @param \\Illuminate\\Http\\Request $request * @return \\Illuminate\\Http\\RedirectResponse|\\Illuminate\\Http\\JsonResponse */ public function reset(Request $request) { $request-\u0026gt;validate($this-\u0026gt;rules(), $this-\u0026gt;validationErrorMessages()); // Here we will attempt to reset the user\u0026#39;s password. If it is successful we // will update the password on an actual user model and persist it to the // database. Otherwise we will parse the error and return the response. $response = $this-\u0026gt;broker()-\u0026gt;reset( $this-\u0026gt;credentials($request), function ($user, $password) { $this-\u0026gt;resetPassword($user, $password); } ); // If the password was successfully reset, we will redirect the user back to // the application\u0026#39;s home authenticated view. If there is an error we can // redirect them back to where they came from with their error message. return $response == Password::PASSWORD_RESET ? $this-\u0026gt;sendResetResponse($request, $response) : $this-\u0026gt;sendResetFailedResponse($request, $response); } /** * Get the password reset validation rules. * * @return array */ protected function rules() { return [ \u0026#39;token\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;required|email\u0026#39;, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;required|confirmed|min:8\u0026#39;, ]; } ... rules()では、min:8とあるから、先ほどのResetPasswordControllerで、rules()を再定義して、min:6とすれば良いということです。\nしかし、そのルールを使用する21行目のvalidate()だけでなく、さらに、26行目のbrokerのreset()でもチェックが入っていたのです。その、brokerのコードを見てみましょう。\nvendor/laravel/framework/src/Illuminate/Auth/Passwords/PasswordBroker.php namespace Illuminate\\Auth\\Passwords; use Closure; use Illuminate\\Support\\Arr; use UnexpectedValueException; use Illuminate\\Contracts\\Auth\\UserProvider; use Illuminate\\Contracts\\Auth\\PasswordBroker as PasswordBrokerContract; use Illuminate\\Contracts\\Auth\\CanResetPassword as CanResetPasswordContract; class PasswordBroker implements PasswordBrokerContract { ... /** * Reset the password for the given token. * * @param array $credentials * @param \\Closure $callback * @return mixed */ public function reset(array $credentials, Closure $callback) { // If the responses from the validate method is not a user instance, we will // assume that it is a redirect and simply return it from this method and // the user is properly redirected having an error message on the post. $user = $this-\u0026gt;validateReset($credentials); if (! $user instanceof CanResetPasswordContract) { return $user; } $password = $credentials[\u0026#39;password\u0026#39;]; // Once the reset has been validated, we\u0026#39;ll call the given callback with the // new password. This gives the user an opportunity to store the password // in their persistent storage. Then we\u0026#39;ll delete the token and return. $callback($user, $password); $this-\u0026gt;tokens-\u0026gt;delete($user); return static::PASSWORD_RESET; } /** * Validate a password reset for the given credentials. * * @param array $credentials * @return \\Illuminate\\Contracts\\Auth\\CanResetPassword|string */ protected function validateReset(array $credentials) { if (is_null($user = $this-\u0026gt;getUser($credentials))) { return static::INVALID_USER; } if (! $this-\u0026gt;validateNewPassword($credentials)) { return static::INVALID_PASSWORD; } if (! $this-\u0026gt;tokens-\u0026gt;exists($user, $credentials[\u0026#39;token\u0026#39;])) { return static::INVALID_TOKEN; } return $user; } /** * Set a custom password validator. * * @param \\Closure $callback * @return void */ public function validator(Closure $callback) { $this-\u0026gt;passwordValidator = $callback; } /** * Determine if the passwords match for the request. * * @param array $credentials * @return bool */ public function validateNewPassword(array $credentials) { if (isset($this-\u0026gt;passwordValidator)) { [$password, $confirm] = [ $credentials[\u0026#39;password\u0026#39;], $credentials[\u0026#39;password_confirmation\u0026#39;], ]; return call_user_func( $this-\u0026gt;passwordValidator, $credentials ) \u0026amp;\u0026amp; $password === $confirm; } return $this-\u0026gt;validatePasswordWithDefaults($credentials); } /** * Determine if the passwords are valid for the request. * * @param array $credentials * @return bool */ protected function validatePasswordWithDefaults(array $credentials) { [$password, $confirm] = [ $credentials[\u0026#39;password\u0026#39;], $credentials[\u0026#39;password_confirmation\u0026#39;], ]; return $password === $confirm \u0026amp;\u0026amp; mb_strlen($password) \u0026gt;= 8; } ... 上のコードで、定義されている関数をトレースすると実行順番は、\nreset() validateReset() validateNewPassword() validatePasswordWithDefaults()\n最後の関数で８文字長の制限がありましたね（118行目）。しっかり、ハードコードされています。\n5.8のドキュメントでは、\nhttps://laravel.com/docs/5.8/upgrade#new-default-password-length Illuminate\\Auth\\Passwords\\PasswordBrokeを継承して、validatePasswordWithDefaults()を上書きすればよいとありますが、ディープ過ぎて面倒です。\n以下で他の人も不平述べています。\nhttps://github.com/laravel/framework/issues/28274\nより簡単な解決策はないかと、調査したら、\nベストの解決策は、ResetPasswordsのトレイトのreset()をResetPasswordControllerで上書きしてbrokerのvalidator()を無能にすることです。つまり、\napp/Http/Controller/Auth/ResetPasswordController namespace App\\Http\\Controllers\\Auth; use App\\Http\\Controllers\\Controller; use Illuminate\\Foundation\\Auth\\ResetsPasswords; use Illuminate\\Http\\Request; // この追加必要！ use Illuminate\\Support\\Facades\\Password; // この追加必要！ class ResetPasswordController extends Controller { ... /** * Reset the given user\u0026#39;s password. * * @param \\Illuminate\\Http\\Request $request * @return \\Illuminate\\Http\\RedirectResponse|\\Illuminate\\Http\\JsonResponse */ public function reset(Request $request) { $request-\u0026gt;validate($this-\u0026gt;rules(), $this-\u0026gt;validationErrorMessages()); // Here we will attempt to reset the user\u0026#39;s password. If it is successful we // will update the password on an actual user model and persist it to the // database. Otherwise we will parse the error and return the response. // PasswordBrokerのvalidatorをここで定義 $this-\u0026gt;broker()-\u0026gt;validator(function (array $credentials) { return true; // rules()ですでにバリデーションを実行しているので、さらにここでバリデーションの必要はなし！ }); $response = $this-\u0026gt;broker()-\u0026gt;reset( $this-\u0026gt;credentials($request), function ($user, $password) { $this-\u0026gt;resetPassword($user, $password); } ); // If the password was successfully reset, we will redirect the user back to // the application\u0026#39;s home authenticated view. If there is an error we can // redirect them back to where they came from with their error message. return $response == Password::PASSWORD_RESET ? $this-\u0026gt;sendResetResponse($request, $response) : $this-\u0026gt;sendResetFailedResponse($request, $response); } /** * Get the password reset validation rules. * * @return array */ protected function rules() { return [ \u0026#39;token\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;required|email\u0026#39;, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;required|confirmed|min:6\u0026#39;,　// min:8でなくmin:6に変える ]; } ... rules()も再定義して６文字長としました。\nさて、この問題6.xのバージョンではどうなんでしょう？ PasswordBroker.phpのコードを見ると、\nvendor/laravel/framework/src/Illuminate/Auth/Passwords/PasswordBroker.php namespace Illuminate\\Auth\\Passwords; use Closure; use Illuminate\\Contracts\\Auth\\CanResetPassword as CanResetPasswordContract; use Illuminate\\Contracts\\Auth\\PasswordBroker as PasswordBrokerContract; use Illuminate\\Contracts\\Auth\\UserProvider; use Illuminate\\Support\\Arr; use UnexpectedValueException; class PasswordBroker implements PasswordBrokerContract { ... /** * Reset the password for the given token. * * @param array $credentials * @param \\Closure $callback * @return mixed */ public function reset(array $credentials, Closure $callback) { $user = $this-\u0026gt;validateReset($credentials); // If the responses from the validate method is not a user instance, we will // assume that it is a redirect and simply return it from this method and // the user is properly redirected having an error message on the post. if (! $user instanceof CanResetPasswordContract) { return $user; } $password = $credentials[\u0026#39;password\u0026#39;]; // Once the reset has been validated, we\u0026#39;ll call the given callback with the // new password. This gives the user an opportunity to store the password // in their persistent storage. Then we\u0026#39;ll delete the token and return. $callback($user, $password); $this-\u0026gt;tokens-\u0026gt;delete($user); return static::PASSWORD_RESET; } /** * Validate a password reset for the given credentials. * * @param array $credentials * @return \\Illuminate\\Contracts\\Auth\\CanResetPassword|string */ protected function validateReset(array $credentials) { if (is_null($user = $this-\u0026gt;getUser($credentials))) { return static::INVALID_USER; } if (! $this-\u0026gt;tokens-\u0026gt;exists($user, $credentials[\u0026#39;token\u0026#39;])) { return static::INVALID_TOKEN; } return $user; } ... validateReset()から、validateNewPassword()は削除されていますね！\nこれに関しては、以下で説明されています。\nhttps://laravel.com/docs/6.x/upgrade#belongs-to-update　（Password Resetの部分）\nさらに、PRとして、 https://github.com/laravel/framework/pull/29480\nもう、この部分で悩むことはなさそうです。\n","date":"2020-02-25T00:10:58+09:00","permalink":"https://www.larajapan.com/2020/02/25/%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E8%AA%8D%E8%A8%BC%E3%81%AE%E3%83%91%E3%82%B9%E3%83%AF%E3%83%BC%E3%83%89%E5%88%B6%E9%99%90%E3%81%AE%E5%A4%89%E6%9B%B4/","title":"ユーザー認証のパスワード制限の変更"},{"content":"LaravelのEloquentのModelを継承したクラスで定義できるアクセッサーはとても便利です。しかし、どうして、あたかもDBテーブルにある項目のようにアクセスできるのか不思議に思ったことありませんか？　アクセッサー 例えば、以下のようにUserのモデルにgetEmailDomainAttribute()を定義します。\napp/User.php namespace App; use Illuminate\\Contracts\\Auth\\MustVerifyEmail; use Illuminate\\Foundation\\Auth\\User as Authenticatable; use Illuminate\\Notifications\\Notifiable; // 注意：AuthenticatableのもとのUserクラスは、EloquentのModelクラスを継承したクラスです。 class User extends Authenticatable { use Notifiable; ... // users.emailのDB項目のドメイン名を返す public function getEmailDomainAttribute() { $array = explode(\u0026#39;@\u0026#39;, $this-\u0026gt;email); return array_pop($array); } } 以下のように、getEmailDomainAttribute()ではなく、email_domainと、モデルの属性のようにアクセスできます。\n$ php artisan tinker Psy Shell v0.9.12 (PHP 7.2.16 — cli) by Justin Hileman \u0026gt;\u0026gt;\u0026gt; factory(App\\User::class)-\u0026gt;create(); =\u0026gt; App\\User {#3039 name: \u0026#34;近藤 篤司\u0026#34;, email: \u0026#34;manabu.takahashi@example.net\u0026#34;, email_verified_at: \u0026#34;2020-02-13 04:27:15\u0026#34;, updated_at: \u0026#34;2020-02-13 04:27:15\u0026#34;, created_at: \u0026#34;2020-02-13 04:27:15\u0026#34;, id: 1, } \u0026gt;\u0026gt;\u0026gt; User::find(1)-\u0026gt;email_domain; [!] Aliasing \u0026#39;User\u0026#39; to \u0026#39;App\\User\u0026#39; for this Tinker session. =\u0026gt; \u0026#34;example.net\u0026#34; \u0026gt;\u0026gt;\u0026gt; どうしたら、このようなことできるのでしょうか？\nマジックメソッド Laravelのアクセッサーのトリックは、実はPHPのマジックメソッドの__get()です。\nまず、以下のようにFooというクラスで、__get()と__set()の２つのマジックメソッドのメソッドを定義します\napp/Foo.php namespace App; class Foo { protected $attributes = []; public function __get( $key ) { return $this-\u0026gt;attributes[ $key ]; } public function __set( $key, $value ) { $this-\u0026gt;attributes[ $key ] = $value; } } さて、これをtinkerでテストしてみると、\n$ php artisan tinker Psy Shell v0.9.12 (PHP 7.2.16 — cli) by Justin Hileman \u0026gt;\u0026gt;\u0026gt; $foo = new App\\Foo; =\u0026gt; App\\Foo {#3018} \u0026gt;\u0026gt;\u0026gt; $foo-\u0026gt;name = \u0026#39;名前\u0026#39;; // Foo::__set()がコールされる =\u0026gt; \u0026#34;名前\u0026#34; \u0026gt;\u0026gt;\u0026gt; $foo-\u0026gt;name; // Foo::__get()がコールされる =\u0026gt; \u0026#34;名前\u0026#34; Laravelはこのメカニズムを拡張したものです。以下のLaravelのModelの定義を見てください。\nvendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php namespace Illuminate\\Database\\Eloquent; use ArrayAccess; use Exception; use Illuminate\\Contracts\\Queue\\QueueableCollection; use Illuminate\\Contracts\\Queue\\QueueableEntity; use Illuminate\\Contracts\\Routing\\UrlRoutable; use Illuminate\\Contracts\\Support\\Arrayable; use Illuminate\\Contracts\\Support\\Jsonable; use Illuminate\\Database\\ConnectionResolverInterface as Resolver; use Illuminate\\Database\\Eloquent\\Relations\\Pivot; use Illuminate\\Support\\Arr; use Illuminate\\Support\\Collection as BaseCollection; use Illuminate\\Support\\Str; use Illuminate\\Support\\Traits\\ForwardsCalls; use JsonSerializable; abstract class Model implements Arrayable, ArrayAccess, Jsonable, JsonSerializable, QueueableEntity, UrlRoutable { use Concerns\\HasAttributes, Concerns\\HasEvents, Concerns\\HasGlobalScopes, Concerns\\HasRelationships, Concerns\\HasTimestamps, Concerns\\HidesAttributes, Concerns\\GuardsAttributes, ForwardsCalls; ... /** * Dynamically retrieve attributes on the model. * * @param string $key * @return mixed */ public function __get($key) { return $this-\u0026gt;getAttribute($key); } /** * Dynamically set attributes on the model. * * @param string $key * @param mixed $value * @return void */ public function __set($key, $value) { $this-\u0026gt;setAttribute($key, $value); } ... __get()と__set()が定義されていますね。\n__get()で使用されている、getAttribute()は、HasAttributesのトレイトで定義されています。 HasAttributeを見てましょう。\nvendor/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php namespace Illuminate\\Database\\Eloquent\\Concerns; use Carbon\\CarbonInterface; use DateTimeInterface; use Illuminate\\Contracts\\Support\\Arrayable; use Illuminate\\Database\\Eloquent\\JsonEncodingException; use Illuminate\\Database\\Eloquent\\Relations\\Relation; use Illuminate\\Support\\Arr; use Illuminate\\Support\\Carbon; use Illuminate\\Support\\Collection as BaseCollection; use Illuminate\\Support\\Facades\\Date; use Illuminate\\Support\\Str; use LogicException; trait HasAttributes { ... /** * Get a plain attribute (not a relationship). * * @param string $key * @return mixed */ public function getAttributeValue($key) { $value = $this-\u0026gt;getAttributeFromArray($key); // If the attribute has a get mutator, we will call that then return what // it returns as the value, which is useful for transforming values on // retrieval from the model to a form that is more useful for usage. if ($this-\u0026gt;hasGetMutator($key)) { return $this-\u0026gt;mutateAttribute($key, $value); } // If the attribute exists within the cast array, we will convert it to // an appropriate native PHP type dependant upon the associated value // given with the key in the pair. Dayle made this comment line up. if ($this-\u0026gt;hasCast($key)) { return $this-\u0026gt;castAttribute($key, $value); } // If the attribute is listed as a date, we will convert it to a DateTime // instance on retrieval, which makes it quite convenient to work with // date fields without having to create a mutator for each property. if (in_array($key, $this-\u0026gt;getDates()) \u0026amp;\u0026amp; ! is_null($value)) { return $this-\u0026gt;asDateTime($value); } return $value; } ... /** * Get the value of an attribute using its mutator. * * @param string $key * @param mixed $value * @return mixed */ protected function mutateAttribute($key, $value) { return $this-\u0026gt;{\u0026#39;get\u0026#39;.Str::studly($key).\u0026#39;Attribute\u0026#39;}($value); } ... 上の、mutateAttribute()において、$keyがemail_domainなら、\nreturn $this-\u0026gt;getEmailDomainAttribute($value) となります。\n","date":"2020-02-16T02:48:42+09:00","permalink":"https://www.larajapan.com/2020/02/16/%E3%83%9E%E3%82%B8%E3%83%83%E3%82%AF%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89/","title":"マジックメソッド"},{"content":"phpunitの実行時のオプションはいろいろあります。今回は私が便利！と思ったオプションを紹介します。\nまず、testsのディレクトリの構造が以下と仮定します。\ntests ├── CreatesApplication.php ├── Feature │ └── ExampleTest.php ├── TestCase.php └── Unit ├── ExampleTest.php └── UserTest.php オプションなしでphpuitを実行すると、tests/Unit, tests/Featureのディレクトリのファイル名ががTest.phpで終わるファイルに含まれるテストすべてを実行します。\n$ vendor/bin/phpunit PHPUnit 8.5.2 by Sebastian Bergmann and contributors. ... 3 / 3 (100%) Time: 621 ms, Memory: 24.00 MB OK (3 tests, 3 assertions) どのテストが実行されるかは、\u0026ndash;list-testsのオプションで出力してくれます。\n$ vendor/bin/phpunit --list-tests PHPUnit 8.5.2 by Sebastian Bergmann and contributors. Available test(s): - Tests\\Unit\\ExampleTest::testBasicTest - Tests\\Unit\\UserTest::test_create_a_user - Tests\\Feature\\ExampleTest::testBasicTest 実行しながら、どのテストが実行されるのを見たいなら、\u0026ndash;testdoxのオプションを付けます。 注意：以下のそれぞれのテストでのxは成功という意味で、実際には✔で表示されます。コードのセクションではエスケープされるので置き換えています。\n$ vendor/bin/phpunit --testdox PHPUnit 8.5.2 by Sebastian Bergmann and contributors. Example (Tests\\Unit\\Example) x Basic test User (Tests\\Unit\\User) x Create a user Example (Tests\\Feature\\Example) x Basic test Time: 582 ms, Memory: 24.00 MB OK (3 tests, 3 assertions) 特定のファイルに含まれるテストのみを実行したいなら、以下のようにファイル名を指定します。\n$ vendor/bin/phpunit tests/Feature/ExampleTest.php あるいは、テスト名のみでもＯＫです。\n$ vendor/bin/phpunit tests/Feature/ExampleTest 実行したいテストをパターンで指定することも可能です。\n例えば、Exampleから始まるテストファイルだけを実行したいなら、以下のように、\u0026ndash;filterオプションを使用します。\n$ vendor/bin/phpunit --filter Example --testdox PHPUnit 8.5.2 by Sebastian Bergmann and contributors. Example (Tests\\Unit\\Example) x Basic test Example (Tests\\Feature\\Example) x Basic test Time: 323 ms, Memory: 16.00 MB OK (2 tests, 2 assertions) Exampleで始まるテストファイル名は、UnitとFeatureに存在するので両方が実行されます。\nテストファイルに含まれる、テスト名の指定も可能です。\n$ vendor/bin/phpunit --filter testBasicTest --testdox PHPUnit 8.5.2 by Sebastian Bergmann and contributors. Example (Tests\\Unit\\Example) x Basic test Example (Tests\\Feature\\Example) x Basic test Time: 304 ms, Memory: 16.00 MB OK (2 tests, 2 assertions) testBasicTestのテストは、これまたUnitとFeatureのExampleTest.phpに存在するので両方が実行されます。\n特定のファイルの特定のテストだけを実行したいなら、以下のようにテスト名とパス名を指定します。\n$ vendor/bin/phpunit --filter testBasicTest tests/Unit/ExampleTest.php --testdox PHPUnit 8.5.2 by Sebastian Bergmann and contributors. Example (Tests\\Unit\\Example) x Basic test Time: 148 ms, Memory: 6.00 MB OK (1 test, 1 assertion) オプションなしでのphpunitの実行は、phpunit.xmlのtestsuitesの編集で変更することが可能です。以下はデフォルトの設定です。\nphpunit.xml ... \u0026lt;testsuites\u0026gt; \u0026lt;testsuite name=\u0026#34;Unit\u0026#34;\u0026gt; \u0026lt;directory suffix=\u0026#34;Test.php\u0026#34;\u0026gt;./tests/Unit\u0026lt;/directory\u0026gt; \u0026lt;/testsuite\u0026gt; \u0026lt;testsuite name=\u0026#34;Feature\u0026#34;\u0026gt; \u0026lt;directory suffix=\u0026#34;Test.php\u0026#34;\u0026gt;./tests/Feature\u0026lt;/directory\u0026gt; \u0026lt;/testsuite\u0026gt; \u0026lt;/testsuites\u0026gt; .. \u0026lt;/phpunit\u0026gt; 現在２つのtestsuiteが存在しますが、Unitの方だけを実行したいなら、\n$ vendor/bin/phpunit --testsuite Unit --testdox PHPUnit 8.5.2 by Sebastian Bergmann and contributors. Example (Tests\\Unit\\Example) x Basic test User (Tests\\Unit\\User) x Create a user Time: 523 ms, Memory: 22.00 MB OK (2 tests, 2 assertions) と指定できます。\n現在、私のあるプロジェクトではdbunitを使用するテストからRefreshDatabaseを使用するテストに書き換えています。新旧のテストを別々にテストしたいために、phpunit.xmlは以下のように４つのtestsuiteが存在します。\n\u0026lt;testsuites\u0026gt; \u0026lt;testsuite name=\u0026#34;Feature\u0026#34;\u0026gt; \u0026lt;directory suffix=\u0026#34;Test.php\u0026#34;\u0026gt;./tests/Feature\u0026lt;/directory\u0026gt; \u0026lt;exclude\u0026gt;./tests/Feature/Http\u0026lt;/exclude\u0026gt; \u0026lt;/testsuite\u0026gt; \u0026lt;testsuite name=\u0026#34;Http\u0026#34;\u0026gt; \u0026lt;directory suffix=\u0026#34;Test.php\u0026#34;\u0026gt;./tests/Feature/Http\u0026lt;/directory\u0026gt; \u0026lt;/testsuite\u0026gt; \u0026lt;testsuite name=\u0026#34;Unit\u0026#34;\u0026gt; \u0026lt;directory suffix=\u0026#34;Test.php\u0026#34;\u0026gt;./tests/Unit\u0026lt;/directory\u0026gt; \u0026lt;exclude\u0026gt;./tests/Unit/Models\u0026lt;/exclude\u0026gt; \u0026lt;/testsuite\u0026gt; \u0026lt;testsuite name=\u0026#34;Models\u0026#34;\u0026gt; \u0026lt;directory suffix=\u0026#34;Test.php\u0026#34;\u0026gt;./tests/Unit/Models\u0026lt;/directory\u0026gt; \u0026lt;/testsuite\u0026gt; \u0026lt;/testsuites\u0026gt; excludeのタグを使用することで、サブディレクトリのテストを非実行とできます。\n実行は以下のように２つ実行します。\n$ vendor/bin/phpunit --testsuite Feature,Unit $ vendor/bin/phpunit --testsuite Models,Http ","date":"2020-02-08T02:08:05+09:00","permalink":"https://www.larajapan.com/2020/02/08/phpunit%E3%81%AE%E5%AE%9F%E8%A1%8C%E3%81%AE%E3%81%82%E3%82%8C%E3%81%93%E3%82%8C/","title":"phpunitの実行のあれこれ"},{"content":"既存のDBのそれぞれのテーブルにおいて、migrationファイルやfactoryファイルが揃ったところで、phpunitのテストを作成して実行してみましょう。\nテストDBの作成 テストDBは、phpunitのためだけのDBとなるゆえに、ブラウザでアクセスする本DBとは違うDBを作成します。つまり、開発サイトでは、プロジェクト１つに対して２つのDBを持つことになります。\n本DBは、mysqlで、テストのDBには、sqliteという設定がドキュメントでも一般のLaravelの記事でも書かれていますが、私はテストといえども、本DBもテストDBも同じmysqlを使用することを勧めます。条件をまったく同じとしたいからです。\nまず、mysqlのコマンドで、テストDB（demo_test）を作成します。\n$ echo \u0026#34;create database demo_test\u0026#34; | mysql -u root -p Query OK, 1 row affected (0.00 sec) 次に、config/database.phpをエディターで開いて、mysqlのコネクションをコピーして、mysql_testのコネクションを作成します。\nconfig/databse.php use Illuminate\\Support\\Str; return [ .... \u0026#39;connections\u0026#39; =\u0026gt; [ ... \u0026#39;mysql\u0026#39; =\u0026gt; [ \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;mysql\u0026#39;, \u0026#39;url\u0026#39; =\u0026gt; env(\u0026#39;DATABASE_URL\u0026#39;), \u0026#39;host\u0026#39; =\u0026gt; env(\u0026#39;DB_HOST\u0026#39;, \u0026#39;127.0.0.1\u0026#39;), \u0026#39;port\u0026#39; =\u0026gt; env(\u0026#39;DB_PORT\u0026#39;, \u0026#39;3306\u0026#39;), \u0026#39;database\u0026#39; =\u0026gt; env(\u0026#39;DB_DATABASE\u0026#39;, \u0026#39;forge\u0026#39;), \u0026#39;username\u0026#39; =\u0026gt; env(\u0026#39;DB_USERNAME\u0026#39;, \u0026#39;forge\u0026#39;), \u0026#39;password\u0026#39; =\u0026gt; env(\u0026#39;DB_PASSWORD\u0026#39;, \u0026#39;\u0026#39;), \u0026#39;unix_socket\u0026#39; =\u0026gt; env(\u0026#39;DB_SOCKET\u0026#39;, \u0026#39;\u0026#39;), \u0026#39;charset\u0026#39; =\u0026gt; \u0026#39;utf8mb4\u0026#39;, \u0026#39;collation\u0026#39; =\u0026gt; \u0026#39;utf8mb4_unicode_ci\u0026#39;, \u0026#39;prefix\u0026#39; =\u0026gt; \u0026#39;\u0026#39;, \u0026#39;prefix_indexes\u0026#39; =\u0026gt; true, \u0026#39;strict\u0026#39; =\u0026gt; true, \u0026#39;engine\u0026#39; =\u0026gt; null, \u0026#39;options\u0026#39; =\u0026gt; extension_loaded(\u0026#39;pdo_mysql\u0026#39;) ? array_filter([ PDO::MYSQL_ATTR_SSL_CA =\u0026gt; env(\u0026#39;MYSQL_ATTR_SSL_CA\u0026#39;), ]) : [], ], \u0026#39;mysql_test\u0026#39; =\u0026gt; [ \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;mysql\u0026#39;, \u0026#39;url\u0026#39; =\u0026gt; env(\u0026#39;DATABASE_URL\u0026#39;), \u0026#39;host\u0026#39; =\u0026gt; env(\u0026#39;DB_HOST\u0026#39;, \u0026#39;127.0.0.1\u0026#39;), \u0026#39;port\u0026#39; =\u0026gt; env(\u0026#39;DB_PORT\u0026#39;, \u0026#39;3306\u0026#39;), \u0026#39;database\u0026#39; =\u0026gt; \u0026#39;demo_test\u0026#39;, \u0026#39;username\u0026#39; =\u0026gt; \u0026#39;root\u0026#39;, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;secret\u0026#39;, \u0026#39;unix_socket\u0026#39; =\u0026gt; env(\u0026#39;DB_SOCKET\u0026#39;, \u0026#39;\u0026#39;), \u0026#39;charset\u0026#39; =\u0026gt; \u0026#39;utf8mb4\u0026#39;, \u0026#39;collation\u0026#39; =\u0026gt; \u0026#39;utf8mb4_unicode_ci\u0026#39;, \u0026#39;prefix\u0026#39; =\u0026gt; \u0026#39;\u0026#39;, \u0026#39;prefix_indexes\u0026#39; =\u0026gt; true, \u0026#39;strict\u0026#39; =\u0026gt; true, \u0026#39;engine\u0026#39; =\u0026gt; null, \u0026#39;options\u0026#39; =\u0026gt; extension_loaded(\u0026#39;pdo_mysql\u0026#39;) ? array_filter([ PDO::MYSQL_ATTR_SSL_CA =\u0026gt; env(\u0026#39;MYSQL_ATTR_SSL_CA\u0026#39;), ]) : [], ], ... mysql_testにおいて編集した項目は、database、username、passwordです。env()を使用せずに、固定としています。必要なら他の項目も同様に編集してください。それから、usernameに指定するmysqlのユーザーは必ず、drop table, create table, truncateのsql文と実行できる権限を持つユーザーである必要があります。それゆえに、ここではrootのmysqlユーザーを使用しています。\n次に、テーブル作成のartisanコマンドを実行して、テストDBにテーブルを作成します。前回で作成したmigrationファイルがここで役立ちます。\n$ php artisan migrate:fresh --database=mysql_test Dropped all tables successfully. Migration table created successfully. Migrating: 2014_10_12_000000_create_users_table Migrated: 2014_10_12_000000_create_users_table (0.03 seconds) Migrating: 2014_10_12_100000_create_password_resets_table Migrated: 2014_10_12_100000_create_password_resets_table (0.02 seconds) Migrating: 2019_08_19_000000_create_failed_jobs_table Migrated: 2019_08_19_000000_create_failed_jobs_table (0.01 seconds) 必ず、\u0026ndash;databaseでmysql_testのコネクションを指定ください。指定しないと、本DBの方が空となってしまうので注意を！\nphpunitの設定 テストDBをテストの実行で使用するには、phpunit.xmlの編集が必要です。\nphpunit.xml \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;phpunit xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:noNamespaceSchemaLocation=\u0026#34;./vendor/phpunit/phpunit/phpunit.xsd\u0026#34; backupGlobals=\u0026#34;false\u0026#34; backupStaticAttributes=\u0026#34;false\u0026#34; bootstrap=\u0026#34;vendor/autoload.php\u0026#34; colors=\u0026#34;true\u0026#34; convertErrorsToExceptions=\u0026#34;true\u0026#34; convertNoticesToExceptions=\u0026#34;true\u0026#34; convertWarningsToExceptions=\u0026#34;true\u0026#34; processIsolation=\u0026#34;false\u0026#34; stopOnFailure=\u0026#34;false\u0026#34;\u0026gt; \u0026lt;testsuites\u0026gt; \u0026lt;testsuite name=\u0026#34;Unit\u0026#34;\u0026gt; \u0026lt;directory suffix=\u0026#34;Test.php\u0026#34;\u0026gt;./tests/Unit\u0026lt;/directory\u0026gt; \u0026lt;/testsuite\u0026gt; \u0026lt;testsuite name=\u0026#34;Feature\u0026#34;\u0026gt; \u0026lt;directory suffix=\u0026#34;Test.php\u0026#34;\u0026gt;./tests/Feature\u0026lt;/directory\u0026gt; \u0026lt;/testsuite\u0026gt; \u0026lt;/testsuites\u0026gt; \u0026lt;filter\u0026gt; \u0026lt;whitelist processUncoveredFilesFromWhitelist=\u0026#34;true\u0026#34;\u0026gt; \u0026lt;directory suffix=\u0026#34;.php\u0026#34;\u0026gt;./app\u0026lt;/directory\u0026gt; \u0026lt;/whitelist\u0026gt; \u0026lt;/filter\u0026gt; \u0026lt;php\u0026gt; \u0026lt;server name=\u0026#34;APP_ENV\u0026#34; value=\u0026#34;testing\u0026#34;/\u0026gt; \u0026lt;server name=\u0026#34;BCRYPT_ROUNDS\u0026#34; value=\u0026#34;4\u0026#34;/\u0026gt; \u0026lt;server name=\u0026#34;CACHE_DRIVER\u0026#34; value=\u0026#34;array\u0026#34;/\u0026gt; \u0026lt;server name=\u0026#34;DB_CONNECTION\u0026#34; value=\u0026#34;mysql_test\u0026#34;/\u0026gt; \u0026lt;server name=\u0026#34;MAIL_DRIVER\u0026#34; value=\u0026#34;array\u0026#34;/\u0026gt; \u0026lt;server name=\u0026#34;QUEUE_CONNECTION\u0026#34; value=\u0026#34;sync\u0026#34;/\u0026gt; \u0026lt;server name=\u0026#34;SESSION_DRIVER\u0026#34; value=\u0026#34;array\u0026#34;/\u0026gt; \u0026lt;/php\u0026gt; \u0026lt;/phpunit\u0026gt; DB_CONNECTIONをmysql_testと指定しています。 さて、これでphpunitを実行できます。vendor/bin/phpunitは、このプロジェクトでインストールされたバージョンのphpunitなので、必ずそのパスを使用してください。\n$ vendor/bin/phpunit PHPUnit 8.5.2 by Sebastian Bergmann and contributors. .. 2 / 2 (100%) Time: 335 ms, Memory: 16.00 MB OK (2 tests, 2 assertions) OKがテスト成功、という意味です。\nRefreshDatabaseトレイト 先のphpunitの実行では、データベースのテストは含まれていません。作成してみましょう。\n$php artisan make:test --unit UserTest Test created successfully. この実行で、tests/Unit/UserTest.phpのファイルが作成されますが、それを以下のように編集します。\nUserTest.php namespace Tests\\Unit; // use PHPUnit\\Framework\\TestCase // Laravel 6.xで生成されるが、コメントして以下を追加 use Tests\\TestCase; use Illuminate\\Foundation\\Testing\\RefreshDatabase; // 追加 use App\\User; class UserTest extends TestCase { use RefreshDatabase; public function test_create_a_user() { factory(User::class)-\u0026gt;create(); // usersのDBテーブルにレコードを１つ作成 $this-\u0026gt;assertCount(1, User::all()); // すべてのレコードを取得してレコード数が１であることを確認 } } 実行してみましょう。\n$ vendor/bin/phpunit --filter UserTest PHPUnit 8.5.2 by Sebastian Bergmann and contributors. . 1 / 1 (100%) Time: 511 ms, Memory: 22.00 MB OK (1 test, 1 assertion) 成功ですね。\nこのテストは何回実行しても成功となります。\n$ vendor/bin/phpunit --filter UserTest PHPUnit 8.5.2 by Sebastian Bergmann and contributors. . 1 / 1 (100%) Time: 511 ms, Memory: 22.00 MB OK (1 test, 1 assertion) $ vendor/bin/phpunit --filter UserTest PHPUnit 8.5.2 by Sebastian Bergmann and contributors. . 1 / 1 (100%) Time: 511 ms, Memory: 22.00 MB OK (1 test, 1 assertion) 試しに、use RefershDatabaseをコメントして実行してみてください。\n$ vendor/bin/phpunit --filter UserTest PHPUnit 8.5.2 by Sebastian Bergmann and contributors. . 1 / 1 (100%) Time: 511 ms, Memory: 22.00 MB OK (1 test, 1 assertion) $ vendor/bin/phpunit --filter UserTest PHPUnit 8.5.2 by Sebastian Bergmann and contributors. F 1 / 1 (100%) Time: 324 ms, Memory: 18.00 MB There was 1 failure: 1) Tests\\Unit\\UserTest::testExample Failed asserting that actual size 2 matches expected size 1. /vol1/usr/www/repos/repos/l6x/tests/Unit/UserTest.php:23 FAILURES! Tests: 1, Assertions: 1, Failures: 1. １回名は成功ですが、２回目は失敗です．なぜなら、RefereshDatabaseなしではレコードは追加されていくばかりだからです。RefereshDatabaseのトレイトを使用することによりDBテーブルが空となり、毎回同じ条件でテストを実行できます。\n","date":"2020-02-01T02:18:37+09:00","permalink":"https://www.larajapan.com/2020/02/01/%E3%83%87%E3%83%BC%E3%82%BF%E3%83%99%E3%83%BC%E3%82%B9%E3%81%AE%E3%83%86%E3%82%B9%E3%83%88/","title":"データベースのテスト"},{"content":"既存のＤＢからmigrationファイルを自動作成によって、テストのために空のDBテーブルの作成が可能となりました。今度は、そこへテストデータを作成するためにfactoryが必要となります。これも自動作成しましょう。\nどうして、factoryのファイルが必要？ factoryのファイルが必要なのは、あらゆるテストの目的のためです。いちいち画面の入力でDBレコードを作成する代わりに、tinkerやユニットテストでfactory()のヘルパーを使用して簡単にレコードが作成できようになります。factoryのタグの記事では、たくさんの使用例を見ることできます。\nしかし、現状の以下のLaravelのコマンドの実行では、\n$ php artisan make:factory UserFactory --model=User 作成されるfactoryは、以下のように項目もない単なるテンプレートです。\ndatabases/factorys/UserFactory.php /** @var \\Illuminate\\Database\\Eloquent\\Factory $factory */ use App\\Model; use Faker\\Generator as Faker; $factory-\u0026gt;define(User::class, function (Faker $faker) { return [ // ]; }); DBテーブルがたくさんあるプロジェクトでは、いちいちこれを編集していくのはとても大変です。それゆえに、既存のDB構造をもとに、自動でfactoryファイルを生成する必要があります。\nfactoryファイル自動生成ツール まずは、パッケージのインストール。以下をコマンドラインで実行します。\n$ composer require --dev mpociot/laravel-test-factory-helper factoryのファイルを作成するには、artisanの実行が必要となります。Laravel 6.x の新規プロジェクトを作成して実行しますが、すでにUserのモデルのためのfactoryファイルが、databases/factories/UserFactory.phpとして存在するので、上書きされないように改名しておきます。そして以下をコマンドラインで実行します。\n$ php artisan generate:model-factory Model factory created: database/factories/UserFactory.php 作成されたfactoryの内容は以下です。\ndatabases/factories/UserFactory.php /* @var $factory \\Illuminate\\Database\\Eloquent\\Factory */ use Faker\\Generator as Faker; $factory-\u0026gt;define(App\\User::class, function (Faker $faker) { return [ \u0026#39;name\u0026#39; =\u0026gt; $faker-\u0026gt;name, \u0026#39;email\u0026#39; =\u0026gt; $faker-\u0026gt;safeEmail, \u0026#39;email_verified_at\u0026#39; =\u0026gt; $faker-\u0026gt;dateTime(), \u0026#39;password\u0026#39; =\u0026gt; bcrypt($faker-\u0026gt;password), \u0026#39;remember_token\u0026#39; =\u0026gt; Str::random(10), ]; }); 元にあったファイルと比べてみましょう。以下が元のファイルです。\n/** @var \\Illuminate\\Database\\Eloquent\\Factory $factory */ use App\\User; use Faker\\Generator as Faker; use Illuminate\\Support\\Str; $factory-\u0026gt;define(User::class, function (Faker $faker) { return [ \u0026#39;name\u0026#39; =\u0026gt; $faker-\u0026gt;name, \u0026#39;email\u0026#39; =\u0026gt; $faker-\u0026gt;unique()-\u0026gt;safeEmail, \u0026#39;email_verified_at\u0026#39; =\u0026gt; now(), \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi\u0026#39;, // password \u0026#39;remember_token\u0026#39; =\u0026gt; Str::random(10), ]; }); いくつか違いがありますね。しかし、これも自動生成されたmigrationファイルと同様に使用するテストに合わせて編集すればよいです。 Fakerのドキュメントは、こちらです。\nfactoryファイル自動作成のコマンドのartisanの実行には、いくつかのオプションを指定することができます。\nすべてのモデルのfactoryを作成する代わりに、指定のモデルだけのfactoryの作成には、\n$ php artisan generate:model-factory User Team モデルのファイルがが、app/Modelsのディレクトリにあるなら、\n$ php artisan generate:model-factory --dir app/Models -- User Team と実行できます。\n最後に、migrationとfactoryの自動作成ツールは、Laravelのバージョンによりインストールが違うのでここでまとめてリストします。\nLaravel 6.x\n$ composer require --dev oscarafdev/migrations-generator 2.0.13 $ composer require --dev mpociot/laravel-test-factory-helper Laravel 5.5~\n$ composer require --dev xethron/migrations-generator $ composer require --dev mpociot/laravel-test-factory-helper v1.3 ","date":"2020-01-25T07:51:08+09:00","permalink":"https://www.larajapan.com/2020/01/25/%E6%97%A2%E5%AD%98%E3%81%AEdb%E3%81%8B%E3%82%89factory%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%92%E8%87%AA%E5%8B%95%E4%BD%9C%E6%88%90/","title":"既存のDBからfactoryファイルを自動作成"},{"content":"またまたLaravelの更新の時期がやってきました。と言っても、またしても遅れての私のキャッチアップです。Laravel 6.xのバージョンは、Laravel 5.5と同様にLTSの長期サポートまでなので、バグの修正は、9/3/2021まで、セキュリティの修正は、9/3/2022までとあるので、できるならアップグレードしたいです。また、6.xのバージョンは、新サービスのサーバーレス対応のVaporでは必須ゆえに、Laravelを実行するサーバーのスケールアップに興味があるなら、なおさら頑張ってアップグレードする必要あります。\n何が変わった？ さて、バージョン6.xともなると、大きな変更は少なくなってきています。私のプロジェクトでのアップグレードで気づいたことの報告をします。\nまず忘れる前に、Laravel 6.xの更新には、最低php 7.2のバージョンが必要となります。と言っても、最新のphpは、バージョン7.4です。phpもLaravelもバージョンアップのペースが速い。もう少し遅くても、とも思います。\nヘルパーの変更 Laravel 5.8に更新で登場したのですが、配列や文字列のヘルパー関数が、クラスのstaticのメソッドに代わる、という話。例えば、array_only()は、Arr::only()に代わる。その時点では、両方使用できましたが、Laravel 6.xでは、もうグローバル関数の方はデフォルトでは使用できません。\nそれらを使用するには、以下を実行してパッケージを追加する必要あります。\n$ composer require laravel/helpers 使用していたパッケージへの影響 phpunitのバージョンは、Laravel 5.8のバージョン7からバージョン8になりました。そして悲しいことに私が愛好していたデータベースのテストのphpunit/dbunitの開発・管理が止まりました。\nここでいつか紹介しようと思っていたのですが、紹介する前に作者の意向で開発ストップです。dbunitは、xmlファイルで用意したデータをユニットテストのためにデータベースにインポートしてくれる便利な機能があります。毎回のテストの前には、データをtruncateしてくれるのでいつでも空の状態から始めることができ、でテストの再現性を実現してくれます。\nしかし、そこはオープンソースの強みというか、githubなどのサービスの便利さというか、他の開発者（以下のリンク）が早速にもレポをフォークしてphpunitバージョン8に対応してくれています。残念ながら、このパッケージのメインテナーとはならないようですが。\nhttps://github.com/kornrunner/dbunit\nこれで暫くは凌げそうですが、この際に、Laravelのmigrationやfactoryを使用した、RefershDatabaseのトレイトを使ったデータベースのテストに書き換えていこうとも思います。\nそして、そのステップ１として、前回の既存のＤＢからmigrationファイルを自動作成のパッケージの紹介でした。\nしかし、このパッケージ、残念ながらLaravel6.xでは互換性で使えない、と思ったら対応しているこれまたレポをフォークしたパッケージがありました。\nhttps://github.com/oscarafdev/migrations-generator\n捨てる神があれば、拾う神ありです。助けられます。\nユーザー認証 Laravel 5.xでは、php artisan make:authの実行でユーザー認証の機能が追加できました。\nLaravel 6.xでは、以下の実行が必要となります。\n$ composer require laravel/ui $ php artisan ui bootstrap --auth uiのタイプとしては、bootstrap, vue, reactの選択があります。これらの実行で、ユーザー認証に必要なrouteの追加や、コントローラーやブレードのファイルが作成されます。\n","date":"2020-01-18T01:14:39+09:00","permalink":"https://www.larajapan.com/2020/01/18/larave-6-x%E3%81%B8%E6%9B%B4%E6%96%B0/","title":"Larave 6.xへ更新"},{"content":"私が現在管理するプロジェクトのほとんどは、Laravelのフレームワーク採用以前から存在したphpのプロジェクトです。嬉しいことにお客様から移行の機会を与えられて、ほとんどLaravelに書き換えることができました。しかし、それらのプロジェクトのDBの管理には、Laravelのmigrationを使用していません。私が開発したオンラインのツールが生成するSQLファイルでDBの変更を行っています。最近になって、プロジェクトのデータベーステストを行う必要が出て、migrationのファイルを用意しなければなくなりました。\nどうして、migrationのファイルが必要？ 今までは、dbunitなるものを使用してデータベーステストを行っていましたが、これが去年に作者（phpunitの作者でもある）の意向で開発が停止となりました。それゆえに、テストにおいてLaravelのRefreshDatabaseトレイトの使用に変えていくことが突然に必要になったわけです。\nRefreshDatabaseは、毎回のテストの実行において、\n$ php artisan migrate:fresh の実行と同様に、migrationファイルで定義されたDBのテーブルをすべて削除し、再度一からそれらを作成します。つまり、データベースのテーブルをすべてゼロレコードの状態としてくれます。それゆえに、既存のDBのすべてのテーブルのmigrationを作成する必要があるのです。\nmigrationファイル自動生成ツール 既存のDBからmigrationファイルを作成するツールはないかと、探した結果、以下のツールが出てきました。\nhttps://github.com/Xethron/migrations-generator\nツールのインストールには、\n$ composer require --dev \u0026#34;xethron/migrations-generator\u0026#34; をコマンドラインで実行します。\nmigrationファイルを作成するには、artisanの実行となります。laravel 5.8で新規のプロジェクトを作成して実行してみました。\n$ php artisan migrate:generate Using connection: mysql Generating migrations for: password_resets, users Do you want to log these migrations in the migrations table? [Y/n] : \u0026gt; y Next Batch Number is: 2. We recommend using Batch Number 0 so that it becomes the \u0026#34;first\u0026#34; migration [Default: 0] : \u0026gt; 2 Setting up Tables and Index Migrations Created: /vol1/usr/www/repos/repos/l58-new/database/migrations/2020_01_08_191742_create_password_resets_table.php Created: /vol1/usr/www/repos/repos/l58-new/database/migrations/2020_01_08_191742_create_users_table.php Setting up Foreign Key Migrations Finished! 上の実行においては、２つ質問が尋ねられます。\n最初は、migrationsのDBテーブルにログを作成するか？ 2番目は、そのログにおいてのバッチ番号をなんとするか？\n上では、yと2と答えたところ、migrationsのデータは以下のようになりました。\nmysql\u0026gt; select * from migrations; +----+------------------------------------------------+-------+ | id | migration | batch | +----+------------------------------------------------+-------+ | 1 | 2014_10_12_000000_create_users_table | 1 | | 2 | 2014_10_12_100000_create_password_resets_table | 1 | | 3 | 2020_01_08_191742_create_password_resets_table | 2 | | 4 | 2020_01_08_191742_create_users_table | 2 | +----+------------------------------------------------+-------+ 4 rows in set (0.00 sec) すでに、php artisan migrateが実行されていたので、最初の２つのレコードはそのときに生成されていたものです。これらはLaraelのデフォルトのmigrationファイルです。\n一方、generateで生成されたのは次の２つのレコードですが、見ての通り既存のものと重複となってしまいました。既存のDBをもとに１からDBテーブルを作成するという目的では、generate実行の前にmigrationsテーブルを空としたほうが良さそうですね。\nさて、生成されたものと、デフォルトのmigrationファイルを比べてみましょう。\nデフォルト：2014_10_12_000000_create_users_table.php use Illuminate\\Support\\Facades\\Schema; use Illuminate\\Database\\Schema\\Blueprint; use Illuminate\\Database\\Migrations\\Migration; class CreateUsersTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create(\u0026#39;users\u0026#39;, function (Blueprint $table) { $table-\u0026gt;bigIncrements(\u0026#39;id\u0026#39;); $table-\u0026gt;string(\u0026#39;name\u0026#39;); $table-\u0026gt;string(\u0026#39;email\u0026#39;)-\u0026gt;unique(); $table-\u0026gt;timestamp(\u0026#39;email_verified_at\u0026#39;)-\u0026gt;nullable(); $table-\u0026gt;string(\u0026#39;password\u0026#39;); $table-\u0026gt;rememberToken(); $table-\u0026gt;timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists(\u0026#39;users\u0026#39;); } } 生成：2020_01_08_191742_create_users_table use Illuminate\\Database\\Migrations\\Migration; use Illuminate\\Database\\Schema\\Blueprint; class CreateUsersTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create(\u0026#39;users\u0026#39;, function(Blueprint $table) { $table-\u0026gt;bigInteger(\u0026#39;id\u0026#39;, true)-\u0026gt;unsigned(); $table-\u0026gt;string(\u0026#39;name\u0026#39;); $table-\u0026gt;string(\u0026#39;email\u0026#39;)-\u0026gt;unique(); $table-\u0026gt;dateTime(\u0026#39;email_verified_at\u0026#39;)-\u0026gt;nullable(); $table-\u0026gt;string(\u0026#39;password\u0026#39;); $table-\u0026gt;string(\u0026#39;remember_token\u0026#39;, 100)-\u0026gt;nullable(); $table-\u0026gt;timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::drop(\u0026#39;users\u0026#39;); } } やや違いがありますね。例えば、email_verified_atの定義、なぜかtimestampからdateTimeとなっています。 また、bigIncrementやrememberTokenに対応するフィールドも生成では変わっています。しかし、これらは、実行されるDBテーブルでは同じとなります。\nということで、mysqldumpの結果と見比べて、手動での調整が必要となりそうです。しかし、かなりの部分を作成してくれるので十分使えそうです。\n","date":"2020-01-10T03:33:08+09:00","permalink":"https://www.larajapan.com/2020/01/10/%E6%97%A2%E5%AD%98%E3%81%AE%EF%BD%84%EF%BD%82%E3%81%8B%E3%82%89migration%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%92%E8%87%AA%E5%8B%95%E4%BD%9C%E6%88%90/","title":"既存のＤＢからmigrationファイルを自動作成"},{"content":"Laravelを使用した開発においてこれは心掛けたい、というガイドがあったらいいなあと、思っていたら、ありました。 オリジナルは英語ですが、日本語訳あります。\nhttps://github.com/alexeymezenin/laravel-best-practices/blob/master/japanese.md\n単一責任の原則 ファットモデル, スキニーコントローラ バリデーション ビジネスロジックはサービスクラスの中に書く 繰り返し書かない (DRY) クエリビルダや生のSQLクエリよりもEloquentを優先して使い、配列よりもコレクションを優先する マスアサインメント Bladeテンプレート内でクエリを実行しない。Eager Lodingを使う(N + 1問題) コメントを書く。ただしコメントよりも説明的なメソッド名と変数名を付けるほうが良い JSとCSSをBladeテンプレートの中に入れない、PHPクラスの中にHTMLを入れない コード内の文字列の代わりにconfigファイルとlanguageのファイル、定数を使う コミュニティに受け入れられた標準のLaravelツールを使う Laravelの命名規則に従う できるだけ短く読みやすい構文で書く newの代わりにIoCコンテナもしくはファサードを使う .envファイルのデータを直接参照しない 日付を標準フォーマットで保存する。アクセサとミューテータを使って日付フォーマットを変更する その他 グッドプラクティス 新年を迎えて、できるだけ実行したいと思っています。\n","date":"2020-01-04T02:05:43+09:00","permalink":"https://www.larajapan.com/2020/01/04/laravel%E3%81%AE%E3%83%99%E3%82%B9%E3%83%88%E3%83%97%E3%83%A9%E3%82%AF%E3%83%86%E3%82%A3%E3%82%B9/","title":"Laravelのベストプラクティス"},{"content":"前回のTurbolinks、再びにおいて掲載した記事に、修正が必要なことに気づきました。登録フォームのPOSTでバリデーションエラーが出て同画面に戻るときは問題ないのですが、エラーがなく登録完了してホーム画面へリダイレクトされたときが問題です。\n以下のように、画面はホーム画面の中身となっていますが、ブラウザのURLがhomeであるべきところ、registerのままでした。\nこれを修正するには、まずブレードのインラインのjsから、\nresources/views/auth/register.blade.php ... @section(\u0026#39;script\u0026#39;) $(document).on(\u0026#39;turbolinks:load\u0026#39;, function() { $(\u0026#34;form\u0026#34;).on(\u0026#34;submit\u0026#34;, function(event) { event.preventDefault(); $.ajax({ url: \u0026#39;/register\u0026#39;, type: \u0026#39;POST\u0026#39;, dataType: \u0026#39;html\u0026#39;, processData: false, contentType: false, cache: false, data: new FormData($(\u0026#34;form\u0026#34;)[0]) }).done(function(response, status, $xhr) { var redirect = $xhr.getResponseHeader(\u0026#39;Turbolinks-Location\u0026#39;); if (redirect) { // 投稿成功でリダイレクトしたとき Turbolinks.visit(redirect); } else { // バリデーションエラーのとき var referrer = window.location.href; Turbolinks.controller.cache.put(referrer, Turbolinks.Snapshot.wrap(response)); Turbolinks.visit(referrer, { action: \u0026#39;restore\u0026#39; }); } }); }); }); @endsection 変更は、投稿で成功したときを認識して、Turbolinks.visit()をリダイレクト先でコールするところです。\nさて、問題は投稿が成功したか否かをどう判定するか、です。\nそれには、サーバーサイドでPOSTが生成するレスポンスのヘッダーにリダイレクト先を含む項目を追加してクライアント（ブラウザ）にコミュニケートする必要があります。上の例では、ヘッダーにTurbolinks-Locationの項目に値があれば、リダイレクトが必要という条件文になりました。その情報がなければ、バリデーションエラーが発生したので同じ画面を表示します。\nさて、POSTのレスポンスにこの特別なヘッダーの情報を追加するには、以下のようにコントローラを編集します。\napp/Http/Controllers/Auth/RegisterController.php namespace App\\Http\\Controllers\\Auth; use App\\User; use App\\Http\\Controllers\\Controller; use Illuminate\\Support\\Facades\\Hash; use Illuminate\\Support\\Facades\\Validator; use Illuminate\\Foundation\\Auth\\RegistersUsers; // register()で使用される以下の２行の宣言が必要！ use Illuminate\\Http\\Request; use Illuminate\\Auth\\Events\\Registered; class RegisterController extends Controller { use RegistersUsers; ... /** * Handle a registration request for the application. * * @param \\Illuminate\\Http\\Request $request * @return \\Illuminate\\Http\\Response */ public function register(Request $request) { $this-\u0026gt;validator($request-\u0026gt;all())-\u0026gt;validate(); event(new Registered($user = $this-\u0026gt;create($request-\u0026gt;all()))); $this-\u0026gt;guard()-\u0026gt;login($user); // オリジナルのコード // return $this-\u0026gt;registered($request, $user) // ?: redirect($this-\u0026gt;redirectPath()); return response(\u0026#39;success\u0026#39;, 200) -\u0026gt;header(\u0026#39;Turbolinks-Location\u0026#39;, $this-\u0026gt;redirectPath()); } } RegistersUsersのトレイトで定義されている、register()メソッドをコピーペーストして上書きします。 ヘッダーのTurbolinks-Locationには、リダイレクト先のURLが含まれることになります。 ちなみに、response()で指定している、\u0026lsquo;success\u0026rsquo;の文字列は使用しないのでなんでもよいです。空でも。\nこれで登録を実行すると、POSTのヘッダーには以下のように, Turbolinks-Locationは/homeの値となります。そして、その値が先のインラインのjsで抽出されて、Turbolinks.visit()に渡されてbodyタグの中身を取り換えてブラウザのURLを変更します。\nHTTP/1.1 200 OK Date: Fri, 13 Dec 2019 17:31:21 GMT Server: Apache/2.4.38 (Fedora) OpenSSL/1.1.1b X-Powered-By: PHP/7.2.16 Cache-Control: no-cache, private Turbolinks-Location: /home ... リダイレクト先には、Turbolinks-Locationというヘッダーの変数名を使用しましたが、他と重複がなければどんな名前でもよいです。サーバーとクラインとで一致している限り。\nコントローラを編集しなければならない部分が残念ですが、たいした編集ではないので、私の中ではやはりサイトのSPA化ではまだまだコストパフォーマンスが大きい技術です。\n","date":"2019-12-15T00:53:46+09:00","permalink":"https://www.larajapan.com/2019/12/15/turbolinks%E3%80%81post%E5%BE%8C%E3%81%AE%E3%83%AA%E3%83%80%E3%82%A4%E3%83%AC%E3%82%AF%E3%83%88/","title":"Turbolinks、POST成功後のリダイレクト"},{"content":"私が注目している技術に関してリストしてみます。もちろん、Laravel関連なのですが、フロントエンドやバックエンド含めて、いやいや、本当にいいものが登場してきています。将来時間を見つけてじっくりと紹介したいものばかりです。\nフロントエンド 私が興味があるのは、既存のLaravelのサーバーサイドのコントローラを書き直さずに、どうやってフロントエンドをSPA化するか、です。\n巷のjsのフレームワークでは、独自のroutingを設定して、サーバーサイドは単にそれにデータを注入するAPIとなることを強制します。新規のプロジェクトならまだしも、こちら何年と管理している既存のプロジェクトを書き換えるのはしんどい、サーバーでどちらにしろやらなければならない、routingやバリデーションを、どうしてまたフロントエンドでプログラムする必要ある？、などなど不満あります。\nつまり、私は楽をしてなるべく現在のLaravelのコントローラの書き替えを避けたいのです。そういう意味では、POSTもSPA化できた今となっては、前回のturbolinksは最高！です。しかし、そこに辿り着く前に、なぜか以下の２つ新技術に巡り合いました。これら両方とも、今年登場したてで新しく、また両方とも、既存のコントローラを活かしている、という点でとても将来性が大いにあります。\ninertiajs https://inertiajs.com\nイナーシャJSは、今年登場で、以下のようにコントローラで、通常view()でブレードを指定して画面をレンダリングする部分を、独自のInertia::render()を使用します。\nclass UsersController extends Controller { public function index() { return Inertia::render(\u0026#39;Users/Index\u0026#39;, [ \u0026#39;users\u0026#39; =\u0026gt; User::all(), ]); } public function store() { User::create( Request::validate([ \u0026#39;first_name\u0026#39; =\u0026gt; [\u0026#39;required\u0026#39;, \u0026#39;max:50\u0026#39;], \u0026#39;last_name\u0026#39; =\u0026gt; [\u0026#39;required\u0026#39;, \u0026#39;max:50\u0026#39;], \u0026#39;email\u0026#39; =\u0026gt; [\u0026#39;required\u0026#39;, \u0026#39;max:50\u0026#39;, \u0026#39;email\u0026#39;], ]) ); return Redirect::route(\u0026#39;users\u0026#39;); } } そして、通常使うブレードの代わりに、なんと、vuejsを使います！\n\u0026lt;template\u0026gt; \u0026lt;form @submit.prevent=\u0026#34;submit\u0026#34;\u0026gt; \u0026lt;label for=\u0026#34;first_name\u0026#34;\u0026gt;First name:\u0026lt;/label\u0026gt; \u0026lt;input id=\u0026#34;first_name\u0026#34; v-model=\u0026#34;form.first_name\u0026#34; /\u0026gt; \u0026lt;div v-if=\u0026#34;$page.errors.first_name\u0026#34;\u0026gt;{{ $page.errors.first_name[0] }}\u0026lt;/div\u0026gt; \u0026lt;label for=\u0026#34;last_name\u0026#34;\u0026gt;Last name:\u0026lt;/label\u0026gt; \u0026lt;input id=\u0026#34;last_name\u0026#34; v-model=\u0026#34;form.last_name\u0026#34; /\u0026gt; \u0026lt;div v-if=\u0026#34;$page.errors.last_name\u0026#34;\u0026gt;{{ $page.errors.last_name[0] }}\u0026lt;/div\u0026gt; \u0026lt;label for=\u0026#34;email\u0026#34;\u0026gt;Email:\u0026lt;/label\u0026gt; \u0026lt;input id=\u0026#34;email\u0026#34; v-model=\u0026#34;form.email\u0026#34; /\u0026gt; \u0026lt;div v-if=\u0026#34;$page.errors.email\u0026#34;\u0026gt;{{ $page.errors.email[0] }}\u0026lt;/div\u0026gt; \u0026lt;button type=\u0026#34;submit\u0026#34;\u0026gt;Submit\u0026lt;/button\u0026gt; \u0026lt;/form\u0026gt; \u0026lt;/template\u0026gt; \u0026lt;script\u0026gt; export default { data() { return { form: { first_name: null, last_name: null, email: null, }, } }, methods: { submit() { this.$inertia.post(\u0026#39;/users\u0026#39;, this.form) }, }, } \u0026lt;/script\u0026gt; これが今のところの私のおおまかな理解ですが、興味あれば以下チェックしてみてください。ちなみに、Laravel + vuejs だけでなく、Laravel + Reactとか、Rails + Reactとか特定のフレームワークに固定されないのも特長です。\nhttps://reinink.ca/articles/introducing-inertia-js\nLaravel Livewire https://laravel-livewire.com\nララベルライブワイヤーは、イナーシャとに比べてよりLaravelワールドです。イナーシャJSと同様に、Laravelのコントローラをキープしますが、ブレードのテンプレートもキープして、独自のシンタックスを追加して、javascriptのコードを書くことなしに、SPAに対応です。\nコントローラを以下とすると、\nuse Livewire\\Component; class ContactForm extends Component { public $name; public $email; public function submit() { $this-\u0026gt;validate([ \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;required|min:6\u0026#39;, \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;required|email\u0026#39;, ]); // Execution doesn\u0026#39;t reach here if validation fails. Contact::create([ \u0026#39;name\u0026#39; =\u0026gt; $this-\u0026gt;name, \u0026#39;email\u0026#39; =\u0026gt; $this-\u0026gt;email, ]); } public function render() { return view(\u0026#39;livewire.contact-form\u0026#39;); } } そのブレードは、\n\u0026lt;form wire:submit=\u0026#34;submit\u0026#34;\u0026gt; \u0026lt;input type=\u0026#34;text\u0026#34; wire:model=\u0026#34;name\u0026#34;\u0026gt; @error(\u0026#39;name\u0026#39;) \u0026lt;span class=\u0026#34;error\u0026#34;\u0026gt;{{ $message }}\u0026lt;/span\u0026gt; @enderror \u0026lt;input type=\u0026#34;text\u0026#34; wire:model=\u0026#34;email\u0026#34;\u0026gt; @error(\u0026#39;email\u0026#39;) \u0026lt;span class=\u0026#34;error\u0026#34;\u0026gt;{{ $message }}\u0026lt;/span\u0026gt; @enderror \u0026lt;button type=\u0026#34;submit\u0026#34;\u0026gt;Save Contact\u0026lt;/button\u0026gt; \u0026lt;/form\u0026gt; wire:modeとかは、vuejsのv-modelみたいですね。\n仕組みは、サーバーサイドでブレードにjavascriptを埋め込んでいくみたいです。\n","date":"2019-12-08T02:07:49+09:00","permalink":"https://www.larajapan.com/2019/12/08/%E6%B0%97%E3%81%AB%E3%81%AA%E3%82%8B%E3%83%95%E3%83%AD%E3%83%B3%E3%83%88%E3%82%A8%E3%83%B3%E3%83%89%E3%81%AE%E6%8A%80%E8%A1%93/","title":"気になるフロントエンドの技術"},{"content":"ほぼ２年前にTurbolinksを紹介しました。全ページロードのウェブサイトを高速のSPA(シングルページアプリ)に変えてくれます。しかも、Laravelのフロントエンドのブレードやバックエンドのコントローラーをまったく変えることなしです。今回は、再びこのTurbolinksに戻って、まずcdnを使わずにnpmを使用してトランスパイルします。そして前回不可能と思っていたajaxによるフォームのPOST投稿もSPA化します。\nSPA移行へのコストが非常に低いTurbolinks ある程度大きな規模のプロジェクトを抱えていると、開発テクノロジー進歩によりプログラムを書き直す必要性が出てきます。それはまったく新しいデバイスへの対応（例：bootstrap）であったり、プログラムの管理性を高めるためのフレームワークの採用（例：Laravel）であったりします。プロジェクトが大きいほど、書き直しには時間とコストがかかります。スマホの対応ならお客さんにとっては理解されやすいですが、フレームワークの採用では外観は何も変わらないときもあるので、何か月（あるいは何年）もかけて書き直す必要性を説くのは大変です。 今回のSPAの採用も同じ状況です。SPAの採用の目的はユーザー体験の改善です。画面のリフレッシュが行われないのでページの遷移がスムーズで高速です。それだけで採用の説得は十分ですが、問題はSPAへ移行のためのコストが非常に高いことです。まず、すでにたくさんある、しかも新しいのが次々と登場する、SPAのJavascriptのフレームワークの中で、どれが有望性があるか長期的に管理されるかを見極めるために時間が必要なこと、さらにどれかに決めたらそれを学習して巧みになること、その上で既存のプロジェクトをSPAへ移行する開発です。これらを考慮すると、ReactJS, AngularJS, VueJSなどを採用してSPAに書き直すには莫大な時間、つまりコストがかかります。それらのフレームワークの経験に乏しい私が持つ小さい開発チームだと、特に。\nそこで輝くのがTurbolinks!\nTurbolinksを採用すると、ウェブページ内に同じサイト内のリンク、つまりがあると、そのリンクがクリックされたら、Turbolinksはajaxでそのページを取ってきてその中ののコンテンツを現在のページのものと取り換えます。は同じなので、全ページ更新とはならず、SPA特有のスムーズなページ遷移となります。しかも、ブラウザのURLはあたかも全ページ更新と同様に変更されるし、遷移したページ内のJavascriptも実行されます。さらに、ブラウザのWeb History APIを利用してキャッシュからの「戻る」ボタンで前のページの出力にも対応しています。これらは、既存のサーバーサイドのプログラムを変更することなく、次に説明する、npmでパッケージを追加するだけでできちゃいます。夢のような話と思いませんか？\nnpmでパッケージを追加 このTurbolinksを前回紹介したときは、\n\u0026lt;html lang=\u0026#34;ja\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;utf-8\u0026#34;\u0026gt; \u0026lt;title\u0026gt;Turbolinks\u0026lt;/title\u0026gt; \u0026lt;script src=\u0026#34;https://cdnjs.cloudflare.com/ajax/libs/turbolinks/5.0.3/turbolinks.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; .. のようにCDNを使う設定でした。\n今回は、npmでパッケージを追加して（関連記事）して、npmでトランスパイルします。\nまず、コマンドラインでパッケージの追加。\n$ npm install turbolinks --save-dev 次に、bootstrap.jsに以下の行を追加して、このパッケージをロードします。\nresources/js/bootstrap.js ... var Turbolinks = require(\u0026#34;turbolinks\u0026#34;); Turbolinks.start(); 最後にトランスパイルの実行です（ライブバージョンは、npm run prod）。\n$ npm run dev これでホーム画面のリンクから以下の会員登録画面にアクセスすると、以下のようにすでにSPA化されています。画面の下は、FirefoxのInspectorのパネルですが、そこのxhrはXMLHttpRequest (XHR) のことでいわゆるajaxの実行のことです。\nフォームの投稿のSPA化 さて、turbolinksは、リンクのようなGETに対しては自動にSPA化してくれますが、POSTは自動で対応していません。同じ登録画面で、入力して意図的にエラーを出すようにして、「Register」ボタンを押すと、以下のように通常の入力データの投稿のPOSTとエラーの出力のGETのアクションが起こります。\n見ての通り、それらのアクションはajaxではありません。通常の全ページロードです。\nturbolinksのレポのgithubサイトには、フォームの投稿後のリダイレクト、つまり入力が成功して違う画面に（例：ホーム画面）に飛ぶことに関して、ajaxを使用しての対応に関して書かれている部分があります。しかし、上のようにバリデーションエラーが発生して入力画面に戻りエラーを出力する場合は書かれていません。\nつい最近まで、私も「そういうものなのか」「まあGETだけSPAになるだけでも結構」と思っていました。しかし、より調査したところ、Rubyではエラーの際に画面をレンダーするパッケージがある、ということを知りました。ちなみに、TurbolinksはRuby in Railsを開発したBaseCampが発明したものなので、とてもRubyとの仲が良い。\nRubyで可能なら、どこかにそれに対応するJavascriptの例があるのでは、と思い、以下にそれらしきディスカッションを見つけました。\nhttps://github.com/turbolinks/turbolinks/issues/85#issuecomment-338784510\nこれをもとに、会員登録をSPA化してみます。\n編集するのは以下のブレードです。\nresources/views/auth/register.blade.php ... @section(\u0026#39;script\u0026#39;) $(document).on(\u0026#39;turbolinks:load\u0026#39;, function() { // Turbolinksがロード完了のイベントにおいて、 $(\u0026#34;form\u0026#34;).on(\u0026#34;submit\u0026#34;, function(event) { event.preventDefault(); $.ajax({　// jqueryのajax関数 url: \u0026#39;/register\u0026#39;, // データ送信宛てのURL type: \u0026#39;POST\u0026#39;, dataType: \u0026#39;html\u0026#39;, processData: false, contentType: false, cache: false, data: new FormData($(\u0026#34;form\u0026#34;)[0])　// フォームのデータを送信 }).done(function(response, status, $xhr) { if (response \u0026amp;\u0026amp; ($xhr.getResponseHeader(\u0026#39;Content-Type\u0026#39;) || \u0026#39;\u0026#39;).substring(0, 9) === \u0026#39;text/html\u0026#39;) { var referrer = window.location.href; //　現在のURLを取得 Turbolinks.controller.cache.put(referrer, Turbolinks.Snapshot.wrap(response));　// キャッシュにレスのデータを入れる Turbolinks.visit(referrer, { action: \u0026#39;restore\u0026#39; });　// キャッシュを呼び出す } }); }); }); @endsection そして、このscriptのセクションは、レイアウトのブレードに入れ込みます。\nresources/views/layouts/app.blade.ophp \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;{{ str_replace(\u0026#39;_\u0026#39;, \u0026#39;-\u0026#39;, app()-\u0026gt;getLocale()) }}\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;utf-8\u0026#34;\u0026gt; \u0026lt;meta name=\u0026#34;viewport\u0026#34; content=\u0026#34;width=device-width, initial-scale=1\u0026#34;\u0026gt; \u0026lt;!-- CSRF Token --\u0026gt; \u0026lt;meta name=\u0026#34;csrf-token\u0026#34; content=\u0026#34;{{ csrf_token() }}\u0026#34;\u0026gt; \u0026lt;title\u0026gt;{{ config(\u0026#39;app.name\u0026#39;, \u0026#39;Laravel\u0026#39;) }}\u0026lt;/title\u0026gt; \u0026lt;!-- Scripts --\u0026gt; \u0026lt;script src=\u0026#34;{{ asset(\u0026#39;js/app.js\u0026#39;) }}\u0026#34; defer\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script type=\u0026#34;module\u0026#34;\u0026gt; @yield(\u0026#39;script\u0026#39;) \u0026lt;/script\u0026gt; ... これで再度、登録画面での入力エラーの実行をすると、\n今度は、documentではなくxhrでの取得になっています。\nさらに、登録成功のときは、以下のようにホーム画面への遷移もSPA化されています。\n前回のVue.js：バックエンドのバリデーションを使うと、まったく同じ動作です。比べてみてください。\n","date":"2019-12-03T00:44:58+09:00","permalink":"https://www.larajapan.com/2019/12/03/turbolinks%E3%80%81%E5%86%8D%E3%81%B3/","title":"Turbolinks、再び"},{"content":"最近、フロントエンドのフレームワークのVue.jsを勉強し始めました。またしても世間の流行から一歩遅れての開始ですが、比較的大きなプロジェクトを管理する私としては、ピッカピッカの流行りをすぐに採用とは行きません。長期的な管理を考えてLaravelのように本当にメージャーになるかを見極めてからです。幸い、Laravelのコミュニティでは、Vue.jsが盛んに利用され情報が多いので、よりメージャーなReactやAngularなどのフレームワークの存在を気にしつつもLaravelとの親近さでVue.jsの習得です。Vue.jsの1からの説明は他の人のブログに任せて、私の方はいきなり実用的なVue.jsを紹介していく予定です。まず今回は、Vue.jsをフォームの入力のバリデーションに使用するところを紹介します。\nフロントエンドのバリデーション フォームのバリデーションと言えば、Laravelでは、コントローラにおけるvalidate()です。ルールを指定するだけでとても便利です。しかし、それはバックエンドの話で、その実行には毎回毎回画面を更新するPOSTの実行が必要です。それが嫌なら、inputのHTMLのタグのrequiredで入力必須の指定やtypeでデータタイプを指定するか、あるいはjqueryのvalidation inputのプラグインの使用で、フロントエンドである程度のバリデーションを肩代わりしてもらいます。\n例えば、Laravelのデフォルトのインストールでの会員登録のフォームでは、以下のようにinputでtypeやrequiredを使いフロントエンドでベーシックなチェックしています。画面の更新を必要とせず高速なので、ユーザーにとっても快適です。\n/resources/views/auth/register.blade.php @extends(\u0026#39;layouts.app\u0026#39;) @section(\u0026#39;content\u0026#39;) \u0026lt;div class=\u0026#34;container\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;row justify-content-center\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;col-md-8\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;card\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;card-header\u0026#34;\u0026gt;{{ __(\u0026#39;Register\u0026#39;) }}\u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;card-body\u0026#34;\u0026gt; \u0026lt;form method=\u0026#34;POST\u0026#34; action=\u0026#34;{{ route(\u0026#39;register\u0026#39;) }}\u0026#34;\u0026gt; @csrf \u0026lt;div class=\u0026#34;form-group row\u0026#34;\u0026gt; \u0026lt;label for=\u0026#34;name\u0026#34; class=\u0026#34;col-md-4 col-form-label text-md-right\u0026#34;\u0026gt;{{ __(\u0026#39;Name\u0026#39;) }}\u0026lt;/label\u0026gt; \u0026lt;div class=\u0026#34;col-md-6\u0026#34;\u0026gt; \u0026lt;input id=\u0026#34;name\u0026#34; type=\u0026#34;text\u0026#34; class=\u0026#34;form-control @error(\u0026#39;name\u0026#39;) is-invalid @enderror\u0026#34; name=\u0026#34;name\u0026#34; value=\u0026#34;{{ old(\u0026#39;name\u0026#39;) }}\u0026#34; required autocomplete=\u0026#34;name\u0026#34; autofocus\u0026gt; @error(\u0026#39;name\u0026#39;) \u0026lt;span class=\u0026#34;invalid-feedback\u0026#34; role=\u0026#34;alert\u0026#34;\u0026gt; \u0026lt;strong\u0026gt;{{ $message }}\u0026lt;/strong\u0026gt; \u0026lt;/span\u0026gt; @enderror \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;form-group row\u0026#34;\u0026gt; \u0026lt;label for=\u0026#34;email\u0026#34; class=\u0026#34;col-md-4 col-form-label text-md-right\u0026#34;\u0026gt;{{ __(\u0026#39;E-Mail Address\u0026#39;) }}\u0026lt;/label\u0026gt; \u0026lt;div class=\u0026#34;col-md-6\u0026#34;\u0026gt; \u0026lt;input id=\u0026#34;email\u0026#34; type=\u0026#34;email\u0026#34; class=\u0026#34;form-control @error(\u0026#39;email\u0026#39;) is-invalid @enderror\u0026#34; name=\u0026#34;email\u0026#34; value=\u0026#34;{{ old(\u0026#39;email\u0026#39;) }}\u0026#34; required autocomplete=\u0026#34;email\u0026#34;\u0026gt; @error(\u0026#39;email\u0026#39;) \u0026lt;span class=\u0026#34;invalid-feedback\u0026#34; role=\u0026#34;alert\u0026#34;\u0026gt; \u0026lt;strong\u0026gt;{{ $message }}\u0026lt;/strong\u0026gt; \u0026lt;/span\u0026gt; @enderror \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; ... 画面ではこんな感じ。\nしかし、フロンドエンドのチェックは完璧ではなく、例えば、パスワードの確認、つまりPasswordとConfirm Passwordの値が同じかどうかはバックエンドのバリデーションに任せます。\nさらに、ブラウザを使用せずにPOSTで特定のURL（ここでは /register）に直接データを送信すれば、フロントエンドのバリデーションをすっ飛ばすことは簡単に可能なので、フロントエンドのバリデーションがあるからバックエンドのバリデーションが要らないということにはなりません。\nもちろん、バリデーションがフロントにあるのは、バックエンドの使用が経済的（サーバーのリソースを消費しない）になるので良いのですが、同じバリデーションを２箇所で、しかも違う言語（片方はhtmlタグあるいはjquery、もう片方はlaravelやphp）で管理するのはやっかいです。そこで、Vue.jsを使用して画面を更新せずにバックエンドのコントローラのバリデーションを実行してエラーを出力すれば、というのが今回のポストです。\nVue.jsのバックエンドバリデーションパッケージ 今回使用するのは以下のパッケージです。Laracastで有名なJeffreyくんがオリジナルに作成したのをベルギーのspatieの会社がパッケージ化してオープンソースしています。ちなみに、spatieは前回紹介したWeb tinkerも開発しています。しかも、それはVue.jsで開発されています。\nhttps://github.com/spatie/form-backend-validation\nまず、インストールとしては、デフォルトのLaravelのプロジェクトを作成したことを仮定して、\n$ php artisan make:auth をコマンドラインで実行後、\n$ npm install を実行して、node_modulesをダウンロードして、\n$ npm install vue $ npm install form-backend-validation とパッケージをインストールします。\nブレードとスクリプトの編集 それから、先の会員登録のフォームのブレードを以下のように編集します。完全にバックエンドのバリデーションのみを使うために、inputのタグ内において、requiredなどのフロントエンドのバリデーションは削除しました。また、emailではinput typeは、emailからtextに変更しています。上のHTMLを比べてみてください。\n/resources/views/auth/register.blade.php @extends(\u0026#39;layouts.app\u0026#39;) @section(\u0026#39;content\u0026#39;) \u0026lt;div class=\u0026#34;container\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;row justify-content-center\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;col-md-8\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;card\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;card-header\u0026#34;\u0026gt;{{ __(\u0026#39;Register\u0026#39;) }}\u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;card-body\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;alert\u0026#34; :class=\u0026#34;messageClass\u0026#34;\u0026gt; @{{ message }} \u0026lt;/div\u0026gt; \u0026lt;form @submit.prevent=\u0026#34;onSubmit\u0026#34; @keydown=\u0026#34;form.errors.clear($event.target.name);\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;form-group row\u0026#34;\u0026gt; \u0026lt;label for=\u0026#34;name\u0026#34; class=\u0026#34;col-md-4 col-form-label text-md-right\u0026#34;\u0026gt;{{ __(\u0026#39;Name\u0026#39;) }}\u0026lt;/label\u0026gt; \u0026lt;div class=\u0026#34;col-md-6\u0026#34;\u0026gt; \u0026lt;input id=\u0026#34;name\u0026#34; type=\u0026#34;text\u0026#34; class=\u0026#34;form-control\u0026#34; :class=\u0026#34;{ \u0026#39;is-invalid\u0026#39;: form.errors.has(\u0026#39;name\u0026#39;) }\u0026#34; v-model=\u0026#34;form.name\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;invalid-feedback\u0026#34; v-if=\u0026#34;form.errors.has(\u0026#39;name\u0026#39;)\u0026#34; v-text=\u0026#34;form.errors.first(\u0026#39;name\u0026#39;)\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;form-group row\u0026#34;\u0026gt; \u0026lt;label for=\u0026#34;email\u0026#34; class=\u0026#34;col-md-4 col-form-label text-md-right\u0026#34;\u0026gt;{{ __(\u0026#39;E-Mail Address\u0026#39;) }}\u0026lt;/label\u0026gt; \u0026lt;div class=\u0026#34;col-md-6\u0026#34;\u0026gt; \u0026lt;input id=\u0026#34;email\u0026#34; type=\u0026#34;text\u0026#34; class=\u0026#34;form-control\u0026#34; :class=\u0026#34;{ \u0026#39;is-invalid\u0026#39;: form.errors.has(\u0026#39;email\u0026#39;) }\u0026#34; v-model=\u0026#34;form.email\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;invalid-feedback\u0026#34; v-if=\u0026#34;form.errors.has(\u0026#39;email\u0026#39;)\u0026#34; v-text=\u0026#34;form.errors.first(\u0026#39;email\u0026#39;)\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;form-group row\u0026#34;\u0026gt; \u0026lt;label for=\u0026#34;password\u0026#34; class=\u0026#34;col-md-4 col-form-label text-md-right\u0026#34;\u0026gt;{{ __(\u0026#39;Password\u0026#39;) }}\u0026lt;/label\u0026gt; \u0026lt;div class=\u0026#34;col-md-6\u0026#34;\u0026gt; \u0026lt;input id=\u0026#34;password\u0026#34; type=\u0026#34;password\u0026#34; class=\u0026#34;form-control\u0026#34; :class=\u0026#34;{ \u0026#39;is-invalid\u0026#39;: form.errors.has(\u0026#39;password\u0026#39;) }\u0026#34; v-model=\u0026#34;form.password\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;invalid-feedback\u0026#34; v-if=\u0026#34;form.errors.has(\u0026#39;password\u0026#39;)\u0026#34; v-text=\u0026#34;form.errors.first(\u0026#39;password\u0026#39;)\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;form-group row\u0026#34;\u0026gt; \u0026lt;label for=\u0026#34;password-confirm\u0026#34; class=\u0026#34;col-md-4 col-form-label text-md-right\u0026#34;\u0026gt;{{ __(\u0026#39;Confirm Password\u0026#39;) }}\u0026lt;/label\u0026gt; \u0026lt;div class=\u0026#34;col-md-6\u0026#34;\u0026gt; \u0026lt;input id=\u0026#34;password_confirmation\u0026#34; type=\u0026#34;password\u0026#34; class=\u0026#34;form-control\u0026#34; :class=\u0026#34;{ \u0026#39;is-invalid\u0026#39;: form.errors.has(\u0026#39;password_confirmation\u0026#39;) }\u0026#34; v-model=\u0026#34;form.password_confirmation\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;invalid-feedback\u0026#34; v-if=\u0026#34;form.errors.has(\u0026#39;password_confirmation\u0026#39;)\u0026#34; v-text=\u0026#34;form.errors.first(\u0026#39;password_confirmation\u0026#39;)\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;form-group row mb-0\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;col-md-6 offset-md-4\u0026#34;\u0026gt; \u0026lt;button type=\u0026#34;submit\u0026#34; class=\u0026#34;btn btn-primary\u0026#34; :disabled=\u0026#34;form.errors.any()\u0026#34;\u0026gt; {{ __(\u0026#39;Register\u0026#39;) }} \u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/form\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; @endsection VueやFormのパッケージを使用するために、app.jsの編集も必要です。\nresources/js/app.js require(\u0026#39;./bootstrap\u0026#39;); window.Vue = require(\u0026#39;vue\u0026#39;); import Form from \u0026#39;form-backend-validation\u0026#39;; window.Form = Form; const app = new Vue({ el: \u0026#39;#app\u0026#39;, data: { form: new Form({ name: \u0026#39;\u0026#39;, email: \u0026#39;\u0026#39;, password: \u0026#39;\u0026#39;, password_confirmation: \u0026#39;\u0026#39; }), message: \u0026#39;\u0026#39;, messageClass: \u0026#39;\u0026#39; }, methods: { onSubmit() { this.form[\u0026#39;post\u0026#39;](\u0026#39;/register\u0026#39;) // 適切なパスに変更してください！ .then(response =\u0026gt; this.displaySuccessMessage(\u0026#39;Your account was created\u0026#39;)) .catch(response =\u0026gt; this.displayErrorMessage(\u0026#39;Your account was not created\u0026#39;)); }, displaySuccessMessage(message) { this.messageClass = \u0026#39;alert-success\u0026#39;; this.message = message; }, displayErrorMessage(message) { this.messageClass = \u0026#39;alert-danger\u0026#39;; this.message = message; }, clearMessage() { this.message = \u0026#39;\u0026#39;; }, } }); 最後に、以下の実行が必要です。\n$ npm run dev ブラウザで見てみましょう。RegisterController.phpのコントローラのvalidationが実行されてエラーが画面を更新せずにエラーが出力されています。\nインラインのスクリプト 上の例では、app.jsでは、登録画面だけにしか対応していません。他にも入力画面があるなら問題です。必要な部分だけをそれぞれに画面のインラインスクリプトとしてみます。\nまず、app.jsからVueのオブジェクト生成のコードを削除します。\nresources/js/app.js require(\u0026#39;./bootstrap\u0026#39;); window.Vue = require(\u0026#39;vue\u0026#39;); import Form from \u0026#39;form-backend-validation\u0026#39;; window.Form = Form; そして、それをコンパイルします。\n$ npm run dev 次に、app.jsで削除したオブジェクト生成を、register.blade.phpのインラインとします。\n/resources/views/auth/register.blade.php @extends(\u0026#39;layouts.app\u0026#39;) @section(\u0026#39;content\u0026#39;) \u0026lt;div class=\u0026#34;container\u0026#34;\u0026gt; ... @endsection @section(\u0026#39;script\u0026#39;) const app = new Vue({ el: \u0026#39;#app\u0026#39;, data: { form: new Form({ name: \u0026#39;\u0026#39;, email: \u0026#39;\u0026#39;, password: \u0026#39;\u0026#39;, password_confirmation: \u0026#39;\u0026#39; }), message: \u0026#39;\u0026#39;, messageClass: \u0026#39;\u0026#39; }, methods: { onSubmit() { this.form[\u0026#39;post\u0026#39;](\u0026#39;/register\u0026#39;) .then(response =\u0026gt; this.displaySuccessMessage(\u0026#39;Your account was created\u0026#39;)) .catch(response =\u0026gt; this.displayErrorMessage(\u0026#39;Your account was not created\u0026#39;)); }, displaySuccessMessage(message) { this.messageClass = \u0026#39;alert-success\u0026#39;; this.message = message; }, displayErrorMessage(message) { this.messageClass = \u0026#39;alert-danger\u0026#39;; this.message = message; }, clearMessage() { this.message = \u0026#39;\u0026#39;; }, } }); @endsection 最後に、レイアウトを編集して、インラインのスクリプトを出力するようにします。以下の@yield(\u0026lsquo;script\u0026rsquo;)の部分です。 必ず、","date":"2019-11-24T05:34:09+09:00","permalink":"https://www.larajapan.com/2019/11/24/vue-js%EF%BC%9A%E3%83%90%E3%83%83%E3%82%AF%E3%82%A8%E3%83%B3%E3%83%89%E3%81%AE%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%92%E4%BD%BF%E3%81%86/","title":"Vue.js：バックエンドのバリデーションを使う"},{"content":"バリデーションの話です。バリデーションのルールには、implicitルールというのがあり、入力値の項目が存在しない、あるいは値が空のときに、実行されるルールです。\nルールと言えば、numericとかのように値があってのexplicitのルールが直感的ですが、値がないときのルールとは何でしょう？\nぱっと思いつかないかもしれませんが、頻繁に使うrequiredは、値がないときのimplicitルールです。必須なのだから、値がなくてもバリデーションを実行しなければなりません。\nImplicitルールが必要なケースは意外にもあります。例えば、以下の管理画面における会員編集の画面。\nここでは、有効のチェックボックスがオン（有効）ですが、これをオフ（無効とする）として投稿するとチェックボックスの入力値はありません。しかし、例えば、会員を無効にするには、未支払いの注文があってはいけない、というルールがあるとすると、これはimplicitルールを作成しないとそのルールを強制することはできません。\nこのカスタムルールを作成してみましょう。\n$ php artisan make:rule MemberDeactivateRule 作成されたファイルを編集します。\napp/Rules/MemberDeactivateRule.php namespace App\\Rules; use App\\Models\\Member; use Illuminate\\Contracts\\Validation\\ImplicitRule; class MemberDeactivateRule implements ImplicitRule { protected $member; /** * Create a new rule instance. * @param \\App\\Models\\Member $member * @return void */ public function __construct(Member $member) { $this-\u0026gt;member = $member; } /** * Determine if the validation rule passes. * * @param string $attribute * @param mixed $value * @return bool */ public function passes($attribute, $value) { // チェックボックスをオンとすると、\u0026#39;Y\u0026#39;を返すとする。 if ($value == \u0026#39;Y\u0026#39;) { return true; } // チェックボックスがオフなら、 // 未払いの注文があるかどうかをチェックして、エラーとする if ($member-\u0026gt;checkUnpaid()) { return false; } return true; } /** * Get the validation error message. * * @return string */ public function message() { return \u0026#39;未払いの注文があります\u0026#39;; } } 通常のカスタムルールと違うのは、カスタムルールのクラスの実装にImplicitRuleのインターフェースを使用していることです、通常は、ImplicitRuleの代わりにRuleのクラスを使用します。\n最後に、コントローラで、このカスタムルールを使用したバリデーションは、以下のようになります。\n... $request-\u0026gt;validate([\u0026#39;active_flag\u0026#39; =\u0026gt; new MemberDeactivateRule($member)]); ","date":"2019-11-17T01:25:11+09:00","permalink":"https://www.larajapan.com/2019/11/17/%E5%80%A4%E3%81%8C%E3%81%AA%E3%81%84%E3%81%A8%E3%81%8D%E3%81%AB%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%92%E5%AE%9F%E8%A1%8C%E3%81%99%E3%82%8B%E3%81%AB%E3%81%AF/","title":"値がないときにバリデーションを実行するには"},{"content":"laravel-mixでフロントエンドを開発で、Laravelのプロジェクトにおいてのフロントエンドの開発の流れを説明しました。今回は、その続きとしてnpmから新規のパッケージをプロジェクトに追加する過程を説明します。\n訂正（7/17/2022） 最近、読者からの以下のコードの間違いの指摘をいただきましたので修正しました。読んでいただいて、しかも間違いを指摘をしてくれる。大変光栄です。kazooさんありがとう。\nresources/js/bootstrap.js ... require(\u0026#39;jquery-ui/ui/widgets/dialog.js\u0026#39;); // dialog.jsでなくdatepicker.jsが正しい！ 上のコードにおいての、dialog.jsはdatepicker.jsであるべきです。\n以降の文中のコードで修正されています。投稿時のL5.8でなく現在の私のL8.xの環境で確認しました。\njquery-uiのパッケージを検索 例として、jquery-uiのdate pickerを使います。会員登録フォームにおいて以下のように誕生日の日付の入力をカレンダーの選択としたいからです。\nまず、パッケージとして登録されているかどうか、https://www.npmjs.comのサイトへ行き検索します。画面の検索窓に、jquery-uiと入れます。\n検索結果として、774ありますが完全マッチ（exact match）は１つです。オリジナルのパッケージであることを確認してそれを使用します。\n次にコマンドラインで、このパッケージをLaravelのプロジェクトにインストールします。\n$ npm install jquery-ui --save-dev これを実行すると、package.jsonが更新されて、以下のようにjquery-uiの行が追加されます。\n{ ... \u0026#34;devDependencies\u0026#34;: { \u0026#34;axios\u0026#34;: \u0026#34;^0.18\u0026#34;, \u0026#34;bootstrap\u0026#34;: \u0026#34;^4.1.0\u0026#34;, \u0026#34;cross-env\u0026#34;: \u0026#34;^5.1\u0026#34;, \u0026#34;jquery\u0026#34;: \u0026#34;^3.2\u0026#34;, \u0026#34;jquery-ui\u0026#34;: \u0026#34;^1.12.1\u0026#34;, \u0026#34;laravel-mix\u0026#34;: \u0026#34;^4.0.7\u0026#34;, \u0026#34;lodash\u0026#34;: \u0026#34;^4.17.5\u0026#34;, \u0026#34;popper.js\u0026#34;: \u0026#34;^1.12\u0026#34;, \u0026#34;resolve-url-loader\u0026#34;: \u0026#34;^2.3.1\u0026#34;, \u0026#34;sass\u0026#34;: \u0026#34;^1.15.2\u0026#34;, \u0026#34;sass-loader\u0026#34;: \u0026#34;^7.1.0\u0026#34;, \u0026#34;vue\u0026#34;: \u0026#34;^2.5.17\u0026#34;, \u0026#34;vue-template-compiler\u0026#34;: \u0026#34;^2.6.10\u0026#34; } } \u0026ndash;save-devの引数を指定したので、上のようにdevのセクションに追加されます。フロントエンドの開発なので開発時にしかパッケージは使用しないためです。\nまた、node_modulesのディレクトリにjquery-uiのソースがダウンロードされていることは、以下のコマンドの実行で確認できます。\n$ npm list --depth=0 ├── axios@0.18.1 ├── bootstrap@4.3.1 ├── cross-env@5.2.1 ├── jquery@3.4.1 ├── jquery-ui@1.12.1 ├── laravel-mix@4.1.4 ├── lodash@4.17.15 ├── popper.js@1.15.0 ├── resolve-url-loader@2.3.2 ├── sass@1.22.12 ├── sass-loader@7.3.1 ├── vue@2.6.10 └── vue-template-compiler@2.6.10 bootstrap.jsの編集 次に、bootstrap.jsに以下の行を追加して、このパッケージをロードします。\nresources/js/bootstrap.js ... require(\u0026#39;jquery-ui/ui/widgets/datepicker.js\u0026#39;); jquery-uiにはいろいろなウィジットがあるので、他も指定したい場合は以下のように指定します。\n... require(\u0026#39;jquery-ui/ui/widgets/datepicker.js\u0026#39;); require(\u0026#39;jquery-ui/ui/widgets/dialog.js\u0026#39;); require(\u0026#39;jquery-ui/ui/widgets/sortable.js\u0026#39;); require(\u0026#39;jquery-ui/ui/widgets/tooltip.js\u0026#39;); app.scssの編集 jquery-uiにはcssの設定も必要です。app.scssを編集します。 resources/sass/app.scss // Fonts @import url(\u0026#39;https://fonts.googleapis.com/css?family=Nunito\u0026#39;); // Variables @import \u0026#39;variables\u0026#39;; // Bootstrap @import \u0026#39;~bootstrap/scss/bootstrap\u0026#39;; // jquery -ui @import \u0026#39;~jquery-ui/themes/base/all.css\u0026#39;; bladeの編集 今度はテンプレートの変更です。ここでは、Laravelのプロジェクトについてくる会員登録ページにお誕生日の項目を追加します。\nresources/views/auth/register.blade.php ... \u0026lt;div class=\u0026#34;form-group row\u0026#34;\u0026gt; \u0026lt;label for=\u0026#34;birthdate\u0026#34; class=\u0026#34;col-md-4 col-form-label text-md-right\u0026#34;\u0026gt;お誕生日\u0026lt;/label\u0026gt; \u0026lt;div class=\u0026#34;col-md-6\u0026#34;\u0026gt; \u0026lt;input id=\u0026#34;birthdate\u0026#34; type=\u0026#34;text\u0026#34; class=\u0026#34;form-control datepicker @error(\u0026#39;birthdate\u0026#39;) is-invalid @enderror\u0026#34; name=\u0026#34;birthdate\u0026#34; value=\u0026#34;{{ old(\u0026#39;birthdate\u0026#39;) }}\u0026#34; required autocomplete=\u0026#34;\\ birthdate\u0026#34;\u0026gt; @error(\u0026#39;birthdate\u0026#39;) \u0026lt;span class=\u0026#34;invalid-feedback\u0026#34; role=\u0026#34;alert\u0026#34;\u0026gt; \u0026lt;strong\u0026gt;{{ $message }}\u0026lt;/strong\u0026gt; \u0026lt;/span\u0026gt; @enderror \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;form-group row mb-0\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;col-md-6 offset-md-4\u0026#34;\u0026gt; \u0026lt;button type=\u0026#34;submit\u0026#34; class=\u0026#34;btn btn-primary\u0026#34;\u0026gt; {{ __(\u0026#39;Register\u0026#39;) }} \u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/form\u0026gt; ... class=\u0026ldquo;form-control datepickerの部分に注意してください。\napp.jsの編集 ブレードで使用したクラス名、datepickerとjquery-uiのdatepickerを繋げるために、以下の定義をapp.jsに加えます。\nresources/js/app.js require(\u0026#39;./bootstrap\u0026#39;); ... $(\u0026#34;.datepicker\u0026#34;).datepicker({ dateFormat: \u0026#39;yy-mm-dd\u0026#39; }); コンパイル いろいろなソースファイルの編集を完了して、以下でコンパイルして、all.jsとall.cssを作成します。\n$ npm run dev プロダクションには、\n$ npm run prod これで完了ですが、最後にカレンダーを日本語にしてみましょう。 そのためには、bootstrap.jsにおいて、以下のように日本語版のカレンダーに代えます。\nresources/js/bootstrap.js ... //require(\u0026#39;jquery-ui/ui/widgets/datepicker.js\u0026#39;); require(\u0026#39;jquery-ui/ui/i18n/datepicker-ja.js\u0026#39;); これでコンパイルすると以下のような画面となります。\n誕生日の項目なので年も選択できるようにしました。そのためには、app.jsでの定義は以下のようになります。\nresources/js/app.js ... $(\u0026#34;.datepicker\u0026#34;).datepicker({ dateFormat: \u0026#39;yy-mm-dd\u0026#39;, changeMonth: true, changeYear: true, yearRange: \u0026#39;1901:\u0026#39; }); ","date":"2019-11-10T04:38:19+09:00","permalink":"https://www.larajapan.com/2019/11/10/npm%E3%81%8B%E3%82%89%E3%83%91%E3%83%83%E3%82%B1%E3%83%BC%E3%82%B8%E3%82%92%E8%BF%BD%E5%8A%A0/","title":"npmからパッケージを追加"},{"content":"今回は、知っていたけれどあまり使っていない、ことの話です。Laravelの入力バリデーションのことなのですが、忘れずに書いておいてより使うようにしたいです。\nLaravelの入力のバリデーションでは複数のルールがあると、それらルールすべてが適用されます。\n例えば、入力項目は年齢で、ルールは入力値は数字（整数）であり、かつ10歳以上とします。\n$php artisan tinker Psy Shell v0.9.9 (PHP 7.2.16 — cli) by Justin Hileman \u0026gt;\u0026gt;\u0026gt; $input = [\u0026#39;age\u0026#39; =\u0026gt; \u0026#39;9\u0026#39;]; =\u0026gt; [ \u0026#34;age\u0026#34; =\u0026gt; \u0026#34;9\u0026#34;, ] \u0026gt;\u0026gt;\u0026gt; $rule = [\u0026#39;age\u0026#39; =\u0026gt; \u0026#39;integer|gte:10\u0026#39;]; =\u0026gt; [ \u0026#34;age\u0026#34; =\u0026gt; \u0026#34;integer|gte:10\u0026#34;, ] \u0026gt;\u0026gt;\u0026gt; validator($input, $rule)-\u0026gt;errors()-\u0026gt;all(); =\u0026gt; [ \u0026#34;The age must be greater than or equal 10.\u0026#34;, ] 入力値が９歳なら、上のようにエラーとなります。エラーは配列で返ります。\nさて、ここで入力値を、数字でなく英字としてみます。\n\u0026gt;\u0026gt;\u0026gt; $input = [\u0026#39;age\u0026#39; =\u0026gt; \u0026#39;abc\u0026#39;]; =\u0026gt; [ \u0026#34;age\u0026#34; =\u0026gt; \u0026#34;9\u0026#34;, ] \u0026gt;\u0026gt;\u0026gt; $rule = [\u0026#39;age\u0026#39; =\u0026gt; \u0026#39;integer|gte:10\u0026#39;]; =\u0026gt; [ \u0026#34;age\u0026#34; =\u0026gt; \u0026#34;integer|gte:10\u0026#34;, ] \u0026gt;\u0026gt;\u0026gt; validator($input, $rule)-\u0026gt;errors()-\u0026gt;all(); =\u0026gt; [ \u0026#34;The age must be an integer.\u0026#34;, \u0026#34;The age must be greater than or equal 10.\u0026#34;, ] あれ、と思うのは最初の数字のルールで、ルールの適用が止まらないことです。これはLaravelの作者によると、すべてのルールの結果を見せたい、という仕様らしい。\nしかし、最初のエラーだけを画面に表示するのは大半のケースです。さらに、例ば、ルールにおいて、2番目のルールがDBのチェックを必要すると、1番目のルールに通らなかったら、わざわざ時間がかかる2番目のDBのクエリの実行は必要はありません。\nそういう場合は、bailをルールの最初に置くと、\n\u0026gt;\u0026gt;\u0026gt; $rule = [\u0026#39;age\u0026#39; =\u0026gt; \u0026#39;bail|integer|gte:10\u0026#39;]; =\u0026gt; [ \u0026#34;age\u0026#34; =\u0026gt; \u0026#34;bail|integer|gte:10\u0026#34;, ] \u0026gt;\u0026gt;\u0026gt; validator($input, $rule)-\u0026gt;errors()-\u0026gt;all(); =\u0026gt; [ \u0026#34;The age must be an integer.\u0026#34;, ] 2番目のルールは実行されません。\nこのbailの登場は、5.6からのようですが、最新の5.5でも対応しています。\n","date":"2019-11-02T01:47:35+09:00","permalink":"https://www.larajapan.com/2019/11/02/%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%83%AB%E3%83%BC%E3%83%AB%E3%80%81bail/","title":"バリデーションルール、bail"},{"content":"Laravelのブレードでは、HTMLを出力するテンプレートをモジュール化できます。ブレードのディレクティブである＠yieldを使用すれば、レイアウトのテンプレートと、その中身のテンプレートを違うファイルとして管理が可能です。今回は、それを利用して会員登録の画面にインラインのjavascriptを埋め込みます。\n@yield('script') まず、レイアウトのブレード、app.blade.phpを変更します。 resources/views/layout/app.blade.php \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;{{ str_replace(\u0026#39;_\u0026#39;, \u0026#39;-\u0026#39;, app()-\u0026gt;getLocale()) }}\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;utf-8\u0026#34;\u0026gt; \u0026lt;meta name=\u0026#34;viewport\u0026#34; content=\u0026#34;width=device-width, initial-scale=1\u0026#34;\u0026gt; \u0026lt;!-- CSRF Token --\u0026gt; \u0026lt;meta name=\u0026#34;csrf-token\u0026#34; content=\u0026#34;{{ csrf_token() }}\u0026#34;\u0026gt; \u0026lt;title\u0026gt;{{ config(\u0026#39;app.name\u0026#39;, \u0026#39;Laravel\u0026#39;) }}\u0026lt;/title\u0026gt; \u0026lt;!-- Scripts --\u0026gt; \u0026lt;script src=\u0026#34;{{ asset(\u0026#39;js/app.js\u0026#39;) }}\u0026#34; defer\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script type=\u0026#34;module\u0026#34;\u0026gt; @yield(\u0026#39;script\u0026#39;) \u0026lt;/script\u0026gt; \u0026lt;!-- Fonts --\u0026gt; \u0026lt;link rel=\u0026#34;dns-prefetch\u0026#34; href=\u0026#34;//fonts.gstatic.com\u0026#34;\u0026gt; \u0026lt;link href=\u0026#34;https://fonts.googleapis.com/css?family=Nunito\u0026#34; rel=\u0026#34;stylesheet\u0026#34;\u0026gt; \u0026lt;!-- Styles --\u0026gt; \u0026lt;link href=\u0026#34;{{ asset(\u0026#39;css/app.css\u0026#39;) }}\u0026#34; rel=\u0026#34;stylesheet\u0026#34;\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;div id=\u0026#34;app\u0026#34;\u0026gt; ... \u0026lt;main class=\u0026#34;py-4\u0026#34;\u0026gt; @yield(\u0026#39;content\u0026#39;) \u0026lt;/main\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; @yield(\u0026lsquo;script\u0026rsquo;)には、@yield(\u0026lsquo;content\u0026rsquo;)と同様に、以下の会員登録画面のブレードの@sectionと@endsectionで囲まれるコードが入ります。\nresources/views/auth/register.blade.php @extends(\u0026#39;layouts.app\u0026#39;) @section(\u0026#39;script\u0026#39;) $(document).ready(function() { $(\u0026#39;:button[type=\u0026#34;submit\u0026#34;]\u0026#39;).prop(\u0026#39;disabled\u0026#39;, true); $(\u0026#39;input[type=\u0026#34;text\u0026#34;]\u0026#39;).keyup(function() { if($(this).val() != \u0026#39;\u0026#39;) { $(\u0026#39;:button[type=\u0026#34;submit\u0026#34;]\u0026#39;).prop(\u0026#39;disabled\u0026#39;, false); } }); }); @endsection @section(\u0026#39;content\u0026#39;) \u0026lt;div class=\u0026#34;container\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;row justify-content-center\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;col-md-8\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;card\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;card-header\u0026#34;\u0026gt;{{ __(\u0026#39;Register\u0026#39;) }}\u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;card-body\u0026#34;\u0026gt; \u0026lt;form method=\u0026#34;POST\u0026#34; action=\u0026#34;{{ route(\u0026#39;register\u0026#39;) }}\u0026#34;\u0026gt; @csrf \u0026lt;div class=\u0026#34;form-group row\u0026#34;\u0026gt; \u0026lt;label for=\u0026#34;name\u0026#34; class=\u0026#34;col-md-4 col-form-label text-md-right\u0026#34;\u0026gt;{{ __(\u0026#39;Name\u0026#39;) }}\u0026lt;/label\u0026gt; \u0026lt;div class=\u0026#34;col-md-6\u0026#34;\u0026gt; \u0026lt;input id=\u0026#34;name\u0026#34; type=\u0026#34;text\u0026#34; class=\u0026#34;form-control @error(\u0026#39;name\u0026#39;) is-invalid @enderror\u0026#34; name=\u0026#34;name\u0026#34; value=\u0026#34;{{ old(\u0026#39;name\u0026#39;) }}\u0026#34; required autocomplete=\u0026#34;name\u0026#34; autofocus\u0026gt; @error(\u0026#39;name\u0026#39;) \u0026lt;span class=\u0026#34;invalid-feedback\u0026#34; role=\u0026#34;alert\u0026#34;\u0026gt; \u0026lt;strong\u0026gt;{{ $message }}\u0026lt;/strong\u0026gt; \u0026lt;/span\u0026gt; @enderror \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; ... \u0026lt;div class=\u0026#34;form-group row mb-0\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;col-md-6 offset-md-4\u0026#34;\u0026gt; \u0026lt;button type=\u0026#34;submit\u0026#34; class=\u0026#34;btn btn-primary\u0026#34;\u0026gt; {{ __(\u0026#39;Register\u0026#39;) }} \u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/form\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; @endsection このjqueryのコードは、「Register」のボタンを最初アクセス不可としておいて、項目に１つでも入力があればアクセス可能とします。\n会員画面として出力されるHTMLは以下のようになります。\n\u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;utf-8\u0026#34;\u0026gt; \u0026lt;meta name=\u0026#34;viewport\u0026#34; content=\u0026#34;width=device-width, initial-scale=1\u0026#34;\u0026gt; \u0026lt;!-- CSRF Token --\u0026gt; \u0026lt;meta name=\u0026#34;csrf-token\u0026#34; content=\u0026#34;5hVqkt6ozgXZZqFlvS7TeoQlqXOyfGCqt5j3KTEz\u0026#34;\u0026gt; \u0026lt;title\u0026gt;Laravel\u0026lt;/title\u0026gt; \u0026lt;!-- Scripts --\u0026gt; \u0026lt;script src=\u0026#34;/l58/js/app.js\u0026#34; defer\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script type=\u0026#34;module\u0026#34;\u0026gt; $(document).ready(function() { $(\u0026#39;:button[type=\u0026#34;submit\u0026#34;]\u0026#39;).prop(\u0026#39;disabled\u0026#39;, true); $(\u0026#39;input[type=\u0026#34;text\u0026#34;]\u0026#39;).keyup(function() { if($(this).val() != \u0026#39;\u0026#39;) { $(\u0026#39;:button[type=\u0026#34;submit\u0026#34;]\u0026#39;).prop(\u0026#39;disabled\u0026#39;, false); } }); }); \u0026lt;/script\u0026gt; \u0026lt;!-- Fonts --\u0026gt; \u0026lt;link rel=\u0026#34;dns-prefetch\u0026#34; href=\u0026#34;//fonts.gstatic.com\u0026#34;\u0026gt; \u0026lt;link href=\u0026#34;https://fonts.googleapis.com/css?family=Nunito\u0026#34; rel=\u0026#34;stylesheet\u0026#34;\u0026gt; \u0026lt;!-- Styles --\u0026gt; \u0026lt;link href=\u0026#34;/l58/css/app.css\u0026#34; rel=\u0026#34;stylesheet\u0026#34;\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; ... type=\"module\" 気づいたと思いますが、レイアウトのapp.blade.phpにおけるインラインの","date":"2019-10-27T06:28:36+09:00","permalink":"https://www.larajapan.com/2019/10/27/%E3%82%A4%E3%83%B3%E3%83%A9%E3%82%A4%E3%83%B3script/","title":"インラインscript"},{"content":"HTMLファイルで、javascriptを実行するときは、","date":"2019-10-21T05:14:25+09:00","permalink":"https://www.larajapan.com/2019/10/21/script-defer/","title":"\u003cscript defer\u003e"},{"content":"app.jsは、laravel-mixでコンパイルされて作成されてall.jsとなります。つまり、ソースファイルです。いったい何が含まれているか興味あります。今回は、このファイルの中身を見てましょう。もうすでにLaravelは、6.1のバージョンになっていますが、ここでは5.8をもとにします。\nresources/js/app.js /** * First we will load all of this project\u0026#39;s JavaScript dependencies which * includes Vue and other libraries. It is a great starting point when * building robust, powerful web applications using Vue and Laravel. */ require(\u0026#39;./bootstrap\u0026#39;); window.Vue = require(\u0026#39;vue\u0026#39;); /** * The following block of code may be used to automatically register your * Vue components. It will recursively scan this directory for the Vue * components and automatically register them with their \u0026#34;basename\u0026#34;. * * Eg. ./components/ExampleComponent.vue -\u0026gt; \u0026lt;example-component\u0026gt;\u0026lt;/example-component\u0026gt; */ // const files = require.context(\u0026#39;./\u0026#39;, true, /\\.vue$/i); // files.keys().map(key =\u0026gt; Vue.component(key.split(\u0026#39;/\u0026#39;).pop().split(\u0026#39;.\u0026#39;)[0], files(key).default)); Vue.component(\u0026#39;example-component\u0026#39;, require(\u0026#39;./components/ExampleComponent.vue\u0026#39;).default); /** * Next, we will create a fresh Vue application instance and attach it to * the page. Then, you may begin adding components to this application * or customize the JavaScript scaffolding to fit your unique needs. */ const app = new Vue({ el: \u0026#39;#app\u0026#39;, }); 上のコードを見て、まず、何これ？、と思うのは、７行目のrequire()の関数です。php言語では昔よく使っていた関数だけれど、これはjavascriptのファイルでphpのファイルではありません。私のように、HTMLのファイルの中でしかjavascriptを触ったことがない人には、まったく見たことがないjavascriptの関数です。\n調べたところ、require()は他のjavascriptのファイルで定義されているモジュールを読み込むためのNode.jsの関数ということがわかりました。一般的にウェブで使用しているjavascriptにはモジュールの概念がないゆえに、Node.jsで発明されたらしい。その後に、ES6の登場でjavascriptにはモジュールの機能が正式に対応されるようになりますが、そこでは、require()でなく、importが使用されています。歴史的な経過により複雑になっているので、混乱しそうなところですね。\nさらに、require(\u0026rsquo;./bootstrap\u0026rsquo;)と書かれているので、もしかして、CSSフレームワークのBootstrapのこと？とも思いますが、ここではそのBootstrapではなく、単に同じディレクトリにある、bootstrap.jsをモジュールとして読み込んで実行する、ということです。またまた、混乱しそうなところですが、忘れてならないのは、これらのjsファイルはNode.jsの世界のjsファイルで、ブラウザーのjsではないということです。以前に書いたように、最終的にはwebpackなどのツールを使用して、Node.jsのプログラムをブラウザー用に変換します。\n次に、そのapp.jsが参照しているbootstrap.jsを見てみましょう。\nresources/js/bootstrap.js window._ = require(\u0026#39;lodash\u0026#39;); /** * We\u0026#39;ll load jQuery and the Bootstrap jQuery plugin which provides support * for JavaScript based Bootstrap features such as modals and tabs. This * code may be modified to fit the specific needs of your application. */ try { window.Popper = require(\u0026#39;popper.js\u0026#39;).default; window.$ = window.jQuery = require(\u0026#39;jquery\u0026#39;); require(\u0026#39;bootstrap\u0026#39;); } catch (e) {} /** * We\u0026#39;ll load the axios HTTP library which allows us to easily issue requests * to our Laravel back-end. This library automatically handles sending the * CSRF token as a header based on the value of the \u0026#34;XSRF\u0026#34; token cookie. */ window.axios = require(\u0026#39;axios\u0026#39;); window.axios.defaults.headers.common[\u0026#39;X-Requested-With\u0026#39;] = \u0026#39;XMLHttpRequest\u0026#39;; /** * Next we will register the CSRF Token as a common header with Axios so that * all outgoing HTTP requests automatically have it attached. This is just * a simple convenience so we don\u0026#39;t have to attach every token manually. */ let token = document.head.querySelector(\u0026#39;meta[name=\u0026#34;csrf-token\u0026#34;]\u0026#39;); if (token) { window.axios.defaults.headers.common[\u0026#39;X-CSRF-TOKEN\u0026#39;] = token.content; } else { console.error(\u0026#39;CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token\u0026#39;); } /** * Echo exposes an expressive API for subscribing to channels and listening * for events that are broadcast by Laravel. Echo and event broadcasting * allows your team to easily build robust real-time web applications. */ // import Echo from \u0026#39;laravel-echo\u0026#39; // window.Pusher = require(\u0026#39;pusher-js\u0026#39;); // window.Echo = new Echo({ // broadcaster: \u0026#39;pusher\u0026#39;, // key: process.env.MIX_PUSHER_APP_KEY, // cluster: process.env.MIX_PUSHER_APP_CLUSTER, // encrypted: true // }); lodash, popper.js, axiosといろいろなもの読み込んでいますが、注意してもらいのは、１１行目からの以下。\nwindow.$ = window.jQuery = require(\u0026#39;jquery\u0026#39;); require(\u0026#39;bootstrap\u0026#39;); 最初の行では、require()でjqueryのモジュールを読み込んでいます。そして、jqueryのDOMセレクターで使われる、$やjQueryをwindowの変数に割り当ててグローバルで使用できるようにしています。\nそして、そう、次の行で今度は本当のCSSフレームワークのBootstrapのモジュールを読み込んでいます。Bootstrapはjqueryに依存しているゆえに、jqueryがその前にロードされています。\nBootstrapのためにロードされているjqueryですが、もちろん通常の目的でも使用できます。次回は、インラインのスクリプトでjqueryを使用してみます。\n","date":"2019-10-14T23:47:36+09:00","permalink":"https://www.larajapan.com/2019/10/14/app-js%E3%81%A8bootstrap-js/","title":"app.jsとbootstrap.js"},{"content":"laravel-mixの続きです。まだ続きそうなので、連載ものにしました。右にリストされているCATEGORIESのフロントエンドの開発です。\n今回は、laravel-mixで作成されたjsやcssのファイルがどのようにbladeで参照されているかにフォーカスします。\nasset() 再びレイアウトのbladeファイルの中身を以下に掲載します。\nresources/views/layouts/app.blade.php \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;{{ str_replace(\u0026#39;_\u0026#39;, \u0026#39;-\u0026#39;, app()-\u0026gt;getLocale()) }}\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;utf-8\u0026#34;\u0026gt; \u0026lt;meta name=\u0026#34;viewport\u0026#34; content=\u0026#34;width=device-width, initial-scale=1\u0026#34;\u0026gt; \u0026lt;!-- CSRF Token --\u0026gt; \u0026lt;meta name=\u0026#34;csrf-token\u0026#34; content=\u0026#34;{{ csrf_token() }}\u0026#34;\u0026gt; \u0026lt;title\u0026gt;{{ config(\u0026#39;app.name\u0026#39;, \u0026#39;Laravel\u0026#39;) }}\u0026lt;/title\u0026gt; \u0026lt;!-- Scripts --\u0026gt; \u0026lt;script src=\u0026#34;{{ asset(\u0026#39;js/app.js\u0026#39;) }}\u0026#34; defer\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;!-- Fonts --\u0026gt; \u0026lt;link rel=\u0026#34;dns-prefetch\u0026#34; href=\u0026#34;//fonts.gstatic.com\u0026#34;\u0026gt; \u0026lt;link href=\u0026#34;https://fonts.googleapis.com/css?family=Nunito\u0026#34; rel=\u0026#34;stylesheet\u0026#34;\u0026gt; \u0026lt;!-- Styles --\u0026gt; \u0026lt;link href=\u0026#34;{{ asset(\u0026#39;css/app.css\u0026#39;) }}\u0026#34; rel=\u0026#34;stylesheet\u0026#34;\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; ... 上の中の以下に注目してください。\n... \u0026lt;script src=\u0026#34;{{ asset(\u0026#39;js/app.js\u0026#39;) }}\u0026#34; defer\u0026gt;\u0026lt;/script\u0026gt; ... \u0026lt;link href=\u0026#34;{{ asset(\u0026#39;css/app.css\u0026#39;) }}\u0026#34; rel=\u0026#34;stylesheet\u0026#34;\u0026gt; ... asset()は、app.jsなどのファイルが存在するURLを生成するヘルパーです。与えた引数の値をURLにしてくれます。\n$ php artisan tinker Psy Shell v0.9.9 (PHP 7.2.16 — cli) by Justin Hileman \u0026gt;\u0026gt;\u0026gt; asset(\u0026#39;js/app.js\u0026#39;) =\u0026gt; \u0026#34;http://localhost/js/app.js\u0026#34; という具合です。\nURLのサイトやディレクトリを変えたければ、.envを編集します。\n$ cat .env ... ASSET_URL=https://example.com/assets と設定すれば、\n\u0026gt;\u0026gt;\u0026gt; asset(\u0026#39;js/app.js\u0026#39;) =\u0026gt; \u0026#34;https://example.com/assets/js/app.js\u0026#34; となります。\nまた、先のレイアウトの出力は、\n... \u0026lt;script src=\u0026#34;https://example.com/assets/js/app.js\u0026#34; defer\u0026gt;\u0026lt;/script\u0026gt; ... \u0026lt;link href=\u0026#34;https://example.com/assets/css/app.css\u0026#34; rel=\u0026#34;stylesheet\u0026#34;\u0026gt; となります。\nブラウザのキャッシュをバスト 通常のブラウザでは、HTMLで参照されるファイル名が同じであるとブラウザにキャッシュされます。キャッシュされること自体は転送時間を節約する意味で素晴らしいのですが、app.jsの中身が更新されたときはブラウザにキャッシュされているものを使用してもらいたくありません。\nそのようなときに（つまりキャッシュをバストしたいとき）によく使われるのは、URLにPHP関数のtime()をURLに追加することです。\n例えば、\n\u0026lt;script src=\u0026#34;{{ asset(\u0026#39;js/app.js\u0026#39;).\u0026#39;?\u0026#39;.time() }}\u0026#34; defer\u0026gt;\u0026lt;/script\u0026gt; は、\n\u0026lt;script src=\u0026#34;https://example.com/assets/js/app.js?1570143025\u0026#34; defer\u0026gt;\u0026lt;/script\u0026gt; となります。\n毎回毎回アクセスする度に、time()の値は変わるのでapp.jsはキャッシュされず、ブラウザはファイルをサーバーから毎回毎回取得します。 しかし、これだと困ります。app.jsが更新されるまではキャッシュして欲しいのです。\nそこで登場するのが、larave-mixのバージョン機能です。\nまず、webpack.mix.jsを編集します。\nconst mix = require(\u0026#39;laravel-mix\u0026#39;); mix.js(\u0026#39;resources/js/app.js\u0026#39;, \u0026#39;public/js\u0026#39;) .sass(\u0026#39;resources/sass/app.scss\u0026#39;, \u0026#39;public/css\u0026#39;); if (mix.inProduction()) { mix.version(); } 追加したのは最後の3行です。プロダクションの実行のときにバージョン化を行う指定です。\n次に、\n$ npm run prod を実行します。app.cssやapp.jsのファイルを生成するとともに、mix-manifest.jsonのファイルをpublicのディレクトリに作成します。\nそのファイルの中身には、必要なファイルのバージョン化のためのマップが定義されています。\n{ \u0026#34;/js/app.js\u0026#34;: \u0026#34;/js/app.js?id=d1c6ff581391a0e168f2\u0026#34;, \u0026#34;/css/app.css\u0026#34;: \u0026#34;/css/app.css?id=33e746eff21c53ed136b\u0026#34; } この情報を使用するには、mix()を使いレイアウトを以下のように編集します。\n... \u0026lt;script src=\u0026#34;{{ asset(mix(\u0026#39;js/app.js\u0026#39;)) }}\u0026#34; defer\u0026gt;\u0026lt;/script\u0026gt; ... \u0026lt;link href=\u0026#34;{{ asset(mix(\u0026#39;css/app.css\u0026#39;)) }}\u0026#34; rel=\u0026#34;stylesheet\u0026#34;\u0026gt; ... 出力は以下となります。\n... \u0026lt;script src=\u0026#34;https://example.com/assets/js/app.js?id=d1c6ff581391a0e168f2\u0026#34; defer\u0026gt;\u0026lt;/script\u0026gt; ... \u0026lt;link href=\u0026#34;https://example.com/assets/css/app.css?id=33e746eff21c53ed136b\u0026#34; rel=\u0026#34;stylesheet\u0026#34;\u0026gt; ... 先のtime()とは違って、更新されたときのみにidの値が変わり、それまではブラウザにキャッシュされてもOKとなります。\n","date":"2019-10-06T07:45:52+09:00","permalink":"https://www.larajapan.com/2019/10/06/js%E3%82%84css%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AE%E3%83%90%E3%83%BC%E3%82%B8%E3%83%A7%E3%83%B3%E5%8C%96/","title":"jsやcssファイルのバージョン化"},{"content":"前回のコマンドラインでのnpm installの実行で、larave-mixのパッケージがインストールされました。今回はそれを利用して、実際にユーザーがブラウザを通して直接使用するapp.jsとapp.cssを作成します。\n関連するディレクトリやファイル 今回関連するファイルは、プロジェクトのディレクトリのあちこちにあります。混乱しないように、フロントエンドのファイルの作成に関わるディレクトリやファイルの説明から始めます。以下は、Laravelのプロジェクトのディレクトリ構造の一部です。今回に関係のないapp/などのディレクトリは含まれていないので注意を。\n. ├── node_modules │ ├── accepts │ ... ├── package.json ├── package-lock.json ├── public │ ├── css │ │ └── app.css │ └── js │ └── app.js ├── resources │ ├── js │ │ ├── app.js │ │ ├── bootstrap.js │ │ └── components │ │ └── ExampleComponent.vue │ ├── sass │ │ ├── app.scss │ │ └── _variables.scss │ └── views │ ├── auth │ │ ├── login.blade.php │ │ ├── passwords │ │ │ ├── email.blade.php │ │ │ └── reset.blade.php │ │ ├── register.blade.php │ │ └── verify.blade.php │ ├── home.blade.php │ ├── layouts │ │ └── app.blade.php │ └── welcome.blade.php └── webpack.mix.js 上において、\nnode_modules は、コマンドラインでnpm installの実行でダウンロードしたjavascriptのモジュールのファイルを収納するディレクトリです。package.jsonで指定したモジュールだけでなく、それらが依存するモジュールもダウンロードされるので、たくさんのディレクトリやファイルがあります。\npackage.json は、プロジェクトで使用するjavascriptのモジュールを指定します。先のnode_modulesのディレクトリのファイルはそこで指定するパッケージをもとにダウンロードされたものです。\npackage-lock.json は、コマンドラインでnpm installを実行したときに作成されます。ダウンロードしたモジュールのバージョン情報も記されるので、プロジェクトのインストール時に同じバージョンのモジュールの使用がそれで可能となります。\npublic は、ユーザーがブラウザを通して使用するcssやjsファイルを収納するディレクトリです。それらのファイルは、後に説明するnpm runの実行により作成されます。\nresources/jsやresources/sass は、publicに収納されているファイル（app.jsなど）の作成の元となるファイルを収納します。\nresources/views は、もちろんLaravelのコントローラで使用されるbladeのファイルを収納します。\nlaravel-mixを使用したワークフロー 上のpublicのディレクトリに保存される、app.jsやapp.cssファイルの作成には以下の作業工程が必要となります。\n１．ソースファイルを編集 resourcesディレクトリにおいて、jsやsassなどのビルドに必要なファイルを編集します。 ここでは、ExampleComponent.vueのvuejsのコンポーネントもソースファイルの１つです。 ２．webpack.mix.jsを編集 これは、ビルドの設計図です。laravel-mixが必要とするもので、webpackの実行に使います。 以下は、プロジェクトのディフォルトでインストールされるwebpack.mix.jsの中身ですが、ソースファイルとそれから作成されるファイルの収納場所が記されています。 webpack.mix.js const mix = require(\u0026#39;laravel-mix\u0026#39;); /* |-------------------------------------------------------------------------- | Mix Asset Management |-------------------------------------------------------------------------- | | Mix provides a clean, fluent API for defining some Webpack build steps | for your Laravel application. By default, we are compiling the Sass | file for the application as well as bundling up all the JS files. | */ mix.js(\u0026#39;resources/js/app.js\u0026#39;, \u0026#39;public/js\u0026#39;) .sass(\u0026#39;resources/sass/app.scss\u0026#39;, \u0026#39;public/css\u0026#39;); 上はシンプルな設定ですが、sassのファイルをコンパイルしてcssファイルを作成し、jsファイルで使用するモジュールを１つのファイルのバンドルし、バベル化し最小化したり、複雑な作業をたくさん行います。バベル化とは、書いたjsのスクリプトがたとえ次世代のjavascriptの新機能でも、現在のブラウザでも使えるようにするようにトランスパイルすることです。\n３．webpackを実行 webpackは、以下のコマンドで実行されます。\n開発用には、\n$ npm run dev あるいは、ライブ用のために、\n$ npm run prod を実行してコンパイルします。devやprodの実行パラメータは、package.jsonで定義されています。以下の、scriptsのセクションを見てください。\npackage.json { \u0026#34;private\u0026#34;: true, \u0026#34;scripts\u0026#34;: { \u0026#34;dev\u0026#34;: \u0026#34;npm run development\u0026#34;, \u0026#34;development\u0026#34;: \u0026#34;cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js\u0026#34;, \u0026#34;watch\u0026#34;: \u0026#34;npm run development -- --watch\u0026#34;, \u0026#34;watch-poll\u0026#34;: \u0026#34;npm run watch -- --watch-poll\u0026#34;, \u0026#34;hot\u0026#34;: \u0026#34;cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js\u0026#34;, \u0026#34;prod\u0026#34;: \u0026#34;npm run production\u0026#34;, \u0026#34;production\u0026#34;: \u0026#34;cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js\u0026#34; }, ... devは、developmentで、prodは、productionの短縮形ということがわかります。\nまた、それらのコマンドでは、webpackを実行して、laravel-mixが提供しているwebpack.config.jsの設定ファイルを使用していることもわかります。\nlaravel-mixのおかげで、複雑なwebpackの作業の設定が、先に掲載したwebpack.mix.jsに見られるようにたったの２行の設定となっています。感激です。\n４．bladeファイルの編集 作成された、app.cssやapp.jsは、例えば、以下のようにレイアウトのbladeテンプレートで使用されます。\nresources/views/layouts/app.blade.php \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;{{ str_replace(\u0026#39;_\u0026#39;, \u0026#39;-\u0026#39;, app()-\u0026gt;getLocale()) }}\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;utf-8\u0026#34;\u0026gt; \u0026lt;meta name=\u0026#34;viewport\u0026#34; content=\u0026#34;width=device-width, initial-scale=1\u0026#34;\u0026gt; \u0026lt;!-- CSRF Token --\u0026gt; \u0026lt;meta name=\u0026#34;csrf-token\u0026#34; content=\u0026#34;{{ csrf_token() }}\u0026#34;\u0026gt; \u0026lt;title\u0026gt;{{ config(\u0026#39;app.name\u0026#39;, \u0026#39;Laravel\u0026#39;) }}\u0026lt;/title\u0026gt; \u0026lt;!-- Scripts --\u0026gt; \u0026lt;script src=\u0026#34;{{ asset(\u0026#39;js/app.js\u0026#39;) }}\u0026#34; defer\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;!-- Fonts --\u0026gt; \u0026lt;link rel=\u0026#34;dns-prefetch\u0026#34; href=\u0026#34;//fonts.gstatic.com\u0026#34;\u0026gt; \u0026lt;link href=\u0026#34;https://fonts.googleapis.com/css?family=Nunito\u0026#34; rel=\u0026#34;stylesheet\u0026#34;\u0026gt; \u0026lt;!-- Styles --\u0026gt; \u0026lt;link href=\u0026#34;{{ asset(\u0026#39;css/app.css\u0026#39;) }}\u0026#34; rel=\u0026#34;stylesheet\u0026#34;\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; ... 以上です。\n","date":"2019-09-30T02:52:13+09:00","permalink":"https://www.larajapan.com/2019/09/30/laravel-mix%E3%81%A7%E3%83%95%E3%83%AD%E3%83%B3%E3%83%88%E3%82%A8%E3%83%B3%E3%83%89%E3%82%92%E9%96%8B%E7%99%BA/","title":"laravel-mixでフロントエンドを開発"},{"content":"Laravelは、いわゆるバックエンド、サーバーサイドのアプリケーションの開発のための使用が主なのですが、フロントエンドの開発にもlarave-mixを通してサポートしています。今回はそちら関連のコマンドやツールの備忘録です。こうたくさん実行するコマンドがあると、もう覚えられない年頃です。\nNode.js Node.jsの登場で、今までのHTMLに以下のようなcdnのリンクを入れてjavascriptで使用するやり方から、 \u0026lt;!-- Latest compiled and minified CSS --\u0026gt; \u0026lt;link rel=\u0026#34;stylesheet\u0026#34; href=\u0026#34;https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css\u0026#34;\u0026gt; \u0026lt;!-- jQuery library --\u0026gt; \u0026lt;script src=\u0026#34;https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;!-- Popper JS --\u0026gt; \u0026lt;script src=\u0026#34;https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;!-- Latest compiled JavaScript --\u0026gt; \u0026lt;script src=\u0026#34;https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; 以下のように、すべて自分のサイトで１つのcssやjsファイルに皆まとめてトランスパイルして最小化してしまうことが可能になりました。\n\u0026lt;link rel=\u0026#34;stylesheet\u0026#34; href=\u0026#34;/assets/css/all.js\u0026#34;\u0026gt; \u0026lt;script src=\u0026#34;/assets/js/all.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; しかし、そのために初めてjavascriptのコマンドラインでの開発環境が必要となりました。\nNode.jsがあなたの環境にインストールされているかどうかは、\n$ node -v v12.4.0 インストールされているなら、nodeコマンドを実行してみてください。\n$ node Welcome to Node.js v12.4.0. Type \u0026#34;.help\u0026#34; for more information. \u0026gt; x = 1 1 \u0026gt; y = 2 2 \u0026gt; x + y 3 \u0026gt; とtinkerみたいに対話式で実行できます。これは役に立ちそうです。いちいちhtmlにスクリプトにconsole.log()を入れてブラウザでデバッグしていた時代から抜けられるかな。\nnpm php言語において、composerというパッケージ管理のツールがあるように、Node.jsでは、npmというパッケージマネージャーがあります。npmがインストールされているかは、以下でチェックします。、\n$ npm -v 6.10.2 composer create-projectでLaraveの新規のプロジェクトを作成したら、composer.jsonの情報をもとに、\n$ composer install が自動的に実行されて、vendorのディレクトリにすべての必要なphpのパッケージがダウンロードされて収納されます。\nそれと同様に、\n$ npm install を実行すると、今度は、package.jsonの情報ををもとにnode_modulesのディレクトリにすべての必要なNode.jsのパッケージがダウンロードされます。\nどのパッケージがインストールされたかは、composerでは以下のコマンドでチェックできます。\n$ composer show beyondcode/laravel-dump-server 1.3.0 Symfony Var-Dump Server for Laravel dnoegel/php-xdg-base-dir 0.1 implementation of xdg base directory specification for php doctrine/inflector v1.3.0 Common String Manipulations with regard to casing and singular/plural rules. doctrine/instantiator 1.2.0 A small, lightweight utility to instantiate objects in PHP without invoking their constructors doctrine/lexer 1.1.0 PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers. dragonmantank/cron-expression v2.3.0 CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due egulias/email-validator 2.1.11 A library for validating emails against several RFCs erusev/parsedown 1.7.3 Parser for Markdown. fideloper/proxy 4.2.1 Set trusted proxies for Laravel filp/whoops 2.5.0 php error handling for cool kids fzaninotto/faker v1.8.0 Faker is a PHP library that generates fake data for you. hamcrest/hamcrest-php v2.0.0 This is the PHP port of Hamcrest Matchers jakub-onderka/php-console-color v0.2 jakub-onderka/php-console-highlighter v0.4 Highlight PHP code in terminal laravel/framework v5.8.35 The Laravel Framework. ... 同様に、npmでは、\n$ npm list /vol1/usr/www/repos/repos/l58 ├─┬ axios@0.19.0 │ ├─┬ follow-redirects@1.5.10 │ │ └─┬ debug@3.1.0 │ │ └── ms@2.0.0 │ └── is-buffer@2.0.3 ├── bootstrap@4.3.1 ├─┬ cross-env@5.2.1 │ └─┬ cross-spawn@6.0.5 │ ├── nice-try@1.0.5 │ ├── path-key@2.0.1 │ ├── semver@5.7.1 │ ├─┬ shebang-command@1.2.0 │ │ └── shebang-regex@1.0.0 │ └─┬ which@1.3.1 │ └── isexe@2.0.0 ├── jquery@3.4.1 ├─┬ laravel-mix@4.1.4 ... 依存するパッケージが階層で表示されているのでたくさんの表示になってしまいます。\u0026ndash;depth 0を付けたら第一階層のみ表示できます。\n$ npm list --depth 0 /vol1/usr/www/repos/repos/l58 ├── axios@0.19.0 ├── bootstrap@4.3.1 ├── cross-env@5.2.1 ├── jquery@3.4.1 ├── laravel-mix@4.1.4 ├── lodash@4.17.15 ├── popper.js@1.15.0 ├── resolve-url-loader@2.3.2 ├── sass@1.22.12 ├── sass-loader@7.3.1 └── vue@2.6.10 ","date":"2019-09-23T09:34:17+09:00","permalink":"https://www.larajapan.com/2019/09/23/node-js%E3%81%A8npm/","title":"Node.jsとnpm"},{"content":"tinker好きの私としては、嬉しいニュースです。今までコンソールからでしか使用できなかったtinkerがブラウザでアクセスできるようになりました。と言っても、Laravelのパッケージの一部ではなく、Laravelのtinkerにフロントエンドを追加したphpのパッケージ、Web Tinkerのことです。\nインストール まず、パッケージのレポは以下のGitHubです。\nhttps://github.com/spatie/laravel-web-tinker\nこれによると、まずcomposerの実行です。\u0026ndash;devのオプションが必要なことに注意してください。ウェブからのアクセスが可能なので、これなしではproduction版でアクセスされて、セキュリティの問題となります。\n$ composer require spatie/laravel-web-tinker --dev 次に、以下の実行でWeb Tinkerのアセットをpublic/vendor/web-tinkerのディレクトリにインストールします。\n$ php artisan web-tinker:install フロントエンド 早速、ブラウザからアクセスしてみましょう。url + \u0026lsquo;/tinker\u0026rsquo;でアクセスできます。\n実行には、cmd+enter あるいは ctrl+enterとあります。コンソール版のtinkerとは違って、１行タイプしてenterで実行とはいかないようです。\nとりあえず、以下のように実行してみました。\n結果が右パネルに出ましたね。\nしかし、以下のように２行タイプすると、右パネルがクリアされて最後の行の結果のみが出力されます。\nコンソール版のtinkerと違い、実行の ctrl+enterを押すと、画面に入力したコードすべて（複数行あっても）が実行されるようです。たとえ最初の１行目にカーソルを持っていき実行しても同じ結果です。これは、複雑なコードのテストには理想かもしれません。例えば、以前に掲載した複雑なコードは、コンソール版にtinkerでは以下のように１行１行タイプ実行していきます。８行目のファクトリーの実行では長い１行となっているので、括弧の数など間違わないように注意が必要です。\n$ php artisan tinker sy Shell v0.9.9 (PHP 7.2.16 — cli) by Justin Hileman \u0026gt;\u0026gt;\u0026gt; use App\\User; \u0026gt;\u0026gt;\u0026gt; use App\\Address; \u0026gt;\u0026gt;\u0026gt; User::truncate(); \u0026gt;\u0026gt;\u0026gt; Address::truncate(); =\u0026gt; Illuminate\\Database\\Eloquent\\Builder {#2974} \u0026gt;\u0026gt;\u0026gt; factory(App\\User::class)-\u0026gt;create()-\u0026gt;each(function ($user) { $user-\u0026gt;addresses()-\u0026gt;saveMany(factory(App\\Address::class, 2)-\u0026gt;make()); }); =\u0026gt; true ... しかし、Web Tinkerでは、以下のように直接プログラムからのチャンクのコードをコピペしてそのまま実行できます。\nこれは便利です。\nそれから、コンソール版と比べて、毎回の実行において環境がリロードされるので、コンソール版のように、プログラム変更するたびに、exitして再度php artisan tinkerする必要がないのもプラスです。\nマイナーなマイナスの面では、編集にemacsのキー、つまり、control + a（行先頭へ移動）、control + e （行末へ移動）などが使えないことです。私（だけ？）にとっては少し残念なことです。コンソール版ではそれらのキーは重宝しています。\n結論としては、コンソール版のtinkerを捨てて、このウェブ版のtinkerに乗り換えることはできませんが、目的によって使い分けをすれば十分パワフルなツールとして使えそうです。\n","date":"2019-09-13T23:14:31+09:00","permalink":"https://www.larajapan.com/2019/09/13/%E3%83%96%E3%83%A9%E3%82%A6%E3%82%B6%E3%81%8B%E3%82%89tinker%E3%81%AB%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9/","title":"ブラウザからtinkerにアクセス"},{"content":"今回は「マスアサインメントの保護を外す」という大胆なタイトルです。マスアサインメントの保護は、Eloquentにおけるモデルの定義においての$fillableや$guardedを通しての設定のことです。その保護を外す、とは、これを指定しない、ということではなく（もちろん外すのは危険）、あるケースにおいて外す必要が出てきたときに、どう外すかということです。\n$fillable \u0026 $guarded 復習として、Eloquentにおけるこれらの変数の説明から始めます。\nLaravelのプロジェクトのデフォルト設定のDBのusersのテーブルは以下のような構造です。\nmysql\u0026gt; describe 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 | | +-------------------+---------------------+------+-----+---------+----------------+ そして、EloquentのモデルのUserでは、以下のように$fillableが定義されています。\napp/User.php namespace App; use Illuminate\\Notifications\\Notifiable; use Illuminate\\Contracts\\Auth\\MustVerifyEmail; use Illuminate\\Foundation\\Auth\\User as Authenticatable; class User extends Authenticatable { use Notifiable; /** * The attributes that are mass assignable. * * @var array */ protected $fillable = [ \u0026#39;name\u0026#39;, \u0026#39;email\u0026#39;, \u0026#39;password\u0026#39;, ]; /** * The attributes that should be hidden for arrays. * * @var array */ protected $hidden = [ \u0026#39;password\u0026#39;, \u0026#39;remember_token\u0026#39;, ]; /** * The attributes that should be cast to native types. * * @var array */ protected $casts = [ \u0026#39;email_verified_at\u0026#39; =\u0026gt; \u0026#39;datetime\u0026#39;, ]; } つまり、name, email, password以外の項目、例えば、idとかとともにマスアサインしても、以下のようにUserのインスタンスの作成時には弾かれる、ということです。\n$ php artisan tinker \u0026gt;\u0026gt;\u0026gt; $input = [\u0026#39;id\u0026#39; =\u0026gt; 1, \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;test\u0026#39;, \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;test@example.com\u0026#39;]; =\u0026gt; [ \u0026#34;id\u0026#34; =\u0026gt; 1, \u0026#34;name\u0026#34; =\u0026gt; \u0026#34;test\u0026#34;, \u0026#34;email\u0026#34; =\u0026gt; \u0026#34;test@example.com\u0026#34;, ] \u0026gt;\u0026gt;\u0026gt; new User($input); [!] Aliasing \u0026#39;User\u0026#39; to \u0026#39;App\\User\u0026#39; for this Tinker session. =\u0026gt; App\\User {#2985 name: \u0026#34;test\u0026#34;, email: \u0026#34;test@example.com\u0026#34;, } \u0026gt;\u0026gt;\u0026gt; 作成されたインスタンスにおいては、idの項目はありません。fill()を使用しても同じことです。\n\u0026gt;\u0026gt;\u0026gt; $user = new User(); =\u0026gt; App\\User {#2984} \u0026gt;\u0026gt;\u0026gt; $user-\u0026gt;fill($input); =\u0026gt; App\\User {#2984 name: \u0026#34;test\u0026#34;, email: \u0026#34;test@example.com\u0026#34;, } \u0026gt;\u0026gt;\u0026gt; そして、$fillableでなく、$guardedを使用しても結果は同じことです。\n... // protected $fillable = [ // \u0026#39;name\u0026#39;, \u0026#39;email\u0026#39;, \u0026#39;password\u0026#39;, // ]; protected $guarded = [ \u0026#39;id\u0026#39;, \u0026#39;email_verified_at\u0026#39;, \u0026#39;remember_token\u0026#39;, \u0026#39;created_at\u0026#39;, \u0026#39;updated_at\u0026#39; ]; ... 保護を外す さて、上の$fillableの保護指定をそのままにして、同じUserのモデルでidなど、name, email, password以外の項目を指定してインスタンスを作成したいときは、どうするのでしょう？\nそこで登場するのが、unguard()のメソッドです。\n\u0026gt;\u0026gt;\u0026gt; User::unguard() =\u0026gt; null \u0026gt;\u0026gt;\u0026gt; new User($input); =\u0026gt; App\\User {#2985 id: 1, name: \u0026#34;test\u0026#34;, email: \u0026#34;test@example.com\u0026#34;, } \u0026gt;\u0026gt;\u0026gt; $user = new User(); =\u0026gt; App\\User {#2986} \u0026gt;\u0026gt;\u0026gt; $user-\u0026gt;fill($input); =\u0026gt; App\\User {#2986 id: 1, name: \u0026#34;test\u0026#34;, email: \u0026#34;test@example.com\u0026#34;, } idが入るようになりましたね。\n解除するには、unguard(false)です。\n\u0026gt;\u0026gt;\u0026gt; User::unguard(false); =\u0026gt; null \u0026gt;\u0026gt;\u0026gt; new User($input); =\u0026gt; App\\User {#2987 name: \u0026#34;test\u0026#34;, email: \u0026#34;test@example.com\u0026#34;, } \u0026gt;\u0026gt;\u0026gt; 以前のようにもうidは入りません。\nunguard()はセッションで有効ですが、１つのインスタンスで保護を外すには、forceFill()を使用できます。\n\u0026gt;\u0026gt;\u0026gt; $user = new User(); =\u0026gt; App\\User {#2985} \u0026gt;\u0026gt;\u0026gt; $user-\u0026gt;forceFill($input); =\u0026gt; App\\User {#2985 id: 1, name: \u0026#34;test\u0026#34;, email: \u0026#34;test@example.com\u0026#34;, } ","date":"2019-09-09T07:12:25+09:00","permalink":"https://www.larajapan.com/2019/09/09/%E3%83%9E%E3%82%B9%E3%82%A2%E3%82%B5%E3%82%A4%E3%83%B3%E3%83%A1%E3%83%B3%E3%83%88%E3%81%AE%E4%BF%9D%E8%AD%B7%E3%82%92%E5%A4%96%E3%81%99/","title":"マスアサインメントの保護を外す"},{"content":"tinkerを使用して開発中の関数をいろいろ試験します。Eloquent関連の関数で複数のDBテーブルに渡り値を更新する関数ゆえに、tinkerがとても役に立つのです。しかし、試験中に不思議なことが起こりました。ちょっと格闘して、なるほどそこではこうすればいいのだな、という話を共有です。\nまず、説明のためにhasManyのフェイクデータの作成のポストで環境を作成します。\n以前のレコードが残っているかもしれないので、truncate()を実行して空にします。そこでfactory()を実行して、Userのレコードを１つ、Addressのレコードを２つ作成します。$user-\u0026gt;addressesは、Userのモデルのrelationshipです。\n$ php artisan tinker sy Shell v0.9.9 (PHP 7.2.16 — cli) by Justin Hileman \u0026gt;\u0026gt;\u0026gt; use App\\User; \u0026gt;\u0026gt;\u0026gt; use App\\Address; \u0026gt;\u0026gt;\u0026gt; User::truncate(); \u0026gt;\u0026gt;\u0026gt; Address::truncate(); =\u0026gt; Illuminate\\Database\\Eloquent\\Builder {#2974} \u0026gt;\u0026gt;\u0026gt; factory(App\\User::class)-\u0026gt;create()-\u0026gt;each(function ($user) { $user-\u0026gt;addresses()-\u0026gt;saveMany(factory(App\\Address::class, 2)-\u0026gt;make()); }); =\u0026gt; true \u0026gt;\u0026gt;\u0026gt; $user = User::find(1); =\u0026gt; App\\User {#2976 id: 1, name: \u0026#34;三宅 桃子\u0026#34;, email: \u0026#34;kaori01@example.org\u0026#34;, email_verified_at: \u0026#34;2019-08-29 20:45:31\u0026#34;, created_at: \u0026#34;2019-08-13 07:57:01\u0026#34;, updated_at: \u0026#34;2019-08-13 07:57:01\u0026#34;, } \u0026gt;\u0026gt;\u0026gt; $user-\u0026gt;addresses; =\u0026gt; Illuminate\\Database\\Eloquent\\Collection {#2986 all: [ App\\Address {#2982 id: 1, user_id: 1, address: \u0026#34;8427481 東京都松本市西区山田町廣川6-3-5 ハイツ喜嶋108号\u0026#34;, created_at: \u0026#34;2019-08-29 20:45:31\u0026#34;, updated_at: \u0026#34;2019-08-29 20:45:31\u0026#34;, }, App\\Address {#2974 id: 2, user_id: 1, address: \u0026#34;9979251 茨城県加藤市北区原田町山口7-7-5\u0026#34;, created_at: \u0026#34;2019-08-29 20:45:31\u0026#34;, updated_at: \u0026#34;2019-08-29 20:45:31\u0026#34;, }, ], } さて、ここで$userの名前を変更してみます。\n\u0026gt;\u0026gt;\u0026gt; $user-\u0026gt;name = \u0026#39;TEST\u0026#39;; =\u0026gt; \u0026#34;TEST\u0026#34; \u0026gt;\u0026gt;\u0026gt; $user =\u0026gt; App\\User {#2976 id: 1, name: \u0026#34;TEST\u0026#34;, email: \u0026#34;kaori01@example.org\u0026#34;, email_verified_at: \u0026#34;2019-08-29 20:45:31\u0026#34;, created_at: \u0026#34;2019-08-13 07:57:01\u0026#34;, updated_at: \u0026#34;2019-08-13 07:57:01\u0026#34;, addresses: Illuminate\\Database\\Eloquent\\Collection {#2986 all: [ App\\Address {#2982 id: 1, user_id: 1, address: \u0026#34;8427481 東京都松本市西区山田町廣川6-3-5 ハイツ喜嶋108号\u0026#34;, created_at: \u0026#34;2019-08-29 20:45:31\u0026#34;, updated_at: \u0026#34;2019-08-29 20:45:31\u0026#34;, }, App\\Address {#2974 id: 2, user_id: 1, address: \u0026#34;9979251 茨城県加藤市北区原田町山口7-7-5\u0026#34;, created_at: \u0026#34;2019-08-29 20:45:31\u0026#34;, updated_at: \u0026#34;2019-08-29 20:45:31\u0026#34;, }, ], }, } 変わりましたね。しかし、まだ$user-\u0026gt;save()を実行していないので、単に$userのインスタンスの値が変わっただけです。これをリセットするには、fresh()を使用します。\n\u0026gt;\u0026gt;\u0026gt; $user-\u0026gt;fresh(); =\u0026gt; App\\User {#2979 id: 1, name: \u0026#34;三宅 桃子\u0026#34;, email: \u0026#34;kaori01@example.org\u0026#34;, email_verified_at: \u0026#34;2019-08-29 20:45:31\u0026#34;, created_at: \u0026#34;2019-08-13 07:57:01\u0026#34;, updated_at: \u0026#34;2019-08-13 07:57:01\u0026#34;, } \u0026gt;\u0026gt;\u0026gt; 戻りましたね。しかし、$user自体は戻っていません。\n\u0026gt;\u0026gt;\u0026gt; $user =\u0026gt; App\\User {#2976 id: 1, name: \u0026#34;TEST\u0026#34;, email: \u0026#34;kaori01@example.org\u0026#34;, email_verified_at: \u0026#34;2019-08-29 20:45:31\u0026#34;, created_at: \u0026#34;2019-08-13 07:57:01\u0026#34;, updated_at: \u0026#34;2019-08-13 07:57:01\u0026#34;, addresses: Illuminate\\Database\\Eloquent\\Collection {#2986 all: [ App\\Address {#2982 id: 1, user_id: 1, address: \u0026#34;8427481 東京都松本市西区山田町廣川6-3-5 ハイツ喜嶋108号\u0026#34;, created_at: \u0026#34;2019-08-29 20:45:31\u0026#34;, updated_at: \u0026#34;2019-08-29 20:45:31\u0026#34;, }, App\\Address {#2974 id: 2, user_id: 1, address: \u0026#34;9979251 茨城県加藤市北区原田町山口7-7-5\u0026#34;, created_at: \u0026#34;2019-08-29 20:45:31\u0026#34;, updated_at: \u0026#34;2019-08-29 20:45:31\u0026#34;, }, ], }, } 不思議ですね。\nAPIのマニュアルを見てみましょう。 https://laravel.com/api/5.8/Illuminate/Database/Eloquent/Model.html#method_fresh\nそこの説明では、\nReload a fresh model instance from the database.\nDBから新しいインスタンスをリロード、とありますが、元のインスタンスをリセットするのではなく、新しいインスタンスを作成して返しているようです。\nつまり、元のインスタンスのリセットには、\n\u0026gt;\u0026gt;\u0026gt; $user = $user-\u0026gt;fresh(); =\u0026gt; App\\User {#2979 id: 1, name: \u0026#34;三宅 桃子\u0026#34;, email: \u0026#34;kaori01@example.org\u0026#34;, email_verified_at: \u0026#34;2019-08-29 20:45:31\u0026#34;, created_at: \u0026#34;2019-08-13 07:57:01\u0026#34;, updated_at: \u0026#34;2019-08-13 07:57:01\u0026#34;, } \u0026gt;\u0026gt;\u0026gt; $user =\u0026gt; App\\User {#2979 id: 1, name: \u0026#34;三宅 桃子\u0026#34;, email: \u0026#34;kaori01@example.org\u0026#34;, email_verified_at: \u0026#34;2019-08-29 20:45:31\u0026#34;, created_at: \u0026#34;2019-08-13 07:57:01\u0026#34;, updated_at: \u0026#34;2019-08-13 07:57:01\u0026#34;, } と元のインスタンスに割り当てが必要となります。面倒ですね。fresh()は違う使用ケースがあるのでしょうか。 と思っていたら、そのためにもう１つのメソッドrefersh()がありました。\n\u0026gt;\u0026gt;\u0026gt; $user-\u0026gt;name = \u0026#39;TEST\u0026#39;; =\u0026gt; \u0026#34;TEST\u0026#34; \u0026gt;\u0026gt;\u0026gt; $user; =\u0026gt; App\\User {#2979 id: 1, name: \u0026#34;TEST\u0026#34;, email: \u0026#34;kaori01@example.org\u0026#34;, email_verified_at: \u0026#34;2019-08-29 20:45:31\u0026#34;, created_at: \u0026#34;2019-08-13 07:57:01\u0026#34;, updated_at: \u0026#34;2019-08-13 07:57:01\u0026#34;, } \u0026gt;\u0026gt;\u0026gt; $user-\u0026gt;refresh(); =\u0026gt; App\\User {#2979 id: 1, name: \u0026#34;三宅 桃子\u0026#34;, email: \u0026#34;kaori01@example.org\u0026#34;, email_verified_at: \u0026#34;2019-08-29 20:45:31\u0026#34;, created_at: \u0026#34;2019-08-13 07:57:01\u0026#34;, updated_at: \u0026#34;2019-08-13 07:57:01\u0026#34;, } \u0026gt;\u0026gt;\u0026gt; $user =\u0026gt; App\\User {#2979 id: 1, name: \u0026#34;三宅 桃子\u0026#34;, email: \u0026#34;kaori01@example.org\u0026#34;, email_verified_at: \u0026#34;2019-08-29 20:45:31\u0026#34;, created_at: \u0026#34;2019-08-13 07:57:01\u0026#34;, updated_at: \u0026#34;2019-08-13 07:57:01\u0026#34;, } \u0026gt;\u0026gt;\u0026gt; 一件落着です。このrefresh()のメソッド、関連しているレコードの反映にも役立つことわかりました。\n例えば、\n\u0026gt;\u0026gt;\u0026gt; $user-\u0026gt;addresses; =\u0026gt; Illuminate\\Database\\Eloquent\\Collection {#2986 all: [ App\\Address {#2982 id: 1, user_id: 1, address: \u0026#34;8427481 東京都松本市西区山田町廣川6-3-5 ハイツ喜嶋108号\u0026#34;, created_at: \u0026#34;2019-08-29 20:45:31\u0026#34;, updated_at: \u0026#34;2019-08-29 20:45:31\u0026#34;, }, App\\Address {#2974 id: 2, user_id: 1, address: \u0026#34;9979251 茨城県加藤市北区原田町山口7-7-5\u0026#34;, created_at: \u0026#34;2019-08-29 20:45:31\u0026#34;, updated_at: \u0026#34;2019-08-29 20:45:31\u0026#34;, }, ], } のように、ユーザーには２つの住所があります。ここの１つを更新してみます。\n\u0026gt;\u0026gt;\u0026gt; $address = Address::find(1); =\u0026gt; App\\Address {#3015 id: 1, user_id: 1, address: \u0026#34;8427481 東京都松本市西区山田町廣川6-3-5 ハイツ喜嶋108号\u0026#34;, created_at: \u0026#34;2019-08-29 20:45:31\u0026#34;, updated_at: \u0026#34;2019-08-29 21:16:30\u0026#34;, } \u0026gt;\u0026gt;\u0026gt; $address-\u0026gt;update([\u0026#39;address\u0026#39; =\u0026gt; \u0026#39;TEST ADDRESS\u0026#39;]); =\u0026gt; true しかし、以下を実行すると、\n\u0026gt;\u0026gt;\u0026gt; $user-\u0026gt;addresses =\u0026gt; Illuminate\\Database\\Eloquent\\Collection {#2971 all: [ App\\Address {#2970 id: 1, user_id: 1, address: \u0026#34;8427481 東京都松本市西区山田町廣川6-3-5 ハイツ喜嶋108号\u0026#34;, created_at: \u0026#34;2019-08-29 20:45:31\u0026#34;, updated_at: \u0026#34;2019-08-29 20:45:31\u0026#34;, }, App\\Address {#2981 id: 2, user_id: 1, address: \u0026#34;9979251 茨城県加藤市北区原田町山口7-7-5\u0026#34;, created_at: \u0026#34;2019-08-29 20:45:31\u0026#34;, updated_at: \u0026#34;2019-08-29 20:45:31\u0026#34;, }, ], } 最初のアドレスのレコードは、オリジナルのままですね。\nそこで、refersh()を使いましょう。\n\u0026gt;\u0026gt;\u0026gt; $user-\u0026gt;refresh(); =\u0026gt; App\\User {#2979 id: 1, name: \u0026#34;三宅 桃子\u0026#34;, email: \u0026#34;kaori01@example.org\u0026#34;, email_verified_at: \u0026#34;2019-08-29 20:45:31\u0026#34;, created_at: \u0026#34;2019-08-13 07:57:01\u0026#34;, updated_at: \u0026#34;2019-08-13 07:57:01\u0026#34;, addresses: Illuminate\\Database\\Eloquent\\Collection {#3022 all: [ App\\Address {#3020 id: 1, user_id: 1, address: \u0026#34;TEST ADDRESS\u0026#34;, created_at: \u0026#34;2019-08-29 20:45:31\u0026#34;, updated_at: \u0026#34;2019-08-29 21:17:12\u0026#34;, }, App\\Address {#3023 id: 2, user_id: 1, address: \u0026#34;9979251 茨城県加藤市北区原田町山口7-7-5\u0026#34;, created_at: \u0026#34;2019-08-29 20:45:31\u0026#34;, updated_at: \u0026#34;2019-08-29 20:45:31\u0026#34;, }, ], }, } 変更が反映されましたね。しかし、fresh()はいったいどういうときに使用するのでしょうかね。\n","date":"2019-08-31T03:40:54+09:00","permalink":"https://www.larajapan.com/2019/08/31/fresh-vs-refresh/","title":"fresh() vs refresh()"},{"content":"Google Apps Scriptで作成したコネクタやGoogleデータポータルで作成したデータソースとレポートは、すべてGoogleの共有設定によりアクセス権限を細かく設定できます。とりあえず、現在はオーナーの私だけが権限となっているので他からのアクセスされることに心配はありません。一方、Googleデータポータル：コミュニティコネクタ（１）コントローラの作成において作成したコントローラは、この状態では誰でもアクセスできるコントローラとなっています。良くないですね。どうかしてみましょう！\nコントローラで認証 コントローラとGoogleデータポータルデータソースで秘密のキーを共有し、データソースがコントローラにアクセスするときに、そのキーを渡してコントローラで認証とします。秘密のキーは一般にAPIのアクセスに使われる、APIキーと同じことです。キーを知らなければ誰もアクセスできません。\nまず、環境設定の.envのファイルを変更します。\n.env ... GDP_KEY=N2JjNDc4MTk2M2Q1MGRlMTY3NGIxOGE3 N2JjNDc4MTk2M2Q1MGRlMTY3NGIxOGE3が秘密のキーです。\nこのキーは、config(\u0026lsquo;app.gdp_key\u0026rsquo;)として取得するので、config/app.phpファイルの編集も必要です。\nconfig/app.php \u0026lt;?php return [ ... \u0026#39;Validator\u0026#39; =\u0026gt; Illuminate\\Support\\Facades\\Validator::class, \u0026#39;View\u0026#39; =\u0026gt; Illuminate\\Support\\Facades\\View::class, ], \u0026#39;gdp_key\u0026#39; =\u0026gt; env(\u0026#39;GDP_KEY\u0026#39;), ]; そしてコントローラでデータソースから渡される認証キーをチェックします。キーがない、キーが間違っているなら403の禁止のエラーとなります。\napp/Http/Controlelrs/GdpController.php namespace App\\Http\\Controllers; use Illuminate\\Http\\Request; use App\\User; class GdpController extends Controller { /** * index * * @param \\Illuminate\\Http\\Request $request * @return \\Illuminate\\Http\\JsonResponse */ public function index(Request $request) { DB::enableQueryLog(); // 秘密のキーをチェック if ((! $request-\u0026gt;key) || ($request-\u0026gt;key != config(\u0026#39;app.gdp_key\u0026#39;))) { abort(403); } $query = User::query(); if ($request-\u0026gt;date_start) { $query-\u0026gt;where(\u0026#39;created_at\u0026#39;, \u0026#39;\u0026gt;=\u0026#39;, $request-\u0026gt;date_start); } if ($request-\u0026gt;date_end) { $query-\u0026gt;where(\u0026#39;created_at\u0026#39;, \u0026#39;\u0026lt;=\u0026#39;, $request-\u0026gt;date_end); } $rows = $query -\u0026gt;select(DB::raw(\u0026#39;id, DATE(created_at) as date_created\u0026#39;)) -\u0026gt;orderBy(\u0026#39;created_at\u0026#39;) -\u0026gt;get(); Log::debug(DB::getQueryLog()); } } 早速、テストしてみましょう。まずは、秘密キーなしでテストします。\n$ php artisan tinker Psy Shell v0.9.9 (PHP 7.2.16 — cli) by Justin Hileman \u0026gt;\u0026gt;\u0026gt; use Zttp\\Zttp; \u0026gt;\u0026gt;\u0026gt; $url = \u0026#39;https://example.com/l58/gdp\u0026#39;; =\u0026gt; \u0026#34;https://example.com/l58/gdp\u0026#34; \u0026gt;\u0026gt;\u0026gt; $r = Zttp::post($url, [\u0026#39;date_start\u0026#39; =\u0026gt; \u0026#39;2019-01-01\u0026#39;, \u0026#39;date_end\u0026#39; =\u0026gt; \u0026#39;2019-01-10\u0026#39;]); =\u0026gt; Zttp\\ZttpResponse {#2956 +\u0026#34;response\u0026#34;: GuzzleHttp\\Psr7\\Response {#3001}, +\u0026#34;cookies\u0026#34;: GuzzleHttp\\Cookie\\CookieJar {#2957}, +\u0026#34;transferStats\u0026#34;: GuzzleHttp\\TransferStats {#3002}, } \u0026gt;\u0026gt;\u0026gt; $r-\u0026gt;status(); =\u0026gt; 403 アクセス禁止のステータスです。次は、秘密キーありでテストします。\n\u0026gt;\u0026gt;\u0026gt; $r = Zttp::post($url, [\u0026#39;date_start\u0026#39; =\u0026gt; \u0026#39;2019-01-01\u0026#39;, \u0026#39;date_end\u0026#39; =\u0026gt; \u0026#39;2019-01-10\u0026#39;, \u0026#39;key\u0026#39; =\u0026gt; \u0026#39;N2JjNDc4MTk2M2Q1MGRlMTY3NGIxOGE3\u0026#39;]); =\u0026gt; Zttp\\ZttpResponse {#2997 +\u0026#34;response\u0026#34;: GuzzleHttp\\Psr7\\Response {#3063}, +\u0026#34;cookies\u0026#34;: GuzzleHttp\\Cookie\\CookieJar {#3008}, +\u0026#34;transferStats\u0026#34;: GuzzleHttp\\TransferStats {#3064}, } \u0026gt;\u0026gt;\u0026gt; $r-\u0026gt;status(); =\u0026gt; 200 \u0026gt;\u0026gt;\u0026gt; $r-\u0026gt;json(); =\u0026gt; [ [ \u0026#34;id\u0026#34; =\u0026gt; 17, \u0026#34;date_created\u0026#34; =\u0026gt; \u0026#34;2019-01-06\u0026#34;, ], [ \u0026#34;id\u0026#34; =\u0026gt; 95, \u0026#34;date_created\u0026#34; =\u0026gt; \u0026#34;2019-01-07\u0026#34;, ], [ \u0026#34;id\u0026#34; =\u0026gt; 73, \u0026#34;date_created\u0026#34; =\u0026gt; \u0026#34;2019-01-08\u0026#34;, ], ] 認証成功です！\nデータソースで秘密のキーを設定 今度は、Googleデータポータルでデータソースの作成する際に、カスタムコネクタを選択したところで、アクセスするURLだけではなく秘密キーも尋ねるようにします。\nこれには、Google Apps Scriptのカスタムコネクタのプログラムの変更が必要です。以下全コードを掲載します。前回のコードgetConfig()と比べてみてください。fetchDataFromApi()の関数の定義に変更があります。\nmain.js var cc = DataStudioApp.createCommunityConnector(); // 認証に必要な関数。ここでは認証なし。 function getAuthType() { var response = { type: \u0026#39;NONE\u0026#39; }; return response; } // このコネクタを使用してデータソースを作成するときに指定するときに必要な関数 function getConfig(request) { var config = cc.getConfig(); config .newTextInput() .setId(\u0026#39;url\u0026#39;) .setName(\u0026#39;データ元のURLを指定してください\u0026#39;) .setHelpText(\u0026#39;e.g. https://example.com/gdp\u0026#39;) .setPlaceholder(\u0026#39;https://\u0026#39;); config .newTextInput() .setId(\u0026#39;key\u0026#39;) .setName(\u0026#39;秘密のキーを入力してください\u0026#39;);　// 秘密のキーを尋ねる config.setDateRangeRequired(true); return config.build(); } // データソースで表示される項目を定義 function getFields() { var fields = cc.getFields(); var types = cc.FieldType; fields .newMetric() .setId(\u0026#39;id\u0026#39;) .setName(\u0026#39;id\u0026#39;) .setType(types.NUMBER); fields .newDimension() .setId(\u0026#39;date_created\u0026#39;) .setName(\u0026#39;date_created\u0026#39;) .setType(types.YEAR_MONTH_DAY); fields .newDimension() .setId(\u0026#39;year_month\u0026#39;) .setName(\u0026#39;年月\u0026#39;) .setFormula(\u0026#34;TODATE(date_created,\u0026#39;%Y-%m\u0026#39;)\u0026#34;) .setType(types.YEAR_MONTH); return fields; } // データソースで表示される項目に必要な関数 function getSchema(request) { var fields = getFields().build(); return { schema: fields }; } // データソースを通して実際のデータ取得のために必要な関数 function getData(request) { var requestedFields = getFields().forIds( request.fields.map(function(field) { return field.name; }) ); try { var response = fetchDataFromApi(request); var rows = JSON.parse(response); var data = []; rows.forEach(function(row) { var values = []; requestedFields.asArray().forEach(function(field) { switch(field.getId()) { case \u0026#39;id\u0026#39;: values.push(row.id); break; case \u0026#39;date_created\u0026#39;: values.push(row.date_created); break; default: values.push(\u0026#39;\u0026#39;); } }); data.push({ values: values }); }); } catch (e) { cc.newUserError() .setDebugText(\u0026#39;Error fetching data from API. Exception details: \u0026#39; + e) .setText( \u0026#39;The connector has encountered an unrecoverable error. Please try again later, or file an issue if this error persists.\u0026#39; ) .throwException(); } return { schema: requestedFields.build(), rows: data }; } /** * コントローラにアクセスして、jsonのデータを取得。開始日と終了日の期間も指定する。 * * @param {Object} request Data request parameters. * @returns {string} Response text for UrlFetchApp. */ function fetchDataFromApi(request) { var formData = { \u0026#39;key\u0026#39; : request.configParams.key, //秘密のキーを送る \u0026#39;date_start\u0026#39; : request.dateRange.startDate, \u0026#39;date_end\u0026#39; : request.dateRange.endDate }; var options = { \u0026#39;method\u0026#39; : \u0026#39;get\u0026#39;, \u0026#39;payload\u0026#39; : formData }; var response = UrlFetchApp.fetch(request.configParams.url, options); return response; } // 以下を設定すると、エラーが起きたときに詳細の説明が得られる function isAdminUser() { return true; } データソースとレポートの再設定 コネクタのコードを保存したら、今度はGoogleデータポータルで、データソースを作成し直します。\n既存のコネクタを使用するなら、初期化が必要なので、現在使用しているコネクタのアクセス権限を外します。\nそこからは、Googleデータポータル：コミュニティコネクタ（３）データソースとレポートの作成の、コネクタの登録とデータソースの作成のステップに従ってください。\n前回と違って、以下のように秘密のキーを入力する必要あります。再接続ボタンを押してデータソースの変更は完了です。\n今度は、前回作成したGoogleデータポータルのレポートを開いて編集です。オープンすると以下のよにデータソースのエラーとなっています。上の変更でレポートのデータソースが更新されたからです。以下のように前回のデータソースは、「不明」となっているので、更新したデータソースを代わりに選択します。\n以前と同じグラフが出てきました。これで完了です。\n","date":"2019-08-26T01:49:05+09:00","permalink":"https://www.larajapan.com/2019/08/26/google%E3%83%87%E3%83%BC%E3%82%BF%E3%83%9D%E3%83%BC%E3%82%BF%E3%83%AB%EF%BC%9A%E3%82%B3%E3%83%9F%E3%83%A5%E3%83%8B%E3%83%86%E3%82%A3%E3%82%B3%E3%83%8D%E3%82%AF%E3%82%BF%EF%BC%88%EF%BC%94%EF%BC%89/","title":"Googleデータポータル：コミュニティコネクタ（４）コントローラのセキュリティ"},{"content":"コネクタを作成したところで、Googleデータポータルでデータソースとレポートを作成します。やっとビジュアルな部分です。\nコネクタの登録の準備 まずは作成したコネクタをコミュニティコネクタとしての登録です。まずは、登録時に尋ねられる情報を前もって取得します。\nGoogle Apps Scriptにおいて以下のメニューを選択します。\nそこで表示される以下において、Get IDをクリックして、Development IDをコピーします。\nコネクタの登録とデータソースの作成 今度は、Googleデータポータルにおいて、データソースの新規を作成します。\n以下の画面において、通常はそこで表示されているコネクタを選択する代わりに、右上のでデベロッパーをクリックします。\n次に表示される画面で、先ほどコピーしたDevelopment IDをペーストして検証ボタンをクリックします。そうすると、マニフェストを読み取り、「カスタムコネクタ」が表示されるので、それを選択します。\n\u0026lt;img src=\u0026ldquo;gds3.png\u0026rdquo; alt=\u0026quot;\u0026quot; width=\u0026ldquo;1125\u0026rdquo; height=\u0026ldquo;854\u0026rdquo; class=\u0026ldquo;alignnone size-full wp-image-4057\u0026rdquo; border=\u0026ldquo;1\u0026rdquo;　/\u0026gt;\n今度は以下の画面で承認します。\n承認を完了すると、以下の画面に、作成したコントローラのURLを入力して、右上の接続ボタンをクリックします。\n以下の画面では、データソースでアクセスできるフィールドが表示されます。MySQLコネクタとは違ってすでに「年月」の計算項目も表示されています。これは、main.gsにおいて定義しておいたからです。\nこれでデータソースの作成は完了です。\nレポートの作成 レポートにおいて、データソースに作成した「カスタムコネクタ」を指定して、ディメンション、指標、並び替えを指定するとグラフが表示されます。\n","date":"2019-08-14T07:27:20+09:00","permalink":"https://www.larajapan.com/2019/08/14/google%E3%83%87%E3%83%BC%E3%82%BF%E3%83%9D%E3%83%BC%E3%82%BF%E3%83%AB%EF%BC%9A%E3%82%B3%E3%83%9F%E3%83%A5%E3%83%8B%E3%83%86%E3%82%A3%E3%82%B3%E3%83%8D%E3%82%AF%E3%82%BF%EF%BC%88%EF%BC%93%EF%BC%89/","title":"Googleデータポータル：コミュニティコネクタ（３）データソースとレポートの作成"},{"content":"前回において、データ供給元となるコントローラを作成しました。今度は、それを使用してデータポータルのデータソースを作成するために、カスタムのコミュニティコネクタの作成する必要があります。コネクトの作成は、Google Apps Scriptでの開発となり、ウェブではここで詳しく説明されています。Google Apps Scriptの開発は私には初めてであり、とても興味があるところです。しかし、開発の言語はjavascript(google script）、ファイルの編集はすべてウェブでと、普段のプロジェクトとは違い、また先のGoogleでの説明で扱う例は、参考になるものの、私のやりたいこととは違うゆえに、試行錯誤たくさんありました。\nコミュニティコネクタの作成にはいくつかステップがあります。\n新しいGoogle Apps Scriptプロジェクトを作成する コネクタコードを作成する プロジェクト マニフェストを作成する Google Apps Scriptのプロジェクトを作成 Googleのアカウントにログインして、以下へ行きます。\nhttps://script.google.com/home\nこのような画面です。\n上の画面で、新規スクリプトをクリックすると、以下の画面になります。\nそこで、まず「無題のプロジェクト」を「カスタムコネクタ」と上書きし、「コード.gs」を「main.gs」と英語名に改名します。英語名にするのは、後にウェブ環境以外で編集するときに扱い易くするためです。main.gsでなくてもCode.gsでも名前はＯＫです。ただし、拡張子は、jsでなくgsであることに注意を。\nコネクタコードの作成 肝心のコードですが、javascript言語で開発します。データポータル以外でも、GoogleシートなどGoogleの他のサービスのプラグイン機能のための開発も同様なようです。\nコネクタのコード開発はリソースも少なく試行錯誤が伴いますが、私が参考としたのは以下です。\nhttps://developers.google.com/datastudio/connector/build https://codelabs.developers.google.com/codelabs/community-connectors/#0 https://developers.google.com/datastudio/connector/reference\n開発したコードは以下となります。main.gsにコピペしてください。\nmain.gs var cc = DataStudioApp.createCommunityConnector(); // 認証に必要な関数。ここでは認証なし。 function getAuthType() { var response = { type: \u0026#39;NONE\u0026#39; }; return response; } // このコネクタを使用してデータソースを作成するときに入力してもらう項目を指定する関数。 // ここでは、アクセス先のurlのみを尋ねる。 function getConfig(request) { var config = cc.getConfig(); config .newTextInput() .setId(\u0026#39;url\u0026#39;) .setName(\u0026#39;データ元のURLを指定してください\u0026#39;) .setHelpText(\u0026#39;e.g. https://example.com/gdp\u0026#39;) .setPlaceholder(\u0026#39;https://\u0026#39;); config.setDateRangeRequired(true); return config.build(); } // データソースで表示される項目を定義をここで定義。 // id, date_created, 年月の３つの項目。年月は、date_createdをもとにした計算式の項目。 function getFields() { var fields = cc.getFields(); var types = cc.FieldType; fields .newMetric() .setId(\u0026#39;id\u0026#39;) .setName(\u0026#39;id\u0026#39;) .setType(types.NUMBER); fields .newDimension() .setId(\u0026#39;date_created\u0026#39;) .setName(\u0026#39;date_created\u0026#39;) .setType(types.YEAR_MONTH_DAY); fields .newDimension() .setId(\u0026#39;year_month\u0026#39;) .setName(\u0026#39;年月\u0026#39;) .setFormula(\u0026#34;TODATE(date_created,\u0026#39;%Y-%m\u0026#39;)\u0026#34;) .setType(types.YEAR_MONTH); return fields; } // データソースを接続した後に表示される項目を定義する関数。上のgetFields()をコール。 function getSchema(request) { var fields = getFields().build(); return { schema: fields }; } // データソースを通して実際のデータ取得してレポートに渡すときにコールする関数。 // データの取得は、fetchDataFromApi()の関数をコール。 function getData(request) { var requestedFields = getFields().forIds( request.fields.map(function(field) { return field.name; }) ); try { var response = fetchDataFromApi(request); var rows = JSON.parse(response); var data = []; rows.forEach(function(row) { var values = []; requestedFields.asArray().forEach(function(field) { switch(field.getId()) { case \u0026#39;id\u0026#39;: values.push(row.id); break; case \u0026#39;date_created\u0026#39;: values.push(row.date_created); break; default: values.push(\u0026#39;\u0026#39;); } }); data.push({ values: values }); }); } catch (e) { cc.newUserError() .setDebugText(\u0026#39;Error fetching data from API. Exception details: \u0026#39; + e) .setText( \u0026#39;The connector has encountered an unrecoverable error. Please try again later, or file an issue if this error persists.\u0026#39; ) .throwException(); } return { schema: requestedFields.build(), rows: data }; } /** * コントローラにアクセスして、jsonのデータを取得。開始日と終了日の期間も指定する。 */ function fetchDataFromApi(request) { var formData = { \u0026#39;date_start\u0026#39; : request.dateRange.startDate, \u0026#39;date_end\u0026#39; : request.dateRange.endDate }; var options = { \u0026#39;method\u0026#39; : \u0026#39;get\u0026#39;, \u0026#39;payload\u0026#39; : formData }; var response = UrlFetchApp.fetch(request.configParams.url, options); return response; } // 以下を設定すると、エラーが起きたときにデバッグに必要な詳細の説明が得られる。 function isAdminUser() { return true; } プロジェクト マニフェストを作成 以下のメニューから、マニフェストファイルを作成します。\nマニフェストファイル、appsscript.jsonは、作成したコネクタをコミュニティコネクタとして登録するのに必要な情報を含みます。以下は私のビジネスの例。\nこれで一応役者は揃ったというところです。今回は残念ながら何も動作するの見せることができません。次回でデータソースとレポートを作成して初めてすべてが繋がります。また、そのときにどこかで間違いがあるなら問題が起こってきます。デバッグの仕方も習得必要となります。次を乞うご期待！\n","date":"2019-08-14T07:08:45+09:00","permalink":"https://www.larajapan.com/2019/08/14/google%E3%83%87%E3%83%BC%E3%82%BF%E3%83%9D%E3%83%BC%E3%82%BF%E3%83%AB%EF%BC%9A%E3%82%B3%E3%83%9F%E3%83%A5%E3%83%8B%E3%83%86%E3%82%A3%E3%82%B3%E3%83%8D%E3%82%AF%E3%82%BF%EF%BC%88%EF%BC%92%EF%BC%89/","title":"Googleデータポータル：コミュニティコネクタ（２）コネクタの作成"},{"content":"前回のMySQLのデータベースコネクタを使用してのレポート作成は便利な一方、たとえIP制限しても直接Googleからデータベースにアクセスさせるのはセキュリティ上恐ろしいです。そのデータベースのusersのテーブルにはハッシュであれパスワードのデータもあります。もちろん、そのためにViewを作成したりMySQLレベルでいろいろ細かく制御することも可能ですが管理が大変です。せっかくLaravelのプロジェクトを使うのだからEloquentで簡単に設定したいところです。ということで、今回はそれを実現すべく、コミュニティコネクタを作成して、GoogleデータポータルからLaravelのコントローラを通してデータにアクセスの仕方を説明します。\nステップとしては以下あります。う～ん、長い道のりになりそうです。分けてポストしていきます。その１は、データの供給元となるコントローラの作成です。\nデータの供給元として、Laravelのコントローラを作成する。 Google Apps Scriptを使い、コミュニティコネクタを作成する。 Googleデータポータルで、そのコネクタを利用してデータソースを作成する。 Googleデータポータルで、そのデータソースを使用してレポートを作成する。 コントローラの作成 前回100個のUserのレコードを作成した、Laravelのプロジェクトにおいてコントローラを作成します。\n以下を実行してコントローラのひな形を作成します。\n$ php artisan make:controller GdpController 作成された、空のコントローラを編集します。\n※修正あります。当初の以下のコードのDATE_FORMAT(created_at, \u0026lsquo;%Y%m%d\u0026rsquo;)は、DATE(created_at) as date_created\u0026rsquo;)に変更となりました。前者でも問題ないですが、後者の方でもポータルでは問題ないことが判明して、MySQLらしい表現ということで。\napp/Http/Controlelrs/GdpController.php namespace App\\Http\\Controllers; use Illuminate\\Http\\Request; use App\\User; class GdpController extends Controller { /** * index * * @param \\Illuminate\\Http\\Request $request * @return \\Illuminate\\Http\\JsonResponse */ public function index(Request $request) { DB::enableQueryLog(); $query = User::query(); if ($request-\u0026gt;date_start) { $query-\u0026gt;where(\u0026#39;created_at\u0026#39;, \u0026#39;\u0026gt;=\u0026#39;, $request-\u0026gt;date_start); } if ($request-\u0026gt;date_end) { $query-\u0026gt;where(\u0026#39;created_at\u0026#39;, \u0026#39;\u0026lt;=\u0026#39;, $request-\u0026gt;date_end); } $rows = $query -\u0026gt;select(DB::raw(\u0026#39;id, DATE(created_at) as date_created\u0026#39;)) -\u0026gt;orderBy(\u0026#39;created_at\u0026#39;) -\u0026gt;get(); Log::debug(DB::getQueryLog()); } } Googleのポータルのレポートでは期間がいつも指定されるので、date_startとdate_endがリクエストに含まれています。これらの値を条件にクエリを実行してデータを返します。返すデータは、idとdate_createdだけで、余計なemailやpasswordなどは含みません。\ndate_createdにおいてオリジナルの日時（例：2019-01-01 01:02:03）でなく、2019-01-01と日付だけとしているのは、前者だとポータルのレポートにおいてフィールドのタイプが思うように変わらないなど正しく処理されないことがあるからです。\nまた、ポータルからどのようなリクエストが来てどのようなSQLを実行するのかも興味があるので、Log::debug(DB::getQueryLog()); を追加しています。\n次に、routeの設定をします。以下の1行だけの追加ですが、getでなく、postであることに注意してください。ポータルは、postしか使用しないようです。\nroutes/web.php ... Route::post(\u0026#39;gdp\u0026#39;, \u0026#39;GdpController@index\u0026#39;); postゆえに、CSRFトークンを無視するように、以下の設定も必要です。\napp/Http/Middleware/VerifyCsrToken.php namespace App\\Http\\Middleware; use Illuminate\\Foundation\\Http\\Middleware\\VerifyCsrfToken as Middleware; class VerifyCsrfToken extends Middleware { /** * Indicates whether the XSRF-TOKEN cookie should be set on the response. * * @var bool */ protected $addHttpCookie = true; /** * The URIs that should be excluded from CSRF verification. * * @var array */ protected $except = [ \u0026#39;gdp\u0026#39;, ]; } コントローラのテスト ブラウザを使ってテストするには、POSTのためのコントローラなので簡単には行きません。FireFoxなら、Http Request Makerのような拡張機能が必要です。\nまあ、それもいいのですが、ここはLaravelゆえに私の好きなtinkerでテストしたいところです。しかし、デフォルトでテストできる関数と言えば、fopen()なのですが、なかなか面倒そうです。\n将来も必要だし何か良いものがないかと調べたところ、以下のライブラリがシンプルで良さそうです。\nhttps://github.com/kitetail/zttp\n以下の実行で、プロジェクトにライブラリを追加します。\n$ composer require kitetail/zttp 早速、tinkerで実行してみましょう。\n$ php artisan tinker Psy Shell v0.9.9 (PHP 7.2.16 — cli) by Justin Hileman \u0026gt;\u0026gt;\u0026gt; use Zttp\\Zttp; \u0026gt;\u0026gt;\u0026gt; $url = \u0026#39;https://example.com/l58/gdp\u0026#39;; =\u0026gt; \u0026#34;https://example.com/l58/gdp\u0026#34; \u0026gt;\u0026gt;\u0026gt; $r = Zttp::post($url, [\u0026#39;date_start\u0026#39; =\u0026gt; \u0026#39;2019-01-01\u0026#39;, \u0026#39;date_end\u0026#39; =\u0026gt; \u0026#39;2019-01-10\u0026#39;]); =\u0026gt; Zttp\\ZttpResponse {#2956 +\u0026#34;response\u0026#34;: GuzzleHttp\\Psr7\\Response {#3001}, +\u0026#34;cookies\u0026#34;: GuzzleHttp\\Cookie\\CookieJar {#2957}, +\u0026#34;transferStats\u0026#34;: GuzzleHttp\\TransferStats {#3002}, } \u0026gt;\u0026gt;\u0026gt; $r-\u0026gt;json(); =\u0026gt; [ [ \u0026#34;id\u0026#34; =\u0026gt; 17, \u0026#34;date_created\u0026#34; =\u0026gt; \u0026#34;2019-01-06\u0026#34;, ], [ \u0026#34;id\u0026#34; =\u0026gt; 95, \u0026#34;date_created\u0026#34; =\u0026gt; \u0026#34;2019-01-07\u0026#34;, ], [ \u0026#34;id\u0026#34; =\u0026gt; 73, \u0026#34;date_created\u0026#34; =\u0026gt; \u0026#34;2019-01-08\u0026#34;, ], ] うまく動作していますね。Zttpは、以前書いた記事のようにヘルパーとしてもいいかもしれません。\nデバッグのログにも以下のように記録されていました。\n[2019-08-04 21:04:43] local.DEBUG: array ( 0 =\u0026gt; array ( \u0026#39;query\u0026#39; =\u0026gt; \u0026#39;select id, DATE_FORMAT(created_at, \u0026#34;%Y%m%d\u0026#34;) as date_created from `users` where `created_at` \u0026gt;= ? and `created_at` \u0026lt;= ? order by `created_at` asc\u0026#39;, \u0026#39;bindings\u0026#39; =\u0026gt; array ( 0 =\u0026gt; \u0026#39;2019-01-01\u0026#39;, 1 =\u0026gt; \u0026#39;2019-01-10\u0026#39;, ), \u0026#39;time\u0026#39; =\u0026gt; 1.47, ), ) ","date":"2019-08-05T06:17:22+09:00","permalink":"https://www.larajapan.com/2019/08/05/google%E3%83%87%E3%83%BC%E3%82%BF%E3%83%9D%E3%83%BC%E3%82%BF%E3%83%AB%EF%BC%9A%E3%82%B3%E3%83%9F%E3%83%A5%E3%83%8B%E3%83%86%E3%82%A3%E3%82%B3%E3%83%8D%E3%82%AF%E3%82%BF%EF%BC%88%EF%BC%91%EF%BC%89/","title":"Googleデータポータル：コミュニティコネクタ（１）コントローラの作成"},{"content":"前回は、Googleデータポータルを使用してプロジェクトのデータベースに直接アクセスしてレポート（グラフ）を作成しました。今回は、作成したレポートを表示のためにLaravelのプロジェクトのダッシュボードに埋め込みます。\nレポートの共有 まず、レポートを共有します。共有の仕方は、Googleドライブなどでのファイルの共有と同じやり方です。\nファイルのメニューから共有を選択します。\nポップアップの画面がでます。以下はその詳細画面ですが、そこで共有したいユーザーを指定します。このユーザーは必ずGoogleアカウントのユーザーである必要があります。\n上に表示されている共有リンクをブラウザにコピペしてレポートを閲覧することも可能です。\nレポートの埋め込み レポートを、例えば管理画面のダッシュボードに埋め込むには、まず、埋め込みを有効にする必要あります。\nファイルのメニューから今度は、レポートを埋め込むを選択します。\n以下のダイアログで、埋め込みを有効にするをチェックします。\n次に表示されるダイアログで、クリップボードにコピーします。\n次は、ダッシュボードのブレードに以下のようにペーストします。17行目のiframeの部分です。\n@extends(\u0026#39;layouts.app\u0026#39;) @section(\u0026#39;content\u0026#39;) \u0026lt;div class=\u0026#34;container\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;row justify-content-center\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;col-md-8\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;card\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;card-header\u0026#34;\u0026gt;Dashboard\u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;card-body\u0026#34;\u0026gt; @if (session(\u0026#39;status\u0026#39;)) \u0026lt;div class=\u0026#34;alert alert-success\u0026#34; role=\u0026#34;alert\u0026#34;\u0026gt; {{ session(\u0026#39;status\u0026#39;) }} \u0026lt;/div\u0026gt; @endif \u0026lt;iframe width=\u0026#34;600\u0026#34; height=\u0026#34;450\u0026#34; src=\u0026#34;https://datastudio.google.com/embed/reporting/xxx/page/xxx\u0026#34; frameborder=\u0026#34;0\u0026#34; style=\u0026#34;border:0\u0026#34; allowfullscreen\u0026gt;\u0026lt;/iframe\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; @endsection 上のiframe内のリンクのURLは、共有時の共有リンクのURLとまったく同じです。\nこれで完了です。ダッシュボードに以下のように表示されます。\n最後に重要なことですが、このレポートの閲覧権限は、Laravelのプロジェクトの管理画面の認証とは関係なく、ブラウズしているユーザーが、すでにレポートを共有したGoogleのユーザーでありGoogleのアカウントでログインしているかどうかに関わります。Googleのログインをしていないと、以下のようにたとえLaravelのプロジェクトで認証されていても、閲覧はできません。\nさて、MySQLのデータベースコネクタを使用してのレポート作成は便利な一方、管理上、セキュリティ上不都合な点がたくさんあります。次回はそれを解決するために、Googleデータポータルにおいてカスタムコネクタを作成の仕方を説明します。\n","date":"2019-07-28T01:36:54+09:00","permalink":"https://www.larajapan.com/2019/07/28/google%E3%83%87%E3%83%BC%E3%82%BF%E3%83%9D%E3%83%BC%E3%82%BF%E3%83%AB%EF%BC%9A%E3%83%AC%E3%83%9D%E3%83%BC%E3%83%88%E3%81%AE%E5%9F%8B%E3%82%81%E8%BE%BC%E3%81%BF/","title":"Googleデータポータル：レポートの埋め込み"},{"content":"以前からお客さんのプロジェクトの管理画面のダッシュボードに日別や月別の売り上げ合計や会員の獲得数などをグラフで表示したいと思っていました。もちろん、グラフを作成するjavascriptのライブラリはいくつかありますが、習得にも開発にも時間がたくさんかかりそうです。そこで私の目に留まったのが、Googleデータポータル。データソースを指定してビジュアルなツールでちょいちょいとグラフ化できそうです。今回は、それを使用してMySQLのデータベースから月別に登録した会員数の棒グラフの作成の仕方を説明します。\n完成は以下のような画面です。\n準備：データベース まず、データポータルが取得してくるデータのソースとなるデータベースの準備が必要です。コマンドラインから、以下を実行します。\n$ mysql -u root mysql -p Enter password: mysql\u0026gt; create database l58; mysql\u0026gt; create user \u0026#39;gds\u0026#39;@\u0026#39;%\u0026#39; identified by \u0026#39;password\u0026#39;; mysql\u0026gt; grant select on l58.* to \u0026#39;gds\u0026#39;@\u0026#39;%\u0026#39;; データベース名　⇒　l58 ユーザー名　⇒　gds パスワード　⇒　password 権限　⇒　SQL文でSELECTのみの実行が可能 アクセス権限　⇒　インターネットのどこからも（上では、\u0026lsquo;gds\u0026rsquo;@\u0026rsquo;%\u0026lsquo;の%の指定により）\nという設定です。しかし、たとえパスワードの認証があってもインターネットのどこからでもこのDBにアクセスできるのは不安ですね。これに関しては後に対応します。\n次は、このデータベースを使用するLaravelのプロジェクトの作成ですが、ここでは最新のLaravelのバージョン（5.8）をインストールしたと仮定します。.envの設定には先のデータベース情報を使用することを忘れずに。\nこのデータベースでのDBテーブルの作成は、以下のようにmigrateを実行します。\n$ php artisan migrate Dropped all tables successfully. Migration table created successfully. Migrating: 2014_10_12_000000_create_users_table Migrated: 2014_10_12_000000_create_users_table (0.02 seconds) Migrating: 2014_10_12_100000_create_password_resets_table Migrated: 2014_10_12_100000_create_password_resets_table (0.02 seconds) 今度は、DBテーブル、usersを会員のテーブルとして、ここにフェイクの会員レコードを100個作成します。もちろん、factory()のヘルパーを使用してレコード作成となりますが、１つ問題があります。デフォルトの設定では、作成された会員のレコード作成日時(created_at)が皆作成日が同じになってしまうのです。これでは月別の棒グラフでは今月のみの表示となってしまいます。\nFakerのドキュメントを閲覧すると、dateTimeThisYear()という関数があり、多分に今年の日付をランダムに作成してくれるようです。この関数を使い、以下のようにUserFactory.phpを編集して、created_atに値がランダムに振り当てられるようにします。\n... $factory-\u0026gt;define(User::class, function (Faker $faker) { $dtm = $faker-\u0026gt;dateTimeThisYear($max = \u0026#39;now\u0026#39;, $timezone = \u0026#39;Asia/Tokyo\u0026#39;); return [ \u0026#39;name\u0026#39; =\u0026gt; $faker-\u0026gt;name, \u0026#39;email\u0026#39; =\u0026gt; $faker-\u0026gt;unique()-\u0026gt;safeEmail, \u0026#39;email_verified_at\u0026#39; =\u0026gt; now(), \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi\u0026#39;, // password \u0026#39;remember_token\u0026#39; =\u0026gt; Str::random(10), \u0026#39;created_at\u0026#39; =\u0026gt; $dtm, \u0026#39;updated_at\u0026#39; =\u0026gt; $dtm, ]; }); 用意ができたところで、いつものようにtinkerでレコード作成してみましょう。\n\u0026gt;\u0026gt;\u0026gt; factory(App\\User::class, 100)-\u0026gt;create(); =\u0026gt; Illuminate\\Database\\Eloquent\\Collection {#2982 all: [ App\\User {#2978 name: \u0026#34;鈴木 直子\u0026#34;, email: \u0026#34;kana93@example.com\u0026#34;, email_verified_at: \u0026#34;2019-07-18 20:06:46\u0026#34;, created_at: \u0026#34;2018-10-13 10:06:32\u0026#34;, updated_at: \u0026#34;2018-10-13 10:06:32\u0026#34;, id: 1, }, App\\User {#2976 name: \u0026#34;伊藤 幹\u0026#34;, email: \u0026#34;shuhei93@example.com\u0026#34;, email_verified_at: \u0026#34;2019-07-18 20:06:46\u0026#34;, created_at: \u0026#34;2019-05-17 21:42:06\u0026#34;, updated_at: \u0026#34;2019-05-17 21:42:06\u0026#34;, id: 2, }, ... 100個レコードできました。意図通りに、作成日時（created_at）もレコードごとに違います。しかし、何故か去年のレコードが作成されているのは変ですね。いつか調べてみます。\n準備：MySQLのセキュリティ 先ほど設定したデータベースは、ユーザー情報があればインターネットのどこからでも3306のポートでアクセスできます。Googleデータポータルだけからのアクセスに制限したいなら、ここで書かれているIPのみに制限を課すことができます。\nAWSならEC2でセキュリティグループを作成して以下のIPだけから、MySQLのポート(3306)にアクセスできる制限を設定できます。そして、このセキュリティグループを使用しているEC2のセキュリティに追加すればよいです。\nデータソースの作成 データベースとデータの準備ができたところで、今度はGoogleデータポータルに移ります。まずは、先のデータベースに接続するために、データソースなるものを作成します。Googleのアカウントですでにログインしていると仮定して、こちらへアクセスしてください。\n以下の画面で、作成ボタンを押して、データソースを選択します。\n次に、数あるデータコネクタから、MySQLを選択します。左上のデータソース名を無題から改名することもお忘れずに。\n今度は、データベース情報を入力して、認証ボタンを押します。\n認証が成功すると、DBテーブルのリストが右に表示されます。そこから、使用したいDBテーブルを選択します。ここでは、会員のusersを選択します。そして、右上の再接続をクリックしてください。\n\u0026lt;img src=\u0026ldquo;connected-e1563491493597.png\u0026rdquo; alt=\u0026quot;\u0026quot; width=\u0026ldquo;1196\u0026rdquo; height=\u0026ldquo;681\u0026rdquo; class=\u0026ldquo;alignnone size-full wp-image-3872\u0026quot;border=\u0026ldquo;1\u0026rdquo; /\u0026gt;\n再接続したのちに表示される画面は、DBテーブル、usersの項目のリストです。なぜか、create_at, updated_atのtimestampの項目は「テキスト」となっているので、以下のように日時のタイプを選択しておいてください。\nもう１つの作業として、最終の棒グラフでは、X軸を年月としたいので、年月のフィールドをcreated_atをもとに作成します。\n以下のように、フィールドを追加で開かれる画面で、計算式をTODATE(created_at,\u0026lsquo;DEFAULT_DECIMAL\u0026rsquo;, \u0026lsquo;%Y-%m\u0026rsquo;)と設定します。ここ、MONTH(created_at)のように思われますが、グラフが月別の表示とならなくうまく行きません。Googleデータポータルは、MySQLのtimestampのデータタイプを文字列とみなしているためかもしれません。\nレポートの作成 フィールドの設定が完了したところで、このデーターソースを使ってレポートを作成します。画面右上のレポートを作成ボタンを押してください。\n開かれた画面で、縦棒グラフを追加します。\n次に、以下のように、棒グラフの右のパネルで指定します。「使用可能な項目」から、適切な項目をドラグして以下にドロップします。\n上の画面で行った設定は、 データソース ⇒ DS MySql（すでにデフォルトで設定されている） 期間のディメンション ⇒　created_at ディメンション ⇒ 年月 指標　⇒　idをドラグして、名前を「会員数」とし、集計方法を「件数」に変更。 並べ替え ⇒　年月。降順から昇順に変更。\nこう設定すると、X軸に年月、Y軸にに会員数と、指定のデータベースのデータをもとにした棒グラフが期待通りに表示されます。\n表示モードとするとこのポストの最初の画面となります。右上の期間の指定を変えてみてください。応じて、棒グラフが変わります。\n最後に、ここで作成したレポートは、どのように管理画面にはめ込むのでしょうか？ それは次回のポストとします。\n","date":"2019-07-22T03:14:11+09:00","permalink":"https://www.larajapan.com/2019/07/22/google%E3%83%87%E3%83%BC%E3%82%BF%E3%83%9D%E3%83%BC%E3%82%BF%E3%83%AB%EF%BC%9A%E3%83%87%E3%83%BC%E3%82%BF%E3%82%BD%E3%83%BC%E3%82%B9%E3%81%A8%E3%83%AC%E3%83%9D%E3%83%BC%E3%83%88%E3%81%AE%E4%BD%9C/","title":"Googleデータポータル：データソースとレポートの作成"},{"content":"もちろんのことなのですが、私のお客さんのプロジェクトは皆パブリックでアクセスできる開発サイトがあります。そこで開発をするわけでなく、あくまでも本サイトにインストールする前に変更をお客さんに見てもらい使ってもらい、フィードバックをもらうためのサイトです。その意味では、ステージングサイトと言った方が正しいですね（お客さんとの間では開発サイトと呼んでいます）。\n本サイトと違う部分 本サイトとステージングサイトの環境はシステムレベルではまったく同じで、OSもPHPもMySqlのバージョンなどすべて同じです。違うのは使用しているAWSのEC2のスケールで、ステージングにはもちろん安いインスタンスを使用しています。 プログラムレベルでは、Laravelの.envの中身。データベースの接続情報やメールの宛先とか、その他たくさん。しかし、この.envのおかげでステージングの設定は非常に簡単になりました。\nそれ以外にも、ずっと昔にどういうわけか間違ってステージングサイトで買い物をする本物のお客さんが出てきたので、サイトには必ず、ベーシック認証、そうあのポップアップの認証、をかけて保護しています。以下はサンプルですが、Laravelのための.htaccessを編集しています。\n.htaccess \u0026lt;IfModule mod_rewrite.c\u0026gt; # 以下4行はベーシック認証。ステージングのみ。パスワードは/etc/httpd/.htpasswdに保存。 AuthType Basic AuthName \u0026#34;Password Protected Area\u0026#34; AuthUserFile .htpasswd Require valid-user \u0026lt;IfModule mod_negotiation.c\u0026gt; Options -MultiViews \u0026lt;/IfModule\u0026gt; RewriteEngine On # Redirect Trailing Slashes... RewriteRule ^(.*)/$ /$1 [L,R=301] # Handle Front Controller... RewriteCond %{REQUEST_FILENAME} !/assets RewriteCond %{REQUEST_FILENAME} !/server-status RewriteCond %{REQUEST_FILENAME} !-f RewriteRule ^ index.php [L] \u0026lt;/IfModule\u0026gt; アナリティクス ステージングではもう１つ重要な違いがあります。ブレードに埋め込んでいるアナリティクスの情報です。\n最近までは、以下のように、.envのAPP_ENV=productionの時だけレンダーするように、ブレードには @if (config(\u0026lsquo;app.env\u0026rsquo;) == \u0026lsquo;production\u0026rsquo;)の条件文を使用していました。\n... @if (config(\u0026#39;app.env\u0026#39;) == \u0026#39;production\u0026#39;) \u0026lt;!-- Global site tag (gtag.js) - Google Analytics --\u0026gt; \u0026lt;script async src=\u0026#34;https://www.googletagmanager.com/gtag/js?id=UA-123456-1\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script\u0026gt; window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag(\u0026#39;js\u0026#39;, new Date()); gtag(\u0026#39;config\u0026#39;, \u0026#39;UA-123456-1\u0026#39;); \u0026lt;/script\u0026gt; @endif ... しかし、アナリティクスも複雑になってきて、グーグルタグマネージャも使用し始めたので、アナリティクスも開発のために本サイトとは違うアカウントあるいはプロパティを作成して本サイトと分ける必要が出てきました。となると、トラッキングID（上では、UA-123456-1）が本サイトとステージングサイトでは異なることになります。\nトラッキングIDの値は、以下のように.envで簡単に設定することにします。\n.env ... GA=UA-123456-1 GTM=GTM-XYZ GAはアナリティクスのトラッキングIDで、GTMはタグマネージャのコンテナIDです。\nさらに、config/local.phpを作成します。\nconfig/local.php \u0026lt;?php return [ \u0026#39;ga\u0026#39; =\u0026gt; env(\u0026#39;GA\u0026#39;), \u0026#39;gtm\u0026#39; =\u0026gt; env(\u0026#39;GTM\u0026#39;), ]; そして、ブレードでは、\n... \u0026lt;!-- Global site tag (gtag.js) - Google Analytics --\u0026gt; \u0026lt;script async src=\u0026#34;https://www.googletagmanager.com/gtag/js?id={{ config(\u0026#39;local.ga\u0026#39;) }}\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script\u0026gt; window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag(\u0026#39;js\u0026#39;, new Date()); gtag(\u0026#39;config\u0026#39;, \u0026#39;{{ config(\u0026#39;local.ga\u0026#39;) }}\u0026#39;); \u0026lt;/script\u0026gt; ... とします。もう、@ifの条件文はもう必要でないので削除されていることに注意してください。\nこれで、本サイトであろうとステージングサイトであろうと、.envによりアナリティクスのIDを変更することができます。\n","date":"2019-07-15T07:26:08+09:00","permalink":"https://www.larajapan.com/2019/07/15/%E3%82%B9%E3%83%86%E3%83%BC%E3%82%B8%E3%83%B3%E3%82%B0%E3%82%B5%E3%82%A4%E3%83%88%E3%81%A8%E3%82%A2%E3%83%8A%E3%83%AA%E3%83%86%E3%82%A3%E3%82%AF%E3%82%B9/","title":"ステージングサイトとアナリティクス"},{"content":"このブログの一番最初に書いた記事において、isDirty()に関して書きました。もう4年くらい前のことです。最近必要になって使用したところ、新たな発見がありました。\n更新あります！\nまず、tinkerを使用して、isDirty()の復習です。 factory()を使って、１つレコードを作成します。\n\u0026gt;\u0026gt;\u0026gt; factory(App\\User::class)-\u0026gt;create(); =\u0026gt; App\\User {#2987 name: \u0026#34;小林 春香\u0026#34;, email: \u0026#34;kana.yamamoto@example.org\u0026#34;, email_verified_at: \u0026#34;2019-07-02 03:01:25\u0026#34;, updated_at: \u0026#34;2019-07-02 03:01:25\u0026#34;, created_at: \u0026#34;2019-07-02 03:01:25\u0026#34;, id: 1, } 作成したレコードの名前を更新しますが、DBでなくオブジェクトのみの更新のために、fill()を使用します。\n\u0026gt;\u0026gt;\u0026gt; $user = User::find(1); =\u0026gt; App\\User {#2950 id: 1, name: \u0026#34;小林 春香\u0026#34;, email: \u0026#34;kana.yamamoto@example.org\u0026#34;, email_verified_at: \u0026#34;2019-07-02 03:01:25\u0026#34;, created_at: \u0026#34;2019-07-02 03:01:25\u0026#34;, updated_at: \u0026#34;2019-07-02 03:01:25\u0026#34;, } \u0026gt;\u0026gt;\u0026gt; $user-\u0026gt;fill([\u0026#39;name\u0026#39; =\u0026gt; \u0026#39;山田花子\u0026#39;]); =\u0026gt; App\\User {#2950 id: 1, name: \u0026#34;山田花子\u0026#34;, email: \u0026#34;kana.yamamoto@example.org\u0026#34;, email_verified_at: \u0026#34;2019-07-02 03:01:25\u0026#34;, created_at: \u0026#34;2019-07-02 03:01:25\u0026#34;, updated_at: \u0026#34;2019-07-02 03:01:25\u0026#34;, } nameが変わりましたね。 実際、オブジェクトが更新したかどうかは、isDirty()を使います。\n\u0026gt;\u0026gt;\u0026gt; $user-\u0026gt;isDirty(); =\u0026gt; true 変更していますね。 項目ごとはどうでしょう？\n\u0026gt;\u0026gt;\u0026gt; $user-\u0026gt;isDirty(\u0026#39;name\u0026#39;); =\u0026gt; true \u0026gt;\u0026gt;\u0026gt; $user-\u0026gt;isDirty(\u0026#39;email\u0026#39;); =\u0026gt; false nameの変更がありますが、emailの変更はなしです。正しいです。\nここで、変更したオブジェクトをもとに対応するレコードをDBに保存します。\n\u0026gt;\u0026gt;\u0026gt; $user-\u0026gt;save(); =\u0026gt; true さて、この時点でのisDirty()の返り値は？\n\u0026gt;\u0026gt;\u0026gt; $user-\u0026gt;isDirty(); =\u0026gt; false \u0026gt;\u0026gt;\u0026gt; $user-\u0026gt;isDirty(\u0026#39;name\u0026#39;); =\u0026gt; false もう、isDirty()では、変更があったかどうかの判断はできません、何もDirtyではありません。\nこの時点、つまりオブジェクトの更新をDBに反映した時点でレコードが更新したかどうかを判断するには、isDirty()ではなく、wasChanged()を使用します。\n\u0026gt;\u0026gt;\u0026gt; $user-\u0026gt;wasChanged(); =\u0026gt; true \u0026gt;\u0026gt;\u0026gt; $user-\u0026gt;wasChanged(\u0026#39;name\u0026#39;); =\u0026gt; true \u0026gt;\u0026gt;\u0026gt; $user-\u0026gt;wasChanged(\u0026#39;email\u0026#39;); =\u0026gt; false save()前のisDirty()の結果とまったく同じです。\n実際には、こんなコードでwasChanged()は使われます。\n... $user-\u0026gt;fill([\u0026#39;name\u0026#39; =\u0026gt; \u0026#39;山田花子\u0026#39;]); if ($user-\u0026gt;isDirty()) { $user-\u0026gt;save(); if ($user-\u0026gt;wasChagned(\u0026#39;name\u0026#39;)) { //nameが更新されたときのみ、以下のコードを実行 ... } } ... ","date":"2019-07-05T01:07:02+09:00","permalink":"https://www.larajapan.com/2019/07/05/isdirty-vs-waschanged/","title":"isDirty() vs wasChanged()"},{"content":"Laravelを最新のバージョンに更新するタイミングは難しい。しかし、5.7,5.8などのメージャーバージョンが更新された直後に更新するのは避けた方が良いです。なぜなら、新バージョン直後は必ずいくつか予想できない問題が出てきて、その修正がマイナーバージョンアップとして暫く続くからです。しかし、ある程度時間が経過して落ち着いたバージョンをインストールしても、やはり予想できない問題が出てきてます。\n例えば、私の大きなプロジェクトでは、その複雑さゆえに、必ず毎日storage/logs/laravel.logの中身をクロンで私宛にメールして、エラーが出ていないかチェックしています。そのメールで６月に入ってからいきなり毎日報告されてきたのが以下のようなエラー。\n[2019-06-07 00:40:22] production.ERROR: Invalid method override \u0026#34;__CONSTRUCT\u0026#34;. {\u0026#34;exception\u0026#34;:\u0026#34;[object] (Symfony\\\\Component\\\\HttpFoundation\\\\Exception\\\\SuspiciousOperationException(code: 0): Invalid method override \\\u0026#34;__CONSTRUCT\\\u0026#34;. at /vol1/usr/www/example/ec/vendor/symfony/http-foundation/Request.php:1244) [stacktrace] #0 /vol1/usr/www/example/ec/vendor/laravel/framework/src/Illuminate/Routing/RouteCollection.php(159): Symfony\\\\Component\\\\HttpFoundation\\\\Request-\u0026gt;getMethod() ... ウェブログを調査すると、以下のようなログで、とても虚弱性を狙うためのスキャンくさい。\n47.106.182.80 - - [06/Jun/2019:11:22:45 +0900] \u0026#34;POST /index.php?s=captcha HTTP/1.1\u0026#34; 500 1695 \u0026#34;http://xx.xx.xx.xx/index.php?s=captcha\u0026#34; \u0026#34;Go-http-client/1.1 しかし、それ同様な情報をサイトにPOSTしてもエラーとはならなく、不思議と思っていました。ほとんど毎日エラーとなっているので、Googleで調べてみましたが情報はなし。SuspiciousOperationExceptionとあるので、app/Exception/Handler.phpで対応すればいいのかな、くらいに考えていました。\nその矢先に、購読しているLaravel Newsにおいて、以下が掲載されていました。\nそう、SuspiciousOperationExceptionがらみの変更があったようです。そのリンクは、PR（pull request）で、読んでみると。\nふむふむ。PR作成した人は、どうも私と同じ状況のようで、Symphonyの更新がこのエラーの登場となったようです。そうそうLaravelはSymphonyをベースとしているの思い出しました。\nこのPRでは、以前のように500でなく404（ページーがないエラー）で返す変更の提案でした。しかし、Laravelの作者であるTaylorくんが、「そう固定してしまうのは自由がなくなり困るケースが出てくるのでは？」と変更に反対の発言をし、開発者の采配にゆだねる方向に進み始めます。\nしかし、違うひとが登場してきて、毎回この500エラーをslackやemailで報告されるのは嫌であるし、それを防ぐためにどのプロジェクトでもそのException をキャッチするのも面倒なのでデフォルトで404エラーとすべきだと発言。つまり最初に提案したPRの変更の方がいいと。\nそして最終的には、そちらがベターということで、5.8.24での変更（修正ではなく）として5.8にマージされることになります。　そう大げさな変更でないですが、開発者として私もバージョンを更新するだけで私のプログラムをいちいち変更しないのは良いことと思います。\n今回のことで、以前はchangelogは細かすぎて無視してきたけれど、意外とコードの変更の決断などが理解できて面白いと思った次第です。\n","date":"2019-07-01T03:38:09+09:00","permalink":"https://www.larajapan.com/2019/07/01/changelog%E3%81%AF%E9%9D%A2%E7%99%BD%E3%81%84/","title":"changelogは面白い"},{"content":"Laravel 5.7の更新を終えて、次は最新の5.8への更新です。こちらも私が経験したことをもとに、いくつかの重要な更新箇所をここで説明です。\nヘルパーの変更 以下は5.6の時点でのLaravelで使用できるヘルパーの一部です。\nそして、以下が5.8(現時点の5.7でも）時点でのヘルパーです。\n配列や文字列処理の関数が変わりましたね。\nPsy Shell v0.9.9 (PHP 7.2.16 — cli) by Justin Hileman \u0026gt;\u0026gt;\u0026gt; $pen = [\u0026#39;color\u0026#39; =\u0026gt; \u0026#39;red\u0026#39;]; =\u0026gt; [ \u0026#34;color\u0026#34; =\u0026gt; \u0026#34;red\u0026#34;, ] \u0026gt;\u0026gt;\u0026gt; array_get($pen, \u0026#39;color\u0026#39;); =\u0026gt; \u0026#34;red\u0026#34; \u0026gt;\u0026gt;\u0026gt; Arr::get($pen, \u0026#39;color\u0026#39;); =\u0026gt; \u0026#34;red\u0026#34; と今までのグローバルの関数ではなくネームスペース（Arr, Str）を使用しての関数に変わります。賛否両論があるようですが、幸いにも、5.8では両方の形態を対応しています。しかし、5.9からはArrやStrの関数に対応するグローバル関数は廃止となる予定です。\n私は、Laravel Shiftのサービスをバージョン更新の最初のステップとして使用しているので、必要なクラスの宣言やヘルパーの変換はすべて、その更新サービス（有料）が行ってくれました。\n変換した後に、１つ問題となったのは、エラーチェックに使用しているphanの出力です。phanはコード解析をして、プログラムにおいての疑わしい問題個所をリストしてくれます。変更後のコードでは、以下のような関数未定義のエラーがたくさん出ました。\n... -app/Helpers/Lob.php:517 PhanUndeclaredStaticMethod Static call to undeclared method \\Illuminate\\Support\\Arr::first -app/Helpers/Lob.php:691 PhanUndeclaredStaticMethod Static call to undeclared method \\Illuminate\\Support\\Arr::get -app/Helpers/Lob.php:728 PhanUndeclaredStaticMethod Static call to undeclared method \\Illuminate\\Support\\Arr::get ... これは、phanをLaravelのプロジェクトで使用できるために使っている_ide helper.phpの問題のようです。\n_ide_helper.php ... namespace Illuminate\\Support { /** * * */ class Arr { } /** * * */ class Str { } } ... の部分を削除すれば、上のようなエラーを余計な吐き出すことはないです（参照）。\nCacheの時間単位の変更 こちらの変更は、Cache関連の関数を使用している開発者には大ごとです。キャッシュの保持時間の単位が今までの分単位から、より細かい時間を指定するために秒単位に変わります。\n// Laravel 5.7 - 30分データを保持から、 Cache::put(\u0026#39;foo\u0026#39;, \u0026#39;bar\u0026#39;, 30); // Laravel 5.8 - 30秒データ保持に変わる Cache::put(\u0026#39;foo\u0026#39;, \u0026#39;bar\u0026#39;, 30); // 30分をキープしたいなら、 Cache::put(\u0026#39;foo\u0026#39;, \u0026#39;bar\u0026#39;, 30 * 60); ","date":"2019-06-24T02:54:22+09:00","permalink":"https://www.larajapan.com/2019/06/24/laravel-5-8%E3%81%AB%E6%9B%B4%E6%96%B0/","title":"Laravel 5.8に更新"},{"content":"Laravel5.7に更新したら、会員登録時にEメール確認リンクを送信する新機能が追加されました。設定は簡単です。\n流れ まず、どういうものか、画面で流れ追ってみましょう。\nこのトップ画面から始まります。\n右上のRegisterのリンクをクリックすると、会員登録画面になります。\n情報を入力して、Registerボタンを押すと登録完了とともに、以下のメールが送信されます。\nそこで、Verify Email Addressのボタンをクリックすると、以下の画面へ遷移してEメールの確認完了です。ログアウトしているなら、ログインをして成功したら確認完了です。\n設定 この設定には、コマンドラインでまず以下を実行します。\n$ php artisan make:auth この実行により会員認証関連のファイルが作成されます。\n次に、routes/web.phpファイルをエディターで開き、Auth::routes([\u0026lsquo;verify\u0026rsquo; =\u0026gt; true])に編集します。\nroutes/web.php /* |-------------------------------------------------------------------------- | Web Routes |-------------------------------------------------------------------------- | | Here is where you can register web routes for your application. These | routes are loaded by the RouteServiceProvider within a group which | contains the \u0026#34;web\u0026#34; middleware group. Now create something great! | */ Route::get(\u0026#39;/\u0026#39;, function () { return view(\u0026#39;welcome\u0026#39;); }); Auth::routes([\u0026#39;verify\u0026#39; =\u0026gt; true]); Route::get(\u0026#39;/home\u0026#39;, \u0026#39;HomeController@index\u0026#39;)-\u0026gt;name(\u0026#39;home\u0026#39;); 次に、app/User.phpを編集します。\nclass User extends Authenticatable implements MustVerifyEmailと変更してインターフェースを実装します。\napp/User.php \u0026lt;?php namespace App; use Illuminate\\Notifications\\Notifiable; use Illuminate\\Contracts\\Auth\\MustVerifyEmail; use Illuminate\\Foundation\\Auth\\User as Authenticatable; class User extends Authenticatable implements MustVerifyEmail { use Notifiable; /** * The attributes that are mass assignable. * * @var array */ protected $fillable = [ \u0026#39;name\u0026#39;, \u0026#39;email\u0026#39;, \u0026#39;password\u0026#39;, ]; /** * The attributes that should be hidden for arrays. * * @var array */ protected $hidden = [ \u0026#39;password\u0026#39;, \u0026#39;remember_token\u0026#39;, ]; /** * The attributes that should be cast to native types. * * @var array */ protected $casts = [ \u0026#39;email_verified_at\u0026#39; =\u0026gt; \u0026#39;datetime\u0026#39;, ]; } これだけで設定完了です。\nデータベースの変更 このために、DBテーブル、usersに新規の項目、email_verified_atが追加されました。\n会員登録時は、その項目の値は、nullです。\n\u0026gt;\u0026gt;\u0026gt; User::find(1); =\u0026gt; App\\User {#3179 id: 1, name: \u0026#34;kenji\u0026#34;, email: \u0026#34;kenji@example.com\u0026#34;, email_verified_at: null, created_at: \u0026#34;2019-06-07 23:04:32\u0026#34;, updated_at: \u0026#34;2019-06-07 23:04:32\u0026#34;, } しかし、Eメール確認後は、\n\u0026gt;\u0026gt;\u0026gt; User::find(1); =\u0026gt; App\\User {#3179 id: 1, name: \u0026#34;kenji\u0026#34;, email: \u0026#34;kenji@example.com\u0026#34;, email_verified_at: \u0026#34;2019-06-07 23:10:30\u0026#34;, created_at: \u0026#34;2019-06-07 23:04:32\u0026#34;, updated_at: \u0026#34;2019-06-07 23:04:32\u0026#34;, } と確認時の日時が入ります。\nEメール確認した会員のみだけが閲覧できる 例えば、ECサイトなら、Eメールを確認した会員のみが注文ができる、とかに設定したいなら、カート画面に保護をかけることも可能です。\nRoute::get(\u0026#39;cart\u0026#39;, function () { // Eメール確認した会員のみアクセス可能 })-\u0026gt;middleware(\u0026#39;verified\u0026#39;); まだ確認してない会員には、route(\u0026lsquo;verification.notice\u0026rsquo;)にレダイレクトして、Eメール確認メールの再送信を促すことも可能です。\n","date":"2019-06-17T23:55:42+09:00","permalink":"https://www.larajapan.com/2019/06/17/%E4%BC%9A%E5%93%A1%E7%99%BB%E9%8C%B2%E6%99%82%E3%81%ABe%E3%83%A1%E3%83%BC%E3%83%AB%E7%A2%BA%E8%AA%8D%E3%83%AA%E3%83%B3%E3%82%AF%E3%82%92%E9%80%81%E4%BF%A1/","title":"会員登録時にEメール確認リンクを送信"},{"content":"Laravelは一昨年あたりから細目に更新を出すようになりました。と言っても、年に2回ですが、Laravelで書いた大きなプログラムを管理する私としてはそれでも「きつい」。それで長期サポートの5.5にあぐらをかいているわけだが、しかし。\n以下は5.7に掲載されていたサポートのスケジュール表。 これによると、5.5のバグ修正は今年の8月で終了ではないですか、ということで、急いで更新のキャッチアップとなりました。更新に関して私が気づいた点をいくつか書きます。5.6の更新はどうしたんだい？と思う方、すいません、変更はマイナーなので記事とするほど書くことは少なく。しかし、実際には5.5 ⇒ 5.6、5.6 ⇒ 5.7と順番に進める必要はあります。\nフェイカーの言語設定 「hasManyのフェイクデータの作成」の記事で、フェイカーを使用して日本語のフェイクデータの作成に、ちらと触れましたが、この設定は、今までのデフォルトのconfig/app.phpのファイルには含まれていませんでした。このバージョンから、以下のように含まれています。\nconfig/app.php ... /* |-------------------------------------------------------------------------- | Faker Locale |-------------------------------------------------------------------------- | | This locale will be used by the Faker PHP library when generating fake | data for your database seeds. For example, this will be used to get | localized telephone numbers, street address information and more. | */ \u0026#39;faker_locale\u0026#39; =\u0026gt; \u0026#39;en_US\u0026#39;, ... もちろん、\n\u0026#39;faker_locale\u0026#39; =\u0026gt; \u0026#39;ja_JP\u0026#39;, と変えましょう。\n早速tinkerでテストしてみましょう。\nPsy Shell v0.9.9 (PHP 7.2.16 — cli) by Justin Hileman \u0026gt;\u0026gt;\u0026gt; factory(App\\User::class)-\u0026gt;make() =\u0026gt; App\\User {#3191 name: \u0026#34;井高 拓真\u0026#34;, email: \u0026#34;wkimura@example.net\u0026#34;, email_verified_at: \u0026#34;2019-06-05 19:46:16\u0026#34;, } Bladeのorが??に変わる layout.blade.phpのように使用するプログラムが同じBladeを共有するときに、例えば、プログラムにより$titleという変数が未定義になるかもしれないとすると、Bladeではこう設定してエラーとなるのを避けていました。\n... {{ $title or \u0026#39;\u0026#39; }} ... しかし、or は、phpの??を使えば良いということで廃止になりました。??は、php7で登場した新しい比較演算子で、いちいちisset()を使用する必要がなくなりました。\n... {{ $tile ?? \u0026#39;\u0026#39; }} ... 私のプロジェクトでもいくつかこの変更の対象となるBladeのファイルがありました。ほとんどが、上記のように未定義は空として表現するので、以下のようにsedコマンドを使用して一括変換としました。\ngrep -rl \u0026#34; or \u0026#39;\u0026#39;\u0026#34; | grep php | xargs sed -i \u0026#34;s/\\sor\\s\u0026#39;\u0026#39;/ ?? \u0026#39;\u0026#39;/g\u0026#34; resources/assetsのディレクトリが廃止となる Laravelのディレクトリ構造は、以前はバージョンが変わるごとに変わっていました。最近は落ち着いてきていいなと思っているとこれです。しかし、今回の変更は、そうたいしたことではないです。\nLaravel 5.6以前は、resourcesのディレクトリの階層は、\nresources ├── assets │ ├── js │ │ ├── app.js │ │ ├── bootstrap.js │ │ └── components │ │ └── ExampleComponent.vue │ └── sass │ ├── app.scss │ └── _variables.scss ├── lang │ └── en │ ├── auth.php │ ├── pagination.php │ ├── passwords.php │ └── validation.php └── views └── welcome.blade.php が、5.7になると、\nresources ├── js │ ├── app.js │ ├── bootstrap.js │ └── components │ └── ExampleComponent.vue ├── lang │ └── en │ ├── auth.php │ ├── pagination.php │ ├── passwords.php │ └── validation.php ├── sass │ ├── app.scss │ └── _variables.scss └── views └── welcome.blade.php と、assetsのディレクトリの中身がすべてその上のディレクトリに移されました。また、それを反映して、webpack.mix.jsも変わっています。\nしかし、この変更はオプションであり、変えたくないなら以前のままでもＯＫです。\nメールログを分ける Laravel 5.5までは、すべてのログはstorage/logs/laravel.logに保存というワンパターンであったのが、5.6ではconfig/logging.phpにおいて、いろいろなカスタマイズの設定が可能となりました。ログを従来のファイルに保存ではなくslackにも転送が可能であるし、ファイルでも従来のように１つのファイルにすべてを保存でなく、ファイル名に日付を入れて違うファイルに保存も可能です。\nさらに、5.7では、config/mail.phpにおいて、\nconfig/mail.php ... /* |-------------------------------------------------------------------------- | Log Channel |-------------------------------------------------------------------------- | | If you are using the \u0026#34;log\u0026#34; driver, you may specify the logging channel | if you prefer to keep mail messages separate from other log entries | for simpler reading. Otherwise, the default channel will be used. | */ \u0026#39;log_channel\u0026#39; =\u0026gt; env(\u0026#39;MAIL_LOG_CHANNEL\u0026#39;), のように、メールのログチャンネルも指定可能です。\n例えば、\n.envにおいて、\n... MAIL_DRIVER=log ... と設定するとプログラムから送信されるメールの内容はすべて、storage/logs/laravel.logに保存されます。開発時において便利な機能です。しかし、他のエラーログとごっちゃになってわかりづらかったです。\nしかし、5.7において、\nconfig/logging.php ... \u0026#39;mail\u0026#39; =\u0026gt; [ \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;single\u0026#39;, \u0026#39;path\u0026#39; =\u0026gt; storage_path(\u0026#39;logs/mail.log\u0026#39;), \u0026#39;level\u0026#39; =\u0026gt; \u0026#39;debug\u0026#39;, ], ... と新しくmailというログチャンネルを作成し、\n.envにおいて、\n... MAIL_DRIVER=log MAIL_LOG_CHANNEL=mail ... と指定すれば、laravel.logではなくstorage/logs/mail.logのファイルにメールがすべて保存されるようになります。\n","date":"2019-06-07T02:36:01+09:00","permalink":"https://www.larajapan.com/2019/06/07/laravel-5-7%E3%81%AB%E6%9B%B4%E6%96%B0/","title":"Laravel 5.7に更新"},{"content":"最近、統計のプログラムが必要になり、Rというプログラミング言語を習得することになりました。ウェブのアプリの開発では、php, javascript, htmlなどがメインなゆえに、それを外れたプログラミング言語を使用する機会は少なく、興味も手伝って取り組んでみました。\nRと言うと、もちろん、R Studioという無料のビジュアルな開発ツールがあります。それはそれでビジュアルでグラフの作成も可能であり、私もそちらでいくらか鍛てもらったのですが、実はコマンドラインでもRのプログラムは使用可能なのです。ということで、コマンドライン好きの私としては、あえてここではコマンドラインの方でRを紹介させていただきます。\nインストール まず、私のFedora 29での仮想環境では、以下のようにRのインストールが可能です。いやいや凄いパッケージの数。\n$ yum install R ... Transaction Summary\tInstall 293 Packages\tTotal download size: 299 M\tInstalled size: 670 M 実行は、単に、Rとタイプするだけ、\n$ R R version 3.5.3 (2019-03-11) -- \u0026#34;Great Truth\u0026#34; Copyright (C) 2019 The R Foundation for Statistical Computing Platform: x86_64-redhat-linux-gnu (64-bit) R is free software and comes with ABSOLUTELY NO WARRANTY. You are welcome to redistribute it under certain conditions. Type \u0026#39;license()\u0026#39; or \u0026#39;licence()\u0026#39; for distribution details. Natural language support but running in an English locale R is a collaborative project with many contributors. Type \u0026#39;contributors()\u0026#39; for more information and \u0026#39;citation()\u0026#39; on how to cite R or R packages in publications. Type \u0026#39;demo()\u0026#39; for some demos, \u0026#39;help()\u0026#39; for on-line help, or \u0026#39;help.start()\u0026#39; for an HTML browser interface to help. Type \u0026#39;q()\u0026#39; to quit R. \u0026gt; 最後の \u0026gt; がRの対話型のプロンプトです。Laravelのプログラマーにとっては、tinkerのようなものです。行の最後にセミコロンが要らないことに注意を！\nデータとその統計 Rにはすでに、サンプルのデータがたくさんあります。data()はそのリストを表示してくれます。\n$ R \u0026gt; data() AirPassengers Monthly Airline Passenger Numbers 1949-1960 BJsales Sales Data with Leading Indicator BJsales.lead (BJsales) Sales Data with Leading Indicator BOD Biochemical Oxygen Demand CO2 Carbon Dioxide Uptake in Grass Plants ChickWeight Weight versus age of chicks on different diets DNase Elisa assay of DNase ... women Average Heights and Weights for American Women とたくさん出てきます。例えば、最後のwomenを見てみましょう。\n\u0026gt; women height weight 1 58 115 2 59 117 3 60 120 4 61 123 5 62 126 6 63 129 7 64 132 8 65 135 9 66 139 10 67 142 11 68 146 12 69 150 13 70 154 14 71 159 15 72 164 これはアメリカの女性の身長と体重のデータです。単位はインチとパウンドですね。\nさあ、ここでRが凄いのは、このデータを主要な統計値を一発で出してくれます。\n\u0026gt; summary(women) height weight Min. :58.0 Min. :115.0 1st Qu.:61.5 1st Qu.:124.5 Median :65.0 Median :135.0 Mean :65.0 Mean :136.7 3rd Qu.:68.5 3rd Qu.:148.0 Max. :72.0 Max. :164.0 Min.最小値 1st Qu.25パーセンタイル Median中央値（50パーセンタイル) Mean平均値 3rd Qu.75パーセンタイル Max.最大値 これらの値のおかげで、このサンプルのデータのだいたいのバラツキみたいのが想像できます。ここでは、Heightにおいて中央値と平均値が同じなので、データの分布は対称ですね。\n80パーセンタイルは、どうなのでしょう？\n\u0026gt; quantile(women$height, 0.8) 80% 69.2 と簡単に計算できます。引数として指定したwomen$heightは、以下のようにデータから、heightだけのデータを取ってきます。これはphpなどの言語では見慣れない$の使い方ですね。\n\u0026gt; women$height [1] 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 グラフ コンソールなので、グラフの作成は不可能と思いきや、txtplotというライブラリが使用できます。 シェルではなく、Rのコマンドラインから、install.packages()でライブラリをインストールします。この際は、rootユーザー権限で実行してください。ダウンロード元の選択が尋ねれられますが、1: 0-Cloud [https]を選択で十分です。\n\u0026gt; install.packages(\u0026#39;txtplot\u0026#39;)\tInstalling package into ‘/usr/lib64/R/library’\t(as ‘lib’ is unspecified)\t--- Please select a CRAN mirror for use in this session ---\tSecure CRAN mirrors\t1: 0-Cloud [https] 2: Algeria [https]\t3: Australia (Canberra) [https] 4: Australia (Melbourne 1) [https]\t5: Australia (Melbourne 2) [https] 6: Australia (Perth) [https]\t7: Austria [https] 8: Belgium (Ghent) [https]\t9: Brazil (PR) [https] 10: Brazil (RJ) [https]\t11: Brazil (SP 1) [https] 12: Brazil (SP 2) [https]\t13: Bulgaria [https] 14: Chile 1 [https]\t.. Selection: 1 trying URL \u0026#39;https://cloud.r-project.org/src/contrib/txtplot_1.0-3.tar.gz\u0026#39; Content type \u0026#39;application/x-gzip\u0026#39; length 6152 bytes ================================================== downloaded 6152 bytes * installing *source* package ‘txtplot’ ... ... インストールしたら、以下のようにRの中で実行すると、\n\u0026gt; library(\u0026#39;txtplot\u0026#39;) \u0026gt; txtplot(women$weight, women$height) +-------+----------+----------+----------+----------+-------+ | * | 70 + * * + | * | | * | | * | | * | 65 + * + | * | | * | | * | | * | 60 + * * + | * | +-------+----------+----------+----------+----------+-------+ 120 130 140 150 160 とAscii文字でのグラフが表示されます！\n最後に、Rのプロンプトから抜けるには、quitでも、exitでもコントロールCでもありません。q()なのです。\n\u0026gt; q() Save workspace image? [y/n/c]: y ","date":"2019-05-28T00:43:55+09:00","permalink":"https://www.larajapan.com/2019/05/28/r%E3%82%92%E3%82%B3%E3%83%9E%E3%83%B3%E3%83%89%E3%83%A9%E3%82%A4%E3%83%B3%E3%81%A7%E4%BD%BF%E3%81%A3%E3%81%A6%E3%81%BF%E3%82%88%E3%81%86%EF%BC%81/","title":"Rをコマンドラインで使ってみよう！"},{"content":"私のあるプロジェクトにおいて、Laravelからエクセルを生成するプログラムがあります。DBからの値を整形してエクセルに流し込み、生成されたエクセルをウェブからダウンロードできるプログラムです。記録として残すため、あるいはそれを他のプログラムにインポートするために、エクセルを生成する必要があるわけなのですが、ちょっとしたややこしい問題がありました。今回はその解決方法を共有です。\nエクセルの作成 エクセルを作成するプログラムは、有名なphpspreadsheetのライブラリを使用して作成します。 まず、ライブラリ使用のために、以下を実行してプロジェクトに取り込みます。\n$ composer require phpoffice/phpspreadsheet 次にエクセル作成のコマンドプログラムの作成です。\n$ php artisan make:command ExcelCommand 作成されたコマンドのファイルを以下のように編集します。phpspreadsheetからの例から取ってきました。実行するとファイルは、storage/app/Hello world.xlsxとして作成されます。\nnamespace App\\Console\\Commands; use Illuminate\\Console\\Command; use PhpOffice\\PhpSpreadsheet\\Spreadsheet; use PhpOffice\\PhpSpreadsheet\\Writer\\Xlsx; class ExcelCommand extends Command { /** * The name and signature of the console command. * * @var string */ protected $signature = \u0026#39;command:excel\u0026#39;; /** * The console command description. * * @var string */ protected $description = \u0026#39;Generate an Excel file\u0026#39;; /** * Create a new command instance. * * @return void */ public function __construct() { parent::__construct(); } /** * Execute the console command. * * @return mixed */ public function handle() { $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet-\u0026gt;getActiveSheet(); $sheet-\u0026gt;setCellValue(\u0026#39;A1\u0026#39;, \u0026#39;Hello World !\u0026#39;); $writer = new Xlsx($spreadsheet); $writer-\u0026gt;save(storage_path(\u0026#39;app/Hello world.xlsx\u0026#39;)); } } セル値に長い番号があるときの表示 さて、上のプログラムを編集して、長い番号を表示してみます。\n... public function handle() { $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet-\u0026gt;getActiveSheet(); $sheet-\u0026gt;getDefaultColumnDimension()-\u0026gt;setWidth(20); //列の幅を広げる $sheet-\u0026gt;setCellValue(\u0026#39;A1\u0026#39;, \u0026#39;12345678901\u0026#39;); $sheet-\u0026gt;setCellValue(\u0026#39;A2\u0026#39;, \u0026#39;123456789012\u0026#39;); // 12桁の数 $sheet-\u0026gt;setCellValue(\u0026#39;A3\u0026#39;, \u0026#39;123456789012345\u0026#39;);　// 15桁の数 $writer = new Xlsx($spreadsheet); $writer-\u0026gt;save(storage_path(\u0026#39;app/numbers.xlsx\u0026#39;)); } ... 生成されたファイルをエクセルでオープンすると、以下のように表示されます。\n見ての通り、番号が12桁以上なら、指数として表示されます。 さらに、15桁以上となると、セル値そのものが変わって15桁からの数字が0に変わってしまいます。\nこの解決方法として一番簡単なのは、数字の前に文字、例えば半角スペースを入れることです。\n... $sheet-\u0026gt;setCellValue(\u0026#39;A1\u0026#39;, \u0026#39;12345678901\u0026#39;); $sheet-\u0026gt;setCellValue(\u0026#39;A2\u0026#39;, \u0026#39; 123456789012\u0026#39;); // スペース＋12桁の数 $sheet-\u0026gt;setCellValue(\u0026#39;A3\u0026#39;, \u0026#39; 123456789012345\u0026#39;);　// スペース＋15桁の数 ... 数字すべて表示されるようになりましたね。しかし、数字の前に半角スペースがあるのが気に入らない、あるいは他のプログラムにインポートするときに、いちいちスペースをトリムする必要があり問題となるかもしれません。\n最終の解決方法 数字を指数として表示せずに、また値に半角を入れずにオリジナルの数値をキープ方法というのは、数字とでなくテキストとして値を扱うことですが、以下のように違う関数、setCEllValueExplicity()の使用となります。\n... $sheet-\u0026gt;setCellValueExplicit(\u0026#39;A1\u0026#39;, \u0026#39;123456789012345\u0026#39;, \\PhpOffice\\PhpSpreadsheet\\Cell\\DataType::TYPE_STRING ); ... 結果は、以下のような表示となり、値もオリジナルと同じとなりました。\n今回役だった情報は以下のphpspreadsheetのgithubから来ました：\nhttps://github.com/PHPOffice/PHPExcel/issues/1148 https://github.com/PHPOffice/PhpSpreadsheet/issues/357\n","date":"2019-05-17T22:46:36+09:00","permalink":"https://www.larajapan.com/2019/05/17/%E3%82%A8%E3%82%AF%E3%82%BB%E3%83%AB%E3%81%A7%E9%95%B7%E3%81%84%E6%95%B0%E5%AD%97%E3%81%AE%E8%A1%A8%E7%A4%BA/","title":"エクセルで長い数字の表示"},{"content":"まだまだLaravelのバージョン5.5から進まない私ですが、5.6以降のバージョン（最新は5.8）を使い始めて、いいと思った改善です。\nLaravel5.5では、ウェブアプリなら実行でエラーがあると、以下のようにエラーの情報を見せてくれてすぐにどこが悪いかわかります。\nしかし、コマンドライン実行のartisanのコマンドでは、以下のようにエラーがあるのはわかるのだけれど、laravel.logを見なければすぐにどこが問題かわからない。\nこれが、5.6以降のバージョンとなると、以下のように表示してくれて、わかりやすくなりました。\n","date":"2019-05-13T04:11:12+09:00","permalink":"https://www.larajapan.com/2019/05/13/laravel-5-6%E3%81%A7%E3%82%8F%E3%81%8B%E3%82%8A%E3%82%84%E3%81%99%E3%81%8F%E3%81%AA%E3%81%A3%E3%81%9F%E3%82%B3%E3%83%9E%E3%83%B3%E3%83%89%E3%82%A8%E3%83%A9%E3%83%BC%E8%A1%A8%E7%A4%BA/","title":"Laravel 5.6でわかりやすくなったコマンドエラー表示"},{"content":"前回において、理想のバリデーションの関数、rules()を作成できたところで、今回は、そのバリデーションチェックを通過した入力データをDBに保存するところを見てみます。\nなんのことはなく、$request-\u0026gt;validate()がそのデータを返してくれるので、以下のように、返ってきた連想配列値をそのまま、create()あるいはupdate()に引数として渡せばいいのです。簡単ですね！\nnamespace App\\Http\\Controllers; use Illuminate\\Http\\Request; use Illuminate\\Validation\\Rule; use App\\User; class UserController extends Controller { public function rule(User $user = null) { // 共有するルール　kana, mailcode, phone_with_dashはカスタムバリデーター $rules = [ \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;name_kana\u0026#39; =\u0026gt; \u0026#39;required|kana\u0026#39;, \u0026#39;mailcode\u0026#39; =\u0026gt; \u0026#39;required|mailcode\u0026#39;, \u0026#39;prefecture\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;city\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;address1\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;phone\u0026#39; =\u0026gt; \u0026#39;required|phone_with_dash\u0026#39;, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;sometimes|required|min:8|max:20|confirmed\u0026#39;, \u0026#39;email\u0026#39; =\u0026gt; [ \u0026#39;required\u0026#39;, \u0026#39;email\u0026#39;, Rule::unique(\u0026#39;users\u0026#39;)-\u0026gt;ignore(optional($user)-\u0026gt;id), //重複がないかチェック ] ]; return [ // rules $rules, // messages [ \u0026#39;password.min\u0026#39; =\u0026gt; \u0026#39;8から20文字長でお願いします\u0026#39;, \u0026#39;password.max\u0026#39; =\u0026gt; \u0026#39;8から20文字長でお願いします\u0026#39; ] // attributes ]; } ... public function store(Request $request) { $validated = $request-\u0026gt;validate(...$this-\u0026gt;rules()); User::create($validated); } ... public function update(Request $request, User $user) { $validated = $request-\u0026gt;validate(...$this-\u0026gt;rules($user)); $user-\u0026gt;update($validated); } ... } しかし、ここで１つ問題です。画面には入力項目、住所２として、address2があります。項目は必須ではないので、上の例ではこれが保存されません。\nこれを解決するには、以下のようにルールにsometimesを使用して項目を作成すればいいですね。\n... public function rule(User $user = null) { // 共有するルール　kana, mailcode, phone_with_dashはカスタムバリデーター $rules = [ \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;name_kana\u0026#39; =\u0026gt; \u0026#39;required|kana\u0026#39;, \u0026#39;mailcode\u0026#39; =\u0026gt; \u0026#39;required|mailcode\u0026#39;, \u0026#39;prefecture\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;city\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;address1\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;address2\u0026#39; =\u0026gt; \u0026#39;sometimes|required\u0026#39;, \u0026#39;phone\u0026#39; =\u0026gt; \u0026#39;required|phone_with_dash\u0026#39;, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;sometimes|required|min:8|max:20|confirmed\u0026#39;, \u0026#39;email\u0026#39; =\u0026gt; [ \u0026#39;required\u0026#39;, \u0026#39;email\u0026#39;, Rule::unique(\u0026#39;users\u0026#39;)-\u0026gt;ignore(optional($user)-\u0026gt;id), //重複がないかチェック ] ]; return [ // rules $rules, // messages [ \u0026#39;password.min\u0026#39; =\u0026gt; \u0026#39;8から20文字長でお願いします\u0026#39;, \u0026#39;password.max\u0026#39; =\u0026gt; \u0026#39;8から20文字長でお願いします\u0026#39; ] // attributes ]; } ... ","date":"2019-04-28T22:12:23+09:00","permalink":"https://www.larajapan.com/2019/04/28/%E3%82%B3%E3%83%B3%E3%83%88%E3%83%AD%E3%83%BC%E3%83%A9%E3%83%BC%E4%B8%AD%E3%81%A7%E3%81%AE%E3%83%AB%E3%83%BC%E3%83%AB%E3%81%AE%E5%85%B1%E6%9C%89%EF%BC%883-%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92db/","title":"コントローラー中でのルールの共有（3) データをDBに保存"},{"content":"前回で披露したコントローラー中でのルールの共有のメソッドrules()では、新規レコード作成と既存レコードの編集における入力項目の違いを、$actionの引数をswitchで分岐して対応しました。今回は、sometimesを使用してそれらの必要性をなくします。\nsometimesは、入力項目があるときのみに、それ以降のルールを適応するルールです。tinkerを使ってテストしてみましょう。\n通常は、\n\u0026gt;\u0026gt;\u0026gt; $input = []; =\u0026gt; [] \u0026gt;\u0026gt;\u0026gt; $rules = [\u0026#39;email\u0026#39; =\u0026gt; \u0026#39;required|email\u0026#39;]; =\u0026gt; [ \u0026#34;email\u0026#34; =\u0026gt; \u0026#34;required|email\u0026#34;, ] \u0026gt;\u0026gt;\u0026gt; validator($input, $rules)-\u0026gt;errors()-\u0026gt;all(); =\u0026gt; [ \u0026#34;必ず指定してください\u0026#34;, ] とエラーとなるところ、sometimesを使用すると、\n\u0026gt;\u0026gt;\u0026gt; $input = []; =\u0026gt; [] \u0026gt;\u0026gt;\u0026gt; $rules = [\u0026#39;email\u0026#39; =\u0026gt; \u0026#39;sometimes|required|email\u0026#39;]; =\u0026gt; [ \u0026#34;email\u0026#34; =\u0026gt; \u0026#34;sometimes|required|email\u0026#34;, ] \u0026gt;\u0026gt;\u0026gt; validator($input, $rules)-\u0026gt;errors()-\u0026gt;all(); =\u0026gt; [] \u0026gt;\u0026gt;\u0026gt; とemailの項目が入力にないのにエラーとはなりません。\nこれを利用して前回のrules()の定義を書き変えてみます。\nnamespace App\\Http\\Controllers; use Illuminate\\Http\\Request; use Illuminate\\Validation\\Rule; use App\\User; class UserController extends Controller { public function rule(User $user = null) { // 共有するルール　kana, mailcode, phone_with_dashはカスタムバリデーター $rules = [ \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;name_kana\u0026#39; =\u0026gt; \u0026#39;required|kana\u0026#39;, \u0026#39;mailcode\u0026#39; =\u0026gt; \u0026#39;required|mailcode\u0026#39;, \u0026#39;prefecture\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;city\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;address1\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;phone\u0026#39; =\u0026gt; \u0026#39;required|phone_with_dash\u0026#39;, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;sometimes|required|min:8|max:20|confirmed\u0026#39;, \u0026#39;email\u0026#39; =\u0026gt; [ \u0026#39;required\u0026#39;, \u0026#39;email\u0026#39;, Rule::unique(\u0026#39;users\u0026#39;)-\u0026gt;ignore(optional($user)-\u0026gt;id), //重複がないかチェック ] ]; return [ // rules $rules, // messages [ \u0026#39;password.min\u0026#39; =\u0026gt; \u0026#39;8から20文字長でお願いします\u0026#39;, \u0026#39;password.max\u0026#39; =\u0026gt; \u0026#39;8から20文字長でお願いします\u0026#39; ] // attributes ]; } ... public function store(Request $request) { $request-\u0026gt;validate(...$this-\u0026gt;rules()); ... } ... public function update(Request $request, User $user) { $request-\u0026gt;validate(...$this-\u0026gt;rules($user)); ... } ... 前回と変わったのは、関数の引数の$actionとその値によるswitchによる分岐がなくなっています。新規作成時しか必要でない、passwordの項目に、sometimesを使用して、その項目がないときはルールを適用しないようにしています。また、emailの重複のチェックにおいては、以下のようにヘルパーのoptional()を使用して、新規には存在しない$userの値でundefinedのエラーが出ないようにしています。\nRule::unique(\u0026lsquo;users\u0026rsquo;)-\u0026gt;ignore(optional($user)-\u0026gt;id)\n最後に、sometimes|requiredは以下のようにfilledに置き換えることも可能です。filledに関してはこちらも参考にしてください。\n\u0026lsquo;password\u0026rsquo; =\u0026gt; \u0026lsquo;filled|min:8|max:20|confirmed\u0026rsquo;\n","date":"2019-04-21T10:01:29+09:00","permalink":"https://www.larajapan.com/2019/04/21/%E3%82%B3%E3%83%B3%E3%83%88%E3%83%AD%E3%83%BC%E3%83%A9%E3%83%BC%E4%B8%AD%E3%81%A7%E3%81%AE%E3%83%AB%E3%83%BC%E3%83%AB%E3%81%AE%E5%85%B1%E6%9C%89%EF%BC%88%EF%BC%92%EF%BC%89sometimes/","title":"コントローラー中でのルールの共有（２）sometimes"},{"content":"１つのコントローラの中で、バリデーションに使用するルールをメソッド間で共有したい、とは誰しも思うこと。私もLaravelを習得して以来、管理性が高くしかもすっきりとした方法を求め続けていました。前回で紹介したカスタムルールの登場で、これは使えるんじゃない、という方法を見つけましたので、ここにて披露です。\n欲しいもの いくつか条件あります。 １．コントローラー内でルールを指定したい これはどちらかというと実用性より好みの問題かもしれませんが、Form Requestのように違うファイルとして定義するのではなく、コントローラー内で定義することにより、あちこちのファイルを見に行くのではなくコントローラーのファイル内で使用されるルールを見たいためです。たいていのコントローラーは、そうたいした数のルールがあるわけでもないので、Form Requestを使用する必要もないです。\n２．コントローラーのメソッド内でルールを指定したくない これは管理性大いにあります。以下のようにそれぞれのメソッド内で重複してルールを定義するのは間違いが起こるもとです。もちろん、これが今回のルールの共有の大きな要因でもあります。\nnamespace App\\Http\\Controllers; use Illuminate\\Http\\Request; use Illuminate\\Validation\\Rule; use App\\User; class UserController extends Controller { ... public function store(Request $request) { $request-\u0026gt;validate([ \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;name_kana\u0026#39; =\u0026gt; \u0026#39;required|kana\u0026#39;, \u0026#39;mailcode\u0026#39; =\u0026gt; \u0026#39;required|mailcode\u0026#39;, \u0026#39;prefecture\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;city\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;address1\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;phone\u0026#39; =\u0026gt; \u0026#39;required|phone_with_dash\u0026#39;, \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;required|email|unique:users\u0026#39;, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;required|min:8|max:20|confirmed\u0026#39;, ]); ... } ... public function update(Request $request, User $user) { $request-\u0026gt;validate([ \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;name_kana\u0026#39; =\u0026gt; \u0026#39;required|kana\u0026#39;, \u0026#39;mailcode\u0026#39; =\u0026gt; \u0026#39;required|mailcode\u0026#39;, \u0026#39;prefecture\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;city\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;address1\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;phone\u0026#39; =\u0026gt; \u0026#39;required|phone_with_dash\u0026#39;, \u0026#39;email\u0026#39; =\u0026gt; [ \u0026#39;required\u0026#39;, \u0026#39;email\u0026#39;, Rule::unique(\u0026#39;users\u0026#39;)-\u0026gt;ignore($user-\u0026gt;id), //現在のレコード以外のレコードで重複がないかチェック ], ]); ... } ... } 解決方法　その１ 上の条件にかなうものとしては、コントローラー内の一箇所にルールーを指定することになります。まず、考えたのは、インスタンス変数を使用すること、\nnamespace App\\Http\\Controllers; use Illuminate\\Http\\Request; use Illuminate\\Validation\\Rule; use App\\User; class UserController extends Controller { protected $rules = [ \u0026#39;shared\u0026#39; =\u0026gt; [ // kana, mailcode, phone_with_dashはカスタムバリデーター \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;name_kana\u0026#39; =\u0026gt; \u0026#39;required|kana\u0026#39;, \u0026#39;mailcode\u0026#39; =\u0026gt; \u0026#39;required|mailcode\u0026#39;, \u0026#39;prefecture\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;city\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;address1\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;phone\u0026#39; =\u0026gt; \u0026#39;required|phone_with_dash\u0026#39;, ], \u0026#39;store\u0026#39; =\u0026gt; [ \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;required|email|unique:users\u0026#39;, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;required|min:8|max:20|confirmed\u0026#39;, ], \u0026#39;update\u0026#39; =\u0026gt; [ \u0026#39;email\u0026#39; =\u0026gt; [ \u0026#39;required\u0026#39;, \u0026#39;email\u0026#39;, Rule::unique(\u0026#39;users\u0026#39;)-\u0026gt;ignore($user-\u0026gt;id), //現在のレコード以外のレコードで重複がないかチェック ], ] ]; ... public function store(Request $request) { $request-\u0026gt;validate($this-\u0026gt;rules[\u0026#39;shared\u0026#39;] + $this-\u0026gt;rules[\u0026#39;store\u0026#39;]); ... } ... public function update(Request $request, User $user) { $request-\u0026gt;validate($this-\u0026gt;rules[\u0026#39;shared\u0026#39;] + $this-\u0026gt;rules[\u0026#39;update\u0026#39;]); ... } ... } いい感じ？\nしかし問題あります。\nRule::unique(\u0026lsquo;users\u0026rsquo;)-\u0026gt;ignore($user-\u0026gt;id)\nこれ、$rulesの変数の宣言時に、このような初期化はできないし、$userを渡すこともできません。update()のメソッド内でその初期化をする必要あります。\n... public function update(Request $request, User $user) { $thus-\u0026gt;rules[\u0026#39;update\u0026#39;][\u0026#39;email\u0026#39;][] = Rule::unique(\u0026#39;users\u0026#39;)-\u0026gt;ignore($user-\u0026gt;id); $request-\u0026gt;validate($this-\u0026gt;rules[\u0026#39;shared\u0026#39;] + $this-\u0026gt;rules[\u0026#39;update\u0026#39;]); ... } ... せっかくコントローラーの先頭で一箇所で定義しようと思ったのに、これは良くないですね。\n欲しかった解決方法 初期化が問題なら、クラスのインスタンス変数の代わりに、Form Requestにあるように、rules()という名前でコントローラーの関数にしてはどうでしょう、ということで以下になりました。\nnamespace App\\Http\\Controllers; use Illuminate\\Http\\Request; use Illuminate\\Validation\\Rule; use App\\User; class UserController extends Controller { public function rule($action, User $user = null) { // 共有するルール　kana, mailcode, phone_with_dashはカスタムバリデーター $rules = [ \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;name_kana\u0026#39; =\u0026gt; \u0026#39;required|kana\u0026#39;, \u0026#39;mailcode\u0026#39; =\u0026gt; \u0026#39;required|mailcode\u0026#39;, \u0026#39;prefecture\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;city\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;address1\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;phone\u0026#39; =\u0026gt; \u0026#39;required|phone_with_dash\u0026#39;, ]; // 共有しないルール switch ($action) { case \u0026#39;store\u0026#39;:　// レコード作成のためのルールを追加 $rules[\u0026#39;password\u0026#39;] = \u0026#39;required|min:8|max:20|confirmed\u0026#39;; $rules[\u0026#39;email\u0026#39;] = \u0026#39;required|email|unique:users\u0026#39;; break; case \u0026#39;update\u0026#39;: // レコード編集のためのルールを追加 $rules[\u0026#39;email\u0026#39;] = [ \u0026#39;required\u0026#39;, \u0026#39;email\u0026#39;, Rule::unique(\u0026#39;users\u0026#39;)-\u0026gt;ignore($user-\u0026gt;id), //現在のレコード以外のレコードで重複がないかチェック ]; break; } return [ // rules $rules, // messages [ \u0026#39;password.min\u0026#39; =\u0026gt; \u0026#39;8から20文字長でお願いします\u0026#39;, \u0026#39;password.max\u0026#39; =\u0026gt; \u0026#39;8から20文字長でお願いします\u0026#39; ] // attributes ]; } ... public function store(Request $request) { $request-\u0026gt;validate(...$this-\u0026gt;rules(\u0026#39;store\u0026#39;)); ... } ... public function update(Request $request, User $user) { $request-\u0026gt;validate(...$this-\u0026gt;rules(\u0026#39;update\u0026#39;, $user)); ... } ... } rules()の関数には、２つの引数があります。最初の$actionはメソッド名で、それにより返す連想配列$rulesを特定し、次の$userは編集で必要とされる値（ここでは重複のチェックのためにレコードid）を取得のためが目的です。\n関数の引数は状況に応じて自由に変えてください。以下は、前回に紹介したカスタムルールの例ですが、同じ入力画面からの違う項目のdate_startの値を取得しています。\npublic function rules($action, Request $request) { // store $rules = [ \u0026#39;date_start\u0026#39; =\u0026gt; \u0026#39;date\u0026#39;, \u0026#39;date_end\u0026#39; =\u0026gt; [ \u0026#39;date\u0026#39;, \u0026#39;after_or_equal:date_start\u0026#39;, new RestrictPeriodRule($request-\u0026gt;date_start), 6) ], ]; return [$rules]; } 最後に、rules()が返す配列は、$request-\u0026gt;validate()の引数にマッチする、rules、messages、attributesとなりますが、必要に応じてrulesだけでもＯＫです。このように不特定数の引数があるときは、コールするときに、 $request-\u0026gt;validate(\u0026hellip;$this-\u0026gt;rules(\u0026lsquo;store\u0026rsquo;))のようにphp5.6より登場した3つのドットを使って渡します。\n","date":"2019-04-15T23:45:08+09:00","permalink":"https://www.larajapan.com/2019/04/15/%E3%82%B3%E3%83%B3%E3%83%88%E3%83%AD%E3%83%BC%E3%83%A9%E3%83%BC%E4%B8%AD%E3%81%A7%E3%81%AE%E3%83%AB%E3%83%BC%E3%83%AB%E3%81%AE%E5%85%B1%E6%9C%89/","title":"コントローラー中でのルールの共有"},{"content":"これまた、5.5で登場した機能で今まで知らなかった機能がありました。\nLaravelではウェブだけでなく、コマンドラインで実行できるプログラムの開発も対応しています。開発は非常に簡単で、以下を実行すると、\n$ php artisan make:command CommandProgram app/Console/Commands/CommandProgram.phpの雛形のファイルを作成してくれます。\nそのプログラムの実行には、app/Console/Kernel.phpを編集して、$commandsに以下の14行目のように追加して登録します。\napp/Console/Kernel.php namespace App\\Console; use Illuminate\\Console\\Scheduling\\Schedule; use Illuminate\\Foundation\\Console\\Kernel as ConsoleKernel; class Kernel extends ConsoleKernel { /** * The Artisan commands provided by your application. * * @var array */ protected $commands = [ Commands\\CommandProgram::class, //ここに新規のコマンドを入れる ]; /** * Define the application\u0026#39;s command schedule. * * @param \\Illuminate\\Console\\Scheduling\\Schedule $schedule * @return void */ protected function schedule(Schedule $schedule) { // $schedule-\u0026gt;command(\u0026#39;inspire\u0026#39;) // -\u0026gt;hourly(); } /** * Register the Closure based commands for the application. * * @return void */ protected function commands() { require base_path(\u0026#39;routes/console.php\u0026#39;); } } 登録されたら、artisanコマンドのリストに表示されます。\n$ php artisan Available commands: ... command command:name Command description ... というのが5.4までだったのですが、5.5からはもうこの手間はもうなくなりました。 app/Console/Commandsのディレクトリにファイルを作成すれば、Kernel.phpを編集しなくても自動的に登録してくれます。\nどうしてこうなるかというと、5.5のKernel.phpを見てみましょう。\nnamespace App\\Console; use Illuminate\\Console\\Scheduling\\Schedule; use Illuminate\\Foundation\\Console\\Kernel as ConsoleKernel; class Kernel extends ConsoleKernel { /** * The Artisan commands provided by your application. * * @var array */ protected $commands = [ // ]; /** * Define the application\u0026#39;s command schedule. * * @param \\Illuminate\\Console\\Scheduling\\Schedule $schedule * @return void */ protected function schedule(Schedule $schedule) { // $schedule-\u0026gt;command(\u0026#39;inspire\u0026#39;) // -\u0026gt;hourly(); } /** * Register the commands for the application. * * @return void */ protected function commands() { $this-\u0026gt;load(__DIR__.\u0026#39;/Commands\u0026#39;); // ここが5.5から追加された行 require base_path(\u0026#39;routes/console.php\u0026#39;); } } メソッドのcommands()に、新しい行（36行目）が追加されています。そして、その1行が自動登録を行ってくれるのです！\nコマンドラインで実行するプログラムのほとんどは、クロンジョブでスケジュールして実行されるプログラムですが、ときどき、アップした画像のサイズを変えるとか、DBのある項目の値をすべて更新変するとか、のバッチジョブが必要になることあります。\nそのようなときは、管理のために、app/Console/Tasksのディレクトリを作成して、クロンジョブのプログラムと分けます。そして、もちろんこれらの登録は、以下のようにして自動にすればよいです。今度は配列で引数を渡していることに注意を。\n... protected function commands() { $this-\u0026gt;load([__DIR__.\u0026#39;/Commands\u0026#39;, __DIR__.\u0026#39;/Tasks\u0026#39;]); // ここが5.5から追加された行 require base_path(\u0026#39;routes/console.php\u0026#39;); } } ","date":"2019-04-06T03:01:35+09:00","permalink":"https://www.larajapan.com/2019/04/06/%E7%9F%A5%E3%82%89%E3%81%AA%E3%81%8B%E3%81%A3%E3%81%9F%E3%82%B3%E3%83%9E%E3%83%B3%E3%83%89%E3%81%AE%E8%87%AA%E5%8B%95%E7%99%BB%E9%8C%B2/","title":"知らなかったコマンドの自動登録"},{"content":"5.5より前のバージョンのLaravelでプログラムをしていたひとたちに取っては、5.5で登場したルールオブジェクトは神の恵みと言ってもいいくらい。カスタムバリデーションを作成するために、Validator::extendはどこに宣言するの？とか、グローバルでどう作成したバリデーションを共有するの？とか、取り掛かる前に悩んでいたのがウソのよう。新登場のルールオブジェクトのおかげで、カスタムルールの作成が楽しくなりました。\nカスタムルールが必要となるときは、たくさんあります。なぜなら既存のバリデーションでは簡単にいかないルールが世の中にたくさんあるからです。\n例えば、登録したユーザーのリストが欲しいとして、開始日を指定して今日までに登録したユーザーのみとします。しかし、開始日は今日から2か月までしか遡れない、とします。以下のようなカレンダーで開始日をしてもらいますが、開始日が2か月以上前ならバリデーションでチェックしてエラーを表示としたいです。\nこのバリデーションには既存のバリデーションのafter_or_equalが使えます、しかし渡すパラメータには、「今日マイナス2か月」が必要です。もちろん前もって計算して渡すこともできます。 Psy Shell v0.9.9 (PHP 7.2.16 — cli) by Justin Hileman \u0026gt;\u0026gt;\u0026gt; $from = \\Carbon\\Carbon::today()-\u0026gt;subMonth(2)-\u0026gt;toDateString(); =\u0026gt; \u0026#34;2019-01-29\u0026#34; \u0026gt;\u0026gt;\u0026gt; $input = [\u0026#39;start_date\u0026#39; =\u0026gt; \u0026#39;2019-01-01\u0026#39;]; =\u0026gt; [ \u0026#34;start_date\u0026#34; =\u0026gt; \u0026#34;2019-01-01\u0026#34;, ] \u0026gt;\u0026gt;\u0026gt; validator($input, [\u0026#39;start_date\u0026#39; =\u0026gt; \u0026#39;required|date|after_or_equal:$from\u0026#39;])-\u0026gt;errors()-\u0026gt;all(); =\u0026gt; [] \u0026gt;\u0026gt;\u0026gt; validator($input, [\u0026#39;start_date\u0026#39; =\u0026gt; \u0026#34;required|date|after_or_equal:$from\u0026#34;])-\u0026gt;errors()-\u0026gt;all(); =\u0026gt; [ \u0026#34;start dateは2019-01-29以後の日付が必要です\u0026#34;, ] \u0026gt;\u0026gt;\u0026gt; しかし、もしかしたら2か月でなく3ヶ月となるかもしれないし、一般的なバリデーションがあってもいいですね。こんな感じで呼び出したいです。\n[\u0026#39;start_date\u0026#39; =\u0026gt; [\u0026#39;required\u0026#39;, \u0026#39;date\u0026#39;, new RestrictPeriodRule(2)]]; そこで、ルールオブジェクトの出番です。\nまず、コマンドでルールのファイルを作成します。\n$ php artisan make:rule RestrictPeriodRule 中身を以下のように編集します。\nappRulesRestrictPeriodRule.php \u0026lt;?php namespace App\\Rules; use Carbon\\Carbon; use Illuminate\\Contracts\\Validation\\Rule; class RestrictPeriodRule implements Rule { protected $months; protected $start; /** * Create a new rule instance. * * @return void */ public function __construct($months) { $this-\u0026gt;months = $months; } /** * Determine if the validation rule passes. * * @param string $attribute * @param mixed $value * @return bool */ public function passes($attribute, $value) { $this-\u0026gt;start = Carbon::today() -\u0026gt;subMonth($this-\u0026gt;months) -\u0026gt;toDateString(); return ($value \u0026gt;= $this-\u0026gt;start); } /** * Get the validation error message. * * @return string */ public function message() { return $this-\u0026gt;start.\u0026#39;以降を指定してください\u0026#39;; } } 出来たところでテストしてみましょう。\nPsy Shell v0.9.9 (PHP 7.2.16 — cli) by Justin Hileman \u0026gt;\u0026gt;\u0026gt; $input = [\u0026#39;start_date\u0026#39; =\u0026gt; \u0026#39;2019-01-01\u0026#39;]; =\u0026gt; [ \u0026#34;start_date\u0026#34; =\u0026gt; \u0026#34;2019-01-01\u0026#34;, ] \u0026gt;\u0026gt;\u0026gt; validator($input, [\u0026#39;start_date\u0026#39; =\u0026gt; [\u0026#39;required\u0026#39;, \u0026#39;date\u0026#39;, new App\\Rules\\RestrictPeriodRule(2)]])-\u0026gt;errors()-\u0026gt;all(); =\u0026gt; [ \u0026#34;2019-01-29以降を指定してください\u0026#34;, ] \u0026gt;\u0026gt;\u0026gt; ","date":"2019-04-01T09:44:21+09:00","permalink":"https://www.larajapan.com/2019/04/01/%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%A0%E3%83%AB%E3%83%BC%E3%83%AB%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88/","title":"カスタムルールオブジェクト"},{"content":"私が使用しているLaravelのバージョンは5.5、ついこの前にリリースされたと思いきや、なんと8/30/2017のことらしくもう1年半以上も経過。LTSということで2年間の長期サポートだからのんびりとしていたら、もうこの夏でサポートの期限切れです。キャッチアップ、ということで去年の2月にリリースされた5.6バージョン（しかし、もうサポートされていない）に追加されたバリデーションをチェックします。\nこれが、5.6のバリデーションの一覧です。ブルーでハイライトしているのが5.5にはなく5.6に追加されたバリデーションです。\n追加されたもので、すぐに使えそうなものは、\nGreater Than gt Greater Than Or Equal　gte Less Than　lt Less Than Or Equal　lte 早速使ってみましょう。validator()はLaravelのヘルパーですが、こちらも読んでください。\nPsy Shell v0.9.9 (PHP 7.1.14 — cli) by Justin Hileman \u0026gt;\u0026gt;\u0026gt; $input = [\u0026#39;quantity\u0026#39; =\u0026gt; 5]; =\u0026gt; [ \u0026#34;quantity\u0026#34; =\u0026gt; 5, ] \u0026gt;\u0026gt;\u0026gt; validator($input, [\u0026#39;quantity\u0026#39; =\u0026gt; \u0026#39;required|integer|gt:4\u0026#39;])-\u0026gt;errors()-\u0026gt;all(); =\u0026gt; [] \u0026gt;\u0026gt;\u0026gt; validator($input, [\u0026#39;quantity\u0026#39; =\u0026gt; \u0026#39;required|integer|gt:5\u0026#39;])-\u0026gt;errors()-\u0026gt;all(); =\u0026gt; [ \u0026#34;The quantity must be greater than 0.\u0026#34;, ] \u0026gt;\u0026gt;\u0026gt; validator($input, [\u0026#39;quantity\u0026#39; =\u0026gt; \u0026#39;required|integer|gte:5\u0026#39;])-\u0026gt;errors()-\u0026gt;all(); =\u0026gt; [] \u0026gt;\u0026gt;\u0026gt; validator($input, [\u0026#39;quantity\u0026#39; =\u0026gt; \u0026#39;required|integer|lt:6\u0026#39;])-\u0026gt;errors()-\u0026gt;all(); =\u0026gt; [] \u0026gt;\u0026gt;\u0026gt; validator($input, [\u0026#39;quantity\u0026#39; =\u0026gt; \u0026#39;required|integer|lte:5\u0026#39;])-\u0026gt;errors()-\u0026gt;all(); =\u0026gt; [] ちなみに、これらがない5.5では、以下のようにmin, max, not_inを組み合わせて同じようなバリデーションが可能です。\n\u0026gt;\u0026gt;\u0026gt; validator($input, [\u0026#39;quantity\u0026#39; =\u0026gt; \u0026#39;required|integer|min:5\u0026#39;])-\u0026gt;errors()-\u0026gt;all(); =\u0026gt; [] \u0026gt;\u0026gt;\u0026gt; validator($input, [\u0026#39;quantity\u0026#39; =\u0026gt; \u0026#39;required|integer|min:5|not_in:5\u0026#39;])-\u0026gt;errors()-\u0026gt;all(); =\u0026gt; [ \u0026#34;The selected quantity is invalid.\u0026#34;, ] しかし、このメッセージでわかりにくいので、\n\u0026gt;\u0026gt;\u0026gt; validator($input, [\u0026#39;quantity\u0026#39; =\u0026gt; \u0026#39;required|integer|min:5|not_in:5\u0026#39;], [\u0026#39;not_in\u0026#39; =\u0026gt; \u0026#39;The quantity must be greater than :input.\u0026#39;])-\u0026gt;errors()-\u0026gt;all(); =\u0026gt; [ \u0026#34;The quantity must be greater than 5.\u0026#34;, ] と、カスタムメッセージを与える必要あります。面倒ですね。\n","date":"2019-03-23T00:58:11+09:00","permalink":"https://www.larajapan.com/2019/03/23/laravel-5-6%E3%81%A7%E8%BF%BD%E5%8A%A0%E3%81%95%E3%82%8C%E3%81%9F%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3/","title":"Laravel 5.6で追加されたバリデーション"},{"content":"これも、前回と同様「実はそうでなかった」のトピックです。今度は、Eloquentのクエリーにおいてwhere()で条件を指定するとき。\n早速、tinkerを使って具体的な例を作成します。\nPsy Shell v0.9.3 (PHP 7.1.14 — cli) by Justin Hileman \u0026gt;\u0026gt;\u0026gt; factory(App\\User::class,2)-\u0026gt;create() =\u0026gt; Illuminate\\Database\\Eloquent\\Collection {#2341 all: [ App\\User {#2337 name: \u0026#34;笹田 洋介\u0026#34;, email: \u0026#34;jun91@example.com\u0026#34;, updated_at: \u0026#34;2019-03-08 08:24:36\u0026#34;, created_at: \u0026#34;2019-03-08 08:24:36\u0026#34;, id: 1, }, App\\User {#2333 name: \u0026#34;高橋 舞\u0026#34;, email: \u0026#34;hideki31@example.com\u0026#34;, updated_at: \u0026#34;2019-03-08 08:24:36\u0026#34;, created_at: \u0026#34;2019-03-08 08:24:36\u0026#34;, id: 2, }, ], } factory()を使用して、２つレコードをusersのテーブルに作成しました。これら、もちろんですが作成したばかりなので、作成時間と更新時間はどちらのレコードも同じ、つまりcreated_at == updated_at　です。\nということは、where()でその条件を指定してやれば、両方のレコードとも取得できますよね。早速クエリを実行してみます。\n\u0026gt;\u0026gt;\u0026gt; User::where(\u0026#39;created_at\u0026#39;, \u0026#39;=\u0026#39;, \u0026#39;updated_at\u0026#39;)-\u0026gt;get(); =\u0026gt; Illuminate\\Database\\Eloquent\\Collection {#2340 all: [], } あれれ、結果は空ですね。この前作成した、tinkerのsql()で実行されたSQL文を見てみると、\n\u0026gt;\u0026gt;\u0026gt; sql() =\u0026gt; [ ... [ \u0026#34;query\u0026#34; =\u0026gt; \u0026#34;select * from `users` where `created_at` = ?\u0026#34;, \u0026#34;bindings\u0026#34; =\u0026gt; [ \u0026#34;updated_at\u0026#34;, ], \u0026#34;time\u0026#34; =\u0026gt; 3.5, ], ] そう、usersテーブルの項目を指定しているのでなく、文字列の\u0026ldquo;updated_at\u0026rdquo;の指定となっているのです。どうりで空の取得となるわけです。\n正しくは、\n\u0026gt;\u0026gt;\u0026gt; User::whereRaw(\u0026#39;created_at = updated_at\u0026#39;)-\u0026gt;get(); =\u0026gt; Illuminate\\Database\\Eloquent\\Collection {#2345 all: [ App\\User {#2332 id: 1, name: \u0026#34;笹田 洋介\u0026#34;, email: \u0026#34;jun91@example.com\u0026#34;, created_at: \u0026#34;2019-03-08 08:24:36\u0026#34;, updated_at: \u0026#34;2019-03-08 08:24:36\u0026#34;, }, App\\User {#2339 id: 2, name: \u0026#34;高橋 舞\u0026#34;, email: \u0026#34;hideki31@example.com\u0026#34;, created_at: \u0026#34;2019-03-08 08:24:36\u0026#34;, updated_at: \u0026#34;2019-03-08 08:24:36\u0026#34;, }, ], } と、whereRaw()を使用して、生の条件文を入れてやるのです。実行されたSQL文を見てみると、\n\u0026gt;\u0026gt;\u0026gt; sql() =\u0026gt; [ ... [ \u0026#34;query\u0026#34; =\u0026gt; \u0026#34;select * from `users` where created_at = updated_at\u0026#34;, \u0026#34;bindings\u0026#34; =\u0026gt; [], \u0026#34;time\u0026#34; =\u0026gt; 0.8, ], ] と正しいSQL文となっています。\n","date":"2019-03-09T06:37:58+09:00","permalink":"https://www.larajapan.com/2019/03/09/%E3%81%93%E3%81%86%E3%81%84%E3%81%86%E3%81%A8%E3%81%8D%E3%81%ABwhereraw%E3%82%92%E4%BD%BF%E3%81%86/","title":"こういうときにwhereRawを使う"},{"content":"日常、いつもの考えや習慣で仮定してしまい、「実はそうでなかった」と後で冷や汗なること多々あります。Laravelのプログラミングもそうです。その過ちを繰り返さないために、出会ったらブログに書いていくことにします。\n今回はCollectionに関して、最近うっかりしたこと。\n例えば、tinkerで空の結果のEloquentのCollectionを作成して、\n\u0026gt;\u0026gt;\u0026gt; $users = User::where(\u0026#39;email\u0026#39;, \u0026#39;=\u0026#39;, \u0026#39;\u0026#39;)-\u0026gt;get(); // 空のCollection =\u0026gt; Illuminate\\Database\\Eloquent\\Collection {#2319 all: [], } \u0026gt;\u0026gt;\u0026gt; if (empty($users)) echo \u0026#34;空です！\u0026#34; empty()で空の判断を試みるが、「実はそうでなかった」。 empty()は引数が配列ならＯＫだけれど、Collectionのオブジェクトなら空を判断できません。\n正しくは、Collectionのメソッド、isEmpty()を使います。\n\u0026gt;\u0026gt;\u0026gt; if ($users-\u0026gt;isEmpty()) echo \u0026#34;空です！\u0026#34; 空です！ 同様に、count()でレコード数を数えてもＯＫですが、否定の!が必要。\n\u0026gt;\u0026gt;\u0026gt; if (!$users-\u0026gt;count()) echo \u0026#34;空です！\u0026#34; 空です！ また、配列を取り出すなら、empty()も使えます。\n\u0026gt;\u0026gt;\u0026gt; if (empty($users-\u0026gt;all())) echo \u0026#34;空です！\u0026#34; 空です！ ","date":"2019-03-02T03:57:47+09:00","permalink":"https://www.larajapan.com/2019/03/02/isempty%E3%81%A7%E3%81%97%E3%81%9F/","title":"正しいのは、isEmptyでした"},{"content":"先日、tinkerに関して私が知らなかった機能を見つけました。tinkerを好んで日夜使用する私としては、これは作業効率が上がるわ、とわくわくする機能です。\ntinkerで私がよく使うのは、以下のようなEloquentの関数で実行される裏側のSQL文の表示です。\nPsy Shell v0.9.3 (PHP 7.1.14 — cli) by Justin Hileman \u0026gt;\u0026gt;\u0026gt; DB::enableQueryLog() =\u0026gt; null \u0026gt;\u0026gt;\u0026gt; User::find(1) [!] Aliasing \u0026#39;User\u0026#39; to \u0026#39;App\\User\u0026#39; for this Tinker session. =\u0026gt; App\\User {#2316 id: 1, name: \u0026#34;江古田 明美\u0026#34;, email: \u0026#34;kazuya24@example.net\u0026#34;, created_at: \u0026#34;2019-01-18 14:02:42\u0026#34;, updated_at: \u0026#34;2019-01-18 14:02:42\u0026#34;, } \u0026gt;\u0026gt;\u0026gt; DB::getQueryLog() =\u0026gt; [ [ \u0026#34;query\u0026#34; =\u0026gt; \u0026#34;select * from `users` where `users`.`id` = ? limit 1\u0026#34;, \u0026#34;bindings\u0026#34; =\u0026gt; [ 1, ], \u0026#34;time\u0026#34; =\u0026gt; 8.01, ], ] \u0026gt;\u0026gt;\u0026gt; この例ではシンプルなSQL文ですが、これがhasManyThroughとかbelongsToManyだとかの複雑なリレーションになると、joinの接続が本当にこちらの期待通りなのか、などのチェックしたくなります。しかし、 DB::enableQueryLog()やDB::getQueryLog()を覚えるのも大変であるし、毎回毎回それをタイプするのも面倒！\nなんか簡単にエイリアスみたいなことができないかと、探していてわかったのが、tinkerのブートストラップファイル。\nまず、以下の内容のPHPファイルを作成します。\nstart.php \u0026lt;?php DB::enableQueryLog(); function sql() { return DB::getQueryLog(); } そして、そのファイル名を引数として、tinkerを実行します。\n$ php artisan tinker start.php [bash] Psy Shell v0.9.3 (PHP 7.1.14 — cli) by Justin Hileman \u0026gt;\u0026gt;\u0026gt; User::find(1) [!] Aliasing \u0026#39;User\u0026#39; to \u0026#39;App\\User\u0026#39; for this Tinker session. =\u0026gt; App\\User {#2316 id: 1, name: \u0026#34;江古田 明美\u0026#34;, email: \u0026#34;kazuya24@example.net\u0026#34;, created_at: \u0026#34;2019-01-18 14:02:42\u0026#34;, updated_at: \u0026#34;2019-01-18 14:02:42\u0026#34;, } \u0026gt;\u0026gt;\u0026gt; sql() =\u0026gt; [ [ \u0026#34;query\u0026#34; =\u0026gt; \u0026#34;select * from `users` where `users`.`id` = ? limit 1\u0026#34;, \u0026#34;bindings\u0026#34; =\u0026gt; [ 1, ], \u0026#34;time\u0026#34; =\u0026gt; 8.01, ], ] \u0026gt;\u0026gt;\u0026gt; そう、start.phpはすでにtinker内で先に実行されるので、SQLのログ機能はすでにオンとなっているし、SQL文を見たいときは、sql()とだけタイプすればいいのです。これなら忘れませんね。\n","date":"2019-02-22T23:44:13+09:00","permalink":"https://www.larajapan.com/2019/02/22/tinker%E3%81%AE%E3%83%96%E3%83%BC%E3%83%88%E3%82%B9%E3%83%88%E3%83%A9%E3%83%83%E3%83%97%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB/","title":"tinkerのブートストラップファイル"},{"content":"以前に、Laravel 5.3　コントローラのコンストラクタの重要な変更として、コントローラで定義するメソッド間で共有するコードをコンストラクタに入れることが可能なことを説明しました。今度は同じコンストラクタ内で、コントローラのメソッドに渡される引数の取り出しかたを説明します。何を言っているかというと、まずは準備から。\n準備 例として、routes/web.phpにおいて、以下のようなrouteを定義します。 ... Route::resource(\u0026#39;product\u0026#39;, \u0026#39;ProductController\u0026#39;); ... これは、以下のようなURIを生成します。\n+--------+-----------+-------------------------------+-----------------------+------------------------------------------------------------------------+----------------------------------------------+ | Domain | Method | URI | Name | Action | Middleware | +--------+-----------+-------------------------------+-----------------------+------------------------------------------------------------------------+----------------------------------------------+ | | GET|HEAD | product | product.index | App\\Http\\Controllers\\ProductController@index | web | | | POST | product | product.store | App\\Http\\Controllers\\ProductController@store | web | | | GET|HEAD | product/create | product.create | App\\Http\\Controllers\\ProductController@create | web | | | GET|HEAD | product/{product} | product.show | App\\Http\\Controllers\\ProductController@show | web | | | PUT|PATCH | product/{product} | product.update | App\\Http\\Controllers\\ProductController@update | web | | | DELETE | product/{product} | product.destroy | App\\Http\\Controllers\\ProductController@destroy | web | | | GET|HEAD | product/{product}/edit | product.edit | App\\Http\\Controllers\\ProductController@edit | web | +--------+-----------+-------------------------------+-----------------------+------------------------------------------------------------------------+----------------------------------------------+ そして、ProductController.phpを以下のように定義します。\nnamespace App\\Http\\Controllers; use Illuminate\\Http\\Request; use App\\Models\\Product; class ProductController extends Controller { /** * 情報画面 * * @param \\App\\Models\\Product $product * @return \\Illuminate\\View\\View */ public function show(Product $product) { // } /** * 編集画面 * * @param \\App\\Models\\Product $product * @return \\Illuminate\\View\\View */ public function edit(Product $product) { // } /** * 編集保存 * * @param \\Illuminate\\Http\\Request $request * @param \\App\\Models\\Product $product * @return \\Illuminate\\Http\\RedirectResponse */ public function update(Request $request, Product $product) { // } } コンストラクタで引数を取り出す 用意ができたところで、\n例えば、\nproduct/456\nのGETのアクセスでは、id = 456に対応するProductのEloquentオブジェクトがshow()のメソッドの引数$productの値として渡されます。\nそして、\nproduct/456/edit\nのGETは、id = 456に対応するProductのオブジェクトがedit()のメソッドの引数$productの値として渡されます。\nさて、このid = 456のProductのオブジェクト、それぞれのメソッドの引数と渡される前に、コンストラクタで取り出すには？\nnamespace App\\Http\\Controllers; use Illuminate\\Http\\Request; use App\\Models\\Product; class ProductController extends Controller { public function __construct() { parent::__construct(); $this-\u0026gt;middleware(function ($request, $next) { $product = $request-\u0026gt;product;　//id = 456のProductのオブジェクトを取得 debug(get_class($product), $product-\u0026gt;id); return $next($request); }); } ... と、middleware()の関数を使えば、その匿名関数の中で簡単に取り出せますね！\nproduct/456\nのURIにブラウザでアクセスすれば、Debugbarでは、\ndebug　App\\Models\\Product debug 456 とデバッグ文を表示します。\nproduct/456/edit\nでも\nproduct/456/update\nでも、同じようにそれぞれのメソッドを実行する前に、引数にアクセスすることが可能となります。\nひとつここで疑うのは、Productのオブジェクトを取得するために以下のようなSQL文が、middleware()で１回、そしてさらにshow()のようなメソッドの引数でもう１回、計２回実行されることになるか、です。\nselect * from product where id = 456 これもDebugbarのQueryの出力で１回しか実行されていないことを確認しました。一度作成したオブジェクトを同じコントローラ内で２度作成することはなりません。\n取り出して、それからどうする？ ここが大事なのですが、例えば、この商品（product）がEコマースで販売される商品とします。商品の登録や編集は、裏側の管理画面で商品の製造元の店舗がログインして行います。しかし、複数の店舗が存在するので、店舗Aが店舗Bの商品を編集できては困ります。しかし、URIには商品のIDが表示されるので、IDの数字を変えて他の店舗の商品を閲覧（公開されていない情報がある）や編集することが可能となってしまいます。それを防ぐためには、\n... public function __construct() { parent::__construct(); $this-\u0026gt;middleware(function ($request, $next) { $product = $request-\u0026gt;product; //id = 456のProductのオブジェクトを取得 $user = auth(\u0026#39;shop\u0026#39;)-\u0026gt;user(); // 現在ログインしている店舗のユーザー if ($product-\u0026gt;shop_id != $user-\u0026gt;shop_id) { //この商品の店舗ではない！ abort(404); } return $next($request); }); } ... のように設定すれば、いちいちshow()やedit()やupdate()などで、同様なコードを繰り返さずに済みプログラムの管理性が高まる、ということです。\n","date":"2019-02-09T00:00:40+09:00","permalink":"https://www.larajapan.com/2019/02/09/%E3%82%B3%E3%83%B3%E3%83%88%E3%83%AD%E3%83%BC%E3%83%A9%E3%81%AE%E3%82%B3%E3%83%B3%E3%82%B9%E3%83%88%E3%83%A9%E3%82%AF%E3%82%BF%E3%81%A7%E5%BC%95%E6%95%B0%E3%82%92%E5%8F%96%E3%82%8A%E5%87%BA%E3%81%99/","title":"コントローラのコンストラクタで引数を取り出す"},{"content":"ファイルのアップロードに関連したコードの開発で、LaravelのStorageのクラスを使用する機会がありました。そのクラスを使用しての出力処理で学んだことを今回は共有します。\nまず、Laravelではアップロードしたファイルや生成したファイルの保管場所としてstorage/appのディレクトリが設けられています。\n例えば、現在開発しているプロジェクトでは以下のようです。\n$ tree storage/app storage/app ├── public ├── shop │ ├── 2 │ │ ├── order.csv │ │ ├── products20181227-024348.csv │ │ ├── products20181227-034433.csv storage/app/shop/2のディレクトリのファイルのリストが欲しいなら、tinkerでは、\n\u0026gt;\u0026gt;\u0026gt; Storage::files(\u0026#39;shop/2\u0026#39;) =\u0026gt; [ \u0026#34;shop/2/order.csv\u0026#34;, \u0026#34;shop/2/products20181227-024348.csv\u0026#34;, \u0026#34;shop/2/products20181227-034433.csv\u0026#34;, ] さて、ここで以下のようなファイル名だけの配列のデータが欲しいです。\n[ \u0026#34;order.csv\u0026#34;, \u0026#34;products20181227-024348.csv\u0026#34;, \u0026#34;products20181227-034433.csv\u0026#34;, ] foreachも使えますが、array_mapを使ってみましょう。\n\u0026gt;\u0026gt;\u0026gt; $files = Storage::files(\u0026#39;shop/2\u0026#39;); =\u0026gt; [ \u0026#34;shop/2/order.csv\u0026#34;, \u0026#34;shop/2/products20181227-024348.csv\u0026#34;, \u0026#34;shop/2/products20181227-034433.csv\u0026#34;, ] \u0026gt;\u0026gt;\u0026gt; array_map(function($file) { return basename($file); }, $files); =\u0026gt; [ \u0026#34;order.csv\u0026#34;, \u0026#34;products20181227-024348.csv\u0026#34;, \u0026#34;products20181227-034433.csv\u0026#34;, ] さらに、productsで始まるファイル名だけが欲しいとすると、\n\u0026gt;\u0026gt;\u0026gt; $products = array_map(function($file) { \\ ... if (substr(basename($file), 0, 8) == \u0026#39;products\u0026#39;) return basename($file); \\ ... }, $files) =\u0026gt; [ null, \u0026#34;products20181227-024348.csv\u0026#34;, \u0026#34;products20181227-034433.csv\u0026#34;, ] あれれ、これでは余計なnullのファイル名が出てきます。 それを抜くために、今度はarray_filterです。\n\u0026gt;\u0026gt;\u0026gt; array_filter($products, function($file) { return ($file !== null); }); =\u0026gt; [ 1 =\u0026gt; \u0026#34;products20181227-024348.csv\u0026#34;, 2 =\u0026gt; \u0026#34;products20181227-034433.csv\u0026#34;, ] ややこしいですね。残念ながら一発で両方を行う関数はphpにはありません。\nLaravelならどうでしょうか。Storage::diskは配列を返すので、まずCollectionに変えます。\n\u0026gt;\u0026gt;\u0026gt; $files = collect(Storage::files(\u0026#39;shop/2\u0026#39;)); =\u0026gt; Illuminate\\Support\\Collection {#3522 all: [ \u0026#34;shop/2/order.csv\u0026#34;, \u0026#34;shop/2/products20181227-024348.csv\u0026#34;, \u0026#34;shop/2/products20181227-034433.csv\u0026#34;, ], } \u0026gt;\u0026gt;\u0026gt; $files-\u0026gt;map(function($file) { \\ ... if (substr(basename($file), 0, 8) == \u0026#39;products\u0026#39;) return basename($file); } \\ ... )-\u0026gt;filter(function($file) { return ($file !== null); }); =\u0026gt; Illuminate\\Support\\Collection {#3534 all: [ 1 =\u0026gt; \u0026#34;products20181227-024348.csv\u0026#34;, 2 =\u0026gt; \u0026#34;products20181227-034433.csv\u0026#34;, ], } phpの関数とは違ってチェーンできるので少しはすっきりします。しかし、foreachの方が簡単かな、とも思うときでもあります。\n最後に、保管場所をstorage/app以外にしたいなら、config/filesystemes.phpの設定で変更できます。\n... \u0026#39;disks\u0026#39; =\u0026gt; [ \u0026#39;local\u0026#39; =\u0026gt; [ \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;local\u0026#39;, \u0026#39;root\u0026#39; =\u0026gt; storage_path(\u0026#39;app\u0026#39;), //ここを編集する ], ... ","date":"2019-01-28T06:34:47+09:00","permalink":"https://www.larajapan.com/2019/01/28/map%E3%81%A8filter/","title":"mapとfilter"},{"content":"正直言って、Laravelのmigration機能は私のLaravelのプロジェクトでは使用したことありません。よく理解していないこともあり、失敗してクライアントのデータベースを空にしてしまうことを考えると悪夢です。とか言ってコマンドラインでSQL文を実行する現状もリスクはそう変わりません。ということで、前回においてmigrationを使用する機会があったので、この際にmigrationの理解を深めたいです。\nDBにインデックスを追加 まず、前回においては以下のようなmigrationを作成することで新規のテーブルaddressesを作成しました。 use Illuminate\\Support\\Facades\\Schema; use Illuminate\\Database\\Schema\\Blueprint; use Illuminate\\Database\\Migrations\\Migration; class CreateAddressesTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create(\u0026#39;addresses\u0026#39;, function (Blueprint $table) { $table-\u0026gt;increments(\u0026#39;id\u0026#39;); $table-\u0026gt;integer(\u0026#39;user_id\u0026#39;)-\u0026gt;unsigned(); $table-\u0026gt;string(\u0026#39;address\u0026#39;); $table-\u0026gt;timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists(\u0026#39;addresses\u0026#39;); } } そして、以下を実行することで、addressesのテーブルを作成できます。\n$ php artisan migrate Migrating: 2019_01_10_140631_create_addresses_table Migrated: 2019_01_10_140631_create_addresses_table しかし、これではusersのテーブルとのjoinにおいてパフォーマンスが良くありません。 例えば、以下のようなクエリを実行するときです。\nPsy Shell v0.9.3 (PHP 7.1.14 — cli) by Justin Hileman \u0026gt;\u0026gt;\u0026gt; User::join(\u0026#39;addresses\u0026#39;, \u0026#39;users.id\u0026#39;, \u0026#39;=\u0026#39;, \u0026#39;addresses.user_id\u0026#39;)-\u0026gt;get(); SQL文では以下となります。\nmysql\u0026gt; select * from `users` inner join `addresses` on `users`.`id` = `addresses`.`user_id` しかし、このjoinでは、データベースはマッチするuser_idを探すために毎回addressesのレコードをすべてチェックしなければなりません。レコード数が多くなれば、クエリの実行時間はとても長くなります。\nしかし、これは、addresses.user_idにインデックスを追加するだけで簡単に解決できます。\nmigrationを作成してインデックスを追加してみましょう。\nまず、migrationを作成します。\n$ php artisan make:migration change_addresses --table=addresses 作成されたファイルを編集します。\nuse Illuminate\\Support\\Facades\\Schema; use Illuminate\\Database\\Schema\\Blueprint; use Illuminate\\Database\\Migrations\\Migration; class ChangeAddressesTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::table(\u0026#39;addresses\u0026#39;, function (Blueprint $table) { $table-\u0026gt;index(\u0026#39;user_id\u0026#39;); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::table(\u0026#39;addresses\u0026#39;, function (Blueprint $table) { $table-\u0026gt;dropIndex(\u0026#39;addresses_user_id_index\u0026#39;); }); } } これだけです。rollbackのためにdown()も定義することを忘れないように。dropIndex()の引数は、DBの項目名ではなく、間に下線文字を入れて、テーブル名と項目名とindexの文字列をコンバインします（例：addresses_user_id_index）。\n次は、このmigrationの反映ですが、その前に\u0026ndash;pretendのオプションでどんなSQL文が実行されるかチェックしてみます。\n$ php artisan migrate --pretend ChangeAddressesTable: alter table `addresses` add index `addresses_user_id_index`(`user_id`) いいですね。思い通りです。\n実際にmigrateします。\n$ php artisan migrate Migrating: 2019_01_15_132109_change_addresses_table Migrated: 2019_01_15_132109_change_addresses_table 住所タイプの導入とユニークインデックスの追加 次は、もう少し複雑なケースを見てみましょう。\n現在は、1ユーザー(usersの1レコード）において、複数の住所（addressesのレコード）を紐づけることができますが、住所が会社のものか自宅なのか別荘（夢）なのかわかりません。そこで、その情報を保存するために住所タイプなるtypeという項目を追加します。\nmigrationを作成します。\n$ php artisan make:migration change_addresses2 --table=addresses 作成されたファイルを編集します。\nuse Illuminate\\Support\\Facades\\Schema; use Illuminate\\Database\\Schema\\Blueprint; use Illuminate\\Database\\Migrations\\Migration; class ChangeAddresses2Table extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::table(\u0026#39;addresses\u0026#39;, function (Blueprint $table) { // 項目の追加 $table-\u0026gt;string(\u0026#39;type\u0026#39;)-\u0026gt;after(\u0026#39;user_id\u0026#39;); // インデックスを付け直す $table-\u0026gt;dropIndex(\u0026#39;addresses_user_id_index\u0026#39;); $table-\u0026gt;unique([\u0026#39;user_id\u0026#39;,\u0026#39;type\u0026#39;]); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::table(\u0026#39;addresses\u0026#39;, function (Blueprint $table) { $table-\u0026gt;string(\u0026#39;type\u0026#39;)-\u0026gt;after(\u0026#39;user_id\u0026#39;); $table-\u0026gt;dropIndex(\u0026#39;addresses_user_id_index\u0026#39;); $table-\u0026gt;unique([\u0026#39;user_id\u0026#39;,\u0026#39;type\u0026#39;]); }); } } typeの項目の追加の後には、現在のインデックスを削除して新規にuser_idとtypeをコンバインしたインデックスを追加しなおします。そして、1ユーザーに対して住所タイプは同じものを使用できない制限のために、unique()の関数を使用します。\nまたもや、\u0026ndash;pretendでSQL文をチェックします。\n$ php artisan migrate --pretend ChangeAddresses2Table: alter table `addresses` add `type` varchar(191) not null after `user_id` ChangeAddresses2Table: alter table `addresses` drop index `addresses_user_id_index` ChangeAddresses2Table: alter table `addresses` add unique `addresses_user_id_type_unique`(`user_id`, `type`) 次は、migrateの実行なのですが、すでにデータがある状態で実行するとレコード重複のエラーが出る可能性あります。とくにすでに１ユーザーに対して複数の住所のレコードがある場合は、それらの項目に新規の項目typeに空が入ってくるからです。\n今回は、このエラーを避けるために、migrate:freshを実行します。すべてのテーブルをdropしてから作成したmigrationをすべて実行してくれます。\n$ php artisan migrate:fresh Dropped all tables successfully. Migration table created successfully. Migrating: 2014_10_12_000000_create_users_table Migrated: 2014_10_12_000000_create_users_table Migrating: 2014_10_12_100000_create_password_resets_table Migrated: 2014_10_12_100000_create_password_resets_table Migrating: 2019_01_10_140631_create_addresses_table Migrated: 2019_01_10_140631_create_addresses_table Migrating: 2019_01_15_132109_change_addresses_table Migrated: 2019_01_15_132109_change_addresses_table Migrating: 2019_01_18_131940_change_addresses2_table Migrated: 2019_01_18_131940_change_addresses2_table しかし、実際のプロダクションではできないことですね。データ皆空になってしまうし。その対応は課題としましょう。\nフェイクデータの作成 さて、DBテーブルができたところで、前回のようにフェイクデータの作成をしてみたいです。\n今回追加したtypeの項目のために、database/factories/AddressFactory.phpを編集します。\nuse Faker\\Generator as Faker; $factory-\u0026gt;define(App\\Address::class, function (Faker $faker) { return [ \u0026#39;type\u0026#39; =\u0026gt; $faker-\u0026gt;randomElement([\u0026#39;自宅\u0026#39;, \u0026#39;会社\u0026#39;]), \u0026#39;address\u0026#39; =\u0026gt; $faker-\u0026gt;address ]; }); tinkerで実行してみます。\n\u0026gt;\u0026gt;\u0026gt; factory(App\\User::class,3)-\u0026gt;create()-\u0026gt;each(function ($user) { $user-\u0026gt;addresses()-\u0026gt;save(factory(App\\Address::class)-\u0026gt;make()); }); ... User::join(\u0026#39;addresses\u0026#39;, \u0026#39;users.id\u0026#39;, \u0026#39;=\u0026#39;, \u0026#39;addresses.user_id\u0026#39;)-\u0026gt;get(); =\u0026gt; Illuminate\\Database\\Eloquent\\Collection {#2346 all: [ App\\User {#2307 id: 1, name: \u0026#34;山本 七夏\u0026#34;, email: \u0026#34;hirokawa.osamu@example.org\u0026#34;, created_at: \u0026#34;2019-01-19 02:33:51\u0026#34;, updated_at: \u0026#34;2019-01-19 02:33:51\u0026#34;, user_id: 1, type: \u0026#34;自宅\u0026#34;, address: \u0026#34;5212837 京都府佐々木市西区近藤町加納9-4-4 コーポ宮沢110号\u0026#34;, }, App\\User {#2353 id: 2, name: \u0026#34;若松 裕樹\u0026#34;, email: \u0026#34;tsuda.rika@example.com\u0026#34;, created_at: \u0026#34;2019-01-19 02:33:51\u0026#34;, updated_at: \u0026#34;2019-01-19 02:33:51\u0026#34;, user_id: 2, type: \u0026#34;自宅\u0026#34;, address: \u0026#34;4853444 奈良県佐々木市南区鈴木町廣川8-3-9\u0026#34;, }, App\\User {#2355 id: 3, name: \u0026#34;江古田 裕太\u0026#34;, email: \u0026#34;ksato@example.org\u0026#34;, created_at: \u0026#34;2019-01-19 02:33:51\u0026#34;, updated_at: \u0026#34;2019-01-19 02:33:51\u0026#34;, user_id: 3, type: \u0026#34;会社\u0026#34;, address: \u0026#34;8386040 愛知県宮沢市中央区井高町加納4-2-10\u0026#34;, }, ], } うまくフェイクデータが作成されましたね。\n最後に１つ有用なコマンドとして、migrate:status。現在のmigrationのステータスを一覧できます。左の1列目がすべてＹとなっているのは現在あるmigrationがすべて実行された状態です。\n$ php artisan migrate:status +------+------------------------------------------------+ | Ran? | Migration | +------+------------------------------------------------+ | Y | 2014_10_12_000000_create_users_table | | Y | 2014_10_12_100000_create_password_resets_table | | Y | 2019_01_10_140631_create_addresses_table | | Y | 2019_01_15_132109_change_addresses_table | | Y | 2019_01_18_131940_change_addresses2_table | +------+------------------------------------------------+ migrate:rollbackしたなら、すべてNoのＮとなります。\n+------+------------------------------------------------+ | Ran? | Migration | +------+------------------------------------------------+ | N | 2014_10_12_000000_create_users_table | | N | 2014_10_12_100000_create_password_resets_table | | N | 2019_01_10_140631_create_addresses_table | | N | 2019_01_15_132109_change_addresses_table | | N | 2019_01_18_131940_change_addresses2_table | +------+------------------------------------------------+ ","date":"2019-01-20T03:17:58+09:00","permalink":"https://www.larajapan.com/2019/01/20/db%E3%81%AB%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%E3%82%92%E8%BF%BD%E5%8A%A0/","title":"DBにインデックスを追加"},{"content":"hasManyリレーションは、Eloquentのモデル間（つまり、DBのテーブル間）に1対多の関係を持たせるリレーションです。 今回は、factory()を使って、このhasManyのリレーションを持つDBテーブルにフェイクデータを作成してみます。\nhasMany DBにおける1対多の関係では、2つのDBテーブルを親子とみなすと、親の１レコードに対してそれに紐づく子のレコードが１あるいは複数存在します。 例えば、親をusers（ユーザー）、子をaddresses（住所）とし、それぞれのEloquentのモデル、User、Addressを定義し、User::addresses()のリレーションを定義すれば、以下のようなデータとなります。\n\u0026gt;\u0026gt;\u0026gt; User::find(1)-\u0026gt;load(\u0026#39;addresses\u0026#39;); =\u0026gt; App\\User {#2312 id: 1, name: \u0026#34;原田 涼平\u0026#34;, email: \u0026#34;esasada@example.com\u0026#34;, created_at: \u0026#34;2019-01-12 01:53:58\u0026#34;, updated_at: \u0026#34;2019-01-12 01:53:58\u0026#34;, addresses: Illuminate\\Database\\Eloquent\\Collection {#2340 all: [ App\\Address {#2349 id: 1, user_id: 1, address: \u0026#34;8731301 山口県石田市中央区田中町高橋1-5-6 コーポ高橋110号\u0026#34;, created_at: \u0026#34;2019-01-12 01:53:58\u0026#34;, updated_at: \u0026#34;2019-01-12 01:53:58\u0026#34;, }, App\\Address {#2348 id: 2, user_id: 1, address: \u0026#34;7159410 京都府坂本市北区加納町加藤9-7-8\u0026#34;, created_at: \u0026#34;2019-01-12 01:53:58\u0026#34;, updated_at: \u0026#34;2019-01-12 01:53:58\u0026#34;, }, ], }, } ここでは、原田さんは２つの住所を持つことが表示されています。もちろん、これらはフェイクデータですが、次の準備をすればこれがtinkerの１行の実行で親子のレコードを作成できます。\n準備 Laravelの新規のプロジェクトを作成すると、usersのmigration、model、factoryがすでに定義されています。ここで必要なのは、addressesにおいて同様な定義です。\nまず、migrationから\n$ php artisan make:migration create_addresses_table --create=addresses 上のコマンドラインでの実行で、\n$ ls -1 database/migrations 2014_10_12_000000_create_users_table.php 2014_10_12_100000_create_password_resets_table.php 2019_01_10_140631_create_addresses_table.php 2019_01_10_140631_create_addresses_table.phpのファイルが作成されます。これを以下のように編集。\nuse Illuminate\\Support\\Facades\\Schema; use Illuminate\\Database\\Schema\\Blueprint; use Illuminate\\Database\\Migrations\\Migration; class CreateAddressesTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create(\u0026#39;addresses\u0026#39;, function (Blueprint $table) { $table-\u0026gt;increments(\u0026#39;id\u0026#39;); $table-\u0026gt;integer(\u0026#39;user_id\u0026#39;)-\u0026gt;unsigned(); $table-\u0026gt;string(\u0026#39;address\u0026#39;); $table-\u0026gt;timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists(\u0026#39;addresses\u0026#39;); } } users.idと紐づけるために、addresses.user_idの項目が追加されていることに注意してください。\nそして、次を実行して、addressesのDBテーブルを作成します。\n$ php artisan migrate Migrating: 2019_01_10_140631_create_addresses_table Migrated: 2019_01_10_140631_create_addresses_table 次に、モデルを作成。これもコマンドラインで実行。\n$ php artian make:model Address 以下のファイルが作成されます。\napp/Address.php\nそしてこれを以下のように編集。\nnamespace App; use Illuminate\\Database\\Eloquent\\Model; class Address extends Model { protected $fillable = [ \u0026#39;address\u0026#39; ]; } Addressのモデルができたところで、UserのモデルにhasManyのリレーション、addresses()を定義します。\napp/User.php namespace App; use Illuminate\\Notifications\\Notifiable; use Illuminate\\Foundation\\Auth\\User as Authenticatable; use App\\Auth\\Passwords\\CanResetPassword; class User extends Authenticatable { use Notifiable; use CanResetPassword; /** * The attributes that are mass assignable. * * @var array */ protected $fillable = [ \u0026#39;name\u0026#39;, \u0026#39;email\u0026#39;, \u0026#39;password\u0026#39;, ]; /** * The attributes that should be hidden for arrays. * * @var array */ protected $hidden = [ \u0026#39;password\u0026#39;, \u0026#39;remember_token\u0026#39;, ]; public function addresses() { return $this-\u0026gt;hasMany(\u0026#39;App\\Address\u0026#39;); } } そして、Addressのフェイクデータを作成するためにfactoryを作成します。\n$ php artisan make:factory AddressFactory この実行でAddressFactory.phpのファイルが作成されます。\n$ ls -1 database/factories AddressFactory.php UserFactory.php これを以下のように編集。\nuse Faker\\Generator as Faker; $factory-\u0026gt;define(App\\Address::class, function (Faker $faker) { return [ \u0026#39;address\u0026#39; =\u0026gt; $faker-\u0026gt;address ]; }); 最後に、フェイクデータを日本語で作成するために、config/app.phpに以下を追加します。\n.. \u0026#39;faker_locale\u0026#39; =\u0026gt; \u0026#39;ja_JP\u0026#39;, .. これで準備終わり。\nフェイクデータの作成 さて、tinkerの出番です。\n$ php artisan tinker \u0026gt;\u0026gt;\u0026gt; factory(App\\User::class)-\u0026gt;create()-\u0026gt;each(function ($user) { $user-\u0026gt;addresses()-\u0026gt;save(factory(App\\Address::class)-\u0026gt;make()); }); =\u0026gt; true \u0026gt;\u0026gt;\u0026gt; User::find(1)-\u0026gt;load(\u0026#39;addresses\u0026#39;); =\u0026gt; App\\User {#2310 id: 1, name: \u0026#34;近藤 亮介\u0026#34;, email: \u0026#34;mikako.koizumi@example.net\u0026#34;, created_at: \u0026#34;2019-01-12 02:56:16\u0026#34;, updated_at: \u0026#34;2019-01-12 02:56:16\u0026#34;, addresses: Illuminate\\Database\\Eloquent\\Collection {#2347 all: [ App\\Address {#2349 id: 1, user_id: 1, address: \u0026#34;8657778 滋賀県松本市南区中島町笹田9-6-2\u0026#34;, created_at: \u0026#34;2019-01-12 02:56:16\u0026#34;, updated_at: \u0026#34;2019-01-12 02:56:16\u0026#34;, }, ], }, } factoryの実行文が長いので、整形して表示してみましょう。\nfactory(App\\User::class)-\u0026gt;create() -\u0026gt;each(function ($user) { $user-\u0026gt;addresses() -\u0026gt;save( factory(App\\Address::class)-\u0026gt;make() ); }); 1行目で、usersのレコードが作成され、それぞれのusersのレコードにおいて、addresses()のリレーションをもとにaddressesのレコードが作成されます。\n１つのusersのレコードに対して、addressesのレコードが複数欲しいなら、save()の部分を\nsaveMany(factory(App\\Address::class, 2)\nに置き換えます。以下のように２つaddressesのレコード作成となります。\n\u0026gt;\u0026gt;\u0026gt; factory(App\\User::class)-\u0026gt;create()-\u0026gt;each(function ($user) { $user-\u0026gt;addresses()-\u0026gt;saveMany(factory(App\\Address::class, 2)-\u0026gt;make()); }); =\u0026gt; true \u0026gt;\u0026gt;\u0026gt; User::find(2)-\u0026gt;load(\u0026#39;addresses\u0026#39;); =\u0026gt; App\\User {#2368 id: 2, name: \u0026#34;津田 七夏\u0026#34;, email: \u0026#34;kudo.kaori@example.org\u0026#34;, created_at: \u0026#34;2019-01-12 02:59:24\u0026#34;, updated_at: \u0026#34;2019-01-12 02:59:24\u0026#34;, addresses: Illuminate\\Database\\Eloquent\\Collection {#2369 all: [ App\\Address {#2354 id: 4, user_id: 2, address: \u0026#34;8621292 長崎県石田市中央区大垣町渡辺3-6-8\u0026#34;, created_at: \u0026#34;2019-01-12 02:59:24\u0026#34;, updated_at: \u0026#34;2019-01-12 02:59:24\u0026#34;, }, App\\Address {#2357 id: 5, user_id: 2, address: \u0026#34;6857809 沖縄県三宅市南区山本町山岸9-5-9 コーポ山口101号\u0026#34;, created_at: \u0026#34;2019-01-12 02:59:24\u0026#34;, updated_at: \u0026#34;2019-01-12 02:59:24\u0026#34;, }, ], }, } ","date":"2019-01-13T02:19:08+09:00","permalink":"https://www.larajapan.com/2019/01/13/hasmany%E3%81%AE%E3%83%95%E3%82%A7%E3%82%A4%E3%82%AF%E3%83%87%E3%83%BC%E3%82%BF%E3%81%AE%E4%BD%9C%E6%88%90/","title":"hasManyのフェイクデータの作成"},{"content":"レコードのコピーという作業は結構起こることです。例えば、Eコーマースのサイトなら、サイズだけが違う商品は、説明などの他の属性はほとんど同じなので、わざわざ新規の商品の情報をすべて手入力というよりは、既存の商品をもとに編集する方がもちろん簡単です。\nさて、このレコードのコピー作業、Eloquentではどうやるのでしょう？\ntinkerを立ち上げます。\nまず、usersのテーブルを空にしましょう。\nPsy Shell v0.9.9 (PHP 7.1.14 — cli) by Justin Hileman \u0026gt;\u0026gt;\u0026gt; User::truncate(); =\u0026gt; Illuminate\\Database\\Eloquent\\Builder {#2862} \u0026gt;\u0026gt;\u0026gt; User::count(); =\u0026gt; 0 次に、ファクトリーを利用してレコードを１つ作成します。\n\u0026gt;\u0026gt;\u0026gt; factory(App\\User::class, 1)-\u0026gt;create(); =\u0026gt; Illuminate\\Database\\Eloquent\\Collection {#2896 all: [ App\\User {#2892 name: \u0026#34;Yessenia Jast\u0026#34;, email: \u0026#34;modesto24@example.org\u0026#34;, updated_at: \u0026#34;2019-01-06 05:13:54\u0026#34;, created_at: \u0026#34;2019-01-06 05:13:54\u0026#34;, id: 1, }, ], } \u0026gt;\u0026gt;\u0026gt; そして、このレコードをcloneして保存。\n\u0026gt;\u0026gt;\u0026gt; $user = User::find(1); =\u0026gt; App\\User {#121 id: 1, name: \u0026#34;Yessenia Jast\u0026#34;, email: \u0026#34;modesto24@example.org\u0026#34;, created_at: \u0026#34;2019-01-06 05:13:54\u0026#34;, updated_at: \u0026#34;2019-01-06 05:13:54\u0026#34;, } \u0026gt;\u0026gt;\u0026gt; $clone = clone $user; =\u0026gt; App\\User {#2894 id: 1, name: \u0026#34;Yessenia Jast\u0026#34;, email: \u0026#34;modesto24@example.org\u0026#34;, created_at: \u0026#34;2019-01-06 05:13:54\u0026#34;, updated_at: \u0026#34;2019-01-06 05:13:54\u0026#34;, } \u0026gt;\u0026gt;\u0026gt; $clone-\u0026gt;save(); =\u0026gt; true \u0026gt;\u0026gt;\u0026gt; User::count(); =\u0026gt; 1 \u0026gt;\u0026gt;\u0026gt; あれれ、新規のレコードを作成してくれませんね。レコード数はオリジナルのレコードの1個です。$cloneは単に同じDBレコードを違うオブジェクトとしただけなので、$clone-\u0026gt;save()は新規登録ではなく既存のレコードの編集となってしまうようです。\n正しいやり方は、Eloquentのreplicate()を使用してコピーのオブジェクトを作成します。\n\u0026gt;\u0026gt;\u0026gt; $clone = $user-\u0026gt;replicate(); =\u0026gt; App\\User {#2862 name: \u0026#34;Yessenia Jast\u0026#34;, email: \u0026#34;modesto24@example.org\u0026#34;, } \u0026gt;\u0026gt;\u0026gt; $clone-\u0026gt;email = \u0026#39;test@example.com\u0026#39;; =\u0026gt; \u0026#34;test@example.com\u0026#34; \u0026gt;\u0026gt;\u0026gt; $clone-\u0026gt;save(); =\u0026gt; true emailはユニークな値でないとDBの重複エラーとなるので違う値にしてsaveしていることに注意してください。\n以下のように、レコードは２つになり、複製が作成されています。\n\u0026gt;\u0026gt;\u0026gt; User::all(); =\u0026gt; Illuminate\\Database\\Eloquent\\Collection {#2863 all: [ App\\User {#2888 id: 1, name: \u0026#34;Yessenia Jast\u0026#34;, email: \u0026#34;modesto24@example.org\u0026#34;, created_at: \u0026#34;2019-01-06 05:13:54\u0026#34;, updated_at: \u0026#34;2019-01-06 05:13:54\u0026#34;, }, App\\User {#2893 id: 2, name: \u0026#34;Yessenia Jast\u0026#34;, email: \u0026#34;test@example.com\u0026#34;, created_at: \u0026#34;2019-01-06 05:16:06\u0026#34;, updated_at: \u0026#34;2019-01-06 05:16:06\u0026#34;, }, ], } ","date":"2019-01-06T05:34:12+09:00","permalink":"https://www.larajapan.com/2019/01/06/db%E3%83%AC%E3%82%B3%E3%83%BC%E3%83%89%E3%81%AE%E3%82%B3%E3%83%94%E3%83%BC/","title":"DBレコードのコピー"},{"content":"またまた、前回に引き続きMySQLのレプリカの話です。しかし、今回はLaravelの設定も関係してきます。\ntimestampのデータが非同期している MySQLのデータタイプの１つに、timestampがあります。通常はレコードの作成や更新の日時の記録に使います。Laravelでは、Modelのクラスにおいて、デフォルトでcreated_atとupdated_atの項目がtimestampのデータタイプです。それらの項目が対応するDBテーブルにあるなら、 \u0026lt;?php namespace App\\Models; use Illuminate\\Database\\Eloquent\\Model; class User extends Model { public $timestamps = true; //デフォルト設定なので、この行は要らなくともよい ... と設定しておけば、保存時にいちいち値をセットしなくとも自動にLaravelが面倒見てくれます。\nしかし、レプリカの複製を使い始めて気づいたのは、timestampの項目のデータがマスターとレプリカが同期していないことです。\nまず、マスターでtinkerを使用してレコードを更新します。\nPsy Shell v0.9.3 (PHP 7.1.23 — cli) by Justin Hileman \u0026gt;\u0026gt;\u0026gt; $user = User::find(1); [!] Aliasing \u0026#39;User\u0026#39; to \u0026#39;App\\User\u0026#39; for this Tinker session. =\u0026gt; App\\User {#2309 id: 1, name: \u0026#34;山田太郎\u0026#34;, email: \u0026#34;test@lotsofbytes.com\u0026#34;, created_at: \u0026#34;2017-01-19 04:57:48\u0026#34;, updated_at: \u0026#34;2018-11-08 05:05:24\u0026#34;, } \u0026gt;\u0026gt;\u0026gt; $user-\u0026gt;update([\u0026#39;name\u0026#39; =\u0026gt; \u0026#39;John Doe\u0026#39;]); =\u0026gt; true \u0026gt;\u0026gt;\u0026gt; $user = User::find(1); =\u0026gt; App\\User {#2314 id: 1, name: \u0026#34;John Doe\u0026#34;, email: \u0026#34;test@lotsofbytes.com\u0026#34;, created_at: \u0026#34;2017-01-19 04:57:48\u0026#34;, updated_at: \u0026#34;2018-12-30 03:28:42\u0026#34;, } nameとupdated_atが変更されていますね。 ここで、pt-table-checksumを実行すると、usersのテーブルでマスターとレプリカで値が異なることが報告されるのです。\nChecking if all tables can be checksummed ... Starting checksum ... TS ERRORS DIFFS ROWS DIFF_ROWS CHUNKS SKIPPED TIME TABLE 12-30T03:30:31 0 0 2 0 1 0 1.020 larajapan.migrations 12-30T03:30:31 0 0 1 0 1 0 0.021 larajapan.password_resets 12-30T03:30:31 0 1 5 0 1 0 0.017 larajapan.users 謎です。\n謎をひも解く いろいろと探ってみると、知っておくべきなのに知らなかったことがぽろぽろと出てきました。 まず、知らなかったこと、その１として、\ntimestampのデータタイプの項目では、値が入るとUTCの時間に変更されて保存される。\n例えば、MySQLのセッションが日本のタイムゾーンなら、\ncreated_atの項目に、2018-12-30 03:00:00と入れれば、2018-12-29 18:00:00とＤＢに保存されます。日本のタイムゾーンは、UTCより9時間先です（この時間差を理解するには、このサイトhttps://www.worldtimebuddy.com/japan-tokyo-to-utcチェックしてみてください）。\nそして、SELECTでこの値を引き出すときは、そのときのセッションのタイムゾーンに変換してMySQLは出力します。つまり、同じ時間を世界のどのタイムゾーンに合わせて表示が可能なわけです。\n次に、知らなかったこと、その2は、\nMySQLのセッションのタイムゾーンは、DBを使用するアプリで指定していないなら、アプリをホストするサーバーの設定となる。\n使用しているサーバーはAWSのEC2で、そこでインストールしたMySQLのサーバーでは、特別に設定しない限り、SYSTEMつまりサーバーのシステムのタイムゾーンの設定となります。\nマスターのタイムゾーンをチェックしてみると、\nmysql\u0026gt; SELECT @@SESSION.time_zone, @@GLOBAL.system_time_zone; +---------------------+---------------------------+ | @@SESSION.time_zone | @@GLOBAL.system_time_zone | +---------------------+---------------------------+ | SYSTEM | JST | +---------------------+---------------------------+ で、このSYSTEMはマシンのOSが設定するタイムゾーンJST(Japan Standard Time)になります。\nさて、AWS RDSのレプリカの方は、\nmysql\u0026gt; SELECT @@SESSION.time_zone, @@GLOBAL.system_time_zone; +---------------------+---------------------------+ | @@SESSION.time_zone | @@GLOBAL.system_time_zone | +---------------------+---------------------------+ | UTC | UTC | +---------------------+---------------------------+ もしかして、この違いが問題？\n実際どんなログがレプリカで受け取られているのでしょう。MySQLが作成しているログファイルを読んでみましょう。\nログを読むには、mysqlbinlogを使用します。\n$ mysqlbinlog --no-defaults -v mysql-bin.000004 この出力から、先ほどのtinkerで実行したSQLを探します。\n... # at 35339 #181230 3:28:42 server id 1 end_log_pos 35420 Query thread_id=492197 exec_time=0 error_code=0 SET TIMESTAMP=1546108122/*!*/; SET @@session.sql_mode=1436549152/*!*/; /*!\\C utf8mb4 *//*!*/; SET @@session.character_set_client=224,@@session.collation_connection=224,@@session.collation_server=8/*!*/; SET @@session.time_zone=\u0026#39;SYSTEM\u0026#39;/*!*/; BEGIN /*!*/; # at 35420 #181230 3:28:42 server id 1 end_log_pos 35587 Query thread_id=492197 exec_time=0 error_code=0 SET TIMESTAMP=1546108122/*!*/; update `users` set `name` = \u0026#39;John Doe\u0026#39;, `updated_at` = \u0026#39;2018-12-30 03:28:42\u0026#39; where `id` = 1 /*!*/; ... updateのSQL文ありましたね！そして、その前に実行されている、SET @@session.time_zone=\u0026lsquo;SYSTEM\u0026rsquo;は、タイムゾーンをSYSTEMと設定しています。つまり、レプリカがこのログを受け取り実行するときに、タイムゾーンをSYSTEMとセットしてからレプリカのレコードを更新します。しかし、レプリカにとってはSYSTEMはマスターと違って、JSTではなくUTCです。日本時間をUTCに変更することなく更新しているのです。それゆえに、pt-table-checksumの実行では非同期となるわけです。\n非同期の解決方法 非同期の理由がわかったら、この非同期を解決するのは簡単です。マスターにおいて、タイムゾーンをSYSTEMではなく、Asia/Tokyoを使うようにすればよいだけです（私の環境では、MySQLではJSTの設定ではエラーとなりました。多分にJSTはOSが返す値？）。\nサーバーでのアプリがすべて日本時間帯なら、/etc/my.cnfで設定可能です。しかし、my.cnfが編集できない共有ホストの環境、あるいは同じサーバーに違うタイムゾーン向けのアプリがあるなら、使用するアプリでの設定がベストです。\nLaravelのプロジェクトでの設定は簡単で、config/database.phpで以下のようにtimezoneの行を追加します。\nconfig/database.php ... \u0026#39;connections\u0026#39; =\u0026gt; [ \u0026#39;mysql\u0026#39; =\u0026gt; [ \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;mysql\u0026#39;, \u0026#39;host\u0026#39; =\u0026gt; env(\u0026#39;DB_HOST\u0026#39;, \u0026#39;127.0.0.1\u0026#39;), \u0026#39;port\u0026#39; =\u0026gt; env(\u0026#39;DB_PORT\u0026#39;, \u0026#39;3306\u0026#39;), \u0026#39;database\u0026#39; =\u0026gt; env(\u0026#39;DB_DATABASE\u0026#39;, \u0026#39;forge\u0026#39;), \u0026#39;username\u0026#39; =\u0026gt; env(\u0026#39;DB_USERNAME\u0026#39;, \u0026#39;forge\u0026#39;), \u0026#39;password\u0026#39; =\u0026gt; env(\u0026#39;DB_PASSWORD\u0026#39;, \u0026#39;\u0026#39;), \u0026#39;unix_socket\u0026#39; =\u0026gt; env(\u0026#39;DB_SOCKET\u0026#39;, \u0026#39;\u0026#39;), \u0026#39;charset\u0026#39; =\u0026gt; \u0026#39;utf8mb4\u0026#39;, \u0026#39;collation\u0026#39; =\u0026gt; \u0026#39;utf8mb4_unicode_ci\u0026#39;, \u0026#39;prefix\u0026#39; =\u0026gt; \u0026#39;\u0026#39;, \u0026#39;strict\u0026#39; =\u0026gt; true, \u0026#39;engine\u0026#39; =\u0026gt; null, \u0026#39;timezone\u0026#39; =\u0026gt; \u0026#39;Asia/Tokyo\u0026#39; ], ... tinkerで確認して、前回非同期となったレコードを再度更新します。\nPsy Shell v0.9.3 (PHP 7.1.23 — cli) by Justin Hileman \u0026gt;\u0026gt;\u0026gt; DB::select(DB::raw(\u0026#34;SELECT @@SESSION.time_zone, @@GLOBAL.system_time_zone\u0026#34;)); =\u0026gt; [ {#2316 +\u0026#34;@@SESSION.time_zone\u0026#34;: \u0026#34;Asia/Tokyo\u0026#34;, +\u0026#34;@@GLOBAL.system_time_zone\u0026#34;: \u0026#34;JST\u0026#34;, }, ] \u0026gt;\u0026gt;\u0026gt; $user = User::find(1); [!] Aliasing \u0026#39;User\u0026#39; to \u0026#39;App\\User\u0026#39; for this Tinker session. =\u0026gt; App\\User {#2307 id: 1, name: \u0026#34;John Doe\u0026#34;, email: \u0026#34;test@lotsofbytes.com\u0026#34;, created_at: \u0026#34;2017-01-19 04:57:48\u0026#34;, updated_at: \u0026#34;2018-12-30 03:28:42\u0026#34;, } \u0026gt;\u0026gt;\u0026gt; $user-\u0026gt;update([\u0026#39;name\u0026#39; =\u0026gt; \u0026#39;Tarou Yamada\u0026#39;]); =\u0026gt; true \u0026gt;\u0026gt;\u0026gt; $user = User::find(1); =\u0026gt; App\\User {#2309 id: 1, name: \u0026#34;Tarou Yamada\u0026#34;, email: \u0026#34;test@lotsofbytes.com\u0026#34;, created_at: \u0026#34;2017-01-19 04:57:48\u0026#34;, updated_at: \u0026#34;2018-12-30 04:50:48\u0026#34;, } pt-table-checksumを実行すると、結果は、差分なし！\nChecking if all tables can be checksummed ... Starting checksum ... TS ERRORS DIFFS ROWS DIFF_ROWS CHUNKS SKIPPED TIME TABLE 12-30T04:51:09 0 0 2 0 1 0 0.019 larajapan.migrations 12-30T04:51:09 0 0 1 0 1 0 0.017 larajapan.password_resets 12-30T04:51:09 0 0 5 0 1 0 0.017 larajapan.users 最後に、MySQLのログを見てみます。タイムゾーンは、正しくAsia/Tokyoの設定となっていますね。\n# at 38550 #181230 4:50:48 server id 1 end_log_pos 38635 Query thread_id=492757 exec_time=0 error_code=0 SET TIMESTAMP=1546113048/*!*/; SET @@session.sql_mode=1436549152/*!*/; /*!\\C utf8mb4 *//*!*/; SET @@session.character_set_client=224,@@session.collation_connection=224,@@session.collation_server=8/*!*/; SET @@session.time_zone=\u0026#39;Asia/Tokyo\u0026#39;/*!*/; BEGIN /*!*/; # at 38635 #181230 4:50:48 server id 1 end_log_pos 38810 Query thread_id=492757 exec_time=0 error_code=0 SET TIMESTAMP=1546113048/*!*/; update `users` set `name` = \u0026#39;Tarou Yamada\u0026#39;, `updated_at` = \u0026#39;2018-12-30 04:50:48\u0026#39; where `id` = 1 /*!*/; ","date":"2018-12-30T05:11:06+09:00","permalink":"https://www.larajapan.com/2018/12/30/%E3%83%AC%E3%83%97%E3%83%AA%E3%82%AB%E3%81%A7%E6%B0%97%E3%81%A5%E3%81%84%E3%81%9F%E3%82%BF%E3%82%A4%E3%83%A0%E3%82%BE%E3%83%BC%E3%83%B3%E3%81%AE%E5%95%8F%E9%A1%8C/","title":"レプリカで気づいたタイムゾーンの問題"},{"content":"今回は以前話した、MySQLデータベースのレプリカの話の続きです。\n前回はデータベースのレプリカの説明をしましたが、本当にマスターのデータベースと同期しているの？とすぐに疑います。 チェックしてみましょう！\n調べると、checksum tableというMySQLコマンドが使えるらしい。チェックサムはある計算式をデータに適用してユニークな数を出力するものです。\nmysql\u0026gt; use larajapan; Database changed mysql\u0026gt; checksum table migrations, password_resets, users; +---------------------------+------------+ | Table | Checksum | +---------------------------+------------+ | larajapan.migrations | 269719583 | | larajapan.password_resets | 3233767743 | | larajapan.users | 669472749 | +---------------------------+------------+ 3 rows in set (0.00 sec) 実行してみるとかなりな速さで結果を出してくれます。私のテストでは500万レコード数もあるテーブルでたったの16秒！\nこれを、マスターとそのレプリカで、すべてのテーブルに対して実行して比較すれば良いですね。しかし、実際に使用されているDBでは、常に新しいレコードは追加される、更新はある。。。と考えると、ある1時点で両者のデータを比較するのは複雑な作業に見えてきます。\nそこで見つけたのが、perconaのツール。perconaはかなり昔からMySQLなどのオープンソースのデータベースの管理をサービスとしている会社です。\n準備 AWS EC2では、以下でインストールできます。 $ sudo yum install http://www.percona.com/downloads/percona-release/redhat/0.1-6/percona-release-0.1-6.noarch.rpm たくさんのツール（perlで書かれている）がインストールされますが、今回必要なツールはマスターとレプリカのデータの同期をチェックする以下のコマンドです。\npt-table-checksum\nしかし、このツール、オプションがたくさんあり、ある程度の準備も要る複雑なツールです。私も理解まで何回も何回も試行しました。特に、マスターのDBとレプリカのDBで、MySQLのユーザー名やパスワードが違うとその指定のためにコマンドラインが長くなり、実行にはスクリプトの作成をした方が良いです。\nちなみに、今回は、以前と同様に、マスターはEC2でのMySQL、レプリカは、AWSのRDSのMySQLという仮定です。もちろんレプリカのサーバーにはsshできないので、マスターで実行します。\n準備として、以下のMySQLの情報を用意してください。\nマスターのユーザー名 マスターのパスワード レプリカのユーザー名 レプリカのパスワード\nマスターやレプリカのMySQLユーザーは以下の権限が必要ですが、以下のように特別に作成しなくとも、通常はrootのような管理権限のあるユーザーで足ります。\nmysql\u0026gt; GRANT SELECT ON larajapan.* TO \u0026#39;checksum_user\u0026#39;@\u0026#39;%\u0026#39; IDENTIFIED BY \u0026#39;checksum_password\u0026#39;; mysql\u0026gt; GRANT REPLICATION SLAVE,PROCESS,SUPER ON *.* TO `checksum_user`@\u0026#39;%\u0026#39;; mysql\u0026gt; GRANT ALL PRIVILEGES ON percona.* TO `checksum_user`@\u0026#39;%\u0026#39;; 次に、ツールが出力するデータを保存するためのデータベース（percona）とテーブル（checksums）を作成します。\nmysql\u0026gt; create databaser percona; ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near \u0026#39;databaser percona\u0026#39; at line 1 mysql\u0026gt; create database percona; Query OK, 1 row affected (0.00 sec) mysql\u0026gt; use percona Database changed mysql\u0026gt; CREATE TABLE checksums ( db CHAR(64) NOT NULL, tbl CHAR(64) NOT NULL, chunk INT NOT NULL, chunk_time FLOAT NULL, chunk_index VARCHAR(200) NULL, lower_boundary TEXT NULL, upper_boundary TEXT NULL, this_crc CHAR(40) NOT NULL, this_cnt INT NOT NULL, master_crc CHAR(40) NULL, master_cnt INT NULL, ts TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (db, tbl, chunk), INDEX ts_db_tbl (ts, db, tbl) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; Query OK, 0 rows affected (0.01 sec) マスターとレプリカの両方で作成が必要ですが、ツールの最初の実行でマスターでは自動的に作成されます。\n実行 ここで、まず実行してみましょう。運が良ければここで以下のような出力となります。 $ pt-table-checksum -d larajapan -u checksum_user -p checksum_password --recursion-method processlist --no-check-binlog-format --no-check-replication-filters Checking if all tables can be checksummed ... Starting checksum ... TS ERRORS DIFFS ROWS DIFF_ROWS CHUNKS SKIPPED TIME TABLE 11-09T07:54:37 0 0 2 0 1 0 1.019 larajapan.migrations 11-09T07:54:37 0 0 1 0 1 0 0.017 larajapan.password_resets 11-09T07:54:37 0 0 5 0 1 0 0.014 larajapan.users ERRORSとDIFFSの列、これが皆ゼロならマスターとレプリカは同期しています。心配なしです。\nさて、どうしてレプリカのホスト情報をコマンドラインに入れていないのにレプリカのチェックが可能なのでしょう？\nそれは、\u0026ndash;recursion-method processlistの部分です（デフォルトでは、ここはprocesslist, hostsとなるのですが、私のトライではhostsの部分でエラーになりました)。\nMySQLのshow processlistコマンドを実行すると、\nmysql\u0026gt; show processlist; +-----+------------+-----------------------------------------------+--------------+-------------+-------+-----------------------------------------------------------------------+------------------+ | Id | User | Host | db | Command | Time | State | Info | +-----+------------+-----------------------------------------------+--------------+-------------+-------+-----------------------------------------------------------------------+------------------+ | 2 | slave_user | ip-10-0-1-32.us-west-2.compute.internal:64617 | NULL | Binlog Dump | 14651 | Master has sent all binlog to slave; waiting for binlog to be updated | NULL | | 918 | root | localhost | NULL | Query | 0 | NULL | show processlist | | 919 | root | localhost | larajapan_wp | Sleep | 0 | | NULL | +-----+------------+-----------------------------------------------+--------------+-------------+-------+-----------------------------------------------------------------------+------------------+ 3 rows in set (0.00 sec) レプリカのホスト情報が出て来るのでそれを利用しているのです。\nまた、ここでは、レプリカでのMySQLのユーザー名とパスワードは、マスターと同じと仮定しています。これが違うなら、\n\u0026ndash;slave-user admin \u0026ndash;slave-password pass\nの指定がコマンドラインで必要となります。\n本当にチェックしている？ ひとまず成功したところで、本当に差分を出してくれるか興味あるところです。レプリカにおいて、情報変えてみましょう。\nmysql\u0026gt; delete from users where id = 6; Query OK, 1 row affected (0.01 sec) レプリカのusersのテーブルのレコードを１つ削除しました。\nそして再度実行です。\n$ pt-table-checksum -d larajapan -u checksum_user -p checksum_password --recursion-method processlist --no-check-binlog-format --no-check-replication-filters Checking if all tables can be checksummed ... Starting checksum ... TS ERRORS DIFFS ROWS DIFF_ROWS CHUNKS SKIPPED TIME TABLE 11-09T08:18:43 0 0 2 0 1 0 0.021 larajapan.migrations 11-09T08:18:43 0 0 1 0 1 0 0.017 larajapan.password_resets 11-09T08:18:43 0 1 5 0 1 0 0.019 larajapan.users チェックしてくれましたね。DIFFSに1が表示されているのがそうです。\nデバッグ このコマンドラインでは、本当にたくさんのオプションがあるのでいろいろなチェックが可能なのですが、エラーが理解不可能なときは、以下のようにデバッグモードを使ってみるのが良いです。stderrに情報がたくさん出力されるので以下のようにファイルにキャプチャする必要あります。\n$ PTDEBUG=1pt-table-checksum \u0026gt; FILE 2\u0026gt;\u0026amp;1 また、オプションのデフォルトも把握することも大事です。\n$ pt-table-checksum --help の実行でデフォルトの情報が得られます。\n","date":"2018-11-10T01:56:48+09:00","permalink":"https://www.larajapan.com/2018/11/10/mysql%E3%83%AC%E3%83%97%E3%83%AA%E3%82%AB%E3%81%AE%E5%90%8C%E6%9C%9F%E3%83%81%E3%82%A7%E3%83%83%E3%82%AF/","title":"MySQLレプリカの同期チェック"},{"content":"前回は、チャンキングでプログラムが使用するメモリの量を抑えることを説明しましたが、実行速度はどうなのでしょう？\n私の仮想マシンで測定してみました。50万のレコードがあります。\n1000レコードごとのチャンキングは、1分32秒。\nチャンキングなしで、メモリ制限を1ギガバイトとして、46秒。\nチャンキングの方が2倍以上時間かかっています。\nチャンキングがどのようなSQLのクエリを実行するか、tinkerで見てみます。数が多いので、ここでは10,000レコードごとのチャンキングにしています。\n\u0026gt;\u0026gt;\u0026gt; DB::enableQueryLog(); =\u0026gt; null \u0026gt;\u0026gt;\u0026gt; DB::table(\u0026#39;users\u0026#39;)-\u0026gt;count(); =\u0026gt; 500000 \u0026gt;\u0026gt;\u0026gt; App\\User::chunk(10000, function($rows) { }); =\u0026gt; true \u0026gt;\u0026gt;\u0026gt; DB::getQueryLog(); =\u0026gt; [ [ \u0026#34;query\u0026#34; =\u0026gt; \u0026#34;select count(*) as aggregate from `users`\u0026#34;, \u0026#34;bindings\u0026#34; =\u0026gt; [], \u0026#34;time\u0026#34; =\u0026gt; 152.78, ], [ \u0026#34;query\u0026#34; =\u0026gt; \u0026#34;select * from `users` order by `users`.`id` asc limit 10000 offset 0\u0026#34;, \u0026#34;bindings\u0026#34; =\u0026gt; [], \u0026#34;time\u0026#34; =\u0026gt; 76.14, ], [ \u0026#34;query\u0026#34; =\u0026gt; \u0026#34;select * from `users` order by `users`.`id` asc limit 10000 offset 10000\u0026#34;, \u0026#34;bindings\u0026#34; =\u0026gt; [], \u0026#34;time\u0026#34; =\u0026gt; 59.68, ], ... 途中はスキップ [ \u0026#34;query\u0026#34; =\u0026gt; \u0026#34;select * from `users` order by `users`.`id` asc limit 10000 offset 480000\u0026#34;, \u0026#34;bindings\u0026#34; =\u0026gt; [], \u0026#34;time\u0026#34; =\u0026gt; 246.93, ], [ \u0026#34;query\u0026#34; =\u0026gt; \u0026#34;select * from `users` order by `users`.`id` asc limit 10000 offset 490000\u0026#34;, \u0026#34;bindings\u0026#34; =\u0026gt; [], \u0026#34;time\u0026#34; =\u0026gt; 227.26, ], [ \u0026#34;query\u0026#34; =\u0026gt; \u0026#34;select * from `users` order by `users`.`id` asc limit 10000 offset 500000\u0026#34;, \u0026#34;bindings\u0026#34; =\u0026gt; [], \u0026#34;time\u0026#34; =\u0026gt; 191.65, ], ] チャンキングでは、limit 10000 offset 0のように、開始するレコードの場所と取得する数をクエリで指定しています。開始するレコードの場所がここでは10,000ごと増えていきます。そして、そのためにクエリの実行がだんだん遅くなっていきます。遅さは加速されているようで、最後の方では最初の5倍くらい時間がかかっています。\nレコード数が5百万あるテーブルの実験では、1000のチャンキングでは百万程度から非常に遅くなっていき、何時間たっても終わらず、いつまでも終わらないのではないか、くらいのスピードになりました。\nメモリの使用は控えてくれるけれど、レコード数が多い（とても多い）ときの処理時間が途方もなく長くなります。\nどう解決したらよいかと考えて、思いついたのが以下の方法です。まずコードを見てください。\n... $max = User::max(\u0026#39;id\u0026#39;); $unit = 1000; for ($i = 0; $i \u0026lt; intval(ceil($max/$unit)); $i++) { $from = $i * $unit + 1; $to = ($i + 1) * $unit; $rows = User::where(\u0026#39;id\u0026#39;, \u0026#39;\u0026gt;=\u0026#39;, $from) -\u0026gt;where(\u0026#39;id\u0026#39;, \u0026#39;\u0026lt;=\u0026#39;, $to) -\u0026gt;get(); foreach ($rows as $row) { $values = [ $row-\u0026gt;id, $row-\u0026gt;created_at, $row-\u0026gt;name, $row-\u0026gt;email ]; fputcsv($fh, $values); } } ... チャンキングと似ていますが、limit, offsetを使用するのではなく、idの値の1000ごとの範囲を指定してループします。idはDBテーブルのプライマリーキーゆえにスピード向上される、という仮定です。\n測定してみました。処理時間は予想通り縮まり45秒となりました。チャンキングなしとほぼ同じです。しかも使用メモリは抑えられて。\nレコード数だけでなく対象のDBテーブルのサイズなどとも関係があると思いますが、同じような状況に至ったときには試してみてください。\n","date":"2018-10-29T01:52:51+09:00","permalink":"https://www.larajapan.com/2018/10/29/%E3%83%81%E3%83%A3%E3%83%B3%E3%82%AD%E3%83%B3%E3%82%B0%E3%81%AF%E9%81%85%E3%81%84/","title":"チャンキングは遅い"},{"content":"データベースの含まれるレコード数が増えてくると今まで経験しなかったことを経験するようになります。\nまず最初に出くわすのが、以下のようなコマンドラインでのコマンド実行のエラーです。\n$ php artisan command:dump HP Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 56000000 bytes) in /vol1/usr/www/larajapan/repos/larajapan/vendor/laravel/framework/src/Illuminate/Database/Connection.php on line 330 In Connection.php line 330: Allowed memory size of 134217728 bytes exhausted (tried to allocate 56000000 bytes) これは、実行しているプログラムのメモリの使用が現在設定しているphpのメモリ制限を超えようとしたよ、という親切なエラーです。確かに、php.iniで設定されているデフォルトの134217728バイト（128メガバイト）は小さ過ぎますね。\nphpのプロセスのメモリ使用制限は、システム変数memory_limitで設定できるので、以下のように簡単に変更できます。 128メガバイトから倍の256メガバイトに増やします。\n... ini_set(\u0026#39;memory_limit\u0026#39;, 256M\u0026#39;); ... しかし、それでもエラーとなります。\nPHP Fatal error: Allowed memory size of 268435456 bytes exhausted (tried to allocate 2097152 bytes) in /vol1/usr/www/larajapan/repos/larajapan/vendor/laravel/framework/src/Illuminate/Database/Connection.php on line 332 In Connection.php line 332: Allowed memory size of 268435456 bytes exhausted (tried to allocate 2097152 bytes) もっと使用可能なメモリを増やすべき？\nここで、どんなプログラムを実行しているか見てみましょう。\napp/Console/Commands/DumpCommand.php namespace App\\Console\\Commands; use Illuminate\\Console\\Command; use App\\User; class DumpCommand extends Command { /** * The name and signature of the console command. * * @var string */ protected $signature = \u0026#39;command:dump\u0026#39;; /** * The console command description. * * @var string */ protected $description = \u0026#39;Dump data in csv format\u0026#39;; /** * Create a new command instance. * * @return void */ public function __construct() { parent::__construct(); } /** * Execute the console command. * * @return mixed */ public function handle() { ini_set(\u0026#39;memory_limit\u0026#39;, \u0026#39;256M\u0026#39;); $fields = [ \u0026#39;id\u0026#39;, \u0026#39;created_at\u0026#39;, \u0026#39;name\u0026#39;, \u0026#39;email\u0026#39; ]; // CSV出力 $fh = fopen(\u0026#39;users.csv\u0026#39;, \u0026#39;w\u0026#39;); fputs($fh, chr(0xEF).chr(0xBB).chr(0xBF)); fputcsv($fh, $fields); $rows = User::all(); foreach ($rows as $row) { $values = [ $row-\u0026gt;id, $row-\u0026gt;created_at, $row-\u0026gt;name, $row-\u0026gt;email ]; fputcsv($fh, $values); } fclose($fh); //ここに使用したメモリを表示 printf(\u0026#34;memory usage: %s\\n\u0026#34;, $this-\u0026gt;formatBytes(memory_get_peak_usage(true))); return 0; } // バイトをメガバイトに変更 public static function formatBytes($size, $precision = 0) { $base = log($size, 1024); $suffixes = array(\u0026#39;\u0026#39;, \u0026#39;K\u0026#39;, \u0026#39;M\u0026#39;, \u0026#39;G\u0026#39;, \u0026#39;T\u0026#39;); return round(pow(1024, $base - floor($base)), $precision) . $suffixes[floor($base)]; } } このプログラムはＤＢテーブルのデータをすべてCSVのファイルにエクスポートするプログラムです。プログラムの最後に、どれだけメモリを使用したかを表示するようにしていますが、先の実行では、途中でエラーとなっためでそこまで到達していません。\nさらにメモリを増やしてみましょう。今度は1ギガバイトまで増やして、つまりini_set(\u0026lsquo;memory_limit\u0026rsquo;, \u0026lsquo;1G\u0026rsquo;)と設定して実行してみます。\nmemory usage: 804M 実行は成功です！そして、メモリがピーク時には804メガバイト使用されていることがわかりました。\n昨今のマシンでは1ギガバイトくらいのメモリ使用はたいしたことありません。しかし、実行の前に途中でエラーにならないように、前もってどこまで増やせばよいのでしょう？\n今回はテストのために、50万のレコードを用意しました。私のお客さんのＤＢではその10倍ものレコード数があるＤＢテーブルがあります。\nここで、もう一度プログラムを見てみます。このプログラムは、データベースのデータをCSVのファイルにエクスポートするプログラムですが、ファイルに書き込む前に、すべてのレコードを変数：$rowsにいったん入れています、つまりメモリの保存しているわけで、レコード数が多くなればなるほどメモリが必要となります。しかし、前もってどれだけメモリを使用するかの計算は難しいです。\nもっとメモリの使用を低く抑えてマネージすることはできないのでしょうか？\nそこで登場するのがEloquentのchunk()、これで上のプログラムのループ部分を以下のように書き変えます。\n... User::chunk(1000, function($rows) use($fh) { foreach ($rows as $row) { $values = [ $row-\u0026gt;id, $row-\u0026gt;created_at, $row-\u0026gt;name, $row-\u0026gt;email ]; fputcsv($fh, $values); } }); ... 最初からすべてのレコードを取ってくるのではなく、まず、レコードを1000個のチャンクで取ってきてクロージャの部分を実行し、そして次の1000個を取得して処理と。。それゆえに、$rowsに保存するデータは前回に比べてはるかに少なくなります。\nこれを実行すると、使用するメモリは驚くほど少なくなりました。\nmemory usage: 14M ","date":"2018-10-21T04:50:49+09:00","permalink":"https://www.larajapan.com/2018/10/21/%E3%83%81%E3%83%A3%E3%83%B3%E3%82%AD%E3%83%B3%E3%82%B0%E3%81%A7%E4%BD%BF%E7%94%A8%E3%83%A1%E3%83%A2%E3%83%AA%E3%82%92%E6%8A%91%E3%81%88%E3%82%8B/","title":"チャンキングで使用メモリを抑える"},{"content":"開発をしていて、未定義のキーや属性でのアクセスエラーは誰しも経験します。\n配列のキー(price)が未定義なら、\n\u0026gt;\u0026gt;\u0026gt; $p1 = [\u0026#39;name\u0026#39; =\u0026gt; \u0026#39;apple\u0026#39;]; =\u0026gt; [ \u0026#34;name\u0026#34; =\u0026gt; \u0026#34;apple\u0026#34;, ] \u0026gt;\u0026gt;\u0026gt; $p1[\u0026#39;name\u0026#39;]; =\u0026gt; \u0026#34;apple\u0026#34; \u0026gt;\u0026gt;\u0026gt; $p1[\u0026#39;price\u0026#39;]; PHP Notice: Undefined index: price in Psy Shell code on line 1 オブジェクトの属性(price)が未定義なら、\n\u0026gt;\u0026gt;\u0026gt; $p2 = (object)[\u0026#39;name\u0026#39; =\u0026gt; \u0026#39;apple\u0026#39;]; =\u0026gt; {#2318 +\u0026#34;name\u0026#34;: \u0026#34;apple\u0026#34;, } \u0026gt;\u0026gt;\u0026gt; $p2-\u0026gt;name; =\u0026gt; \u0026#34;apple\u0026#34; \u0026gt;\u0026gt;\u0026gt; $p2-\u0026gt;price; PHP Notice: Undefined property: stdClass::$price in Psy Shell code on line 1 なんのことはない、このような場合は、未定義のpriceを初期化してあげればよいですね。\nしかし、この初期化、都合によってはできないときあります。画面のフォームにおいて条件によりpriceの入力をするときと、しないときがあるとか。\nそのときには、isset()の関数を使用して、キーや属性が定義されているかどうかをチェックします。\n\u0026gt;\u0026gt;\u0026gt; isset($p1[\u0026#39;name\u0026#39;]); =\u0026gt; true \u0026gt;\u0026gt;\u0026gt; isset($p1[\u0026#39;price\u0026#39;]); =\u0026gt; false \u0026gt;\u0026gt;\u0026gt; isset($p2[\u0026#39;name\u0026#39;]); =\u0026gt; true \u0026gt;\u0026gt;\u0026gt; isset($p2-\u0026gt;price); =\u0026gt; false そして、未定義のエラーを出さずにデフォルトの値を割り当てたいなら、三項演算子を利用します。\n\u0026gt;\u0026gt;\u0026gt; $name = isset($p1[\u0026#39;name\u0026#39;]) ? $p1[\u0026#39;name\u0026#39;] : \u0026#39;\u0026#39;; =\u0026gt; \u0026#34;apple\u0026#34; \u0026gt;\u0026gt;\u0026gt; $price = isset($p1[\u0026#39;price\u0026#39;]) ? $p1[\u0026#39;price\u0026#39;] : 0; =\u0026gt; 0 \u0026gt;\u0026gt;\u0026gt; $name = isset($p2-\u0026gt;name) ? $p2-\u0026gt;name : \u0026#39;\u0026#39;; =\u0026gt; \u0026#34;apple\u0026#34; \u0026gt;\u0026gt;\u0026gt; $price = isset($p2-\u0026gt;price) ? $p2-\u0026gt;price : 0; =\u0026gt; \u0026#34;\u0026#34; 繰り返しの記述が面倒なら、Laravelでは便利なヘルパーあります：array_get(), data_get()\n\u0026gt;\u0026gt;\u0026gt; $name = array_get($p1, \u0026#39;name\u0026#39;, \u0026#39;\u0026#39;); =\u0026gt; \u0026#34;apple\u0026#34; \u0026gt;\u0026gt;\u0026gt; $price = array_get($p1, \u0026#39;price\u0026#39;, 0); =\u0026gt; 0 \u0026gt;\u0026gt;\u0026gt; $name = data_get($p2, \u0026#39;name\u0026#39;, \u0026#39;\u0026#39;); =\u0026gt; \u0026#34;apple\u0026#34; \u0026gt;\u0026gt;\u0026gt; $price = data_get($p2, \u0026#39;price\u0026#39;, 0); =\u0026gt; 0 そして、php7なら、Null合体演算子を利用しても同様なことができます。\n\u0026gt;\u0026gt;\u0026gt; $name = $p1[\u0026#39;name\u0026#39;] ?? \u0026#39;\u0026#39;; =\u0026gt; \u0026#34;apple\u0026#34; \u0026gt;\u0026gt;\u0026gt; $price = $p1[\u0026#39;price\u0026#39;] ?? 0; =\u0026gt; 0 \u0026gt;\u0026gt;\u0026gt; $name = $p2-\u0026gt;name ?? \u0026#39;\u0026#39;; =\u0026gt; \u0026#34;apple\u0026#34; \u0026gt;\u0026gt;\u0026gt; $price = $p2-\u0026gt;price ?? 0; =\u0026gt; 0 ","date":"2018-10-16T03:01:51+09:00","permalink":"https://www.larajapan.com/2018/10/16/%E6%9C%AA%E5%AE%9A%E7%BE%A9%E3%81%AE%E3%82%AD%E3%83%BC%E3%82%84%E5%B1%9E%E6%80%A7%E3%82%92%E3%83%81%E3%82%A7%E3%83%83%E3%82%AF/","title":"未定義のキーや属性をチェック"},{"content":"今回は、他の項目により対象の項目が必須となるかどうかのルールを紹介します。\nrequired_if required_unless required_with required_with_all required_without required_without_all\n","date":"2018-10-07T09:55:41+09:00","permalink":"https://www.larajapan.com/2018/10/07/%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E5%AE%9F%E4%BE%8B%EF%BC%9A%E6%9D%A1%E4%BB%B6%E4%BB%98%E3%81%8D%E3%81%AE%E5%BF%85%E9%A0%88/","title":"バリデーションの実例：条件付きの必須"},{"content":"以下のリンクの記事掲載のコードに更新があります。\nバリデーションの実例：基本データタイプ バリデーションの実例：特殊データタイプ バリデーションの実例：日付 バリデーションの実例：定数値を引数とする条件 バリデーションの実例：他の項目の値を引数とする条件 バリデーションの実例：DBの情報を引数とする条件\n入力には項目があり値がある（nullや空でない）という仮定として、チェックのルールにrequiredを追加して、以下のテストを削除しました。すっきりしてわかりやすくなったと思います。\n[[\u0026#39;field\u0026#39; =\u0026gt; null], false], [[\u0026#39;field\u0026#39; =\u0026gt; \u0026#39;\u0026#39;], true], [[\u0026#39;field\u0026#39; =\u0026gt; \u0026#39; \u0026#39;], true], // space しかし、acceptedのように空の値が重要なときには上のテストは省かれていません。\n","date":"2018-10-07T05:31:08+09:00","permalink":"https://www.larajapan.com/2018/10/07/%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E5%AE%9F%E4%BE%8B%EF%BC%9A%E6%9B%B4%E6%96%B0/","title":"バリデーションの実例：更新"},{"content":"昔からMySQLでレプリカ（複製）が作成できる機能の存在は知っていたけれど、大きなスケールのサイトで、ロードバランスに使用される読み込み専門のレプリカとして使われるだろうな、くらいに思っていました。しかし、最近はこれがほぼリアルタイムに近いバックアップとしても使える可能性を知り、早速取り組んだ次第です。\nAWS RDS MySQLのデータベースのレプリカを作成するには、もちろんレプリカのソースとなるマスターのMySQLサーバーがあります。私のケースでは、LAMPなのでマスターはAWSのEC2のマシンに常駐サービスの１つとして存在しますが、さてレプリカはどこに置いたら良いでしょう？\nレプリカと言ってもこれもまたMySQLサーバーが必要なので、新規のEC2のマシンを用意して、そこにレプリカを置くことになります。しかし単にレプリカのためだけの目的で、もう１つマシンを用意するのはちょっとです。将来においてマシンのOS更新とかバックアップの設定とかメンテが面倒です。そこで注目したのは、AWSのRDSです。今時流行りのSAAS（Software AS Service）です。\n今まで使ったことがないAWSのサービスなので親交を深める良い機会です。と思って取り組みましたが、はっきり言って、手順が複雑です。数日かけて何回も失敗して、レプリカのＤＢを何回も作成し直して。。。ということで、忘れないように貴重な（少なくとも私には）手順の作成となりました。\n１．レプリカのDBインスタンスの作成（AWS） まずは、AWS RDSのページへ行き、レプリカを保存するDBインスタンス（マシン）の作成です。以下へのアクセスは、AWSへのアカウントへのログインが必要です。\nhttps://ap-northeast-1.console.aws.amazon.com/rds/home?region=ap-northeast-1#launch-dbinstance:\nその後以下のステップを経て、DBインスタンスの作成です。AWSでは「データベースの作成」という言葉が使用されていますが、実際は、DBインスタンス、つまりマシンの作成です。１つのDBインスタンスで、いくつものデータベースを作成が可能ですので。混乱しないように注意してください。\nステップ１　エンジンの選択 ここでは、「エンジン」にはMySQLを選択します。また、最初のテスト段階では必ず「無料バージョン」を選択するのを忘れずに。\nステップ２ DB詳細の指定 ここでは、とくに「DBエンジンのバージョン」に注意してください。なるべくマスターと同じMySQLのバージョンの使用を薦めます。すでに「無料利用枠」を選択しているなら、「DBインスタンスのクラス」は、一番小さいマシンのt2.microしか選択できません。他のオプションも選択不可となっています。下の画面には表示されていませんが、「DBインスタンの識別子」や「マスターユーザーの名前」と「マスターパスワード」の入力も必要です。\nステップ３ 詳細設定の指定 この画面での注意としては、「パブリックアクセシビリティ」では「いいえ」を選択すること。いろいろなケースがありますが、今回はマスターとのコミュニケーションをプライベートのIPアドレスを通して行うゆえにです。さらに、「アベイラビリティーゾーン」では、必ずマスターとは違うゾーンを選ぶことです。ゾーンは物理的に独立した場所にあるデータセンターなので、片方で火事などが起こってデータを失ってもも片方ではＯＫという仮定です。ここでは触れませんが、このためにVPCのサブネットを前もって作成する必要があるかもしれません。\n完了！ これでDBインスタンス作成の設定が完了です。これから数分間でDBインスタンスへのアクセスが可能となります。\n２．マスターでの設定と作業 レプリカのDBインスタンスを作成したところで、今度はマスターでの設定です。いくつかの作業があります。\nレプリカのログの作成 マスターのMySQLサーバーにおいて、レプリカのためのログの作成が必要です。このログは、マスターのDBで実行されるすべてのSQL文を皆バイナリのファイルに入れたようなものです。それをレプリカが定期的にのMySQLサーバーが取りに行き、レプリカでそれらを実行してマスターと同期するわけです。\nログのファイルは以下のように番号が付いたファイルとして作成されます。最新のものほど大きい番号となります。\n$ ls -1 mysql-bin.000001 mysql-bin.000002 mysql-bin.000003 mysql-bin.index このログ作成の設定は、マスターのマシンの/etc/my.cnfで行います。\n/etc/my.cnf ... [mysqld] ... server-id=1\tbinlog-do-db = test1_db binlog-do-db = test2_db log-bin=/var/lib/mysql/logs/mysql-bin.log\t... server-idはユニークな数字のIDでレプリカのIDとは重複しないようにしてください。私の設定ではAWS RDSのレプリカのserver-idは非常に大きな数字（例：2092607505）なので小さい数字ならとりあえずＯＫです。binlog-do-dbではレプリカを作成したいDB名を指定します。複数のときは例のように複数行で指定します。log-binは、ログファイルの場所とそのファイル名のフォーマットを指定します。\n設定後に、MySQLサーバーをリスタートします。もちろんその前にLaravelのアプリなら、\n$ php artisan down や、クロンジョブなどのサービスをダウンさせること忘れないでください。\nリスタート後に、MySQLのコマンドで、マスターのステータスを見ることができます。\nmysql\u0026gt; show master status; +------------------+----------+----------------------------------+------------------+ | File | Position | Binlog_Do_DB | Binlog_Ignore_DB | +------------------+----------+----------------------------------+------------------+ | mysql-bin.000001 | 107 | test1_db, test2_db | | +------------------+----------+----------------------------------+------------------+ 1 row in set (0.00 sec) この時点では、Laravelのアプリもクロンジョブも再開してＯＫです。\nレプリカのためのDBユーザーの作成 今度は、レプリカのMySQLがマスターのログからデータを取得するために、マスターにおいてDBユーザーを作成します。\n$ mysql -u root -p ... mysql\u0026gt; GRANT REPLICATION SLAVE ON *.* TO \u0026#39;slave_user\u0026#39;@\u0026#39;%\u0026#39; IDENTIFIED BY \u0026#39;testtest\u0026#39;; mysql\u0026gt; FLUSH PRIVILEGES; 最初の行は、slave_userがtesttestのパスワードでログインして複製の情報を取得することをＯＫしています。ここではどこのホストからでもマスターのどのデータベースの複製がＯＫという設定です。\nレプリカからマスターへのアクセス制限 ここではインスタンス（マシン）レベルで、レプリカがマスターにアクセスできる設定をします。 まず、レプリカのDBインスタンスのＩＰアドレスが必要です。DBインスタンスの情報画面で、まず「エンドポイント」のホスト名を取得します。\nそれを、シェルでpingします。\n$ ping db1.xxxxxx.us-west-2.rds.amazonaws.com PING db1.xxxxxx.us-west-2.rds.amazonaws.com (10.0.1.32) 56(84) bytes of data. そのIPアドレスを今度は、AWSのコンソールの「セキュリティグループ」で以下のように、ポート3306に対してオープンします。このセキュリティグループはマスターで使用されているものです。\nレプリカの設定 もうゴールは近しです。マスターのデータをレプリカにコピーしてレプリカの開始です。\nマスターからレプリカへのアクセスの設定 先ほどとは逆に、今度はマスターからレプリカへアクセスできるための設定です。AWSコンソールで先と同様な作業ですが、今度は、レプリカの「ステップ３ 詳細設定の指定」の画面で作成したあるいは指定したセキュリティグループにおいて、マスターからのアクセスを許します。\n画面での、10.0.0.137は、マスターのプライベートのIPアドレスです。\nマスターのデータをレプリカにコピーして複製を開始 まず、マスターで対象のデータベースのデータをエクスポートします。\n$ mysqldump -u root test1_db --master-data -p \u0026gt; test1_db.sql 次には、レプリカにアクセスしてそのデータをレプリカにインポートします。\n$ mysql -u root -h db1.xxxxxx.us-west-2.rds.amazonaws.com -p ... mysql\u0026gt; source test1_db.sql ... 引き続いて、レプリカのMySQLコマンドでマスターを設定して、複製を開始します。\nmysql\u0026gt; CALL mysql.rds_set_external_master (\u0026#39;10.0.0.137\u0026#39;, 3306, \u0026#39;slave_user\u0026#39;, \u0026#39;testtest\u0026#39;, \u0026#39;mysql-bin.000001\u0026#39;, 107, 0);\tmysql\u0026gt; CALL mysql.rds_start_replication; 最初の行の、\u0026lsquo;mysql-bin.000001\u0026rsquo;, 107の数字は、test_db1.sqlのファイルの最初のページから情報が得られます（以下）。\n-- -- Position to start replication or point-in-time recovery from -- CHANGE MASTER TO MASTER_LOG_FILE=\u0026#39;mysql-bin.000001\u0026#39;, MASTER_LOG_POS=107; エクスポートの時点でのどのログファイル、どの位置かがこれでわかるため、エクスポートからインポートの間にどんなに時間が開いても、どんな変更があっても、そこの場所から複製を開始してマスターと同期できるわけです。\n最後に、レプリカの複製の状況を見たいときは、コマンドshow slave statusを実行します。ここで問題があれば、エラーとして表示されますので注意をしてください。\nmysql\u0026gt; show slave status\\G *************************** 1. row *************************** Slave_IO_State: Waiting for master to send event Master_Host: 10.0.0.137 Master_User: slave_user Master_Port: 3306 Connect_Retry: 60 Master_Log_File: mysql-bin.000003 Read_Master_Log_Pos: 8148599 Relay_Log_File: relaylog.002965 Relay_Log_Pos: 658 Relay_Master_Log_File: mysql-bin.000003 Slave_IO_Running: Yes Slave_SQL_Running: Yes Replicate_Do_DB: Replicate_Ignore_DB: Replicate_Do_Table: Replicate_Ignore_Table: mysql.rds_sysinfo,mysql.rds_history,mysql.rds_replication_status Replicate_Wild_Do_Table: Replicate_Wild_Ignore_Table: Last_Errno: 0 Last_Error: Skip_Counter: 0 Exec_Master_Log_Pos: 8148599 Relay_Log_Space: 1357 Until_Condition: None Until_Log_File: Until_Log_Pos: 0 Master_SSL_Allowed: No Master_SSL_CA_File: Master_SSL_CA_Path: Master_SSL_Cert: Master_SSL_Cipher: Master_SSL_Key: Seconds_Behind_Master: 0 Master_SSL_Verify_Server_Cert: No Last_IO_Errno: 0 Last_IO_Error: Last_SQL_Errno: 0 Last_SQL_Error: Replicate_Ignore_Server_Ids: Master_Server_Id: 1 1 row in set (0.00 sec) マスター側では、レプリカのRDSからログを取得に来ているのが以下でわかります。\n$ mysql -u root -p ... mysql\u0026gt; show processlist; +--------+------------+-----------------------------------------------+------+-------------+--------+-----------------------------------------------------------------------+------------------+ | Id | User | Host | db | Command | Time | State | Info | +--------+------------+-----------------------------------------------+------+-------------+--------+-----------------------------------------------------------------------+------------------+ | 54532 | slave_user | ip-10-0-1-32.us-west-2.compute.internal:47756 | NULL | Binlog Dump | 440946 | Master has sent all binlog to slave; waiting for binlog to be updated | NULL | | 102446 | root | localhost | NULL | Query | 0 | NULL | show processlist | +--------+------------+-----------------------------------------------+------+-------------+--------+-----------------------------------------------------------------------+------------------ ","date":"2018-10-02T11:33:08+09:00","permalink":"https://www.larajapan.com/2018/10/02/mysql%E3%83%87%E3%83%BC%E3%82%BF%E3%83%99%E3%83%BC%E3%82%B9%E3%81%AE%E3%83%AC%E3%83%97%E3%83%AA%E3%82%AB%E3%81%ABaws-rds%E3%82%92%E5%88%A9%E7%94%A8/","title":"MySQLデータベースのレプリカにAWS RDSを利用"},{"content":"募集は終了しました。\nLaravelのプログラマーを募集しています。もちろん欲しいスキルはいくつか（以下）あるのですが、支給は大企業レベルではありません。私のように独立して、いくつかのプロジェクトを抱えているフリーランス系の人が欲しいです。社員となるのではなく、お互いにプロジェクトや知識を、将来共有できる人が良いです。\n仕事内容 ●　ウェブアプリ開発・管理 ●　システム管理（システムを熟知してから） ●　お客さんサポート（システムを熟知してから） 必須のスキル！ ●　Laravel 5.5以上のアプリ開発経験 ●　gitを毎日使用している ●　Linuxを毎日使用している ●　jqueryなどのjavascriptの開発経験 ●　AWSのEC2、S3などのクラウドサービスの経験 これがあればもっと良い ●　phpunitでユニットテストを書ける ●　オンラインビジネスの仕組みの知識と理解がある（Eコマースのプロジェクトが多いので） ●　技術書の英語が問題なく読める（最近は自動翻訳が優れているからそうでもないかな） ●　ブログを書いている 勤務地 なし\n最後に このブログを読んでいるあなたは、もう資格十分かも。こちらにご連絡を。資格足りないかも？と思っていてもとりあえず声をかけてください！\n","date":"2018-09-29T04:53:46+09:00","permalink":"https://www.larajapan.com/2018/09/29/laravel%E3%81%AE%E3%82%A8%E3%83%B3%E3%82%B8%E3%83%8B%E3%82%A2%E5%8B%9F%E9%9B%86%E4%B8%AD%EF%BC%81/","title":"Laravelのエンジニア募集中！"},{"content":"新規のLaravelのプロジェクトを作成するときや、ライブラリを追加するときに使用するcomposerのコマンド。Laravelの陰で一見地味な存在に見えます。しかし、このおかげでphp言語のライブラリのインフラが整備され、アプリのモダンな開発の基盤を提供しているなくてはならないコマンドです。今回はこのコマンドに注目してみます。\ncomposerのインストール 1回実行すれば何もしないのですっかり忘れていたけれど、以下が正しいインストール方法。これによると。\n$ php -r \u0026#34;copy(\u0026#39;https://getcomposer.org/installer\u0026#39;, \u0026#39;composer-setup.php\u0026#39;);\u0026#34; $ php -r \u0026#34;if (hash_file(\u0026#39;SHA384\u0026#39;, \u0026#39;composer-setup.php\u0026#39;) === \u0026#39;544e09ee996cdf60ece3804abc52599c22b1f40f4323403c44d44fdfdd586475ca9813a858088ffbc1f233e9b180f061\u0026#39;) { echo \u0026#39;Installer verified\u0026#39;; } else { echo \u0026#39;Installer corrupt\u0026#39;; unlink(\u0026#39;composer-setup.php\u0026#39;); } echo PHP_EOL;\u0026#34; $ php composer-setup.php $ php -r \u0026#34;unlink(\u0026#39;composer-setup.php\u0026#39;);\u0026#34; 上を実行すると、composer.pharのファイルが作成されます。開発環境なら、自分のホームのbinのディレクトリに、グローバルで使用したいなら、ルート権限で以下のように適切なディレクトリへ移します。\n$ mv composer.phar /usr/local/bin/composer composer.pharの作成時には、ホームディレクトリに、.composerのディレクトリ（あるいは、.config/composer）が作成されます。\n.composer ├── cache ├── keys.dev.pub └── keys.tags.pub 今はほとんど空ですが、将来はすでにインストールしたパッケージのキャッシュやlaravelなどのコマンドの保存場所となります。\nLaravelのパッケージのインストール composerのコマンドが使えるようになったところで、laravelのパッケージをインストールします。\n$ composer global require \u0026#34;laravel/installer\u0026#34; globalのオプションは、ホームディレクトリの.composerのディレクトリにファイルをインストールすることを指定します。インストール後は、.composerのディレクトリは以下のような感じになります。\n.composer ├── cache │ ├── files │ │ ├── guzzlehttp │ │ ├── laravel │ │ ├── psr │ │ └── symfony │ └── repo │ └── https---repo.packagist.org ├── composer.json ├── composer.lock ├── keys.dev.pub ├── keys.tags.pub └── vendor ├── autoload.php ├── bin │ └── laravel -\u0026gt; ../laravel/installer/laravel ├── composer ├── guzzlehttp ├── laravel │ └── installer ├── psr └── symfony vendor/bin/laravelのコマンドに気づいたと思いますが、Laravelのプロジェクトの作成に使われるコマンドです。簡単に実行できるように、.bashrcなどのファイルにおいて、$PATHに、$HOME/.composer/vendor/binを追加しておいてください。\nLaravelのプロジェクトの作成 ここまで来ると、プロジェクトの作成は簡単です。インストールしたいディレクトリへ行き以下を実行します。\n$ laravel new new-project 注意が必要なのは、上のコマンドの実行では最新のLaravelのバージョン（今なら、5.7）がインストールされることです。\nバージョンを指定したいなら、composerコマンドの登場です。\n$ composer create-project --prefer-dist laravel/laravel new-project \u0026#34;5.5.*\u0026#34; composerのオプション 新規のLaravelのプロジェクトを作成したところで、いったい何のパッケージがインストールされたのでしょう？ ホームディレクトリの.composerと同様に、vendorのディレクトリを見るとパッケージに対応していろいろなディレクトリが作成されています。\nパッケージの情報をより見たいなら、\ncomposer show\nの実行です。\n$ composer show beyondcode/laravel-dump-server 1.2.1 Symfony Var-Dump Server for Laravel dnoegel/php-xdg-base-dir 0.1 implementation of xdg base directory specification for php doctrine/inflector v1.3.0 Common String Manipulations with regard to casing and singular/plural rules. doctrine/instantiator 1.1.0 A small, lightweight utility to instantiate objects in PHP without invoking their constructors doctrine/lexer v1.0.1 Base library for a lexer that can be used in Top-Down, Recursive Descent Parsers. dragonmantank/cron-expression v2.2.0 CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due egulias/email-validator 2.1.5 A library for validating emails against several RFCs erusev/parsedown 1.7.1 Parser for Markdown. fideloper/proxy 4.0.0 Set trusted proxies for Laravel filp/whoops 2.2.1 php error handling for cool kids fzaninotto/faker v1.8.0 Faker is a PHP library that generates fake data for you. hamcrest/hamcrest-php v2.0.0 This is the PHP port of Hamcrest Matchers jakub-onderka/php-console-color 0.1 jakub-onderka/php-console-highlighter v0.3.2 laravel/framework v5.7.3 The Laravel Framework. laravel/tinker v1.0.7 Powerful REPL for the Laravel framework. ... 他のパッケージをインストールしたいなら、例えば、スプレッドシートのライブラリをインストールしたいなら、\ncomposer require \u0026ldquo;phpoffice/phpspreadsheet\u0026rdquo;\nを実行します。\ncomposer.jsonのファイルも更新されて、\u0026ldquo;phpoffice/phpspreadsheet\u0026rdquo;: \u0026ldquo;^1.4\u0026rdquo;の行が追加されます。\n{ \u0026#34;name\u0026#34;: \u0026#34;laravel/laravel\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;The Laravel Framework.\u0026#34;, \u0026#34;keywords\u0026#34;: [\u0026#34;framework\u0026#34;, \u0026#34;laravel\u0026#34;], \u0026#34;license\u0026#34;: \u0026#34;MIT\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;project\u0026#34;, \u0026#34;require\u0026#34;: { \u0026#34;php\u0026#34;: \u0026#34;^7.1.3\u0026#34;, \u0026#34;fideloper/proxy\u0026#34;: \u0026#34;^4.0\u0026#34;, \u0026#34;laravel/framework\u0026#34;: \u0026#34;5.7.*\u0026#34;, \u0026#34;laravel/tinker\u0026#34;: \u0026#34;^1.0\u0026#34;, \u0026#34;phpoffice/phpspreadsheet\u0026#34;: \u0026#34;^1.4\u0026#34; }, ... 開発のみに使用するパッケージは、\ncomposer require barryvdh/laravel-debugbar \u0026ndash;dev\nと、\u0026ndash;devオプションをつけて実行します。実行後は、composer.jsonのrequire-devセクションも以下のように更新されています。\n... \u0026#34;require-dev\u0026#34;: { \u0026#34;barryvdh/laravel-debugbar\u0026#34;: \u0026#34;^3.2\u0026#34;, \u0026#34;beyondcode/laravel-dump-server\u0026#34;: \u0026#34;^1.0\u0026#34;, \u0026#34;filp/whoops\u0026#34;: \u0026#34;^2.0\u0026#34;, \u0026#34;fzaninotto/faker\u0026#34;: \u0026#34;^1.4\u0026#34;, \u0026#34;mockery/mockery\u0026#34;: \u0026#34;^1.0\u0026#34;, \u0026#34;nunomaduro/collision\u0026#34;: \u0026#34;^2.0\u0026#34;, \u0026#34;phpunit/phpunit\u0026#34;: \u0026#34;^7.0\u0026#34; }, ... 手動でこのファイルを編集してパッケージを追加したときには、\ncomposer update\nを実行します。他のパッケージの更新やパッケージ間の依存のチェックもあり、時間がかかります。\n上記のコマンドの実行で、composer.jsonが編集されることを見てきましたが、composer.lockというファイルも更新されます。このファイルは、プロジェクトのデプロイにおいて重要です。開発環境でテストされたプログラムと同じパッケージの同じパージョンを、本環境にプログラムをデプロイする必要あります。それには、\ncomposer install\nを本サイトで実行します。このコマンドは、composer.lockのデータに従って開発環境と同じパッケージの同じバージョンのファイルをインストールしてくれます。\nさらに、\ncomposer install \u0026ndash;no-dev\nと、\u0026ndash;no-devオプションを付ければ、開発だけに必要なパッケージは一切インストールはしません。\ncomposer.jsonとcomposer.lockはそれゆえに、gitなどで必ずバージョン管理する必要があります。\n","date":"2018-09-17T06:13:39+09:00","permalink":"https://www.larajapan.com/2018/09/17/composer%E3%81%AE%E3%81%82%E3%82%8C%E3%81%93%E3%82%8C/","title":"composerのあれこれ"},{"content":"今回はLaravelを離れてPHPのベーシックの話となります。連想配列（associative array）のマージに関してです。なかな知っているようで使用を間違うことがあります。\nそういうきは、私が大好きなツール、tinkerを使ってちょいちょいと実行して確認してみるのが一番。\narray_merge まずは、array_mergeを使用して２つの配列、$productと$moreをマージ。 \u0026gt;\u0026gt;\u0026gt; $product = [\u0026#39;product_id\u0026#39; =\u0026gt; 1, \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;商品名\u0026#39;] =\u0026gt; [ \u0026#34;product_id\u0026#34; =\u0026gt; 1, \u0026#34;name\u0026#34; =\u0026gt; \u0026#34;商品名\u0026#34;, ] \u0026gt;\u0026gt;\u0026gt; $more = [\u0026#39;price\u0026#39; =\u0026gt; 200] =\u0026gt; [ \u0026#34;price\u0026#34; =\u0026gt; 200, ] \u0026gt;\u0026gt;\u0026gt; array_merge($product, $more) =\u0026gt; [ \u0026#34;product_id\u0026#34; =\u0026gt; 1, \u0026#34;name\u0026#34; =\u0026gt; \u0026#34;商品名\u0026#34;, \u0026#34;price\u0026#34; =\u0026gt; 200, ] 両方の配列の項目が１つに収まります。\nさて、両方の配列に同じ項目があるときはどうでしょう？\n\u0026gt;\u0026gt;\u0026gt; $product = [\u0026#39;product_id\u0026#39; =\u0026gt; 1, \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;商品名\u0026#39;] =\u0026gt; [ \u0026#34;product_id\u0026#34; =\u0026gt; 1, \u0026#34;name\u0026#34; =\u0026gt; \u0026#34;商品名\u0026#34;, ] \u0026gt;\u0026gt;\u0026gt; $more = [\u0026#39;price\u0026#39; =\u0026gt; 200, \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;上書き商品名\u0026#39;] =\u0026gt; [ \u0026#34;price\u0026#34; =\u0026gt; 200, \u0026#34;name\u0026#34; =\u0026gt; \u0026#34;上書き商品名\u0026#34;, ] \u0026gt;\u0026gt;\u0026gt; array_merge($product, $more) =\u0026gt; [ \u0026#34;product_id\u0026#34; =\u0026gt; 1, \u0026#34;name\u0026#34; =\u0026gt; \u0026#34;上書き商品名\u0026#34;, \u0026#34;price\u0026#34; =\u0026gt; 200, ] 後者の配列に前者と同じ項目があるなら、後者の配列の値が前者を上書きとなります。\n+ 連想配列をマージできるのは、array_mergeだけではありません。足し算の+のオペレータも同様なことができます。 \u0026gt;\u0026gt;\u0026gt; $product = [\u0026#39;product_id\u0026#39; =\u0026gt; 1, \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;商品名\u0026#39;] =\u0026gt; [ \u0026#34;product_id\u0026#34; =\u0026gt; 1, \u0026#34;name\u0026#34; =\u0026gt; \u0026#34;商品名\u0026#34;, ] \u0026gt;\u0026gt;\u0026gt; $more = [\u0026#39;price\u0026#39; =\u0026gt; 200, \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;上書き商品名\u0026#39;] =\u0026gt; [ \u0026#34;price\u0026#34; =\u0026gt; 200, \u0026#34;name\u0026#34; =\u0026gt; \u0026#34;上書き商品名\u0026#34;, ] \u0026gt;\u0026gt;\u0026gt; $product + $more =\u0026gt; [ \u0026#34;product_id\u0026#34; =\u0026gt; 1, \u0026#34;name\u0026#34; =\u0026gt; \u0026#34;商品名\u0026#34;, \u0026#34;price\u0026#34; =\u0026gt; 200, ] しかし今度は、後者の配列に前者と同じ項目があるなら、上書きは起こりません。\nキーが数字のとき 上の例は、連想配列のキーが文字列でしたが、数字のときにはどうなるでしょう？　例えば、キーにDBのレコードのID（数字）を入れて配列を作成するケースは良くあります。\n\u0026gt;\u0026gt;\u0026gt; $products = [11 =\u0026gt; \u0026#39;商品A\u0026#39;, 12 =\u0026gt;\u0026#39;商品B\u0026#39;] =\u0026gt; [ 11 =\u0026gt; \u0026#34;商品A\u0026#34;, 12 =\u0026gt; \u0026#34;商品B\u0026#34;, ] \u0026gt;\u0026gt;\u0026gt; $more_products = [13 =\u0026gt; \u0026#39;商品C\u0026#39;, 14 =\u0026gt; \u0026#39;商品D\u0026#39;] =\u0026gt; [ 13 =\u0026gt; \u0026#34;商品C\u0026#34;, 14 =\u0026gt; \u0026#34;商品D\u0026#34;, ] \u0026gt;\u0026gt;\u0026gt; array_merge($products, $more_products) =\u0026gt; [ [ \u0026#34;商品A\u0026#34;, ], [ \u0026#34;商品B\u0026#34;, ], [ \u0026#34;商品C\u0026#34;, ], [ \u0026#34;商品D\u0026#34;, ], ] \u0026gt;\u0026gt;\u0026gt; $products + $more_products =\u0026gt; [ 11 =\u0026gt; [ \u0026#34;商品A\u0026#34;, ], 12 =\u0026gt; [ \u0026#34;商品B\u0026#34;, ], 13 =\u0026gt; [ \u0026#34;商品C\u0026#34;, ], 14 =\u0026gt; [ \u0026#34;商品D\u0026#34;, ], ] サプライズ！上の例のように両方の配列のキーがどれも数字なら、array_mergeでは、割り当てたキーが皆消えてしまいます。実際には消えるのではなく、キーが0,1,2,3と順番に与えられて通常の配列になります。しかし、+で足し算をするなら、キーは結果においてキープされます。しかし、両者の配列に同じキーがあるなら、さきほどのように後者は前者を上書きはしません。\n\u0026gt;\u0026gt;\u0026gt; $products = [11 =\u0026gt; \u0026#39;商品A\u0026#39;, 12 =\u0026gt;\u0026#39;商品B\u0026#39;] =\u0026gt; [ 11 =\u0026gt; \u0026#34;商品A\u0026#34;, 12 =\u0026gt; \u0026#34;商品B\u0026#34;, ] \u0026gt;\u0026gt;\u0026gt; $more_products = [12 =\u0026gt; \u0026#39;商品C\u0026#39;, 13 =\u0026gt; \u0026#39;商品D\u0026#39;] =\u0026gt; [ 12 =\u0026gt; \u0026#34;商品C\u0026#34;, 13 =\u0026gt; \u0026#34;商品D\u0026#34;, ] \u0026gt;\u0026gt;\u0026gt; $products + $more_products =\u0026gt; [ 11 =\u0026gt; \u0026#34;商品A\u0026#34;, 12 =\u0026gt; \u0026#34;商品B\u0026#34;, 13 =\u0026gt; \u0026#34;商品D\u0026#34;, ] ","date":"2018-09-08T06:15:58+09:00","permalink":"https://www.larajapan.com/2018/09/08/%E9%80%A3%E6%83%B3%E9%85%8D%E5%88%97%E3%81%AE%E3%83%9E%E3%83%BC%E3%82%B8/","title":"連想配列のマージ"},{"content":"今回は、指定の項目の存在、その項目の値の存在を判定するルールを紹介します。これらのルールは主に他のルールと混合して使用します。\nnullable present filled required\nrequired以外は、ややわかりづらいルールなので表にしてみました。結果がtrueとなる条件です。値が「なし」というのは、空の文字列（半角の空白文字があるならトリムします）あるいはnullのことです。\nルール項目値 nullableあってもなくてもあってもなくても presentありあってもなくても filledあってもなくてもあり requiredありあり 以下の実例では、nullable|stringのように他のルールとの混合の例としています。また比較のために、stringだけのテストも掲載しました。\n","date":"2018-09-02T05:17:50+09:00","permalink":"https://www.larajapan.com/2018/09/02/%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E5%AE%9F%E4%BE%8B%EF%BC%9A%E9%A0%85%E7%9B%AE%E3%81%AE%E5%AD%98%E5%9C%A8%E3%80%81%E9%A0%85%E7%9B%AE%E3%81%AE%E5%80%A4%E3%81%AE/","title":"バリデーションの実例：項目の存在、項目の値の存在"},{"content":"前回、前々回のレスポンスのマクロの定義は、app/Providers/AppServiceProvider.phpのアプリのサービスプロバイダーに入れましたが、マクロの定義が増えてくると整理した方が良いのと、コントローラ以外では必要もないので、単独のサービスに移します。\nまず、app/Providers/ResponseMacroServiceProvider.phpを作成して、２つのマクロを定義します。\napp/Providers/ResponseMacroServiceProvider.php namespace App\\Providers; use Illuminate\\Routing\\ResponseFactory; use Illuminate\\Support\\Facades\\View; use Illuminate\\Support\\ServiceProvider; class ResponseMacroServiceProvider extends ServiceProvider { /** * Bootstrap the application services. * * @param ResponseFactory $response * @return void */ public function boot(ResponseFactory $response) //レスポンスのオブジェクトを渡すこと可能 { $response-\u0026gt;macro(\u0026#39;viewWithS3\u0026#39;, function ($view, $data) { $host = \u0026#39;//example.s3.amazonaws.com\u0026#39;; $html = View::make($view)-\u0026gt;with($data)-\u0026gt;render(); $output = preg_replace(\u0026#39;#/user/images/#\u0026#39;, $host . \u0026#39;/user/images/, $html); return $this-\u0026gt;make($output); }); $response-\u0026gt;macro(\u0026#39;downloadNoCache\u0026#39;, function ($path) { return $this-\u0026gt;download($path, basename($path), [ \u0026#39;Cache-Control\u0026#39; =\u0026gt; \u0026#39;no-store\u0026#39;, ])-\u0026gt;setPrivate(); // Cache-Controlにpublicが追加されるのを回避 }); } } 上のboot()の関数の定義では、引数にレスポンスのオブジェクトを渡すことが可能です。以下に説明あります。\nhttps://laravel.com/docs/5.5/providers#the-boot-method\nこの新しいサービスプロバイダーを使うには、以下のようにconfig/app.phpで登録が必要です。\nconfig/app.php return [ ... \u0026#39;providers\u0026#39; =\u0026gt; [ ... /* * Application Service Providers... */ App\\Providers\\AppServiceProvider::class, App\\Providers\\AuthServiceProvider::class, // App\\Providers\\BroadcastServiceProvider::class, App\\Providers\\EventServiceProvider::class, App\\Providers\\ResponseMacroServiceProvider::class, App\\Providers\\RouteServiceProvider::class, ], ... ]; これで以下のように、コントローラでこれらのマクロの使用が可能となります。\nreturn response()-\u0026gt;viewWithS3(\u0026#39;user/product\u0026#39;, compact(\u0026#39;page\u0026#39;, \u0026#39;product\u0026#39;)); return response()-\u0026gt;downloadNoCache($pathToFile); ","date":"2018-08-25T05:51:31+09:00","permalink":"https://www.larajapan.com/2018/08/25/%E3%83%AC%E3%82%B9%E3%83%9D%E3%83%B3%E3%82%B9%E3%83%9E%E3%82%AF%E3%83%AD%E3%82%92%E4%B8%80%E7%AE%87%E6%89%80%E3%81%AB%E3%81%BE%E3%81%A8%E3%82%81%E3%81%A6%E5%AE%9A%E7%BE%A9/","title":"レスポンスマクロを一箇所にまとめて定義"},{"content":"一度気に入ると、より使って見たくなるのが人の常。ということでさらにマクロの活用です。今回は、前回と同様なレスポンスマクロを使用して、サイトからダウンロードするファイルを保護します。\nファイルのダウンロード ファイルのダウンロードの機能は、すでにLaravelでは提供されています。例えば、storage/data.txtのファイルのダウンロードは以下のようにコントローラのレスポンスにdownload()をチェーンして、ファイルダウンロードのレスポンスとします。 ... class DownloadFileController extends Controller { ... public function download() { $pathToFile = storage_path() . \u0026#39;/data.csv\u0026#39;; return response()-\u0026gt;download($pathToFile); } } さて、ブラウザーで、このファイルのダウンロードのレスポンスのヘッダーを見ると、\nHTTP/1.1 200 OK Date: Fri, 17 Aug 2018 22:05:43 GMT Server: Apache/2.4.27 (Fedora) OpenSSL/1.0.2k-fips PHP/7.1.14 X-Powered-By: PHP/7.1.14 Cache-Control: public ... Cache-Control: publicの部分、パブリックと設定されているので、ファイルの中身がサーバーとクライアント（ユーザー）の間のプロキシでキャッシュされるかもしれません。このファイルが例えば公共に見られてよい画像とかなら問題はないのですが、ファイルがサイトの会員とか注文データだとセキュリティの問題となります。私としては以下と設定したいのです。\n... Cache-Control: private ... ここを修正すべく、まず思ったのは、download()のパラメータを使用してのCache-Controlの設定です。\nreturn response()-\u0026gt;download($pathToFile, \u0026#39;data.csv\u0026#39;, [\u0026#39;Cache-control\u0026#39; =\u0026gt; \u0026#39;private\u0026#39;]); しかし、これが効きません! 相変わらず、レスポンスのヘッダーはCache-Control: publicです。試しに、以下のように違うCache-Controlの属性を設定してみると、\nreturn response()-\u0026gt;download($pathToFile, \u0026#39;data.csv\u0026#39;, [\u0026#39;Cache-control\u0026#39; =\u0026gt; \u0026#39;no-store\u0026#39;]); 今度は、Cache-Control: no-store, publicという変な結果になりましたが、設定はできるようです。となると、どこかでprivateを弾いているみたいです。\nどこでpublicが設定されている？ ymikomeさんが探偵のようにコードを追跡してくれました。\n問題は、\nvendor/laravel/framework/src/Illuminate/Routing/ResponseFactory.php namespace Illuminate\\Routing; ... class ResponseFactory implements FactoryContract { ... public function download($file, $name = null, array $headers = [], $disposition = \u0026#39;attachment\u0026#39;) { $response = new BinaryFileResponse($file, 200, $headers, true, $disposition); //ここの4番目のtrueの設定が問題！ if (! is_null($name)) { return $response-\u0026gt;setContentDisposition($disposition, $name, str_replace(\u0026#39;%\u0026#39;, \u0026#39;\u0026#39;, Str::ascii($name))); } return $response; } ... download()の定義の中の、BinaryFileResponse()のコールで、その4番目のtrueの値が、以下ようにsetPublic()のコールとなります。注意してください。もうここからは、Laravelのコードでなく、Symfonyのコードです。\nvendor/symfony/http-foundation/BinaryFileResponse.php namespace Symfony\\Component\\HttpFoundation; ... class BinaryFileResponse extends Response { ... public function __construct($file, $status = 200, $headers = array(), $public = true, $contentDisposition = null, $autoEtag = false, $autoLastModified = true) { parent::__construct(null, $status, $headers); $this-\u0026gt;setFile($file, $contentDisposition, $autoEtag, $autoLastModified); if ($public) { $this-\u0026gt;setPublic(); // $public = trueだからここを実行！ } } ... そして、このsetPublic()の定義は、\nvendor/symfony/http-foundation/Response.php namespace Symfony\\Component\\HttpFoundation; /** * Response represents an HTTP response. * * @author Fabien Potencier \u0026lt;fabien@symfony.com\u0026gt; */ class Response { ... public function setPublic() { $this-\u0026gt;headers-\u0026gt;addCacheControlDirective(\u0026#39;public\u0026#39;); $this-\u0026gt;headers-\u0026gt;removeCacheControlDirective(\u0026#39;private\u0026#39;);　//ここでprivateが弾かれる！ return $this; } ... headersでprivateを設定してもここで抜かれる訳です。\nどうprivateを設定する？ さて、Cache-Controlがいつもpublicとなる原因はわかりましたが、どうこれをprivateと設定するのが今度は問題となりました。ResponseFactory::download()を上書きするのが良いように思えますが、ResponseFactoryのクラスそのものを継承とかなるようで、trueをfalseとする目的のためには大袈裟すぎます。\n困ったと思ったときに、見たのは先のSymfonyのコード、setPublic()の定義を含むファイルには、setPrivate()もあるではないですか！\nということで、以下のようなマクロの定義となりました。内部では一旦publicにしたものをprivateにすることになりますが、ここでは意図が明確でスッキリとしています。\nResponse::macro(\u0026#39;downloadNoCache\u0026#39;, function ($path) { return $this-\u0026gt;download($path, basename($path), [ \u0026#39;Cache-Control\u0026#39; =\u0026gt; \u0026#39;no-store\u0026#39;, ])-\u0026gt;setPrivate(); // Cache-Controlにpublicが追加されるのを回避 }); no-storeですが、これはブラウザがファイルをキャッシュするのを防ぎます。毎回ダウンロードでデータが変わるときには必要です。\nこのマクロの使用は、以下のようになります。\nreturn response()-\u0026gt;downloadNoCache($pathToFile); この実行は以下のように思い通りのヘッダーとなりました。\nHTTP/1.1 200 OK Date: Fri, 17 Aug 2018 23:40:33 GMT Server: Apache/2.4.27 (Fedora) OpenSSL/1.0.2k-fips PHP/7.1.14 X-Powered-By: PHP/7.1.14 Cache-Control: no-store, private ... ","date":"2018-08-19T07:32:37+09:00","permalink":"https://www.larajapan.com/2018/08/19/%E3%83%AC%E3%82%B9%E3%83%9D%E3%83%B3%E3%82%B9%E3%83%9E%E3%82%AF%E3%83%AD%E3%81%A7%E3%83%80%E3%82%A6%E3%83%B3%E3%83%AD%E3%83%BC%E3%83%89%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%92%E4%BF%9D%E8%AD%B7/","title":"レスポンスマクロでダウンロードファイルを保護"},{"content":"前回、validate()のソースコードで登場したマクロ、今回は有用なマクロの活用の例を紹介します。\nマクロに関してのLaravelのマニュアルでは以下で、response()のマクロの例が紹介されています。\nhttps://laravel.com/docs/5.5/responses#response-macros\nこれを見て「あ、これはここで使える！」と思ったのは、コントローラから出力するHTMLの中身の画像のURLの変換です。\nまずは、ちょっと背景説明を。\n私のお客さんのサイトでは画像がかなり多いので、サーバー（EC2）の負担を減らすために画像はすべてAWSのS3にアップしています。どれだけアクセスがあっても画像はS3から配信されるのでサーバーが落ちる心配もないし、サーバーをアップグレードする必要もなくコストセーブとなります。\nしかし、ブレードを管理するデザイナーに、\n\u0026lt;img src=\u0026#34;/user/images/example.jpg\u0026#34;\u0026gt; のような参照を、\n\u0026lt;img src=\u0026#34;//example.s3.amazonaws.com/user/images/example.jpg\u0026#34;\u0026gt; と毎回書き換えてもらうのは、やっかい。もちろん、エディターやsassとかでのプリ処理は可能だけれども、１つステップ増えるしプログラマでないデザイナーにはちょっと。また、S3のURLも本サイトと開発サイトでは変えたいケースもあり、そうしてもらうとこちらも困ります。\nということで、出力時にダイナミックに変えるのは必須となりました。ダイナミックに変えるには、ウェブサーバーでの設定でも可能ですが、やはり、HTMLの中にS3のURLが入るのがサーバーにも負担にならず、ユーザーにとっても表示速度の点で最適です。\nこのために、以下のようなユーザー定義のヘルパーを作成して、\nLob.php // Lobは、私の会社 Lots of Bytesの略です。 class Lob { ... public static function view($view, $data) { $host = \u0026#39;//example.s3.amazonaws.com\u0026#39;; $html = View::make($view)-\u0026gt;with($data)-\u0026gt;render()); $output = preg_replace(\u0026#39;#/user/images/#\u0026#39;, $host.\u0026#39;/user/images/, $html); return response($output); } ... } コントローラで以下のように、コールしていました。\nreturn Lob::view(\u0026#39;user/product\u0026#39;, compact(\u0026#39;page\u0026#39;, \u0026#39;product\u0026#39;)); これで用は足りるのですが、なんかLaravelらしくない。\nそこで、マクロの登場です。\napp/Providers/AppServiceProvider.phpを編集して、viewWithS3のマクロを定義します。\napp/Providers/AppServiceProvider.php namespace App\\Providers; use Illuminate\\Support\\ServiceProvider; use Illuminate\\Support\\Facades\\Schema; use Illuminate\\Support\\Facades\\Response; use Illuminate\\Support\\Facades\\View; class AppServiceProvider extends ServiceProvider { /** * Bootstrap any application services. * * @return void */ public function boot() { Schema::defaultStringLength(191); Response::macro(\u0026#39;viewWithS3\u0026#39;, function ($view, $data) { $host = \u0026#39;//example.s3.amazonaws.com\u0026#39;; $html = View::make($view)-\u0026gt;with($data)-\u0026gt;render()); $output = preg_replace(\u0026#39;#/user/images/#\u0026#39;, $host.\u0026#39;/user/images/, $html); // 以下の$thisは、AppServiceProviderのインスタンスでなく、Illuminate\\Http\\Response return $this-\u0026gt;make($output); }); } ... } コントローラでは、以下のようにコールします。Laravelらしくわかりやすくなりましたね。\nreturn response()-\u0026gt;viewWithS3(\u0026#39;user/product\u0026#39;, compact(\u0026#39;page\u0026#39;, \u0026#39;product\u0026#39;)); ","date":"2018-08-12T01:02:23+09:00","permalink":"https://www.larajapan.com/2018/08/12/%E3%83%AC%E3%82%B9%E3%83%9D%E3%83%B3%E3%82%B9%E3%83%9E%E3%82%AF%E3%83%AD%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6laravel%E9%A2%A8%E3%81%AB%E5%95%8F%E9%A1%8C%E8%A7%A3%E6%B1%BA/","title":"レスポンスマクロを使ってLaravel風に問題解決"},{"content":"バリデーションの実例をちょっと休んで、実際にバリデーションを実行する部分を見てみます。お客さんのプロジェクトで最近この辺を詳しく見る機会があったので、皆さんと共有です。\n一般的にはコントローラで使わるのですが、以下のようにいくつかあります。\nリクエストのvalidate()メソッド まず、代表的（5.5の時点で）なのがLaravelのドキュメンテーションの例としても使われている、Requestオブジェクトのメソッド、validate()。\n... public function store(Request $request) { $request-\u0026gt;validate([ \u0026#39;title\u0026#39; =\u0026gt; \u0026#39;required|unique:posts|max:255\u0026#39;, \u0026#39;body\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, ]); // The blog post is valid... } このvalidateでは、それだけで与えられたルールの配列をもとに入力値、この場合、$request-\u0026gt;all()を判定して、問題があるなら、エラーメッセージと入力値とともに前のページに戻ります。\nしかし、驚くことに、これはRequestのクラスのメソッドではありません！メソッドの形をしたマクロで、以下のファイルで定義されています（このマクロに関しては将来投稿を作成する予定です）。\nvendor/laravel/framework/src/illuminate/Foundation/Providers/FoundationServiceProvider.php \u0026lt;?php namespace Illuminate\\Foundation\\Providers; use Illuminate\\Http\\Request; use Illuminate\\Support\\AggregateServiceProvider; class FoundationServiceProvider extends AggregateServiceProvider { /** * The provider class names. * * @var array */ protected $providers = [ FormRequestServiceProvider::class, ]; /** * Register the service provider. * * @return void */ public function register() { parent::register(); $this-\u0026gt;registerRequestValidate(); } /** * Register the \u0026#34;validate\u0026#34; macro on the request. * * @return void */ public function registerRequestValidate() { Request::macro(\u0026#39;validate\u0026#39;, function (array $rules, ...$params) { validator()-\u0026gt;validate($this-\u0026gt;all(), $rules, ...$params); return $this-\u0026gt;only(collect($rules)-\u0026gt;keys()-\u0026gt;map(function ($rule) { return str_contains($rule, \u0026#39;.\u0026#39;) ? explode(\u0026#39;.\u0026#39;, $rule)[0] : $rule; })-\u0026gt;unique()-\u0026gt;toArray()); }); } } コントローラのValidatesRequestsトレイトのvalidate()メソッド 先の例を使えば、以下のようになります。\n... public function store(Request $request) { $this-\u0026gt;validate($request, [ \u0026#39;title\u0026#39; =\u0026gt; \u0026#39;required|unique:posts|max:255\u0026#39;, \u0026#39;body\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, ]); ... } validate()の第１引数にリクエストのオブジェクトを渡します。こちらも、$request-\u0026gt;all()を判定して、問題があるなら、エラーメッセージと入力値とともに前のページに戻ります。\nこのvalidateは、ValidateRequestsのトレイトのメソッドで、以下のようにコントローラの親クラスで宣言されています。\napp/Http/Controllers/Controller.php namespace App\\Http\\Controllers; use Illuminate\\Foundation\\Bus\\DispatchesJobs; use Illuminate\\Routing\\Controller as BaseController; use Illuminate\\Foundation\\Validation\\ValidatesRequests; use Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests; class Controller extends BaseController { use AuthorizesRequests, DispatchesJobs, ValidatesRequests; } Validatorクラス 上の２つが存在しなかった時代からLaravelをやってきている人たちにはこのやり方は馴染みあります。上の２つに比べてコードが長くなるものの、何をやっているか明確になります。\n... public function store(Request $request) { $validator = Validator::make($request-\u0026gt;all(), [ \u0026#39;title\u0026#39; =\u0026gt; \u0026#39;required|unique:posts|max:255\u0026#39;, \u0026#39;body\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, ]); if ($validator-\u0026gt;fails()) { return redirect(\u0026#39;post/create\u0026#39;) -\u0026gt;withErrors($validator) -\u0026gt;withInput(); } // Store the blog post... } バリデーターのオブジェクトを作成してから、判定作業(fails())を実行して、エラーならリダイレクトです。\nvalidator()ヘルパー 最後に、Validator::make()を簡略化したヘルパー、validator()があります。\n... public function store(Request $request) { $validator = validator($request-\u0026gt;all(), [ \u0026#39;title\u0026#39; =\u0026gt; \u0026#39;required|unique:posts|max:255\u0026#39;, \u0026#39;body\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, ]); $validator-\u0026gt;validate(); ... $validator-\u0026gt;validate();これ、どこかで見たような。そう、最初のvalidateのマクロの定義の中で使用されています。\nこのようにバリデーションの実行にいろいろやり方があるのは、Laravelが進化を続けている証拠ですね。\n","date":"2018-08-05T05:15:42+09:00","permalink":"https://www.larajapan.com/2018/08/05/%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E3%81%82%E3%82%8C%E3%81%93%E3%82%8C/","title":"バリデーションのあれこれ"},{"content":"今回は、引数にDBの情報を指定するバリデーションルールです。今回はデータベースを伴うテストなので、use RefreshDatabase;のトレイトや、factory()-\u0026gt;create()が使用されています。\nexists unique\n後者の方は、会員登録では必須のバリデーションです。会員編集の方でも自分のレコードを外す引数を指定すれば使用可能です。しかし、Laravel 5.5からのRuleの登場で、よりわかりやすい形でも指定できます。これらは皆、今回のテストに含まれています。\n","date":"2018-07-29T05:10:01+09:00","permalink":"https://www.larajapan.com/2018/07/29/%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E5%AE%9F%E4%BE%8B%EF%BC%9Adb%E3%81%AE%E6%83%85%E5%A0%B1%E3%82%92%E5%BC%95%E6%95%B0%E3%81%A8%E3%81%99%E3%82%8B%E6%9D%A1%E4%BB%B6/","title":"バリデーションの実例：DBの情報を引数とする条件"},{"content":"今回は、引数にフォームの他の項目名を指定するバリデーションルールです。\nconfirmed different in_array same\n個人的には、最初のconfirmed以外は使用したことないですね。in_arrayでは引数として指定する項目が配列であることを示すために、field2.*のように指定する必要あることに注意してください。\nさて、他の項目の値を引数としては使わないのですが、入力値が配列のときに、それらの値に重複がないというチェックもここに掲載します。他に入れるところがないというのですが、本当は。配列という投稿を作成してもよいかもしれませんね。\ndistinct\n","date":"2018-07-22T04:08:56+09:00","permalink":"https://www.larajapan.com/2018/07/22/%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E5%AE%9F%E4%BE%8B%EF%BC%9A%E4%BB%96%E3%81%AE%E9%A0%85%E7%9B%AE%E3%81%AE%E5%80%A4%E3%82%92%E5%BC%95%E6%95%B0%E3%81%A8%E3%81%99/","title":"バリデーションの実例：他の項目の値を引数とする条件"},{"content":"入力値のデータタイプの判定のバリデーションから、今度は条件で判定するバリデーションに移っていきます。条件で判定するバリデーションには、その条件で使用される引数の指定が必要となります。引数には、\n文字列や数字の定数値（例：min, maxなど） 同じフォームの他の項目名（例：same、differentなど） DBの情報（例：exists、unique） が指定されます。\n今回は、最初の引数タイプ、定数値を引数とする条件の以下のバリデーションのテストを掲載します。\ndigits digits_between size between min max in not_in regex\nまた、前回紹介した日付のバリデーションの以下も定数値を引数とする条件に含まれますね。\ndate_format, date_equals, after, after_or_equal, before, before_or_equal\n今回の注意点は、size, between, min, maxにおいて、入力値を文字列でなく数値として扱うには、numericあるいはintegerのルールの指定が必要なことです。例えば、integer|between:3,10なルールになります。\n","date":"2018-07-15T07:22:29+09:00","permalink":"https://www.larajapan.com/2018/07/15/%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E5%AE%9F%E4%BE%8B%EF%BC%9A%E5%AE%9A%E6%95%B0%E5%80%A4%E3%82%92%E5%BC%95%E6%95%B0%E3%81%A8%E3%81%99%E3%82%8B%E6%9D%A1%E4%BB%B6/","title":"バリデーションの実例：定数値を引数とする条件"},{"content":"今回は日付関連のバリデーションを掲載します。\ndate date_format date_equals after after_or_equal before before_or_equal\ndate以外、今回は皆ルールの指定にはパラメータが必要となります。\n","date":"2018-07-07T07:11:01+09:00","permalink":"https://www.larajapan.com/2018/07/07/%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E5%AE%9F%E4%BE%8B%EF%BC%9A%E6%97%A5%E4%BB%98/","title":"バリデーションの実例：日付"},{"content":"特殊データタイプとしてのチェックとして以下のテストを掲載します。以下からのリンクだけでなく、この投稿にも掲載してあります。\nalpha alpha_dash alpha_num accepted active_url email ip timezone url\n今回、まず注意する必要があるのは、alpha, alpha_dash, alpha_numです。これらの「アルファ」は通常の半角英字と仮定してしまいますが、実際はそれぞれの言語でのアルファベット、という意味で、英語ではなく一般的に言語の文字列という意味です。つまり、日本語なら何でもＯＫということになります。ログインなどに使用する半角英数字のアルファベットのバリデーションには、これでなくユーザー定義が必要となりますね。\nそれから、emailのバリデーション、日本のユーザーに対応するならRFC非準拠のメールアドレス（「.」を「..」などのように連続で使用することや@マークの直前で使用）は対応していません。これも、ユーザー定義のバリデーションが必要となります。\n","date":"2018-07-01T05:57:20+09:00","permalink":"https://www.larajapan.com/2018/07/01/%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E5%AE%9F%E4%BE%8B%EF%BC%9A%E7%89%B9%E6%AE%8A%E3%83%87%E3%83%BC%E3%82%BF%E3%82%BF%E3%82%A4%E3%83%97/","title":"バリデーションの実例：特殊データタイプ"},{"content":"基本データタイプのチェックとして以下のテストを掲載します。以下からのリンクだけでなく、この投稿にも掲載してあります。\nboolean integer numeric string array json\ndateも入れようかと思いましたが、その他の日付関連のバリデーションとともに掲載します。\n以下の掲載で説明が必要な部分は、ブーリアンです。入力値がfalseなのにどうしてtrueを返すのか？ データをチェックしているのでなく、データがブーリアンタイプであるかをチェックしている、と考えてください。\nそれから、配列のテストで、どうして空の[]がrequiredによりエラーとなりますが、jsonの{}はエラーとならないのかは謎です。\n","date":"2018-06-24T01:22:59+09:00","permalink":"https://www.larajapan.com/2018/06/24/%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E5%AE%9F%E4%BE%8B%EF%BC%9A%E5%9F%BA%E6%9C%AC%E3%83%87%E3%83%BC%E3%82%BF%E3%82%BF%E3%82%A4%E3%83%97/","title":"バリデーションの実例：基本データタイプ"},{"content":"新しい連載を始めます。\nLaravelは、出来合いのヘルパーやバリデーションがあり便利です。もの凄くたくさんあり覚えらないというほどの数ではないですが、すべてを頭の中で保持しておくのも大変です。また、こういう使い方では結果はどうなるのとか、実際に実行してみないとわからないことあります。\n例えば、バリデーションのalphaは、文字列がすべて英字からなる（本当？）かをチェックします。このバリデーションを、ログイン名のチェックとかに使うとします。しかし、大文字でもＯＫなのとか、空白文字が入ってもＯＫなのとか、日本語の入力はどうなのとか、調べないとわからないことあります。Laravelのドキュメントではそこまで細かくは説明していません。\n前回で説明したようにtinkerで簡単にチェックすることも可能ですが、面倒だなと思うときに、実例を素早く見ることができれば便利だと思いません？\nそこで今回から、そんな便利な実例特集を連載する予定です。まず、その１つを見てください。以下は、Laravelのバリデーションのacceptedのユニットテストです。\n実例の使い方 先に掲載したコードは、github.comのgistを使用しています。gistは、一般的にスニペットと言われる、コピペ専用が目的のgitのレポジトリです。gitでコード管理するのと同様、cloneやforkの利用も可能です。\n上のコードを実際にテストしてみたいなら、ララベル5.5のプロジェクトを作成して、そのプロジェクトのルートのディレクトリで、以下を実行してください。\n$ git clone git@gist.github.com:cf781699c05c73d07fdbff8489052ef8.git tests/Unit/Validations/Accepted コマンドラインで指定したURLは、\ngistの画面において、embedのドロップダウンのリストから、Clone via SSHを指定して、表示されたURLを使います。以下の画面では、赤いボックスのところをクリックしてコピーできます。\nコマンドの実行は、以下のように、tests/Unit/Validations/Acceptedのディレクトリを作成して、テストのファイルを作成してくれます。\ntests/Unit ├── ExampleTest.php └── Validations └── Accepted ├── 0.md └── AcceptedTest.php ダウンロードしたところで、テストを実行してみましょう。\n$ vendor/bin/phpunit --filter AcceptedTest PHPUnit 6.5.8 by Sebastian Bergmann and contributors. ......... 9 / 9 (100%) Time: 201 ms, Memory: 12.00MB OK (9 tests, 9 assertions) と９つテストがすべて成功の結果を表示してくれます。\nphpunitのデータプロバイダー 先のテストの実行では、９つのテストが成功、とありましたが、９つのテストなるゆえは、上のコードのprovider_acceptedの関数内に９つのデータ配列があるからです。\nこのユニットテストでは、phpunitのデータプロバイダー機能を使用しています。その機能では、コメントに入れる情報で、どこからデータを受け取るかを指定します。\n/** * @test ←　関数がテストの関数であることを指定 * @dataProvider provider_accepted　←　この関数からデータを取ってくることを指定 */ public function accepted($input, $expected) { .. provider_accepted()では、\n例えば、最初のデータ\n[[\u0026lsquo;field\u0026rsquo; =\u0026gt; null], false]\nは、\n[\u0026lsquo;field\u0026rsquo; =\u0026gt; null]　が　accepted()の最初のパラメの$input\nとなり、\nfalseが、$expected\nとなります。\nテストの実行では、９つのデータ配列があるので、データを変えて9回のテストの実行となります。そして、さらにいろいろテストしたいなら、そのデータと期待する結果をそこに追加すればよいだけです。\nバリデーションの実例の連載開始 ということで、次回からバリデーションの実例を投稿していきます。まずは、Laravelにある規定のバリデーションですが、それだけでなくユーザー定義のバリデーションもアップしていく予定です。\n","date":"2018-06-17T05:54:57+09:00","permalink":"https://www.larajapan.com/2018/06/17/%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E5%AE%9F%E4%BE%8B/","title":"バリデーションの実例"},{"content":"前回に引き続いてリクエストの話です。ブラウザがないとリクエスト（Request）のオブジェクトが作成できない、とは限りません。Laravelならコマンドラインで作成できます。どうやって？\nリクエストオブジェクトの作成 コマンドラインと言えばもちろん、tinkerのことです。以下のように、クラスのcreate()メソッドを使って、リクエストのオブジェクトを作成できます。\nPsy Shell v0.9.3 (PHP 7.1.14 — cli) by Justin Hileman \u0026gt;\u0026gt;\u0026gt; $request = \\Illuminate\\Http\\Request::create(\u0026#39;http://localhost\u0026#39;, \u0026#39;GET\u0026#39;, [\u0026#39;name\u0026#39; =\u0026gt; \u0026#39;test\u0026#39;]); =\u0026gt; Illuminate\\Http\\Request {#2324 ⚠: Symfony\\Component\\VarDumper\\Exception\\ThrowingCasterException {#2309 #message: \u0026#34;Unexpected ErrorException thrown from a caster: Undefined index: \u0026#34;, }, +attributes: Symfony\\Component\\HttpFoundation\\ParameterBag {#2320}, +request: Symfony\\Component\\HttpFoundation\\ParameterBag {#2322}, +query: Symfony\\Component\\HttpFoundation\\ParameterBag {#2321}, +server: Symfony\\Component\\HttpFoundation\\ServerBag {#2317}, +files: Symfony\\Component\\HttpFoundation\\FileBag {#2318}, +cookies: Symfony\\Component\\HttpFoundation\\ParameterBag {#2319}, +headers: Symfony\\Component\\HttpFoundation\\HeaderBag {#2316}, } Laravelのリクエストは、Symfonyのクラスがベースになっているようです。createは、Laravelのクラスのメソッドではなく、Symfonyのクラスのメソッドです。参考のために、以下にその定義の一部を掲載します。\nSymfonyComponentHttpFoundationRequest.php /** * Creates a Request based on a given URI and configuration. * * The information contained in the URI always take precedence * over the other information (server and parameters). * * @param string $uri The URI * @param string $method The HTTP method * @param array $parameters The query (GET) or request (POST) parameters * @param array $cookies The request cookies ($_COOKIE) * @param array $files The request files ($_FILES) * @param array $server The server parameters ($_SERVER) * @param string|resource|null $content The raw body data * * @return static */ public static function create($uri, $method = \u0026#39;GET\u0026#39;, $parameters = array(), $cookies = array(), $files = array(), $server = array(), $content = null) { .. 先のtinkerの実行では、create()の3番目のパラメータに、連想配列で値（[\u0026rsquo;name\u0026rsquo; =\u0026gt; \u0026rsquo;test\u0026rsquo;]）を渡しました。ということは、以下のtinkerの実行で変数の取り出し可能です。\n\u0026gt;\u0026gt;\u0026gt; $request-\u0026gt;all(); =\u0026gt; [ \u0026#34;name\u0026#34; =\u0026gt; \u0026#34;test\u0026#34;, ] \u0026gt;\u0026gt;\u0026gt; $request-\u0026gt;input(); =\u0026gt; [ \u0026#34;name\u0026#34; =\u0026gt; \u0026#34;test\u0026#34;, ] \u0026gt;\u0026gt;\u0026gt; $request-\u0026gt;name; =\u0026gt; \u0026#34;test\u0026#34; \u0026gt;\u0026gt;\u0026gt; $request-\u0026gt;input(\u0026#39;name\u0026#39;); =\u0026gt; \u0026#34;test\u0026#34; \u0026gt;\u0026gt;\u0026gt; $request-\u0026gt;get(\u0026#39;name\u0026#39;); =\u0026gt; \u0026#34;test\u0026#34; \u0026gt;\u0026gt;\u0026gt; 前回に説明したことが、ここで簡単にシミュレートできるのです。\nそして、さらに、こんなこともテストできます。簡単なValidationのテストです。\n\u0026gt;\u0026gt;\u0026gt; $request-\u0026gt;validate([\u0026#39;name\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;]); =\u0026gt; [ \u0026#34;name\u0026#34; =\u0026gt; \u0026#34;test\u0026#34;, ] \u0026gt;\u0026gt;\u0026gt; $request-\u0026gt;validate([\u0026#39;name\u0026#39; =\u0026gt; \u0026#39;required|alpha\u0026#39;]); =\u0026gt; [ \u0026#34;name\u0026#34; =\u0026gt; \u0026#34;test\u0026#34;, ] \u0026gt;\u0026gt;\u0026gt; $request-\u0026gt;validate([\u0026#39;name\u0026#39; =\u0026gt; \u0026#39;alpha\u0026#39;]); =\u0026gt; [ \u0026#34;name\u0026#34; =\u0026gt; \u0026#34;test\u0026#34;, ] \u0026gt;\u0026gt;\u0026gt; $request-\u0026gt;validate([\u0026#39;name\u0026#39; =\u0026gt; \u0026#39;numeric\u0026#39;]); Illuminate/Validation/ValidationException with message \u0026#39;The given data was invalid.\u0026#39; \u0026gt;\u0026gt;\u0026gt; Validationに成功したら、入力の連想配列を返し、失敗したらValidationExceptionを返します。\ntinkerの便利さは、いちいちプログラムを書かずに、ブラウザを立ち上げることなしに、テストできることです。こんな便利なツールを習得せずに、Laravelを語ることはできません。\n","date":"2018-06-09T09:00:44+09:00","permalink":"https://www.larajapan.com/2018/06/09/%E3%83%86%E3%82%A3%E3%83%B3%E3%82%AB%E3%83%BC%E3%81%A7%E3%83%AA%E3%82%AF%E3%82%A8%E3%82%B9%E3%83%88%EF%BC%88request%EF%BC%89/","title":"ティンカーでリクエスト（Request）"},{"content":"Laravelで、リクエスト(Request)というのは、ブラウザを通してユーザーから送られる情報をすべて含んでいるオブジェクトのことです。例えば、会員登録のフォームなら、画面でユーザーが入力したＥメール、パスワード、名前、住所だけでなく、何のブラウザを使用したか（User Agent）、どのIPから送られたか、どのURLからアクセスしたかなど、また、会員ログイン後の画面なら、会員認証において保存されたクッキーもブラウザを介して、リクエストに含まれます。\nリクエストの使用 リクエストの代表的な使用は、以下のようにコントローラのメソッドのパラメータです。以下は、Laravelのドキュメントからのコードから引用からですが、会員登録画面でのPOSTされたときにコールされるstoreメソッドです。 UserController.php namespace App\\Http\\Controllers; use Illuminate\\Http\\Request; class UserController extends Controller { /** * 新規ユーザーの作成 * * @param Request $request * @return Response */ public function store(Request $request) { $name = $request-\u0026gt;input(\u0026#39;name\u0026#39;); // } } また、Httpのミドルウェアでも、handle()のパラメータとして利用されています。\nRedirectIfAuthenticated.php use Closure; use Illuminate\\Support\\Facades\\Auth; class RedirectIfAuthenticated { /** * Handle an incoming request. * * @param \\Illuminate\\Http\\Request $request * @param \\Closure $next * @param string|null $guard * @return mixed */ public function handle($request, Closure $next, $guard = null) { if (Auth::guard($guard)-\u0026gt;check()) { return redirect(\u0026#39;/home\u0026#39;); } return $next($request); } } ミドルウェアと言えば、コントローラのコンストラクタでも$requestにアクセスが可能です。\nnamespace App\\Http\\Controllers; class UserBaseController extends Controller { protected $ip, $userAgent; public function __construct() { $this-\u0026gt;middleware(function ($request, $next) { $this-\u0026gt;ip = $request-\u0026gt;ip(); $this-\u0026gt;userAgent = $request-\u0026gt;userAgent(); return $next($request); }); // } } ユーザーの入力値の抽出 先のリクエストのオブジェクトから、ユーザーが入力した値を取り出すには、以下のようにいろいろなメソッドがあります。\nまず、入力値を全部とりだす。$inputは、連想配列の変数です。\n// 入力値を全部とりだす。 $input = $request-\u0026gt;all(); // これも、入力値を全部とりだすが、all()と違ってファイル関連のデータはなし $input = $request-\u0026gt;input(); // これはヘルパー。input()と同じ $input = request(); フォームからではなく、クエリのデータ、例えば、http://localhost/user?x=10\u0026amp;y=20　のxとyの値　を取り出すには、\n$input = $request-\u0026gt;query(); ちなみに、all()や、input()には、すでにクエリのデータも含まれています。\n今度は、指定した入力値だけを取り出すには、\n// \u0026#39;name\u0026#39;の値を取り出す $name = $request-\u0026gt;get(\u0026#39;name\u0026#39;); // これも、\u0026#39;name\u0026#39;の値を取り出す $name = $request-\u0026gt;input(\u0026#39;name\u0026#39;); // これも、\u0026#39;name\u0026#39;の値を取り出す $name = $request-\u0026gt;name; // これはヘルパー $name = request(\u0026#39;name\u0026#39;); いろいろありますね。違いとしては、\n入力値が連想配列なら、\nつまり、\n\u0026lt;form\u0026gt; \u0026lt;input type=\u0026#34;text\u0026#34; name=\u0026#34;items[][\u0026#39;price\u0026#39;]\u0026#34; value=\u0026#34;100\u0026#34;\u0026gt; \u0026lt;input type=\u0026#34;text\u0026#34; name=\u0026#34;items[][\u0026#39;price\u0026#39;]\u0026#34; value=\u0026#34;200\u0026#34;\u0026gt; \u0026lt;/form\u0026gt; のようなフォームのときは、\n$price = $request-\u0026gt;input(\u0026#39;items.0.price\u0026#39;);　// 100を返す $price = $request-\u0026gt;get(\u0026#39;items.0.price\u0026#39;); // nullを返す と、inputは使えますが、get()は使えません。\n最後に、例えば、http://localhost/user?name=yamadaのような、クエリからの指定の値を取ってくるには、以下のどれでもＯＫです。\n$name = $request-\u0026gt;query(\u0026#39;name\u0026#39;); $name = $request-\u0026gt;input(\u0026#39;name\u0026#39;); $name = $request-\u0026gt;get(\u0026#39;name\u0026#39;); となります。\n","date":"2018-06-02T05:02:11+09:00","permalink":"https://www.larajapan.com/2018/06/02/%E3%83%AA%E3%82%AF%E3%82%A8%E3%82%B9%E3%83%88%EF%BC%88request%EF%BC%89%E3%81%AE%E3%81%82%E3%82%8C%E3%81%93%E3%82%8C/","title":"リクエスト（Request）のあれこれ"},{"content":"Laravelをウェブアプリの開発の使い始めてから、もうかれこれ5年になろうとしています。Laravelのバージョンも4から5になりさらに進化し続けています。こう経験が長くなると、使用する開発ツールも年々増えてきます。その中で、もうこれがないと開発できない！というものもいくつかあります。\nバージョン管理：Git コードのバージョン管理まったくなしで開発していた時代があったのが信じられないくらい、gitは毎日毎日使用しています。ブランチの作成、差分のチェック(git diff)、コードのマージ(git merge)、プログラムのリバート(git checkout)、ときにハードのリセット(git reset \u0026ndash;hard)などなど、もうこれなしでプログラマとして生きていくのは不可能。Laravelだから必要というわけでないですが、ほとんどLaravelの採用とともに、バージョン管理をsubversionからgitに乗り換えてから、後ろを振り向くことはありません。\nまた、コマンドラインで実行するgitのプログラムだけでなく、それに伴うインフラのサービスも必須です。このブログで紹介しているLaravelの日本語レポジトリは、有名なgithub.comを使用し、プライベートのプロジェクトには、bitbucket.comを使用しています。それぞれ長所、短所ありますが、後者はプライベートのレポをいくら作成しても無料なゆえに開発チームで使用しています。\n開発ヘルパー：debugbar 以前にも紹介したツールです。\nDebugbarで楽々デバッグ\nhttps://github.com/barryvdh/laravel-debugbar\nLaravelのコミュニティーには、Laravelの作者も含めて、賢い人がたくさんいます。debugbarを作成した人もそのひとり。このようなツール、Laravelの以前から欲しかったものです。ブラウザに、現在の画面でLaravelで使用された、リクエスト(Request)、セッション(Session)、DBクエリ(Queries)、認証(Auth)の情報がすべて出てくるので、すぐにコード内の状況がわかります。また、debug()をコードに入れることで、dd()を使用せずに、情報をデバッグ画面に表示できます。最近気づいたことでは、実行したDBクエリーに対応するファイルの行数まで表示されます。嬉しいこと、このうえない！\n開発ヘルパー：tinker これも以前紹介したコマンドツール。\nもっとティンカー(tinker)を使おう！\nコマンドツールというと、覚えることが多くて大変と思われるかもしれないですが、慣れてくるともう離れられない。プログラムのファイルを作成することなしにちょいちょいとテストできるからです。\nそして、Laravel5.5のバージョンでは、嬉しい改善があります。\n例えば、Laravel 5.5以前のバージョンでは、\nPsy Shell v0.9.3 (PHP 7.1.15 — cli) by Justin Hileman \u0026gt;\u0026gt;\u0026gt; use App\\User; \u0026gt;\u0026gt;\u0026gt; User::find(5); =\u0026gt; App\\User {#2307 id: 5, name: \u0026#34;hogeuser01\u0026#34;, email: \u0026#34;hoge01@example.com\u0026#34;, created_at: \u0026#34;2018-04-04 14:20:37\u0026#34;, updated_at: \u0026#34;2018-04-04 14:20:37\u0026#34;, } 今までいちいちとタイプしていたクラスの宣言（上のuse App\\User;）を、\nPsy Shell v0.9.3 (PHP 7.1.15 — cli) by Justin Hileman \u0026gt;\u0026gt;\u0026gt; User::find(5); [!] Aliasing \u0026#39;User\u0026#39; to \u0026#39;App\\User\u0026#39; for this Tinker session. =\u0026gt; App\\User {#2316 id: 5, name: \u0026#34;hogeuser01\u0026#34;, email: \u0026#34;hoge01@example.com\u0026#34;, created_at: \u0026#34;2018-04-04 14:20:37\u0026#34;, updated_at: \u0026#34;2018-04-04 14:20:37\u0026#34;, } このように指定しなくても自動的に探してエイリアスとして覚えてくれるので、1行分のタイプが必要なくなり、ますます使いやすくなりました。\nコードチェック：phan これも以前紹介したコマンドツール。\nphanは楽しい！\nタイプレスのphp言語にＣ言語のようなタイプチェックをもたらしてくれるツールです。これが開発に貢献する度合いは、すでに紹介したツールと比べて小さいのだけれど、これで発見したエラーは貴重です。改善して欲しいのは、Traitを使用すると誤診の警告が多いこと。将来に期待しています。\nテスト：phpunit アプリの機能が増えるにつれ、コードの複雑さが増してくると、バグの修正であれ機能の変更であれ、他の部分への影響が大きくなります。もちろん、仕様を整理したり、Laravelのようなフレームワークを導入して管理性を高めるのだけれど、人間がなす業、見過ごしや間違いがもちろん起きます。自分自身でないテスターにテストさせても間違いがスルーしてしまいます。\nこうした中で、頼りとなってくるのはユニットテスト。単に、今までの機能が変更により壊れていないかを確認してくれだけでなく、今まで作成・蓄積してきた関数の使用方法もテストのプログラムを読むことで理解できます。特にLaravelのバージョンの更新後に、phpunitの実行でエラーがないことを確認するだけでも自分が作成したプログラムに自信が持てます。\n最近は、開発自体もまずはユニットテストから始めるテストドリブン（Test Driven Development）の開発に移行しつつあります。もちろん、通常の開発だけに比べてより時間がかかるのですが、その時間が将来に起こるかもしれない、冷っとする経験を少なくしてくれると思うとかけがえのないものです。\nその他 他にも開発に関わるツールは、たくさんあります。テキストエディタのsublimeや、仕様管理に使っているGoogle Sheet、スマホもＰＣも一度に閲覧できるブラウザ、blixなどあります。感慨深いのは、5年前にはどれもこれも使用したこともないツールばかり、中にはこの世に存在しないものも。次の5年間はどうなるのでしょう？\n","date":"2018-05-26T02:27:27+09:00","permalink":"https://www.larajapan.com/2018/05/26/laravel%E3%81%AE%E9%96%8B%E7%99%BA%E3%81%AB%E5%BF%85%E9%A0%88%E3%81%AA%E7%A7%81%E3%81%AE%E3%83%84%E3%83%BC%E3%83%AB/","title":"Laravelの開発に必須な私のツール"},{"content":"前回に作成したLaravelの日本語のリポジトリ（Laravel 5.5）。今回はその作成の仕方を説明します。ほとんどは、Laravel 5.4のときと同じですが、いくつか違いがあります。\nコマンドの実行 まずは、以下のcomposerのコマンドを実行します。\ncomposer create-project --prefer-dist laravel/laravel larajapan \u0026#34;5.5.*\u0026#34; 上で使用されているコマンドの引数は、\n\u0026ndash;prefer-dist laravel/laravel\nhttps://packagist.org/packages/laravel/laravelからパッケージをダウンロードすることを指示します。\nlarajapan\nパッケージのダウンロード先。その名前でディレクトリを作成します。このディレクトリ名は、先のコマンドラインで違う名前を指定可能であるし、実行完了してから改名も可能です。\n5.5.*\nパッケージのバージョンを指定。ここでは、laravelの5.5を使用します。マイナーバージョンを指定したいなら、5.5.40のように指定します。\n実行すると、パッケージに含まれるファイル、さらにパッケージが依存するパッケージのファイルが多数ダウンロードされ少々時間がかかります。\n最終的には、実行したディレクトリのもとにlarajapanのディレクトリが作成され、ダウンロードされたファイルが収納されます。\nlarajapan ├── app/ ├── bootstrap/ ├── config/ ├── database/ ├── public/ ├── resources/ ├── routes/ ├── storage/ ├── tests/ ├── vendor/ ├── artisan ├── composer.json ├── composer.lock ├── package.json ├── phpunit.xml ├── readme.md ├── server.php └── webpack.mix.js 次に、ユーザー認証のためのファイル作成を以下の実行で行います。\nphp artisan make:auth この実行により、resources/viewsのディレクトリにおいて、すでにインストールされている以下のコントローラで使用されるbladeファイルが作成されます。\napp/Http/Controllers ├── Auth/ │ ├── ForgotPasswordController.php（パスワードのリセットのリンク送信画面） │ ├── LoginController.php（ログイン画面） │ ├── RegisterController.php（会員登録画面） │ └── ResetPasswordController.php（パスワードリセット画面） ├── Controller.php └── HomeController.php（ログイン後のホーム画面） 最後に以下のコマンドを実行して、先のパスワードのリセットのリンク送信画面から発行されるＥメールで使用されるHTMLのテンプレートを作成作成します。\nphp artisan vendor:publish 5.4と違って、上の実行は以下のような選択が出てきますが、0を選択してすべてをインストールします。\nWhich provider or tag\u0026#39;s files would you like to publish?: [0] Publish files from all providers and tags listed below [1] Provider: Fideloper\\Proxy\\TrustedProxyServiceProvider [2] Provider: Illuminate\\Mail\\MailServiceProvider [3] Provider: Illuminate\\Notifications\\NotificationServiceProvider [4] Provider: Illuminate\\Pagination\\PaginationServiceProvider [5] Provider: Laravel\\Tinker\\TinkerServiceProvider [6] Tag: laravel-mail [7] Tag: laravel-notifications [8] Tag: laravel-pagination \u0026gt; 0 Copied Directory [/vendor/laravel/framework/src/Illuminate/Notifications/resources/views] To [/resources/views/vendor/notifications] Copied Directory [/vendor/laravel/framework/src/Illuminate/Pagination/resources/views] To [/resources/views/vendor/pagination] Copied File [/vendor/fideloper/proxy/config/trustedproxy.php] To [/config/trustedproxy.php] Copied Directory [/vendor/laravel/framework/src/Illuminate/Mail/resources/views] To [/resources/views/vendor/mail] Copied File [/vendor/laravel/tinker/config/tinker.php] To [/config/tinker.php] Publishing complete. 日本語化 さて、ここからが日本語化の作業です。\nまず、config/app.phpの編集から。\nconfig/app.php ... \u0026#39;timezone\u0026#39; =\u0026gt; \u0026#39;UTC\u0026#39;, \u0026#39;locale\u0026#39; =\u0026gt; \u0026#39;en\u0026#39;, ... を\nconfig/app.php ... \u0026#39;timezone\u0026#39; =\u0026gt; \u0026#39;Asia/Tokyo\u0026#39;, \u0026#39;locale\u0026#39; =\u0026gt; \u0026#39;ja\u0026#39;, ... と変えて保存します。\ntimezone\nこれは、通常、プログラム内の日時設定のタイムゾーンとして使用されるもので、PHPの以下の関数で使用されます。\ndate_default_timezone_set()\nここで設定すれば、後はLaravelが面倒みてくれます。\n日本時間の場合は、Aisa/Tokyoの設定だけで十分。\nlocale\nresources/langで言語のファイルが以下のように存在します。これらは、Laravelのプロジェクトでバリデーションのエラーメッセージなどを定義しています。\nresources/lang └── en/ ├── auth.php ├── pagination.php ├── passwords.php └── validation.php デフォルトの設定では、英語のenのディレクトリしかありません。日本語の翻訳を作成するには、上で設定したjaと同じ名前のディレクトリをそこに作成します。以下の実行でディレクトリごとコピーしてください。\n$ cp -pr en ja バリデーションに関しては、見米氏のバリデーション（１）Validatorファサードのextend　を参照してください。\n今回は、新しく追加されたバリデーションの以下のエントリーもあります。\nafter_or_equal before_or_equal\n次は、ユーザー認証画面などで使用されるブレードファイルの翻訳です。\n私のLaravelの日本語レポジトリでは、以下は、すべて翻訳してあります。\nresources/views ├── auth │ ├── login.blade.php │ ├── passwords │ │ ├── email.blade.php │ │ └── reset.blade.php │ └── register.blade.php ├── errors │ └── 404.blade.php ├── home.blade.php ├── layouts │ └── app.blade.php ├── vendor │ ├── mail │ │ ├── html │ │ │ ├── button.blade.php │ │ │ ├── footer.blade.php │ │ │ ├── header.blade.php │ │ │ ├── layout.blade.php │ │ │ ├── message.blade.php │ │ │ ├── panel.blade.php │ │ │ ├── promotion │ │ │ │ └── button.blade.php │ │ │ ├── promotion.blade.php │ │ │ ├── subcopy.blade.php │ │ │ ├── table.blade.php │ │ │ └── themes │ │ │ └── default.css │ │ └── markdown │ │ ├── button.blade.php │ │ ├── footer.blade.php │ │ ├── header.blade.php │ │ ├── layout.blade.php │ │ ├── message.blade.php │ │ ├── panel.blade.php │ │ ├── promotion │ │ │ └── button.blade.php │ │ ├── promotion.blade.php │ │ ├── subcopy.blade.php │ │ └── table.blade.php │ ├── notifications │ │ └── email.blade.php │ └── pagination │ ├── bootstrap-4.blade.php │ ├── default.blade.php │ ├── semantic-ui.blade.php │ ├── simple-bootstrap-4.blade.php │ └── simple-default.blade.php └── welcome.blade.php 5.4と比べて異なるのは、paginationにsemantic-ui.blade.phpが追加されています。errorsのフォルダーは例として追加しました。\nパスワードリセットで送信されるＥメールの翻訳 ここまで来ても、残念ながら、パスワードを忘れたときに送信される、パスワードリセットを含むＥメールの内容がまだ翻訳されていません。なぜなら、本文がハードコードされているからです。\nこれはちょっと頭をひねりましたが、多分以下が最小の変更で対応できると思います。\nまず、\nvendor/laravel/framework/src/Illuminate/AuthのディレクトリからResetPassword.phpとCanResetPassword.phpのファイルを以下の場所にコピーします。\napp/Auth ├── Notifications/ │ └── ResetPassword.php └── Passwords/ └── CanResetPassword.php 次に、以下のようにファイルを編集します。app/User.phpのファイルも変更必要です。\napp/Auth/Notifications/ResetPassword.php namespace App\\Auth\\Notifications; use Illuminate\\Notifications\\Notification; use Illuminate\\Notifications\\Messages\\MailMessage; class ResetPassword extends Notification { ... /** * Build the mail representation of the notification. * * @param mixed $notifiable * @return \\Illuminate\\Notifications\\Messages\\MailMessage */ public function toMail($notifiable) { return (new MailMessage) -\u0026gt;subject(\u0026#39;パスワードリセット\u0026#39;) -\u0026gt;greeting(\u0026#39;パスワードリセット\u0026#39;) -\u0026gt;line(\u0026#39;パスワードリセットリンクの送信のリクエストがありました。\u0026#39;) -\u0026gt;action(\u0026#39;リセットパスワード\u0026#39;, url(config(\u0026#39;app.url\u0026#39;).route(\u0026#39;password.reset\u0026#39;, $this-\u0026gt;token, false))) -\u0026gt;line(\u0026#39;リクエストされていなかったら、無視してください。\u0026#39;); } } app/Auth/Passwords/CanResetPassword.php namespace App\\Auth\\Passwords; use App\\Auth\\Notifications\\ResetPassword as ResetPasswordNotification; trait CanResetPassword { /** * Get the e-mail address where password reset links are sent. * * @return string */ public function getEmailForPasswordReset() { return $this-\u0026gt;email; } /** * Send the password reset notification. * * @param string $token * @return void */ public function sendPasswordResetNotification($token) { $this-\u0026gt;notify(new ResetPasswordNotification($token)); } } app/User.hpp namespace App; use Illuminate\\Notifications\\Notifiable; use Illuminate\\Foundation\\Auth\\User as Authenticatable; use App\\Auth\\Passwords\\CanResetPassword; class User extends Authenticatable { use Notifiable; use CanResetPassword; ... 認証のroutesの設定 日本語化とは関係ないですが、私がこうした方がわかりやすいと思ったことです。\nオリジナルのroutes.phpは、\nroutes/web.php Route::get(\u0026#39;/\u0026#39;, function () { return view(\u0026#39;welcome\u0026#39;); }); Auth::routes(); Route::get(\u0026#39;/home\u0026#39;, \u0026#39;HomeController@index\u0026#39;); とシンプルですが、Auth::routes()で認証のrouteが隠されてしまって不透明。\nということで、私のLaravelの日本語レポジトリでは、以下のように編集しました。\nroutes/web.php // 以下は、Auth::routes()の中身を移したもの。将来において変更が可能なように // Authentication Routes... Route::get(\u0026#39;login\u0026#39;, \u0026#39;Auth\\LoginController@showLoginForm\u0026#39;)-\u0026gt;name(\u0026#39;login\u0026#39;); Route::post(\u0026#39;login\u0026#39;, \u0026#39;Auth\\LoginController@login\u0026#39;); Route::post(\u0026#39;logout\u0026#39;, \u0026#39;Auth\\LoginController@logout\u0026#39;)-\u0026gt;name(\u0026#39;logout\u0026#39;); // Registration Routes... Route::get(\u0026#39;register\u0026#39;, \u0026#39;Auth\\RegisterController@showRegistrationForm\u0026#39;)-\u0026gt;name(\u0026#39;register\u0026#39;); Route::post(\u0026#39;register\u0026#39;, \u0026#39;Auth\\RegisterController@register\u0026#39;); // Password Reset Routes... Route::get(\u0026#39;password/reset\u0026#39;, \u0026#39;Auth\\ForgotPasswordController@showLinkRequestForm\u0026#39;)-\u0026gt;name(\u0026#39;password.request\u0026#39;); Route::post(\u0026#39;password/email\u0026#39;, \u0026#39;Auth\\ForgotPasswordController@sendResetLinkEmail\u0026#39;)-\u0026gt;name(\u0026#39;password.email\u0026#39;); Route::get(\u0026#39;password/reset/{token}\u0026#39;, \u0026#39;Auth\\ResetPasswordController@showResetForm\u0026#39;)-\u0026gt;name(\u0026#39;password.reset\u0026#39;); Route::post(\u0026#39;password/reset\u0026#39;, \u0026#39;Auth\\ResetPasswordController@reset\u0026#39;); Route::get(\u0026#39;/home\u0026#39;, \u0026#39;HomeController@index\u0026#39;); Debugbar このバージョンから、開発に非常に有用なDebugbarがdevの環境だけにインストールできるようになりました。プロダクションの環境にインストールする必要はありません。\n以下の実行により、composer.jsonが編集されvendorのディレクトリにライブラリがインストールされます。\n$ composer require barryvdh/laravel-debugbar --dev また、Laravel 5.5のauto-discoveryの機能により、以前のようにconfig/app.phpで設定の必要もありません。\nしかし、今までコード内で、debug()を使用していてそれを残しておくと、プロダクションにおいて関数未定義のエラーが出ます。防ぐには、以下のように、bootstrap/app.phpで空の関数を定義しておきます。\nbootstrap/app.php /* |-------------------------------------------------------------------------- | Create The Application |-------------------------------------------------------------------------- | | The first thing we will do is create a new Laravel application instance | which serves as the \u0026#34;glue\u0026#34; for all the components of Laravel, and is | the IoC container for the system binding all of the various parts. | */ $app = new Illuminate\\Foundation\\Application( realpath(__DIR__.\u0026#39;/../\u0026#39;) ); // no-dev でエラーにならないように定義 if (!function_exists(\u0026#39;debug\u0026#39;)) { function debug($value) { } } .. その他 インストールにおいて経験した問題として、\nphp artisan migrate を実行したときに、以下のエラーとなりました。\n[Illuminate\\Database\\QueryException] SQLSTATE[42000]: Syntax error or access violation: 1071 Specified key was too long; max key length is 767 bytes (SQL: alter table users add unique users_email_unique(email)) [PDOException] SQLSTATE[42000]: Syntax error or access violation: 1071 Specified key was too long; max key length is 767 bytes これは、使用しているデータベースがMariaDBで10.2.2のバージョンより古いか、あるいはMySQLで5.7.7より古いときに起こるエラーです。Laravelは5.4より、絵文字対応のutf8mb4のエンコーディングがデフォルトとなりました。\n古いバージョンを使用する場合（大半がそうでは？）は、以下の変更が必要となります。日本語のレポにはこの変更が含まれています。\napp/Providers/AppServiceProvider.php namespace App\\Providers; use Illuminate\\Support\\ServiceProvider; use Illuminate\\Support\\Facades\\Schema; class AppServiceProvider extends ServiceProvider { /** * Bootstrap any application services. * * @return void */ public function boot() { Schema::defaultStringLength(191); } /** * Register any application services. * * @return void */ public function register() { // } } ","date":"2018-05-19T05:00:25+09:00","permalink":"https://www.larajapan.com/2018/05/19/laravel%E3%81%AE%E6%97%A5%E6%9C%AC%E8%AA%9E%E3%83%AC%E3%83%9D%E3%82%B8%E3%83%88%E3%83%AA%E3%81%AE%E4%BD%9C%E6%88%90%EF%BC%88laravel-5-5%EF%BC%89/","title":"Laravelの日本語レポジトリの作成（Laravel 5.5）"},{"content":"Laravelの日本語のリポジトリをLaravel5.5に更新しました。もうLaravel5.6がリリースされていますが、Laravel5.5は長期サポート（2年間）のバージョンなゆえに重要です。前回同様に、以下インストールの手順です。\nこのバージョンでは、PHP \u0026gt;= 7.0.0が必須となりますので現在のPHPのバージョンをチェックしてください。過去のバージョンは以下を参照してください。 Laravel 5.4\nLaravel 5.3\nLaravelにおいて新規のプロジェクト作成はとても簡単。コマンドラインでいくつかのコマンドを実行をちょちょいとすれば完了。しかし、インストールされるのは英語のプロジェクト。テンプレートやメッセージの翻訳をいちいちしなければ日本語のプロジェクトにはならない。\nここのプロセスを簡単にと、Laravelバージョン5.5をもとに、開発者のために日本語化したレポジトリを作成してみました。\nこのレポジトリには、\nデフォルトのユーザー認証の機能：会員登録、パスワードリセット、会員ログイン 日本語に翻訳されたデフォルトのテンプレートとＥメールメッセージ 日本語に翻訳されたデフォルトの入力エラーメッセージ デバッグのためのDebugbarツール ウェブ解析ツールGoogle Analyticsのトラッキングスクリプト 以上を含みます。\nさらに、今回は、実際動作するデモとして以下に用意しました。 https://larajapan.lotsofbytes.com/larajapan\nさて、このレポジトリのインストールは以下の手順で簡単にできます。\nレポジトリのインストール SSHを利用しているなら、以下をコマンドラインで実行してレポジトリをインストールします。\n$ git clone -b 5.5 git@github.com:lotsofbytes/larajapan.git あるいは、Httpsを使用するなら、以下を実行します。\n$ git clone -b 5.5 https://github.com/lotsofbytes/larajapan.git 実行後は、larajapanのディレクトリが作成されますが、お好きな名前に改名してもらってＯＫです。\nその後、\ncd larajapan それから、ファイルのパーミッションを与えるべく以下を実行。\n$ chmod -R a+w bootstrap/cache $ chmod -R a+w storage インストール後は、以下を実行してください。\n$ composer install .envの編集 .env.example をコピーして、.env　を作成し編集して以下のように設定します。*****の部分を適切な値に変更してください。\nAPP_NAME=私のララベル APP_ENV=local APP_KEY= APP_DEBUG=true APP_LOG_LEVEL=debug DB_CONNECTION=mysql DB_DATABASE=***** DB_USERNAME=***** DB_PASSWORD=***** MAIL_DRIVER=sendmail\nその後、以下を実行して.env内のAPP_KEYを更新します。\n$ php artisan key:generate APP_DEBUG=trueこれによりDebugbarが画面下方に表示されます。Debugbarに関しては、Debugbarで楽々デバッグも読んでください。\nまた、 ANALYTICS=UA-XXXXXXのようにサイトのためにGoogleから取得したコードを設定すれば、Google Analyticsでウェブでのユーザーの動向が追跡できます。\nDBを作成 .envで指定したDBを作成。\n$ echo \u0026#39;CREATE DATABASE mydatabase CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci\u0026#39; | mysql -u root -p php artisan migrate ウェブサーバーの立ち上げ 最後に以下を実行して、ウェブサーバーを立ち上げると、\n$ php artisan serve 以下のアドレスでブラウザーからアクセスできます。\nhttp://localhost:8000\n以上です。\n上の設定は私の開発環境Fedora LinuxとAmazon Linux OSで、7.0のバージョンのPHPで、DBにはMysqlあるいはMaria DBを使用して動作確認しています。しかし、皆さんの環境ではいろいろ異なることがあると思います。問題や指摘があれば、ご連絡ください。\n","date":"2018-05-13T05:52:32+09:00","permalink":"https://www.larajapan.com/2018/05/13/laravel%E3%81%AE%E6%97%A5%E6%9C%AC%E8%AA%9E%E3%81%AE%E3%83%AC%E3%83%9D%E3%82%B8%E3%83%88%E3%83%AA%EF%BC%88laravel-5-5%EF%BC%89/","title":"Laravelの日本語のレポジトリ（Laravel 5.5）"},{"content":"今回はパスワードのリセットのテストです。\nパスワードのリセットは、以下のステップがあります。\nパスワードリセットのリクエスト画面へ行き、Eメールアドレスを入力してリセットをリクエスト メールでパスワードリセットのトークン付きのURLを受信 パスワードリセット画面へ行きパスワードをリセット テストケースとして考えられるのは、\nパスワードリセットリクエスト画面へのアクセスができる 登録された会員のEメールアドレスでパスワードのリセットのリクエストが成功がする 登録されていない会員のEメールアドレスでパスワードのリセットのリクエストが失敗 成功したパスワードのリセットのリクエストで受信したURLでパスワードリセットの画面にアクセスができる その画面でパスワードリセットができる パスワードリセットリクエスト画面でのテスト まずは、画面にアクセスできることを確認。\ntests/Features/ResetPasswordTest.php /** * @test * パスワードリセットをリクエストする画面の閲覧可能 */ public function user_can_view_reset_request() { $response = $this-\u0026gt;get(\u0026#39;password/reset\u0026#39;); $response-\u0026gt;assertStatus(200); } そして、画面にEメールアドレスを入力して、成功のメッセージが表示されることを確認。\ntests/Features/ResetPasswordTest.php /** * @test * パスワードリセットのリクエスト成功 */ public function valid_user_can_request_reset() { // ユーザーを1つ作成 $user = factory(User::class)-\u0026gt;create(); // パスワードリセットをリクエスト $response = $this-\u0026gt;from(\u0026#39;password/email\u0026#39;)-\u0026gt;post(\u0026#39;password/email\u0026#39;, [ \u0026#39;email\u0026#39; =\u0026gt; $user-\u0026gt;email, ]); // 同画面にリダイレクト $response-\u0026gt;assertStatus(302); $response-\u0026gt;assertRedirect(\u0026#39;password/email\u0026#39;); // 成功のメッセージ $response-\u0026gt;assertSessionHas(\u0026#39;status\u0026#39;, \u0026#39;リクエストを受け付けました。パスワードの再設定方法をメールでお知らせします。\u0026#39;); } ここで注目することとして2点。\nまず、$this-\u0026gt;from(\u0026lsquo;password/email\u0026rsquo;)-\u0026gt;postで使用されているfrom()。これがないと、 $response-\u0026gt;assertRedirect(\u0026lsquo;password/email\u0026rsquo;);以下のようなエラーとなります。\nThere was 1 failure: 1) Tests\\Feature\\ResetPasswordTest::valid_user_can_request_reset Failed asserting that two strings are equal. --- Expected +++ Actual @@ @@ -\u0026#39;http://localhost/password/email\u0026#39; +\u0026#39;http://localhost\u0026#39; この画面では成功時には、back()で前の画面に戻されます。しかし、テストでは前の画面の場所は保存していないので、デフォルトのホーム/になります。password/emailに戻るには、セッションあるいはリファラーで前の画面のURIを記録しておく必要があります。以下のように、TestCase.phpにfrom()の定義をしておきます。\ntests/TestCase.php namespace Tests; use Illuminate\\Foundation\\Testing\\TestCase as BaseTestCase; abstract class TestCase extends BaseTestCase { use CreatesApplication; /** * Set the referer header to simulate a previous request. * * @param string $url * @return $this */ public function from(string $url) { session()-\u0026gt;setPreviousUrl(url($url)); return $this; } } 次に注目してもらいたいのは、成功のメッセージが表示されるかどうかのチェック。これは、セッションにstatusのキーが含まれているかどうかをチェックします。成功時には以下のメソッドでstatusにレスの文字列がセッションを介して保存されます。\nvendor/laravel/framework/src/Illuminate/Foundation/Auth/SendsPasswordResetEmails.php /** * Get the response for a successful password reset link. * * @param string $response * @return \\Illuminate\\Http\\RedirectResponse */ protected function sendResetLinkResponse($response) { return back()-\u0026gt;with(\u0026#39;status\u0026#39;, trans($response)); } このリクエスト画面で、今度は失敗するケース。単に、存在しない会員のEメールアドレスを入力してみます。 今度は、セッションに含まれるerrorsに、emailのエントリがあるかをチェックして確認です。\ntests/Features/ResetPasswordTest.php /** * @test * 存在しないメールアドレスでパスワードリセットのリクエストをして失敗 */ public function invalid_user_cannot_request_reset() { // ユーザーを1つ作成 $user = factory(User::class)-\u0026gt;create(); // 存在しないユーザーのメールアドレスでパスワードリセットをリクエスト $response = $this-\u0026gt;from(\u0026#39;password/email\u0026#39;)-\u0026gt;post(\u0026#39;password/email\u0026#39;, [ \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;nobody@example.com\u0026#39; ]); $response-\u0026gt;assertStatus(302); $response-\u0026gt;assertRedirect(\u0026#39;password/email\u0026#39;); // 失敗のエラーメッセージ $response-\u0026gt;assertSessionHasErrors(\u0026#39;email\u0026#39;, \u0026#39;指定のメールアドレスは見つかりませんでした\u0026#39;); } パスワードリセット画面でのテスト 今度はリクエストではなく、実際にパスワードをリセットする画面に関してのテストです。このテストは２つの部分からなります。\nパスワードリセットをリクエストした後に受信するEメールのURLをもとに、パスワードリセット画面にアクセスできるかを確認 その画面でパスワードを更新可能なことを確認 少しコードは長くなりますが、まさしくユーザーが辿るステップをそのままここで実現します。\ntests/Features/ResetPasswordTest.php /** * @test * パスワードリセットのトークンでパスワードをリセット */ public function valid_user_can_reset_password() { Notification::fake(); // ユーザーを1つ作成 $user = factory(User::class)-\u0026gt;create(); // パスワードリセットをリクエスト $response = $this-\u0026gt;post(\u0026#39;password/email\u0026#39;, [ \u0026#39;email\u0026#39; =\u0026gt; $user-\u0026gt;email ]); // トークンを取得 $token = \u0026#39;\u0026#39;; Notification::assertSentTo( $user, ResetPassword::class, function ($notification, $channels) use ($user, \u0026amp;$token) { $token = $notification-\u0026gt;token; return true; } ); // パスワードリセットの画面へ $response = $this-\u0026gt;get(\u0026#39;password/reset/\u0026#39;.$token); $response-\u0026gt;assertStatus(200); // パスワードをリセット $new = \u0026#39;reset1111\u0026#39;; $response = $this-\u0026gt;post(\u0026#39;password/reset\u0026#39;, [ \u0026#39;email\u0026#39; =\u0026gt; $user-\u0026gt;email, \u0026#39;token\u0026#39; =\u0026gt; $token, \u0026#39;password\u0026#39; =\u0026gt; $new, \u0026#39;password_confirmation\u0026#39; =\u0026gt; $new ]); // ホームへ遷移 $response-\u0026gt;assertStatus(302); $response-\u0026gt;assertRedirect(\u0026#39;/home\u0026#39;); // リセット成功のメッセージ $response-\u0026gt;assertSessionHas(\u0026#39;status\u0026#39;, \u0026#39;パスワードはリセットされました!\u0026#39;); // 認証されていることを確認 $this-\u0026gt;assertTrue(Auth::check()); // 変更されたパスワードが保存されていることを確認 $this-\u0026gt;assertTrue(Hash::check($new, $user-\u0026gt;fresh()-\u0026gt;password)); } まず、最初パートでは、リクエストで送信されたEメールに含まれるトークン付きのURLを取得する必要あります。それがなければ、リセット画面にアクセスできません。送信したメールをログにしてそれを読み込んで、URLで抽出ということも可能です。しかし、ここではNotification::fakeを利用することによって、綺麗に簡単に、トークンを抽出します。\nそして、そのトークンを利用してリセット画面でEメールアドレスを更新します。更新成功後では、\nリセット成功のメッセージが画面に表示されること ユーザが認証されていること 更新されたパスワードが保存されていること を確認します。更新されたパスワードを取得するのに、$user-\u0026gt;passwordでなく、$user-\u0026gt;fresh()-\u0026gt;passwordとなっていることに注意してください。fresh()により、$userのオブジェクトの中身が更新されます。\n他に考えられるテストは？ 以上のテストで十分と思われるかもしれませんが、テストケースは切りがないほど考えられます。例えば、\n期限切れのトークンでリセット画面にアクセスしたとき 有効のトークンでアクセスして、トークンを発行したものと違うとEメールアドレスを入力したとき リセットの画面でパスワードと確認パスワードが異なるとき などなど。\n最後に今回のテストのコードは以下から利用可能です。\nhttps://github.com/lotsofbytes/larajapan/blob/5.4-test/tests/Feature/ResetPasswordTest.php\n","date":"2018-05-05T06:07:34+09:00","permalink":"https://www.larajapan.com/2018/05/05/%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E8%AA%8D%E8%A8%BC%E3%81%AE%E3%83%86%E3%82%B9%E3%83%884-laravel-5-4%E3%80%80%E3%83%91%E3%82%B9%E3%83%AF%E3%83%BC%E3%83%89%E3%83%AA%E3%82%BB%E3%83%83%E3%83%88/","title":"ユーザー認証のテスト(4) Laravel 5.4　パスワードリセット"},{"content":"前回のログインのテストに対して、今回はログアウトのテスト。\nまず、以下を見てください。\n/** @test */ public function logout() { // ユーザーを１つ作成 $user = factory(User::class)-\u0026gt;create(); // 認証済み、つまりログイン済みしたことにする $this-\u0026gt;actingAs($user); // 認証されていることを確認 $this-\u0026gt;assertTrue(Auth::check()); // ログアウトを実行 $response = $this-\u0026gt;post(\u0026#39;logout\u0026#39;); // 認証されていない $this-\u0026gt;assertFalse(Auth::check()); // Welcomeページにリダイレクトすることを確認 $response-\u0026gt;assertRedirect(\u0026#39;/\u0026#39;); } 前回と違うのは、ログインの実行$response = $this-\u0026gt;post(..)が今回のテストにはないことです。その代わりに、actingAs()を利用して、プログラムで認証済みにしてしまいます。\nactingAs()のおかげで、コードが短くなりわかりやすくなっただけでなく、その使用では以下のようにセッション値を入れたり、リダイレクトすることも可能です。認証された後の、つまりパスワード保護された中の機能のテストではいつも必要なことなので、将来重宝しそうです。\n.. $response = $this-\u0026gt;actingAs($user) -\u0026gt;withSession([\u0026#39;foo\u0026#39; =\u0026gt; \u0026#39;bar\u0026#39;]) -\u0026gt;get(\u0026#39;/\u0026#39;); ... さて、テストとは関係ないですが、ログアウト後はどうして、/loginでなく/にリダイレクトされるのでしょう？また、その変更は可能なのでしょうか？\nリダイレクト先は、残念ながら、以下のlaravelのパッケージの中のファイルでハードコードされています。\nvendor/laravel/framework/src/Illuminate/Foundation/Auth/AuthenticateUsers.php\nvendor/laravel/framework/src/Illuminate/Foundation/Auth/AuthenticateUsers.php ... /** * Log the user out of the application. * * @param \\Illuminate\\Http\\Request $request * @return \\Illuminate\\Http\\Response */ public function logout(Request $request) { $this-\u0026gt;guard()-\u0026gt;logout(); $request-\u0026gt;session()-\u0026gt;invalidate(); return redirect(\u0026#39;/\u0026#39;); } ... 上のファイルは編集するべきではありません。しかし、ログアウト後のリダイレクト先を変更したいなら、\napp/Http/Controllers/Auth/LoginController.php\nに、上のlogoutのコードを追加して、リダイレクト先を編集すればよいです。親のメソッドが上書きされて変更されることになります。use Illuminate\\Http\\Request;の行の追加も忘れないように。以下に全コードを掲載します。\nLoginController.php namespace App\\Http\\Controllers\\Auth; use App\\Http\\Controllers\\Controller; use Illuminate\\Foundation\\Auth\\AuthenticatesUsers; use Illuminate\\Http\\Request; class LoginController extends Controller { /* |-------------------------------------------------------------------------- | Login Controller |-------------------------------------------------------------------------- | | This controller handles authenticating users for the application and | redirecting them to your home screen. The controller uses a trait | to conveniently provide its functionality to your applications. | */ use AuthenticatesUsers; /** * Where to redirect users after login. * * @var string */ protected $redirectTo = \u0026#39;/home\u0026#39;; /** * Create a new controller instance. * * @return void */ public function __construct() { $this-\u0026gt;middleware(\u0026#39;guest\u0026#39;)-\u0026gt;except(\u0026#39;logout\u0026#39;); } /** * Log the user out of the application. * * @param \\Illuminate\\Http\\Request $request * @return \\Illuminate\\Http\\Response */ public function logout(Request $request) { $this-\u0026gt;guard()-\u0026gt;logout(); $request-\u0026gt;session()-\u0026gt;invalidate(); return redirect(\u0026#39;/\u0026#39;); } } 最後に今回のテストのコードは以下から利用可能です。\nhttps://github.com/lotsofbytes/larajapan/blob/5.4-test/tests/Feature/LoginTest.php\n","date":"2018-02-10T03:02:18+09:00","permalink":"https://www.larajapan.com/2018/02/10/%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E8%AA%8D%E8%A8%BC%E3%81%AE%E3%83%86%E3%82%B9%E3%83%883-laravel-5-4%E3%80%80%E3%83%AD%E3%82%B0%E3%82%A2%E3%82%A6%E3%83%88/","title":"ユーザー認証のテスト(3) Laravel 5.4　ログアウト"},{"content":"前回は、\nログイン画面にアクセスできる ログインしないで、ホームページにアクセスするとログイン画面にリダイレクトされる のテストを作成しました。\n今回は、同じくログイン画面において以下のテストを作成します。\n登録されているユーザーのＥメールとパスワードでログインできる 間違ったパスワードでログインした場合、ログイン失敗してエラーが出力される モデルファクトリ さて、前回と違って今回は、ログインのテストゆえにテストのデータベースにユーザーのレコードが必要となります。もちろん、手動でレコードを作成しておいてテストも可能です。しかし、テストが複雑になって様々なケースのテストが増えてくると非常に面倒です。\nそこで登場するのは、モデルファクトリ。テストのために、必要なデータを必要な数だけ自動生成してDBレコードを作成してくれます。ファクトリの設定は、LaravelのEloquentのモデルを指定するのでとても簡単。以下ではUserのモデルをもとにしています。Fakerのパッケージを利用しているので各項目での詳細はそちらを参照してください。\ndatabase/factories/ModelFactory.php use Faker\\Generator as Faker; /** @var \\Illuminate\\Database\\Eloquent\\Factory $factory */ $factory-\u0026gt;define(App\\User::class, function (Faker $faker) { static $password; return [ \u0026#39;email\u0026#39; =\u0026gt; $faker-\u0026gt;unique()-\u0026gt;safeEmail, // 複数レコード作成時には重複しないＥメールを生成 \u0026#39;password\u0026#39; =\u0026gt; $password ?: $password = bcrypt(\u0026#39;test1234\u0026#39;),　// 複数レコード作成時には同じパスワードを生成 \u0026#39;name\u0026#39; =\u0026gt; $faker-\u0026gt;name, \u0026#39;remember_token\u0026#39; =\u0026gt; str_random(100),　//ランダム値 ]; }); 上の設定では、複数のレコード作成時には、emailは重複しないように、パスワードはstatic変数を利用してどのレコードも同じパスワードを作成するようなっています。bcryptは、LaravelのヘルパーでHash::makeと同じです。\n日本語のデータを生成したいなら、config/app.phpで、\nconfig/app.php .. /* |-------------------------------------------------------------------------- | Application Locale Configuration |-------------------------------------------------------------------------- | | The application locale determines the default locale that will be used | by the translation service provider. You are free to set this value | to any of the locales which will be supported by the application. | */ \u0026#39;locale\u0026#39; =\u0026gt; \u0026#39;ja\u0026#39;, \u0026#39;faker_locale\u0026#39; =\u0026gt; \u0026#39;ja_JP\u0026#39;, .. と、faker_localeを設定してください。\nテストでレコードの自動作成 ユニットテストでは、毎回のテストにおいてテストDBを空にしてモデルファクトリで新しいデータを埋めます。これを実行するために、Laravelで２つの異なるメカニズムが用意されています。\nDataMigrations：Laravelのmigrationのメカニズムを使用して、毎回テストごとにDBテーブルを１から作成する DatabaseTransactions：DBのトランザクションの機能を利用して、毎回テストごとにDBテーブルを空にする 違いは重要で、前者はテストDBにテーブルが何も存在しなくてもＯＫですが、後者では空のDBテーブルが必要です。\n前者は、新規のプロジェクトで最初からLaravelのmigrationを使用してDBを管理しているならお薦めですが、既存のプログラムをLaravelに移行したプロジェクト（私の場合はこれが多い）では、違うメカニズムでDBを管理しているため前者が使用できないので後者となります。\n後者では、本DBと同じDB構造とするため、以下のようにmysqldumpなどを使用してコピーする必要あります。\n$ mysqludmp -u root larajapan -p -d \u0026gt; larajapan_test.sql $ mysql -u root larajapan_test -p \u0026lt; larajapan_test.sql ここでは、すでにmigrationの設定がなされているうえに、DatabaseMigrationsのトレイトをユニットテストの中で宣言します。\ntests/Feature/LoginTest.php namespace Tests\\Feature; use Tests\\TestCase; use Illuminate\\Foundation\\Testing\\WithoutMiddleware; use Illuminate\\Foundation\\Testing\\DatabaseMigrations; //use Illuminate\\Foundation\\Testing\\DatabaseTransactions; use App\\User; class LoginTest extends TestCase { use DatabaseMigrations; .. ログインテスト 準備整ったところで、テストしてみましょう。まずは、ログイン成功のテスト。\n先ほど設定したファクトリを使用して、最初に１つだけユーザーのレコードを作成します。その後、作成したのと同じパスワードを使用してログインを実行。実行後は、認証されていることと、ホームページにリダイレクトされることを確認します。\n/** @test */ public function valid_user_can_login() { // ユーザーを１つ作成 $user = factory(User::class)-\u0026gt;create([ \u0026#39;password\u0026#39; =\u0026gt; bcrypt(\u0026#39;test1111\u0026#39;) ]); // まだ、認証されていない $this-\u0026gt;assertFalse(Auth::check()); // ログインを実行 $response = $this-\u0026gt;post(\u0026#39;login\u0026#39;, [ \u0026#39;email\u0026#39; =\u0026gt; $user-\u0026gt;email, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;test1111\u0026#39; ]); // 認証されている $this-\u0026gt;assertTrue(Auth::check()); // ログイン後にホームページにリダイレクトされるのを確認 $response-\u0026gt;assertRedirect(\u0026#39;home\u0026#39;); } 次はログイン失敗のテスト。ここでもファクトリで１つだけレコードを作成しますが、わざと作成したのとは違うパスワードを使用してログインして、エラーメッセージが表示されることを確認します。\n/** @test */ public function invalid_user_cannot_login() { // ユーザーを１つ作成 $user = factory(User::class)-\u0026gt;create([ \u0026#39;password\u0026#39; =\u0026gt; bcrypt(\u0026#39;test1111\u0026#39;) ]); // まだ、認証されていないことを確認 $this-\u0026gt;assertFalse(Auth::check()); // 異なるパスワードでログインを実行 $response = $this-\u0026gt;post(\u0026#39;login\u0026#39;, [ \u0026#39;email\u0026#39; =\u0026gt; $user-\u0026gt;email, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;test2222\u0026#39; ]); // 認証失敗で、認証されていないことを確認 $this-\u0026gt;assertFalse(Auth::check()); // セッションにエラーを含むことを確認 $response-\u0026gt;assertSessionHasErrors([\u0026#39;email\u0026#39;]); // エラメッセージを確認 $this-\u0026gt;assertEquals(\u0026#39;メールアドレスあるいはパスワードが一致しません\u0026#39;, session(\u0026#39;errors\u0026#39;)-\u0026gt;first(\u0026#39;email\u0026#39;)); $responseに使用されるassertの関数は、phpunitからのassertTrueと違い、Laravelで宣言されているものです。\n参照としては、本サイトの以下\nhttps://laravel.com/docs/5.4/http-tests#available-assertions\nですが、そこには掲載されていない関数もあるので、以下もチェックしてください。\nhttps://laravel.com/api/5.4/Illuminate/Foundation/Testing/TestResponse.html\n最後に、テストを実行してみましょう。\n$ vendor/bin/phpunit --filter LoginTest PHPUnit 5.7.22 by Sebastian Bergmann and contributors. .... 4 / 4 (100%) Time: 1.09 seconds, Memory: 16.00MB OK (4 tests, 12 assertions) エラーもなく成功ですね！\n今回のコードは以下から利用可能です。\nhttps://github.com/lotsofbytes/larajapan/blob/5.4-test/tests/Feature/LoginTest.php\n","date":"2018-02-07T00:21:29+09:00","permalink":"https://www.larajapan.com/2018/02/07/%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E8%AA%8D%E8%A8%BC%E3%81%AE%E3%83%86%E3%82%B9%E3%83%882-laravel-5-4%E3%80%80%E3%83%AD%E3%82%B0%E3%82%A4%E3%83%B3/","title":"ユーザー認証のテスト(2) Laravel 5.4　ログイン"},{"content":"Laravelでのユーザー認証は私のブログの中では最も人気のあるトピックです。今回は、私のLaravelの日本語のレポジトリ（Laravel 5.4）のコードをもとに、ユーザー認証のテストに取り組んでいきます。\nテストの種類 テストと言っても、いくつか種類があり、Laravel5.4からはtestsのディレクトリ構造も変わり、 tests ├── CreatesApplication.php ├── Feature │ └── ExampleTest.php ├── TestCase.php └── Unit └── ExampleTest.php のようにFeatureとUnitの２つのサブディレクトリができました。\nUnitには、一般的にはユニットテストと呼ばれるもので、大方は画面の表示を伴わないModelのメソッドに対するテストを作成します。一方、Featureには、機能テストやアクセプタンステストとも言われ、複数のクラスの複数のメソッドが関わる主にコントローラの機能を検証するためで、あたかもユーザーがテストするようなテストを作成します。\n私の今までの開発では、比較的作成しやすいUnitテストが主で、Featureテストは皆無に近く、人間のテスターがその仕事を行っています。人間をFeatureテストに置き換える予定はないですが、Laravelが提供するテストの環境がFeatureテストを作成しやすくなってきているので、この機会に習得しようということです。\nLaravel5.4では、Featureテストのために、２つのテストのフレームワークが提供されています。\nHTTPテスト：ブラウザを通さないエンドポイントのテスト ブラウザテスト(Dusk)：ブラウザキットを利用したテスト ここでは、まず、追加のパッケージのインストールも要らない、高速なHTTPテストを作成していきます。\n準備 github.comにおいて以下のブランチを用意したので利用してください。\nhttps://github.com/lotsofbytes/larajapan/tree/5.4-test\nインストールは以下を参照してください。\nLaravelの日本語のレポジトリ（Laravel 5.4）\n設定したら、必ず以下を実行してブランチを変えてください。\n$ git checkout l54-test また、そこでは、phpunit.xmlを編集も必要です。以下を参考にしてください。\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;phpunit backupGlobals=\u0026#34;false\u0026#34; backupStaticAttributes=\u0026#34;false\u0026#34; bootstrap=\u0026#34;bootstrap/autoload.php\u0026#34; colors=\u0026#34;true\u0026#34; convertErrorsToExceptions=\u0026#34;true\u0026#34; convertNoticesToExceptions=\u0026#34;true\u0026#34; convertWarningsToExceptions=\u0026#34;true\u0026#34; processIsolation=\u0026#34;false\u0026#34; stopOnFailure=\u0026#34;false\u0026#34;\u0026gt; \u0026lt;testsuites\u0026gt; \u0026lt;testsuite name=\u0026#34;Feature\u0026#34;\u0026gt; \u0026lt;directory suffix=\u0026#34;Test.php\u0026#34;\u0026gt;./tests/Feature\u0026lt;/directory\u0026gt; \u0026lt;/testsuite\u0026gt; \u0026lt;testsuite name=\u0026#34;Unit\u0026#34;\u0026gt; \u0026lt;directory suffix=\u0026#34;Test.php\u0026#34;\u0026gt;./tests/Unit\u0026lt;/directory\u0026gt; \u0026lt;/testsuite\u0026gt; \u0026lt;/testsuites\u0026gt; \u0026lt;filter\u0026gt; \u0026lt;whitelist processUncoveredFilesFromWhitelist=\u0026#34;true\u0026#34;\u0026gt; \u0026lt;directory suffix=\u0026#34;.php\u0026#34;\u0026gt;./app\u0026lt;/directory\u0026gt; \u0026lt;/whitelist\u0026gt; \u0026lt;/filter\u0026gt; \u0026lt;php\u0026gt; \u0026lt;env name=\u0026#34;APP_ENV\u0026#34; value=\u0026#34;testing\u0026#34;/\u0026gt; \u0026lt;env name=\u0026#34;APP_URL\u0026#34; value=\u0026#34;http://localhost\u0026#34;/\u0026gt; \u0026lt;env name=\u0026#34;CACHE_DRIVER\u0026#34; value=\u0026#34;array\u0026#34;/\u0026gt; \u0026lt;env name=\u0026#34;SESSION_DRIVER\u0026#34; value=\u0026#34;array\u0026#34;/\u0026gt; \u0026lt;env name=\u0026#34;DB_HOST\u0026#34; value=\u0026#34;localhost\u0026#34;/\u0026gt; \u0026lt;env name=\u0026#34;DB_DATABASE\u0026#34; value=\u0026#34;larajapan_test\u0026#34;/\u0026gt; \u0026lt;env name=\u0026#34;DB_USERNAME\u0026#34; value=\u0026#34;test\u0026#34;/\u0026gt; \u0026lt;env name=\u0026#34;DB_PASSWORD\u0026#34; value=\u0026#34;password\u0026#34;/\u0026gt; \u0026lt;env name=\u0026#34;MAIL_DRIVER\u0026#34; value=\u0026#34;log\u0026#34; /\u0026gt; \u0026lt;env name=\u0026#34;QUEUE_DRIVER\u0026#34; value=\u0026#34;sync\u0026#34;/\u0026gt; \u0026lt;/php\u0026gt; \u0026lt;/phpunit\u0026gt; テストのDBは.envで設定しているものと違うDBが必要なことに注意してください。\n最初のテスト もとからあるtests/Feature/ExampleTest.phpのファイルを、LoginTest.phpと改名して以下のように編集します。\nnamespace Tests\\Feature; use Tests\\TestCase; use Illuminate\\Foundation\\Testing\\WithoutMiddleware; use Illuminate\\Foundation\\Testing\\DatabaseMigrations; use Illuminate\\Foundation\\Testing\\DatabaseTransactions; class LoginTest extends TestCase { /** @test */ public function user_can_view_login() { $response = $this-\u0026gt;get(\u0026#39;login\u0026#39;); $response-\u0026gt;assertStatus(200); } /** @test */ public function unauthenticated_user_cannot_view_home() { $this-\u0026gt;get(\u0026#39;home\u0026#39;) -\u0026gt;assertRedirect(\u0026#39;login\u0026#39;); } } 最初のテストuser_can_view_loginは、ログイン画面が閲覧できるかどうかのテストです。 返ってくるHTTPのステータスのコードが200なら、成功ということです。これが404（ページが見つかりません）とかだと何かがおかしいということになります。\n次のテストunauthenticated_user_cannot_view_homeは、認証が必要なホームページに、認証もなしにアクセスしてみます。もちろん、アクセスできないでログイン画面にリダイレクトされるはずです。\nここ、メソッドが連結されていることに気づきましたか？\n最初のテストの例のように、以下のよう2つの文に分けても書くこともできます。\n$response = $this-\u0026gt;get(\u0026#39;home\u0026#39;); $response-\u0026gt;assertRedirect(\u0026#39;login\u0026#39;); これらのテストの実行ですが、Laravelのインストールにより、すでにvendorのディレクトリにphpunitのパッケージもインストールされています。\nということで、\n$ vendor/bin/phpunit PHPUnit 5.7.23 by Sebastian Bergmann and contributors. ... 3 / 3 (100%) Time: 171 ms, Memory: 10.00MB OK (3 tests, 4 assertions) と実行してテストは皆成功となります。3 testsとあるのは、app/tests/Unit/ExampleTestがあるからです。 以下のようにフィルタを書ければ、LoginTestの中のテストだけや、user_can_view_loginの1つテストだけの実行も可能です。\n$ vendor/bin/phpunit --filter=LoginTest HPUnit 5.7.23 by Sebastian Bergmann and contributors. .. 2 / 2 (100%) Time: 157 ms, Memory: 10.00MB OK (2 tests, 3 assertions) $ vendor/bin/phpunit --filter=user_can_view_login PHPUnit 5.7.23 by Sebastian Bergmann and contributors. . 1 / 1 (100%) Time: 111 ms, Memory: 10.00MB OK (1 test, 1 assertion) ","date":"2018-01-31T13:54:46+09:00","permalink":"https://www.larajapan.com/2018/01/31/%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E8%AA%8D%E8%A8%BC%E3%81%AE%E3%83%86%E3%82%B9%E3%83%881-laravel-5-4/","title":"ユーザー認証のテスト(1) Laravel 5.4"},{"content":"Laravelのメジャーのバージョンは、以下のように、ほぼ年に2回のペースでリリースされています。\n頻繁に更新されることは、Laravelがいつもアクティブである証拠でとても良いのですが、追い付いていくのが大変なのが現状です。\nもちろん便利な機能が登場してきたり、今まで複雑だったことが簡単にできるようなったりとかプラスの面が多いのですが、過去には、バージョン間でフレームワークのディレクトリのレイアウトが変わったり、関数名が変わったりと、ほとんど書き直しを強制される結果となることもあります。\nさらに、お客さんのプロジェクトとなれば、バージョンの更新ではＵＩは何も変わらないので、どうコストを正当化するのかも頭が痛いことです。しかし、バージョンアップしないとLaravelのサポートが期限切れとなるし、便利な新しい機能も使えないし。。。\nこういうときに、見つけたのが、バージョン更新サービスの、Laravel Shift\n早速、このサービスを利用して、私のLaravel5.3の日本語のリポジトリを5.4に更新してみましょう。\n残念ながら英語のサービスですが、すでにgithubやbitbucketのリポジトリを使用しているならことは簡単です。\nホームページで「Get Started Now」をクリックすると、以下のようなポップアップが登場します。 Gitのレポジトリにログインします。 Laravel Shiftがgithubの私のレポジトリにアクセスすることを許可します。 Welcome画面が表示されます。そこの右上の「New Shift」をクリックして次の画面へ進みます。 更新先のバージョンを選択します。ここでは5.3から5.4なので、「Laravel 5.4」にマウスを持っていき、「Purchase」をクリックします。 更新元のレポジトリとブランチ名を入力します。 支払い画面です。「Purchase Shift」をクリックします。 クレジットカード情報を入れます。 カード決済が完了すると、作業が開始されます。プロジェクトのサイズにもよりますが、完了するとステータスのアイコンが変わり終了です。 これで、Laravel Shiftのサイトでの作業は終わりです。領収書のＥメールも送信されます。\n一方、gifthub.comの方では、プルリクエストが作成され、shift-のプレフィックスのブランチが作成されています。\nプルリクエストの内容は以下で閲覧できます。\nhttps://github.com/lotsofbytes/larajapan/pull/2\nこのブランチでは、コミットは６つ存在し、先のプルリクエストでそれらに関して説明が記されています。この説明は十分理解する必要あり。\n作成されたブランチがそのまま使用できるのではなく、以下の作業が必要と書かれています。\n作成されたブランチをgitでチェックアウトしてください。 プルリクエストのすべてのコメントをレビューしてください。追加の変更を行う必要があるかもしれません。 Laravel 5.4の更新のために依存部分を更新してください。 composer updateを実行してください。失敗するなら--no-scriptsを付けて再度実行してください 徹底的にテストしてください/li\u003e 以上をこなして実行してみると、動作しましたね！\nもちろん今回はとてもシンプルなプロジェクトでの更新なので、調整も最低限度と思いますが、大きなプロジェクトではShiftのブランチで簡単にはいかないと思います。しかし、LaravelのサイトでのUpgrade Guideを読んで、現在のプロジェクトを手動で更新、または新バージョンをまっさらでインストールして、そこにプログラムを１つ１つ移行していくなどの作業よりは、Shiftを使うとスタート時点でかなり違うかなと感じです。今回は１１ドルとコストも低いし。\n私のクライアントのプロジェクトでは、5.2 ⇒ 5.3 ⇒ 5.4 ⇒ 5.5と3回Larvel Shiftの作業が必要となるわけですが、試してみようと思います。\n","date":"2018-01-18T04:59:38+09:00","permalink":"https://www.larajapan.com/2018/01/18/laravel-shift%E3%81%A7%E3%83%90%E3%83%BC%E3%82%B8%E3%83%A7%E3%83%B3%E3%82%92%E6%9B%B4%E6%96%B0/","title":"Laravel Shiftでバージョンを更新"},{"content":"今年最後の投稿です。まだまだ投稿のネタはたくさんあります。来年も楽しみに！\ntinkerに関しては、以前に紹介していますが、私には現在もなくてはならないコマンドラインツール。\nこのツール、実は機能が豊富にあるということ最近気づきました。ますます好きになりました。\n私がtinkerを利用する一番の理由は、Laravelの知らない、あるいは正確なシンタックスを覚えていないメソッドをテストしたいときです。dd()入れてブラウザで実行して結果をチェックするのは面倒、unit testを書くには大袈裟なとき、tinkerが一番です。\n例えば、以下pluckの関数のパラメータ、どっちが配列のインデックスになるのだっけ？　all()のコールは必要だっけ？　・・というときに。\n$ php artisan tinker Psy Shell v0.8.11 (PHP 7.0.23 — cli) by Justin Hileman \u0026gt;\u0026gt;\u0026gt; use App\\User; \u0026gt;\u0026gt;\u0026gt; User::all()-\u0026gt;pluck(\u0026#39;id\u0026#39;, \u0026#39;name\u0026#39;); =\u0026gt; Illuminate\\Support\\Collection {#705 all: [ \u0026#34;kenji\u0026#34; =\u0026gt; 1, \u0026#34;test\u0026#34; =\u0026gt; 2, ], } \u0026gt;\u0026gt;\u0026gt; User::all()-\u0026gt;pluck(\u0026#39;name\u0026#39;, \u0026#39;id\u0026#39;); =\u0026gt; Illuminate\\Support\\Collection {#703 all: [ 1 =\u0026gt; \u0026#34;kenji\u0026#34;, 2 =\u0026gt; \u0026#34;test\u0026#34;, ], } \u0026gt;\u0026gt;\u0026gt; User::pluck(\u0026#39;name\u0026#39;, \u0026#39;id\u0026#39;); =\u0026gt; Illuminate\\Support\\Collection {#705 all: [ 1 =\u0026gt; \u0026#34;kenji\u0026#34;, 2 =\u0026gt; \u0026#34;test\u0026#34;, ], } \u0026gt;\u0026gt;\u0026gt; と、いちいちマニュアルをチェックすることなしに、まずは実行して試す。\n実行した結果だけでなく、プログラム自体の表示も可能です。\n\u0026gt;\u0026gt;\u0026gt; show User \u0026gt; 9| class User extends Authenticatable 10| { 11| use Notifiable; 12| use CanResetPassword; 13| 14| /** 15| * The attributes that are mass assignable. 16| * 17| * @var array 18| */ 19| protected $fillable = [ 20| \u0026#39;name\u0026#39;, \u0026#39;email\u0026#39;, \u0026#39;password\u0026#39;, 21| ]; 22| 23| /** 24| * The attributes that should be hidden for arrays. 25| * 26| * @var array 27| */ 28| protected $hidden = [ 29| \u0026#39;password\u0026#39;, \u0026#39;remember_token\u0026#39;, 30| ]; 31| } クラス全体だけでなく、指定のメソッドのコード部分も。実際は以下の画像のように、綺麗な色付きで表示されるので読みやすいです。\nbashのhistoryのように、今まで実行した履歴も閲覧可能です。\n\u0026gt;\u0026gt;\u0026gt; hist 0: use App\\User; 1: User::all()-\u0026gt;pluck(\u0026#39;id\u0026#39;, \u0026#39;name\u0026#39;); 2: User::all()-\u0026gt;pluck(\u0026#39;name\u0026#39;, \u0026#39;id\u0026#39;); 3: User::pluck(\u0026#39;name\u0026#39;, \u0026#39;id\u0026#39;); histの結果は、とても長くなるので、一部だけを閲覧することも可能です。\n\u0026gt;\u0026gt;\u0026gt; hist --show 1..3 0: use App\\User; 1: User::all()-\u0026gt;pluck(\u0026#39;id\u0026#39;, \u0026#39;name\u0026#39;); 2: User::all()-\u0026gt;pluck(\u0026#39;name\u0026#39;, \u0026#39;id\u0026#39;); 3: User::pluck(\u0026#39;name\u0026#39;, \u0026#39;id\u0026#39;); さらに、履歴を\u0026ndash;replayで再実行可能です。\n\u0026gt;\u0026gt;\u0026gt; hist --show 2 --replay Replaying 1 line of history --\u0026gt; User::all()-\u0026gt;pluck(\u0026#39;name\u0026#39;, \u0026#39;id\u0026#39;); =\u0026gt; Illuminate\\Support\\Collection {#708 all: [ 1 =\u0026gt; \u0026#34;kenji\u0026#34;, 2 =\u0026gt; \u0026#34;test\u0026#34;, ], } histには、他にも\u0026ndash;head、\u0026ndash;tail、\u0026ndash;grepのオプションがあります。\nさらに、tinkerは、php関数のマニュアルも出力することも可能です。\nまず、設定として、マニュアルをダウンロードします。ファイルは11MBのサイズです。\nmkdir ~/.local/share/psysh cd ~/.local/share/psysh wget http://psysh.org/manual/ja/php_manual.sqlite 関数のマニュアルを表示するには、docを利用します。例えば、\n\u0026gt;\u0026gt;\u0026gt; doc array_shift とタイプすれば、\nと日本語で表示してくれます。また、Laravelの関数では、クラスのパス名を指定すれば、\n\u0026gt;\u0026gt;\u0026gt; doc \\Illuminate\\Http\\Request::input public function input($key = null, $default = null) Description: Retrieve an input item from the request. Param: string $key string|array|null $default Return: string|array と説明が見れます。\n最後に、Laravelのtinkerは、以下のPsySHのラッパーです。 https://github.com/bobthecow/psysh こんな便利なツールを開発してくれて、ありがとう！\n","date":"2017-12-27T06:29:23+09:00","permalink":"https://www.larajapan.com/2017/12/27/%E3%82%82%E3%81%A3%E3%81%A8%E3%83%86%E3%82%A3%E3%83%B3%E3%82%AB%E3%83%BCtinker%E3%82%92%E4%BD%BF%E3%81%8A%E3%81%86%EF%BC%81/","title":"もっとティンカー(tinker)を使おう！"},{"content":"前回で説明したPHPのコーディングスタイルの基準PSR2、これをすべて覚えて手動で実行しようというのは到底無理なこと、そこで登場するのが、php-cs-fixer、自動でそれを行ってくれるツールです。\nインストールはいたって簡単。\n今回は、phanやphpunitとは違い、プロジェクトが使用しているLaravelやPHPのバージョンに合わせる必要はないので、プロジェクトごとではなくグローバルに、つまり自分のホームディレクトリでのインストールとします。\n$ composer global require friendsofphp/php-cs-fixer これを実行すると、いくつかの必要なパッケージがインストールされます。\nそして、~/.composer/vendor/bin/php-cs-fixerにファイル（実際はシムリンク）が作成されます。\nどこでもこれが実行できるように、.bahsrcに以下の設定を入れておくことを勧めます。\n.bashrc ... export PATH=\u0026#34;$PATH:$HOME/.composer/vendor/bin\u0026#34; ... 実行も簡単。例えば、変換対象のファイルを、test.phpとすると、\n$ php-cs-fixer fix test.php Loaded config default. Using cache file \u0026#34;.php_cs.cache\u0026#34;. 1) test.php Fixed all files in 0.193 seconds, 10.000 MB memory used test.phpが、デフォルトのルールで、PSR1とPSR2に基づいて変換されます。\n以下は、前回の例のファイルを崩したものです。\n\u0026lt;?php namespace Vendor\\Package; use FooInterface; use BarClass as Bar; use OtherVendor\\OtherPackage\\BazClass; class Foo extends Bar implements FooInterface { protected $a, $b, $c; public function sampleMethod( $a,$b = null ) { if ( $a === $b ) { bar(); } elseif ($a \u0026gt; $b) { $foo-\u0026gt;bar($arg1); } else { BazClass::bar($arg2,$arg3); } } } ?\u0026gt; これをphp-cs-fixerで修正すると、前回のように\n\u0026lt;?php namespace Vendor\\Package; use FooInterface; use BarClass as Bar; use OtherVendor\\OtherPackage\\BazClass; class Foo extends Bar implements FooInterface { protected $a; protected $b; protected $c; public function sampleMethod($a, $b = null) { if ($a === $b) { bar(); } elseif ($a \u0026gt; $b) { $foo-\u0026gt;bar($arg1); } else { BazClass::bar($arg2, $arg3); } } } となります。\nphp-cs-fixerの実行では、.php_cs.cacheのファイルが作成されます。この中には、使用されたJSON設定が収められています。バージョン管理には要らないファイルなので、.gitignoreなどに含むようにしてください。 さて、php-cs-fixerを使用してLaravelのプロジェクト全体をPSR2準拠に変換します。Laravelのプロジェクトにはいろいろなディレクトリがあるために、変換必要なファイルだけを指定する必要があります。そのためには以下のような設定ファイル.php_csが必要です。\n.php_cs \u0026lt;?php $finder = PhpCsFixer\\Finder::create() -\u0026gt;exclude(\u0026#39;bootstrap/cache\u0026#39;) -\u0026gt;exclude(\u0026#39;storage\u0026#39;) -\u0026gt;exclude(\u0026#39;vendor\u0026#39;) -\u0026gt;in(__DIR__) -\u0026gt;name(\u0026#39;*.php\u0026#39;) -\u0026gt;notName(\u0026#39;*.blade.php\u0026#39;) -\u0026gt;ignoreDotFiles(true) -\u0026gt;ignoreVCS(true) ; return PhpCsFixer\\Config::create() -\u0026gt;setRules(array( \u0026#39;@PSR2\u0026#39; =\u0026gt; true, )) -\u0026gt;setFinder($finder) ; 設定ファイル自体がphpのプログラムです。そこで見られるように、bootstrap/cache, storage, vendorのディレクトリ以外のphpファイルはすべてPSR2準拠に変換してくれます。しかし、bladeのファイルはphpファイルでないゆえに変換しません。\nこのファイルを、Laravelプロジェクトのルートディレクトリに置きます、そして変換は、そこで以下の実行だけです。\n$ php-cs-fixer fix 変換前に、gitのブランチをこのために作成しておいてから実行してください。例えば、新ブランチをpsr2と命名して、変換後に差分をチェックしたいなら、\n$ git diff master -w と実行すれば、タブからスペースへの変更以外の変更をチェックできます。\n最後に、このツールの作者は、有名なphpフレームワーク、symfonyのフレームワークの作者でもあります。Laravelのライブラリの一部はsymfonyのを利用しています。\n","date":"2017-12-09T02:30:18+09:00","permalink":"https://www.larajapan.com/2017/12/09/psr2%E5%A4%89%E6%8F%9B%E3%83%84%E3%83%BC%E3%83%AB/","title":"PSR2変換ツール"},{"content":"PHP5.6からPHP7.0へ移行したところで、クライアントのプロジェクトもLaravel5.2からLaravel5.5へのアップグレードが必要と思う最近。\nというのは、Laravel5.5は、LTS(Long Term Support)のバージョンであり、長期サポートということで2年間続くからです。Laravelは、現在ほぼ1年に2回にメジャーなバージョンアップをします。あるバージョンでは前バージョンと異なる部分が大きいこともあり、現実的には変更や検証の作業が大変で毎回毎回はとても。ということで、Laravel5.5に移行したら当分はバージョンアップはマイナーだけで済むのではないかという考えです。\nしかし、この検討中に、私らが開発で使用しているコーディングスタイルがLaravelのそれとは違うことに気づきました。一番大きい違いは、私らはインデントにタブ文字を使用していますが、現在のLaravelは、タブの代わりに空白文字（スペース）の使用です。なんと、Laravel5.1からすでにそうなっていたらしい。\n調べてみると、Laravelは、PSR2というコーディングスタイルを現在採用しています。PSRは、PHP Standard Recommendationの略で、PHPのお勧めの標準という意味。標準にもいろいろなレベルがあって、コーディングスタイルのスタンダードだけでなく、ローディング、ログ、キャッシュなどいろいろなスタンダードがあります。\n参照：http://www.php-fig.org/psr\nさて、Laravelが使用しているPSR2は、\nhttp://www.php-fig.org/psr/psr-2/\nで詳しく説明されていますが、\n要は、複数のプログラマが同じプロジェクトで開発を行うときに、コードの読みやすさのためのコーディングの規則ということです。\n取り決めの例としては、\nインデントには、タブではなく4文字のスペース文字を使用する。 namespaceの宣言の後には空行を１行入れる。useの宣言のブロックの後にも空行１行を入れる。 クラスの定義の括弧は、 class Foo { ... } でなく、 Class Foo { ... } である。 条件文(if, switch)やループ文(for, foreach)の括弧は、 if (true) { ... } else { ... } でなく、 if (true) { ... } else { ... } である。 PHPの定数、true, false, nullは必ず小文字 などなど、変数名に関しての規則も含み結構たくさんあります。\nPSR2準拠のサンプルとして以下が表示されています。\n\u0026lt;?php namespace Vendor\\Package; use FooInterface; use BarClass as Bar; use OtherVendor\\OtherPackage\\BazClass; class Foo extends Bar implements FooInterface { public function sampleMethod($a, $b = null) { if ($a === $b) { bar(); } elseif ($a \u0026gt; $b) { $foo-\u0026gt;bar($arg1); } else { BazClass::bar($arg2, $arg3); } } final public static function bar() { // method body } } 私は、昔からタブ派でなので、ここに来てスペース（空白）派になることには抵抗あります。また、bitbucketやgithubのサイトでコードが表示されるときにタブだと上下で揃わなくなったりとマイナスな面にも気づいていました。しかし、sublimeのようなエディターは特別に指定しなくても既存のインデントに合わせてくれるし、コーディングにおいてはタブからスペースへの変換はそう問題ないだろうと、PSRの標準に移行することに決めました。\n決めたところで、問題は、現行のプログラムをどうすべてPSR2のコーディングスタイルに変換するか。次回の課題です。\n","date":"2017-12-08T02:15:25+09:00","permalink":"https://www.larajapan.com/2017/12/08/psr2/","title":"PSR2"},{"content":"プログラマというのは、その性質上、いかにプログラムの行数を少なくして、やりたいことをクリーンに明確に表現できるかに時間を費やしたりします。そして、重複の表現はすぐに気づき、忌み嫌い、どうしたらそれをなくすことができることを日夜考えます。\n私もそのひとりで、例えば、昔以下のようなコードありました、「どうにかならんかな？」と気になっていました。\nroutes/web.php .. // 会員登録 Route::get (\u0026#39;member/signup\u0026#39;, \u0026#39;MemberController@signup\u0026#39;)-\u0026gt;name(\u0026#39;member.signup\u0026#39;); Route::post(\u0026#39;member/signup\u0026#39;, \u0026#39;MemberController@postSignup\u0026#39;)-\u0026gt;name(\u0026#39;member.signup\u0026#39;); .. 見ての通り、どちらもメソッド（get, post）は違いますが、URLは同じmember/signupとなります。もちろん、Route::resourceを使うことも可能ですが、URLは、createでなくsignupとしたいし、必要な処理も登録だけです。\n上のコードのphp artisan route:listでの出力はこんな感じです。\n+--------+----------+-------------------+---------------+---------------------------------------------------+--------------+ | Domain | Method | URI | Name | Action | Middleware | +--------+----------+-------------------+---------------+---------------------------------------------------+--------------+ | | GET|HEAD | member/signup | member.signup | App\\Http\\Controllers\\MemberController@signup | web,guest | | | POST | member/signup | member.signup | App\\Http\\Controllers\\MemberController@postSignup | web,guest | +--------+----------+-------------------+---------------+---------------------------------------------------+--------------+ 2行も必要でしょうかね？\nこう思っているのは私だけではなかったようです。LaravelのマニュアルにRoute::matchを見つけました。\n先の2行のルートの指定を以下のように1行にすることができました。\nroutes/web.php .. // 会員登録 Route::match([\u0026#39;get\u0026#39;, \u0026#39;post\u0026#39;], \u0026#39;member/signup\u0026#39;, \u0026#39;MemberController@signup\u0026#39;)-\u0026gt;name(\u0026#39;member.signup\u0026#39;); .. php artisan route:listでの出力はこうなりました。\n+--------+---------------+-------------------+---------------+---------------------------------------------------+--------------+ | Domain | Method | URI | Name | Action | Middleware | +--------+---------------+-------------------+---------------+---------------------------------------------------+--------------+ | | GET|POST|HEAD | member/signup | member.signup | App\\Http\\Controllers\\MemberController@signup | web,guest | +--------+---------------+-------------------+---------------+---------------------------------------------------+--------------+ 素晴らしい！と思うとともに、さてどうやって１つのメソッドsignupでGETとPOSTの２つに対応したらよいものか？\n解決方法は、\nMemberController namespace App\\Http\\Controllers; use Illuminate\\Http\\Request use App\\Models\\Member; class MemberController extends Controller { ... /** * Signup * * @param \\Illuminate\\Http\\Request $request * @return \\Illuminate\\Http\\Response */ public function signup(Request $request) { if ($request-\u0026gt;method() == \u0026#39;POST\u0026#39;) { return $this-\u0026gt;postSignup($request); } return view(\u0026#39;user/member_add\u0026#39;); } /** * Save Signup * * @param \\Illuminate\\Http\\Request $request * @return \\Illuminate\\Http\\Response */ public function postSignup(Request $request) { ... } のように、signupメソッドにおいて、POSTで呼ばれたかを判断して、postSignupをコールします。\nしかし、routesは1行になったけれど、コントローラはちょっとわかりにくいかな、とも思いますね。\n","date":"2017-12-02T06:32:00+09:00","permalink":"https://www.larajapan.com/2017/12/02/routematch/","title":"Route::match"},{"content":"前回に続いて、turbolinksの話。\n今回は、turbolinksをもとで、ページに含まれる","date":"2017-11-19T02:24:02+09:00","permalink":"https://www.larajapan.com/2017/11/19/turbolinks%E3%81%A7%E7%94%BB%E9%9D%A2%E3%81%AE%E8%A1%A8%E7%A4%BA%E3%82%92%E3%82%B9%E3%83%94%E3%83%BC%E3%83%89%E3%82%A2%E3%83%83%E3%83%97-2-script/","title":"turbolinksで画面の表示をスピードアップ (2) \u003cscript\u003e"},{"content":"Laravelのフレームワークのおかげで、自分で作成した古いフレームワークもどきや、CodeIgniterの「もうサポートしません」（注１）フレームワークを脱出できて、以前よりしっかりした開発の領域に入ってきたと感じているこの頃。そして、ファサード、ネームスペース、クロージャ、トレイトなどを活用して、とてもモダン。しかし、最近人気が出てきたJavascriptのフレームワーク、Angular, React, Vuejsを使用したシングルページアプリ（SPA）がとても気になります。\nLaravelは基本的にサーバーサイドで、Angularなどはクライアントサイドなので、共存は可能と言えばそうなのだけれど、せっかく時間かけてマスターしたLaravelのプログラムを書き直すとか、今度はどのJavascriptフレームワークをマスターすればよいのとか、ajaxばかりでプログラム複雑になるのでは、とか、過去には、CodeIgniterで苦い目にあったし、最近やっと大きいプロジェクトを3年かけてLaravelに書き直したばかりなので、ちょっとポジティブにはなれません。\nそんな中で、知り合ったのが題名のturbolinks。このテクノロジーの謳い文句は、\nJavascriptのフレームワークを使用して複雑にすることなしに、シングルページアプリ（SPA）のパフォーマンスが得られる！ なんか「何もしなくても痩せる！」というダイエットサプリメントのような感じですが、今の私にピッタリ。とりあえず、紹介しましょう。\nまず、メカニズムですが、そう難しくはありません。ページ内に同じサイト内のリンク、つまりがあると、turbolinksはajaxでそのページを取ってきてその中ののデータを現在のと取り換えます。は同じなので、結果的には、\nのように、タブのところに表示される「ページロード中」のぐるぐるがなくなります。さらに、\u003c/code\u003eやURLも正しく変わります。\n\u003cp\u003eインストールはいたって簡単。\n\u003cp\u003e以下の２つのファイルを用意してください。最初のファイルだけに、\u003ccode\u003eturbolinks.js\u003c/code\u003eが入っていることに注意してください。\n\u003cdiv class=\"code-filename\"\u003eindex.html\u003c/div\u003e \u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-html\" data-lang=\"html\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u0026lt;\u003cspan style=\"color:#f92672\"\u003ehtml\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003elang\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;ja\u0026#34;\u003c/span\u003e\u0026gt; \u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e \u0026lt;\u003cspan style=\"color:#f92672\"\u003ehead\u003c/span\u003e\u0026gt; \u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e \u0026lt;\u003cspan style=\"color:#f92672\"\u003emeta\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003echarset\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;utf-8\u0026#34;\u003c/span\u003e\u0026gt; \u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e \u0026lt;\u003cspan style=\"color:#f92672\"\u003etitle\u003c/span\u003e\u0026gt;Turbolinks\u0026lt;/\u003cspan style=\"color:#f92672\"\u003etitle\u003c/span\u003e\u0026gt; \u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e \u0026lt;\u003cspan style=\"color:#f92672\"\u003escript\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003esrc\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;https://cdnjs.cloudflare.com/ajax/libs/turbolinks/5.0.3/turbolinks.js\u0026#34;\u003c/span\u003e\u0026gt;\u0026lt;/\u003cspan style=\"color:#f92672\"\u003escript\u003c/span\u003e\u0026gt; \u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e \u0026lt;/\u003cspan style=\"color:#f92672\"\u003ehead\u003c/span\u003e\u0026gt; \u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e \u0026lt;\u003cspan style=\"color:#f92672\"\u003ebody\u003c/span\u003e\u0026gt; \u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e \u0026lt;\u003cspan style=\"color:#f92672\"\u003eh1\u003c/span\u003e\u0026gt;Turbolinks\u0026lt;/\u003cspan style=\"color:#f92672\"\u003eh1\u003c/span\u003e\u0026gt; \u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e \u0026lt;\u003cspan style=\"color:#f92672\"\u003eul\u003c/span\u003e\u0026gt; \u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e \u0026lt;\u003cspan style=\"color:#f92672\"\u003eli\u003c/span\u003e\u0026gt;\u0026lt;\u003cspan style=\"color:#f92672\"\u003ea\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003ehref\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;page1.html\u0026#34;\u003c/span\u003e\u0026gt;ページ 1\u0026lt;/\u003cspan style=\"color:#f92672\"\u003ea\u003c/span\u003e\u0026gt;\u0026lt;/\u003cspan style=\"color:#f92672\"\u003eli\u003c/span\u003e\u0026gt; \u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e \u0026lt;/\u003cspan style=\"color:#f92672\"\u003eul\u003c/span\u003e\u0026gt; \u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e \u0026lt;/\u003cspan style=\"color:#f92672\"\u003ebody\u003c/span\u003e\u0026gt; \u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u0026lt;/\u003cspan style=\"color:#f92672\"\u003ehtml\u003c/span\u003e\u0026gt; \u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cdiv class=\"code-filename\"\u003epage1.html\u003c/div\u003e \u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-html\" data-lang=\"html\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u0026lt;\u003cspan style=\"color:#f92672\"\u003ehtml\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003elang\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;ja\u0026#34;\u003c/span\u003e\u0026gt; \u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e \u0026lt;\u003cspan style=\"color:#f92672\"\u003ehead\u003c/span\u003e\u0026gt; \u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e \u0026lt;\u003cspan style=\"color:#f92672\"\u003emeta\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003echarset\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;utf-8\u0026#34;\u003c/span\u003e\u0026gt; \u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e \u0026lt;\u003cspan style=\"color:#f92672\"\u003etitle\u003c/span\u003e\u0026gt;ページ 1\u0026lt;/\u003cspan style=\"color:#f92672\"\u003etitle\u003c/span\u003e\u0026gt; \u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e \u0026lt;/\u003cspan style=\"color:#f92672\"\u003ehead\u003c/span\u003e\u0026gt; \u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e \u0026lt;\u003cspan style=\"color:#f92672\"\u003ebody\u003c/span\u003e\u0026gt; \u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e \u0026lt;\u003cspan style=\"color:#f92672\"\u003eh1\u003c/span\u003e\u0026gt;ページ 1\u0026lt;/\u003cspan style=\"color:#f92672\"\u003eh1\u003c/span\u003e\u0026gt; \u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e \u0026lt;\u003cspan style=\"color:#f92672\"\u003ep\u003c/span\u003e\u0026gt;\u0026lt;\u003cspan style=\"color:#f92672\"\u003ea\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003ehref\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;index.html\u0026#34;\u003c/span\u003e\u0026gt;戻る\u0026lt;/\u003cspan style=\"color:#f92672\"\u003ea\u003c/span\u003e\u0026gt;\u0026lt;/\u003cspan style=\"color:#f92672\"\u003ep\u003c/span\u003e\u0026gt; \u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e \u0026lt;/\u003cspan style=\"color:#f92672\"\u003ebody\u003c/span\u003e\u0026gt; \u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u0026lt;/\u003cspan style=\"color:#f92672\"\u003ehtml\u003c/span\u003e\u0026gt; \u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eこれらのファイルをサーバーにアップして、「ページ1」や「戻る」リンクをクリックしてください。タブの部分において、もうロード中のぐるぐるは見えませんね。「すっ」とページが変わる感じです。また、URLやタイトルも正しく更新されています。\n\u003cp\u003eしかし、ページ１においてブラウザの更新ボタンを押して画面を更新した後に、「戻る」のリンクをクリックするときは、ロード中のぐるぐるが見えます。これはpage1.htmlには、turbolinksがないからです。\n\u003cp\u003e以下のデモでも体験できます。\n\u003cp\u003e\u003ca href=\"https://larajapan.lotsofbytes.com/turbolinks/index.html\" rel=\"noopener noreferrer\" target=\"_blank\"\u003e\u003ca class=\"link\" href=\"https://larajapan.lotsofbytes.com/turbolinks/index.html\" target=\"_blank\" rel=\"noopener\" \u003ehttps://larajapan.lotsofbytes.com/turbolinks/index.html\u003c/a\u003e\u003c/a\u003e\n\u003cp\u003eブラウザのインスペクトツールでも、ajaxが使用されていることがわかります。\n\u003cimg src=\"inspec.png\" alt=\"\" width=\"759\" height=\"216\" class=\"alignnone size-full wp-image-2457\" /\u003e index.htmlのturbolinksをコールしている\u003ccode\u003escript\u003c/code\u003eの行を削除して違いも見てください。 \u003cp\u003eCDNではなく、turbolinksのファイルが欲しいなら、\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e$ wget https://github.com/turbolinks/turbolinks/archive/master.zip \u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eで取得可能です。unzipしてから、\u003ccode\u003edist/turbolinks.js\u003c/code\u003eのファイルが取り出せます。\n\u003cp\u003e知っておくこととして、\n\u003cp\u003eページの特定のリンクにおいてturbolinksを無効にしたいなら、\u003ccode\u003edata-turbolinks=\u0026ldquo;false\u0026rdquo;\u003c/code\u003eを入れてください。\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-html\" data-lang=\"html\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e.. \u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e \u0026lt;\u003cspan style=\"color:#f92672\"\u003ebody\u003c/span\u003e\u0026gt; \u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e \u0026lt;\u003cspan style=\"color:#f92672\"\u003eh1\u003c/span\u003e\u0026gt;Turbolinks\u0026lt;/\u003cspan style=\"color:#f92672\"\u003eh1\u003c/span\u003e\u0026gt; \u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e \u0026lt;\u003cspan style=\"color:#f92672\"\u003eul\u003c/span\u003e\u0026gt; \u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e \u0026lt;\u003cspan style=\"color:#f92672\"\u003eli\u003c/span\u003e\u0026gt;\u0026lt;\u003cspan style=\"color:#f92672\"\u003ea\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003ehref\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;page1.html\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003edata-turbolinks\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;false\u0026#34;\u003c/span\u003e\u0026gt;ページ 1\u0026lt;/\u003cspan style=\"color:#f92672\"\u003eli\u003c/span\u003e\u0026gt; \u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e \u0026lt;/\u003cspan style=\"color:#f92672\"\u003eul\u003c/span\u003e\u0026gt; \u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e \u0026lt;/\u003cspan style=\"color:#f92672\"\u003ebody\u003c/span\u003e\u0026gt; \u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e.. \u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eクリック先のページを読み直したいときとかに必要です。私の経験では、ページを変更してセッションに値を入れるときには、これが必要でした。期待した動作にならないとか問題があるときに試してみること必要です。それから、\u003ccode\u003ehref\u003c/code\u003eのリンクのクリックは\u003ccode\u003eGET\u003c/code\u003eのアクションとなりますが、\u003ccode\u003ePOST\u003c/code\u003eのアクションには、turbolinksは関与しないので、フォームの投稿はスピードアップはしません。通常の画面の更新となります。\n\u003cp\u003e実際のプロジェクトで使用となると、他にも知る必要なことがいくつか（たくさんではない）あります。例えば、Google Analyticsは正しく反映されるのか、とか、Wordpressでも使用できるかとか。知識を整理して、将来により情報を共有します。\n\u003ch2\u003e注1\u003c/h2\u003e \u003cp\u003eCodeIgniterは私が初めて使用したPHP言語のフレームワークです。どこかのサイトで紹介されていて簡単そうなので使い始めました。日本語のデータ処理に問題があったので使える部分はほとんどコントローラの部分だけでしたが、いくつかのお客さんのプロジェクトの開発に使いました。当時は一番人気のフレームワークでしたが、その絶頂期の2013年にCodeIgniterを開発した会社がリソースがないことを理由に開発を辞める宣言をしました。そこで開発が止まり1年後にはカナダの専門学校がメンテナーとなり現在はそこでオープンソースとして管理されています。\n\u003cp\u003eということで、CodeIgniterはまったく消え去ったわけではありませんが、Laravelを使い始めて振り返ると、CodeIgniter自体のフレームワークはその時点ですでに古く、現在のPHP言語のネームスペースに基づくcomposerのパッケージを基本として作成されたものでもなく、より複雑になるウェブアプリの開発にはそのままでは不可能です。\n","date":"2017-11-17T14:47:54+09:00","permalink":"https://www.larajapan.com/2017/11/17/turbolinks%E3%81%A7%E7%94%BB%E9%9D%A2%E3%81%AE%E8%A1%A8%E7%A4%BA%E3%82%92%E3%82%B9%E3%83%94%E3%83%BC%E3%83%89%E3%82%A2%E3%83%83%E3%83%97-1/","title":"turbolinksで画面の表示をスピードアップ (1)"},{"content":"前回紹介した楽しい開発ツールphanを、今回はLaravelのプロジェクトに適用してみます。さて、どれだけ楽しくなるか！\nまず、Laravel 5.4の私の日本語開発用のレポジトリをインストールします。\n$ git clone git@github.com:lotsofbytes/larajapan.git 次に、phanのパッケージをインストールします（注１）\n$ cd larajapan $ composer require --dev phan/phan インストールできたところで、phanの設定をします。\n.phanのディレクトリを作成して、config.phpのファイルを作成します。\nconfig.php return [ // 解析のためにクラスやメソッドの情報を取得するディレクトリ。 // これらと、以下の exclude_analysis_directory_list で指定した // ディレクトリとの差分が解析チェックの対象となる \u0026#39;directory_list\u0026#39; =\u0026gt; [ \u0026#39;app\u0026#39;, \u0026#39;routes\u0026#39;, \u0026#39;vendor\u0026#39; ], // 解析チェックの対象から除外するディレクトリ。 \u0026#39;exclude_analysis_directory_list\u0026#39; =\u0026gt; [ \u0026#39;vendor\u0026#39; ], ]; 設定ファイルが用意できたところで、最初のphanの実行をしてみましょう。\n$ vendor/bin/phan -p app/Auth/Notifications/ResetPassword.php:51 PhanTypeMismatchArgument Argument 2 (parameters) is string but \\route() takes array defined at vendor/laravel/framework/src/Illuminate/Foundation/helpers.php:707 app/Auth/Passwords/CanResetPassword.php:16 PhanUndeclaredProperty Reference to undeclared property \\App\\Auth\\Passwords\\CanResetPassword-\u0026gt;email app/Auth/Passwords/CanResetPassword.php:27 PhanUndeclaredMethod Call to undeclared method \\App\\Auth\\Passwords\\CanResetPassword::notify app/Providers/RouteServiceProvider.php:54 PhanUndeclaredMethod Call to undeclared method \\Illuminate\\Routing\\Route::namespace app/Providers/RouteServiceProvider.php:68 PhanUndeclaredMethod Call to undeclared method \\Illuminate\\Routing\\Route::namespace routes/api.php:16 PhanUndeclaredClassMethod Call to method middleware from undeclared class \\Route routes/channels.php:14 PhanUndeclaredClassMethod Call to method channel from undeclared class \\Broadcast routes/console.php:16 PhanUndeclaredClassMethod Call to method command from undeclared class \\Artisan routes/console.php:17 PhanUndeclaredVariable Variable $this is undeclared routes/web.php:14 PhanUndeclaredClassMethod Call to method get from undeclared class \\Route routes/web.php:22 PhanUndeclaredClassMethod Call to method get from undeclared class \\Route routes/web.php:23 PhanUndeclaredClassMethod Call to method post from undeclared class \\Route routes/web.php:24 PhanUndeclaredClassMethod Call to method post from undeclared class \\Route routes/web.php:27 PhanUndeclaredClassMethod Call to method get from undeclared class \\Route routes/web.php:28 PhanUndeclaredClassMethod Call to method post from undeclared class \\Route routes/web.php:31 PhanUndeclaredClassMethod Call to method get from undeclared class \\Route routes/web.php:32 PhanUndeclaredClassMethod Call to method post from undeclared class \\Route routes/web.php:33 PhanUndeclaredClassMethod Call to method get from undeclared class \\Route routes/web.php:34 PhanUndeclaredClassMethod Call to method post from undeclared class \\Route routes/web.php:36 PhanUndeclaredClassMethod Call to method get from undeclared class \\Route たくさんエラー出てきました。\nその中でもPhanUndeclaredClassMethodが多い。これは宣言されていないクラスのメソッドをコールしているエラーです。\n例えば、一番多いエラーとなっているRouteのクラスは、Laravel特有のFacade（ファサード）です。ネームスペースnamespaceがあるクラスと違い、phanにとっては解析が難しいようです。\n調査したところ、IDEヘルパーの開発ツールを使用して、stabのファイルを作成することで回避できそうです。Laravelのstabのファイルの作成には、まず以下を実行してide-helperのパッケージをインストールします。\n$ composer require --dev barryvdh/laravel-ide-helper 次に、config/app.phpを編集して以下を追加、\nBarryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider::class\nこのパッケージは、debugbarの開発者が作成したものです。本来は、phpStormなどのIDEのエディターでのヘルパーが目的（それゆえにlaravel-ide-helper）ですが、ファサードのクラスのメソッドを宣言してくれるために、phanでも役に立つようです。\napp.php .. \u0026#39;providers\u0026#39; =\u0026gt; [ .. /* * Package Service Providers... */ Laravel\\Tinker\\TinkerServiceProvider::class, Barryvdh\\Debugbar\\ServiceProvider::class, Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider::class, .. そして、以下を実行。\n$ php artisan ide-helper:generate _ide_helper.phpのファイルが実行したディレクトリに作成されます。これを、.phan/stabsのディレクトリを作成して移します。\n$ mkdir .phan/stabs $ mv _ide_helper.php .phan/stabs さらに、このファイルを利用するために、.phan/config.phpも編集が必要です。\nconfig.php return [ // 解析のためにクラスやメソッドの情報を取得するディレクトリ。 // これらと、以下の exclude_analysis_directory_list で指定した // ディレクトリとの差分が解析チェックの対象となる \u0026#39;directory_list\u0026#39; =\u0026gt; [ \u0026#39;.phan/stabs\u0026#39;, \u0026#39;app\u0026#39;, \u0026#39;routes\u0026#39;, \u0026#39;vendor\u0026#39; ], // 解析チェックの対象から除外するディレクトリ。 \u0026#39;exclude_analysis_directory_list\u0026#39; =\u0026gt; [ \u0026#39;.phan\u0026#39;, \u0026#39;vendor\u0026#39; ], ]; これで実行準備完了です。再度実行してみましょう。\n$ vendor/bin/phan -p app/Auth/Notifications/ResetPassword.php:51 PhanTypeMismatchArgument Argument 2 (parameters) is string but \\route() takes array defined at vendor/laravel/framework/src/Illuminate/Foundation/helpers.php:707 app/Auth/Passwords/CanResetPassword.php:16 PhanUndeclaredProperty Reference to undeclared property \\App\\Auth\\Passwords\\CanResetPassword-\u0026gt;email app/Auth/Passwords/CanResetPassword.php:27 PhanUndeclaredMethod Call to undeclared method \\App\\Auth\\Passwords\\CanResetPassword::notify app/Providers/RouteServiceProvider.php:54 PhanUndeclaredStaticMethod Static call to undeclared method \\Illuminate\\Support\\Facades\\Route::middleware app/Providers/RouteServiceProvider.php:68 PhanUndeclaredStaticMethod Static call to undeclared method \\Illuminate\\Support\\Facades\\Route::prefix routes/api.php:16 PhanUndeclaredStaticMethod Static call to undeclared method \\Route::middleware routes/channels.php:14 PhanUndeclaredStaticMethod Static call to undeclared method \\Broadcast::channel routes/console.php:17 PhanUndeclaredVariable Variable $this is undeclared PhanUndeclaredClassMethodのエラーが少なくなりました。しかし、残り未宣言のエラーの解決は現段階では無理そうです。実際宣言されていにも関わらず、その情報をphanに伝えることが自動のツールでは不可能だからです。将来に期待しましょう。\nとりあえず、ここでは-iオプションで、この未宣言のエラーを無視することにします。\n$ vendor/bin/phan -p -i app/Auth/Notifications/ResetPassword.php:51 PhanTypeMismatchArgument Argument 2 (parameters) is string but \\route() takes array defined at vendor/laravel -iで、エラーは１つだけとなりました。\nこのエラーは、以下のroute()の2番目の引数が配列である必要があるということで、\napp/Auth/Notifications/ResetPassword.php .. public function toMail($notifiable) { return (new MailMessage) -\u0026gt;subject(\u0026#39;パスワードリセット\u0026#39;) -\u0026gt;greeting(\u0026#39;パスワードリセット\u0026#39;) -\u0026gt;line(\u0026#39;パスワードリセットリンクの送信のリクエストがありました。\u0026#39;) -\u0026gt;action(\u0026#39;リセットパスワード\u0026#39;, url(config(\u0026#39;app.url\u0026#39;).route(\u0026#39;password.reset\u0026#39;, [$this-\u0026gt;token], false))) -\u0026gt;line(\u0026#39;リクエストされていなかったら、無視してください。\u0026#39;); } .. と編集して解決できました。\nということで、インストールは”そう楽しくなさそうな”ツールですが、もし大きなプログラムがあるなら一度でも試してください。私のプロジェクトではいくつか重要なバグをみつけてくれてそれだけでも十分な貢献です。これからも、検証のツールとしてunit testとともに重要なものになりそうです。\nひとつ注意することは、この実行には空きメモリが２ＧＢ以上必要です。残念ながら私のテスト環境のawsのsmall（メモリ2GB）のインスタンスでは実行不可能でした。仮想の開発環境（メモリ4GB）で実行可能でした。\n注１ composerで、phpdocumentor/reflection-docblockのバージョンのコンフリクトが出ました。すでにインストールされているそのパッケージのバージョンは4.1.1ですが、phan/phanが必要とするものはそれよりも昔のバージョン。ということで、\ncoomposer.json .. \u0026#34;require\u0026#34;: { \u0026#34;php\u0026#34;: \u0026#34;\u0026gt;=5.6.4\u0026#34;, \u0026#34;barryvdh/laravel-debugbar\u0026#34;: \u0026#34;~2.4\u0026#34;, \u0026#34;laravel/framework\u0026#34;: \u0026#34;5.4.*\u0026#34;, \u0026#34;laravel/tinker\u0026#34;: \u0026#34;~1.0\u0026#34;, \u0026#34;phpdocumentor/reflection-docblock\u0026#34;: \u0026#34;4.1.1 as 3.3.0\u0026#34; }, .. 4.1.1 as 3.3.0として4.1.1のバージョンを3.3.0のバージョンとして扱ってもらうことで解決できました。\n","date":"2017-11-10T02:45:14+09:00","permalink":"https://www.larajapan.com/2017/11/10/phan%E3%81%AF%E6%A5%BD%E3%81%97%E3%81%84%EF%BC%812/","title":"phanは楽しい！(2) laravel編"},{"content":"php７に更新したら使ってみたいと思っていたツールがありました。\nこのphpの静的解析ツールツールの名前は、Phan。ファンと呼びます。多分、楽しいという英語のfunにひっかけて。Githubでは、以下で公開されています。\nhttps://github.com/phan/phan\nさて、これがどうして重要なツールかというと、\nphpはもともと開発の敷居を落とすためにCやJavaなどと違ってデータタイプを宣言しなくてよいプログラム言語、しかしプログラムが大きくなり複雑になってくると逆にその緩さが多くの間違いの原因となります。\n例えば、\ntest.php function double($i) { return $i*2; } $x = \u0026#39;文字列\u0026#39;; echo double($x); これを実行するとphpではエラーなしに、0を返します。関数doubleは数字を引数に期待しているはずなのに、phpは数字でなく文字列を渡しても問題なく処理してしまいます。もちろん、ここでの実行は意図的ですが、もし間違いで文字列を渡してしまったら、そのコードがもし他のコードの奥に隠れていたら、原因がわからないバグになりかねません。\nphanは、ここの間違いをphpのプログラムを実行することなし（それゆえに静的解析）に見つけてくれる重要な開発検証ツールなのです。\nまずは、phanのインストールからなのですが、今回はphanの紹介ということで、Laravelなしの環境でのインストールです。次回には、より複雑なLaravelプログラムの環境でのphanの実行を説明します。\nさて、簡単にインストールできるよ、と行きたいのですが、実際は少々ややこしく、astのモジュールとphanのパッケージの２ステップのインストールとなります。\n１．astのモジュールのインストール ast(abstract syntax tree、抽象シンタックスツリー)は、phanに不可欠なphpプログラムの解析モジュールで、特定のプログラムでなくマシン全体でのグローバルのインストールとなります。そして、多分たいていの環境ではデフォルトでインストールされていないと思います。\nまずは、そのモジュールが存在するかどうかのチェックとして、\n$ php -m の実行で、astがでて来なかったら、以下を実行してください。\n$ pecl install ast その後、/etc/php.iniを編集して、\nextension=ast.so\nを追加します。\n残念ながらここでくじけたら、もうphanは使えません。くじけたくないなら、以下の開発者のgithubを見てください。\nhttps://github.com/nikic/php-ast\n話それますが、このastの開発者、いろいろ面白いツール作成しています。プログラムのコンパイルなどの理論に興味のある方は是非以下も。\nhttps://github.com/nikic\n２．phanのインストール 次は以下を実行して、パッケージをインストールします。\n$ composer require --dev phan/phan \u0026ndash;devは、開発だけに使用という意味です。\nこれで、現在のディレクトリは、\n. ├── composer.json ├── composer.lock ├── test.php └── vendor ├── autoload.php ├── bin ├── composer ├── felixfbecker ├── netresearch ├── nikic ├── phan ├── phpdocumentor ├── psr ├── sabre ├── symfony └── webmozart となります。test.phpは先ほどの例のプログラム。\n３．phanの実行 実行は簡単です。ターゲットのファイルを指定するのみです。\n$ ./vendor/bin/phan test.php これを実行すると、何も出力ありません！\n何がおかしいのでしょう？\nphanは、エラーを出すには関数の引数のデータタイプが必要なのです。phpではそのためにDocblock（ドクブロック）をコメントの中に入れる必要あります。test.phpを編集して、@param float $iを関数定義の直上のコメントに入れます。\n/** * @param float $i * @return float */ function double($i) { return $i*2; } $x = \u0026#39;文字列\u0026#39;; echo double($x); 再度、phanを実行すると、\ntest.php:14 PhanTypeMismatchArgument Argument 1 (i) is string but \\double() takes float defined at test.php:7\nとエラーを出してくれます。もちろん、$x = 1.5と数字ならエラーは出ません。\n簡単なphanの実演でしたが、このツールを活用するにはDocblockの作成がまず必要ということです。Docblockの作成はちょっと面倒な作業かもしれません。しかし、phpはバージョン5から、関数で型宣言ができ、sublimeなどの最近のエディターはそれを探知して、自動的にエディタでドクブロックも作成してくれます。また、それをもとにAPIドキュメントも作成してくれるツールもあります。ドクブロックは必須となっている昨今です。しかし、最終的には、１つの間違いでも減らしたいかどうかなのです。 最後に、このツールは、つい最近までは、phan/phanではなくetsy/phanという名前でした。 このetsy（エッチーあるいはイッチーと発音）というのはetsy.comハンドクラフトの人が自分の作品を販売できるサイトです。基本的にはハンドメードのものしか販売ダメという方針で人気があり、2014年末で登録会員（売る方でなく買う方）がなんと5400万人という。\nそして凄いことに、この大きなスケールのサイトは、なんとphpで動いています。さらにでかいFacebookもPHPという話だからそう驚く必要はないですけれど。そして、それゆえに、今回のようなphpの開発のためのプログラムツールを公開してくれています。\n","date":"2017-10-28T02:40:40+09:00","permalink":"https://www.larajapan.com/2017/10/28/phan%E3%81%AF%E6%A5%BD%E3%81%97%E3%81%84%EF%BC%811/","title":"phanは楽しい！(1)"},{"content":"Gitは、開発になくてはならないバージョン管理のツール。Laravelのバージョンをアップグレードがあるときは、フォルダーやファイルが入れ替わるためにGitで新ブランチを作成してから、更新作業を行います。しかし、問題はこの新ブランチは現在のマスターの複製であるため、以前のバージョンのファイルがすでに存在することです。\n新しいバージョンと入れ替える１つの方法は、.git以外のフォルダーやファイルをすべて削除して、そこに新バージョンのLaravelを入れ直す。しかし、これを行うとGitで保存されている以前の変更やログぼ履歴が残ってしまい、新しくゼロからスタートができません。新しいバージョンでの開発は、あたかもまっさらからのように始めたい。\n前回のLaravel 5.4のレポジトリ作成時にこの問題とぶつかり、どうしたら良いものやらと調査した結果、以下のような手順になりました。\nmasterを5.3ブランチと改名 現在は、masterにいます。\n$ git branch -a * master remotes/origin/HEAD -\u0026gt; origin/master remotes/origin/master masterを5.3のブランチに改名します。\n$ git branch -m master 5.3 リモートレポジトリ(github)にプッシュします。\n$ git push origin 5.3 5.4ブランチを作成してmasterに改名 現在は、5.3のブランチにいます。\n$ git branch -a * 5.3 remotes/origin/5.3 remotes/origin/HEAD -\u0026gt; origin/master remotes/origin/master まずは、親なしの5.4ブランチを作成します。親なし\u0026ndash;orphanとすることによりブランチの変更やログの履歴が削除されます。この時点ではすべてのフォルダーやファイルはチェックアウトされていません。そして、それらをresetで全部削除します。残っているのは、.gitのディレクトリのみです。\n$ git checkout --orphan 5.4 $ git reset --hard 次に、ここにLaravel 5.4の初期ファイルを入れますが、.gitのディレクトリがあるためにエラーがでて入れることができません。それゆえに、一旦tmpのディレクトリに入れてから中身を移します。.env.exampleなどのドットファイルも移していることに注意してください。.gitを違うディレクトリに移してから、同ディレクトリに作成してもいいですね。そして、gitのディレクトリを戻す。\n$ composer create-project --prefer-dist laravel/laravel tmp 5.4.* $ mv tmp/* tmp/.[^.]* . $ rmdir tmp 追加されたフォルダーやファイルをコミットします。そして、5.4ブランチをmasterに改名します。\n$ git add . $ git commit -m \u0026#39;Laravel 5.4 init\u0026#39; $ git branch -m master そして最後にレポジトリ(github)にプッシュします。全部入れ替えるので \u0026ndash;forceオプションが必要です。\n$ git push -u origin master --force $ git branch -a * master remotes/origin/5.3 remotes/origin/HEAD -\u0026gt; origin/master remotes/origin/master ということで、githubでは以下のようにドロップダウンに２つのブランチが見えます。 ","date":"2017-10-26T04:41:12+09:00","permalink":"https://www.larajapan.com/2017/10/26/laravel%E3%81%AE%E3%82%A2%E3%83%83%E3%83%97%E3%82%B0%E3%83%AC%E3%83%BC%E3%83%89%E3%81%A8git%E3%81%A7%E3%81%AE%E3%83%90%E3%83%BC%E3%82%B8%E3%83%A7%E3%83%B3%E7%AE%A1%E7%90%86/","title":"Laravelのアップグレードとgitでのバージョン管理"},{"content":"前回に作成したLaravelの日本語のリポジトリ（Laravel 5.4）。今回はその作成の仕方を説明します。ほとんどは、Laravel 5.3のときと同じですが、いくつか違いがあります。\nコマンドの実行 まずは、以下のcomposerのコマンドを実行します。\ncomposer create-project --prefer-dist laravel/laravel larajapan \u0026#34;5.4.*\u0026#34; 上で使用されているコマンドの引数は、\n\u0026ndash;prefer-dist laravel/laravel\nhttps://packagist.org/packages/laravel/laravelからパッケージをダウンロードすることを指示します。\nlarajapan\nパッケージのダウンロード先。その名前でディレクトリを作成します。このディレクトリ名は、先のコマンドラインで違う名前を指定可能であるし、実行完了してから改名も可能です。\n5.4.*\nパッケージのバージョンを指定。ここでは、laravelの5.4を使用します。マイナーバージョンを指定したいなら、5.4.36のように指定します。\n実行すると、パッケージに含まれるファイル、さらにパッケージが依存するパッケージのファイルが多数ダウンロードされ少々時間がかかります。\n最終的には、実行したディレクトリのもとにlarajapanのディレクトリが作成され、ダウンロードされたファイルが収納されます。\nlarajapan ├── app/ ├── bootstrap/ ├── config/ ├── database/ ├── public/ ├── resources/ ├── routes/ ├── storage/ ├── tests/ ├── vendor/ ├── artisan ├── composer.json ├── composer.lock ├── package.json ├── phpunit.xml ├── readme.md ├── server.php └── webpack.mix.js ＊5.3と違いgulpfile.jsの代わりにwebpack.mix.jsがあります。asset（sassやjs）のコンパイルは、gulpでなくwebpackを使用したlaravel-mixに変わったからです。\n次に、ユーザー認証のためのファイル作成を以下の実行で行います。\nphp artisan make:auth この実行により、resources/viewsのディレクトリにおいて、すでにインストールされている以下のコントローラで使用されるbladeファイルが作成されます。\napp/Http/Controllers ├── Auth/ │ ├── ForgotPasswordController.php（パスワードのリセットのリンク送信画面） │ ├── LoginController.php（ログイン画面） │ ├── RegisterController.php（会員登録画面） │ └── ResetPasswordController.php（パスワードリセット画面） ├── Controller.php └── HomeController.php（ログイン後のホーム画面） 最後に以下のコマンドを実行して、先のパスワードのリセットのリンク送信画面から発行されるＥメールので使用されるHTMLのテンプレートを作成作成します。\nphp artisan vendor:publish 日本語化 さて、ここからが日本語化の作業です。\nまず、config/app.phpの編集から。\nconfig/app.php ... \u0026#39;timezone\u0026#39; =\u0026gt; \u0026#39;UTC\u0026#39;, \u0026#39;locale\u0026#39; =\u0026gt; \u0026#39;en\u0026#39;, ... を\nconfig/app.php ... \u0026#39;timezone\u0026#39; =\u0026gt; \u0026#39;Asia/Tokyo\u0026#39;, \u0026#39;locale\u0026#39; =\u0026gt; \u0026#39;ja\u0026#39;, ... と変えて保存します。\ntimezone\nこれは、通常、プログラム内の日時設定のタイムゾーンとして使用されるもので、PHPの以下の関数で使用されます。\ndate_default_timezone_set()\nここで設定すれば、後はLaravelが面倒みてくれます。\n日本時間の場合は、Aisa/Tokyoの設定だけで十分。\nlocale\nresources/langで言語のファイルが以下のように存在します。これらは、Laravelのプロジェクトでバリデーションのエラーメッセージなどを定義しています。\nresources/lang └── en/ ├── auth.php ├── pagination.php ├── passwords.php └── validation.php デフォルトの設定では、英語のenのディレクトリしかありません。日本語の翻訳を作成するには、上で設定したjaと同じ名前のディレクトリをそこに作成します。以下の実行でディレクトリごとコピーしてください。\n$ cp -pr en ja バリデーションに関しては、見米氏のバリデーション（１）Validatorファサードのextend　を参照してください。\n今回は、新しく追加されたバリデーションの以下のエントリーもあります。\nafter_or_equal before_or_equal\n次は、ユーザー認証画面などで使用されるブレードファイルの翻訳です。\n私のLaravelの日本語レポジトリでは、以下は、すべて翻訳してあります。\nresources/views ├── auth │ ├── login.blade.php │ ├── passwords │ │ ├── email.blade.php │ │ └── reset.blade.php │ └── register.blade.php ├── errors │ └── 404.blade.php ├── home.blade.php ├── layouts │ └── app.blade.php ├── vendor │ ├── mail │ │ ├── html │ │ │ ├── button.blade.php │ │ │ ├── footer.blade.php │ │ │ ├── header.blade.php │ │ │ ├── layout.blade.php │ │ │ ├── message.blade.php │ │ │ ├── panel.blade.php │ │ │ ├── promotion │ │ │ │ └── button.blade.php │ │ │ ├── promotion.blade.php │ │ │ ├── subcopy.blade.php │ │ │ ├── table.blade.php │ │ │ └── themes │ │ │ └── default.css │ │ └── markdown │ │ ├── button.blade.php │ │ ├── footer.blade.php │ │ ├── header.blade.php │ │ ├── layout.blade.php │ │ ├── message.blade.php │ │ ├── panel.blade.php │ │ ├── promotion │ │ │ └── button.blade.php │ │ ├── promotion.blade.php │ │ ├── subcopy.blade.php │ │ └── table.blade.php │ ├── notifications │ │ └── email.blade.php │ └── pagination │ ├── bootstrap-4.blade.php │ ├── default.blade.php │ ├── simple-bootstrap-4.blade.php │ └── simple-default.blade.php └── welcome.blade.php 5.3と違い、errorsのフォルダーは作成されていませんでしたので追加しました。また、vendor/mailのフォルダーは新規のもので、HTMLだけでなくマークダウンも対応しています。\nパスワードリセットで送信されるＥメールの翻訳 ここまで来ても、残念ながら、パスワードを忘れたときに送信される、パスワードリセットを含むＥメールの内容がまだ翻訳されていません。なぜなら、本文がハードコードされているからです。\nこれはちょっと頭をひねりましたが、多分以下が最小の変更で対応できると思います。\nまず、\nvendor/laravel/framework/src/Illuminate/AuthのディレクトリからResetPassword.phpとCanResetPassword.phpのファイルを以下の場所にコピーします。\napp/Auth ├── Notifications/ │ └── ResetPassword.php └── Passwords/ └── CanResetPassword.php 次に、以下のようにファイルを編集します。app/User.phpのファイルも変更必要です。\napp/Auth/Notifications/ResetPassword.php namespace App\\Auth\\Notifications; use Illuminate\\Notifications\\Notification; use Illuminate\\Notifications\\Messages\\MailMessage; class ResetPassword extends Notification { ... /** * Build the mail representation of the notification. * * @param mixed $notifiable * @return \\Illuminate\\Notifications\\Messages\\MailMessage */ public function toMail($notifiable) { return (new MailMessage) -\u0026gt;subject(\u0026#39;パスワードリセット\u0026#39;) -\u0026gt;greeting(\u0026#39;パスワードリセット\u0026#39;) -\u0026gt;line(\u0026#39;パスワードリセットリンクの送信のリクエストがありました。\u0026#39;) -\u0026gt;action(\u0026#39;リセットパスワード\u0026#39;, url(config(\u0026#39;app.url\u0026#39;).route(\u0026#39;password.reset\u0026#39;, $this-\u0026gt;token, false))) -\u0026gt;line(\u0026#39;リクエストされていなかったら、無視してください。\u0026#39;); } } app/Auth/Passwords/CanResetPassword.php namespace App\\Auth\\Passwords; use App\\Auth\\Notifications\\ResetPassword as ResetPasswordNotification; trait CanResetPassword { /** * Get the e-mail address where password reset links are sent. * * @return string */ public function getEmailForPasswordReset() { return $this-\u0026gt;email; } /** * Send the password reset notification. * * @param string $token * @return void */ public function sendPasswordResetNotification($token) { $this-\u0026gt;notify(new ResetPasswordNotification($token)); } } app/User.hpp namespace App; use Illuminate\\Notifications\\Notifiable; use Illuminate\\Foundation\\Auth\\User as Authenticatable; use App\\Auth\\Passwords\\CanResetPassword; class User extends Authenticatable { use Notifiable; use CanResetPassword; ... 認証のroutesの設定 日本語化とは関係ないですが、私がこうした方がわかりやすいと思ったことです。\nオリジナルのroutes.phpは、\nroutes/web.php Route::get(\u0026#39;/\u0026#39;, function () { return view(\u0026#39;welcome\u0026#39;); }); Auth::routes(); Route::get(\u0026#39;/home\u0026#39;, \u0026#39;HomeController@index\u0026#39;); とシンプルですが、Auth::routes()で認証のrouteが隠されてしまって不透明。\nということで、私のLaravelの日本語レポジトリでは、以下のように編集しました。\nroutes/web.php // 以下は、Auth::routes()の中身を移したもの。将来において変更が可能なように // Authentication Routes... Route::get(\u0026#39;login\u0026#39;, \u0026#39;Auth\\LoginController@showLoginForm\u0026#39;)-\u0026gt;name(\u0026#39;login\u0026#39;); Route::post(\u0026#39;login\u0026#39;, \u0026#39;Auth\\LoginController@login\u0026#39;); Route::post(\u0026#39;logout\u0026#39;, \u0026#39;Auth\\LoginController@logout\u0026#39;)-\u0026gt;name(\u0026#39;logout\u0026#39;); // Registration Routes... Route::get(\u0026#39;register\u0026#39;, \u0026#39;Auth\\RegisterController@showRegistrationForm\u0026#39;)-\u0026gt;name(\u0026#39;register\u0026#39;); Route::post(\u0026#39;register\u0026#39;, \u0026#39;Auth\\RegisterController@register\u0026#39;); // Password Reset Routes... Route::get(\u0026#39;password/reset\u0026#39;, \u0026#39;Auth\\ForgotPasswordController@showLinkRequestForm\u0026#39;)-\u0026gt;name(\u0026#39;password.request\u0026#39;); Route::post(\u0026#39;password/email\u0026#39;, \u0026#39;Auth\\ForgotPasswordController@sendResetLinkEmail\u0026#39;)-\u0026gt;name(\u0026#39;password.email\u0026#39;); Route::get(\u0026#39;password/reset/{token}\u0026#39;, \u0026#39;Auth\\ResetPasswordController@showResetForm\u0026#39;)-\u0026gt;name(\u0026#39;password.reset\u0026#39;); Route::post(\u0026#39;password/reset\u0026#39;, \u0026#39;Auth\\ResetPasswordController@reset\u0026#39;); Route::get(\u0026#39;/home\u0026#39;, \u0026#39;HomeController@index\u0026#39;); その他 インストールにおいて経験した問題として、\nphp artisan migrate を実行したときに、以下のエラーとなりました。\n[Illuminate\\Database\\QueryException] SQLSTATE[42000]: Syntax error or access violation: 1071 Specified key was too long; max key length is 767 bytes (SQL: alter table users add unique users_email_unique(email)) [PDOException] SQLSTATE[42000]: Syntax error or access violation: 1071 Specified key was too long; max key length is 767 bytes これは、使用しているデータベースがMariaDBで10.2.2のバージョンより古いか、あるいはMySQLで5.7.7より古いときに起こるエラーです。Laravelは5.4より、絵文字対応のutf8mb4のエンコーディングがデフォルトとなりました。\n古いバージョンを使用する場合（大半がそうでは？）は、以下の変更が必要となります。日本語のレポにはこの変更が含まれています。\napp/Providers/AppServiceProvider.php namespace App\\Providers; use Illuminate\\Support\\ServiceProvider; use Illuminate\\Support\\Facades\\Schema; class AppServiceProvider extends ServiceProvider { /** * Bootstrap any application services. * * @return void */ public function boot() { Schema::defaultStringLength(191); } /** * Register any application services. * * @return void */ public function register() { // } } ","date":"2017-10-21T03:59:00+09:00","permalink":"https://www.larajapan.com/2017/10/21/laravel%E3%81%AE%E6%97%A5%E6%9C%AC%E8%AA%9E%E3%83%AC%E3%83%9D%E3%82%B8%E3%83%88%E3%83%AA%E3%81%AE%E4%BD%9C%E6%88%90%EF%BC%88laravel-5-4%EF%BC%89/","title":"Laravelの日本語レポジトリの作成（Laravel 5.4）"},{"content":"Laravelの日本語のリポジトリをLaravel5.4に更新しました。もうLaravel5.5がリリースされていますが、Laravel5.5と違ってLaravel5.4は、php7でなくphp5.6のバージョンで動作する最後のバージョンなゆえに重要です。前回同様に、以下インストールの手順です。\nLaravelにおいて新規のプロジェクト作成はとても簡単。コマンドラインでいくつかのコマンドを実行をちょちょいとすれば完了。しかし、インストールされるのは英語のプロジェクト。テンプレートやメッセージの翻訳をいちいちしなければ日本語のプロジェクトにはならない。\nここのプロセスを簡単にと、Laravelバージョン5.4をもとに、開発者のために日本語化したレポジトリを作成してみました。\nこのレポジトリには、\nデフォルトのユーザー認証の機能：会員登録、パスワードリセット、会員ログイン 日本語に翻訳されたデフォルトのテンプレートとＥメールメッセージ 日本語に翻訳されたデフォルトの入力エラーメッセージ デバッグのためのDebugbarツール ウェブ解析ツールGoogle Analyticsのトラッキングスクリプト 以上を含みます。\nさらに、今回は、実際動作するデモとして以下に用意しました。 https://larajapan.lotsofbytes.com/larajapan\nさて、このレポジトリのインストールは以下の手順で簡単にできます。\nレポジトリのインストール SSHを利用しているなら、以下をコマンドラインで実行してレポジトリをインストールします。\n$ git clone -b 5.4 git@github.com:lotsofbytes/larajapan.git あるいは、Httpsを使用するなら、以下を実行します。\n$ git clone -b 5.4 https://github.com/lotsofbytes/larajapan.git 実行後は、larajapanのディレクトリが作成されますが、お好きな名前に改名してもらってＯＫです。\nその後、\ncd larajapan それから、ファイルのパーミッションを与えるべく以下を実行。\n$ chmod -R a+w bootstrap/cache $ chmod -R a+w storage インストール後は、以下を実行してください。\n$ composer install .envの編集 .env.example をコピーして、.env　を作成し編集して以下のように設定します。*****の部分を適切な値に変更してください。\nAPP_NAME=私のララベル APP_ENV=local APP_KEY= APP_DEBUG=true APP_LOG_LEVEL=debug DB_CONNECTION=mysql DB_DATABASE=***** DB_USERNAME=***** DB_PASSWORD=***** MAIL_DRIVER=sendmail\nその後、以下を実行して.env内のAPP_KEYを更新します。\n$ php artisan key:generate APP_DEBUG=trueこれによりDebugbarが画面下方に表示されます。Debugbarに関しては、Debugbarで楽々デバッグも読んでください。\nまた、 ANALYTICS=UA-XXXXXXのようにサイトのためにGoogleから取得したコードを設定すれば、Google Analyticsでウェブでのユーザーの動向が追跡できます。\nDBを作成 .envで指定したDBを作成。\n$ echo \u0026#39;CREATE DATABASE larajapan CHARACTER SET utf8\u0026#39; | mysql -u root -p php artisan migrate ウェブサーバーの立ち上げ 最後に以下を実行して、ウェブサーバーを立ち上げると、\n$ php artisan serve 以下のアドレスでブラウザーからアクセスできます。\nhttp://localhost:8000\n以上です。\n上の設定は私の開発環境Fedora LinuxとAmazon Linux OSで、7.0のバージョンのPHPで、DBにはMysqlあるいはMaria DBを使用して動作確認しています。しかし、皆さんの環境ではいろいろ異なることがあると思います。問題や指摘があれば、ご連絡ください。\n","date":"2017-10-16T23:58:24+09:00","permalink":"https://www.larajapan.com/2017/10/16/laravel%E3%81%AE%E6%97%A5%E6%9C%AC%E8%AA%9E%E3%81%AE%E3%83%9D%E3%82%B8%E3%83%88%E3%83%AA%EF%BC%88laravel-5-4%EF%BC%89/","title":"Laravelの日本語のレポジトリ（Laravel 5.4）"},{"content":"最近になってようやくphp5.6の環境からphp7.0へと移行をしました。すでにphp7.2RCも登場している昨今、今さらなんて声聞こえますね。\nカスタマイスのプログラムを書いてその管理を長い間していると、プログラムは大きくなるし複雑になるし、簡単にphpの更新ができない、いや馴染んだ古巣から新しい世界へ行くのが恐ろしい！\n私の言い訳はともかく、世の中の状況はいかにと調べたところ、こんな統計が出てきました。 （参照元）\n去年から今年へと、php7の使用率がphp5.6を抜いています。\nphpのサポートはどうなのでしょう？phpのサイトによると、\n（参照元） 緑は「サポート中」、オレンジは「セキュリティ修正のみ」、赤は「終了」です。\n5.6のサポートはセキュリティ修正のみが2018年いっぱい（まだ人気が高い証拠？）とまだ使用OKのようですが、８月にリリースされたLaravelの最新版5.5が長期サポート（LTS）のバージョンでは、バグ修正が２年間、セキュリティ修正が３年とサポートがあり、そしてなんと、\nPHP 7.0.0以降が必須 となりました。\n私のお客さんのプログラムは、最高でLaravel 5.4どまりですぐに更新する必要ないと言えば必要ないですが、php7.0が発表（上のグラフによるとほぼ２年前！）されてからとても気になるパフォーマンスの改善が「どうしても更新しなければならない」と駆り立てます。\nこれまた古い資料で申し訳ないですが、\n（参照元） phpで書かれているが非常にスローなWordpressがなんと２倍に近い速さとなるではないですか。\n実際、このブログをホストしているサーバーもphp7にしたところサクサク動くようになりました。お客さんのサイトでもapacheのabテストをphp7更新前後でLaravelのプログラムの実行を計測したところ、こちらも1/2の実行時間になりました。バージョンを更新しただけでこの改善は凄い！\nさて、php5.6からphp7.0にアップしたときの苦労と言えば、laravelのプログラムに関する限りほとんどゼロに近い変更でした。私の場合は、php7をサポートしていないpearのライブラリを使用した部分の変換が唯一の問題でした。\nそして最後に、php7にアップデートしたお陰で、前から注目していて、とても開発に役に立つ、特に大きくて複雑なプログラムには多分必須のツールが使えるようになります。それは次回に紹介しましょう。\n","date":"2017-10-12T12:55:16+09:00","permalink":"https://www.larajapan.com/2017/10/12/php5-6%E3%81%8B%E3%82%89php7-0%E3%81%B8/","title":"php5.6からphp7.0へ"},{"content":"前回に作成したLaravelの日本語レポジトリ（Laravel 5.3）。今回はその作成の仕方を説明します。\nコマンドの実行 まずは、以下のcomposerのコマンドを実行します。\ncomposer create-project --prefer-dist laravel/laravel larajapan 5.3.* 上で使用されているコマンドの引数は、\n\u0026ndash;prefer-dist laravel/laravel\nhttps://packagist.org/packages/laravel/laravelからパッケージをダウンロードすることを指示します。\nlarajapan\nパッケージのダウンロード先。その名前でディレクトリを作成します。このディレクトリ名は、先のコマンドラインで違う名前を指定可能であるし、実行完了してから改名も可能です。\n5.3.*\nパッケージのバージョンを指定。ここでは、laravelの5.3を使用します。マイナーバージョンを指定したいなら、5.3.30のように指定します。\n実行すると、パッケージに含まれるファイル、さらにパッケージが依存するパッケージのファイルが多数ダウンロードされ少々時間がかかります。\n最終的には、実行したディレクトリのもとにlarajapanのディレクトリが作成され、ダウンロードされたファイルが収納されます。\nlarajapan ├── app/ ├── bootstrap/ ├── config/ ├── database/ ├── public/ ├── resources/ ├── routes/ ├── storage/ ├── tests/ ├── vendor/ ├── artisan* ├── composer.json ├── composer.lock ├── gulpfile.js ├── package.json ├── phpunit.xml ├── readme.md └── server.php 次に、ユーザー認証のためのファイル作成を以下の実行で行います。\nphp artisan make:auth この実行により、resources/viewsのディレクトリにおいて、すでにインストールされている以下のコントローラで使用されるbladeファイルが作成されます。\napp/Http/Controllers ├── Auth/ │ ├── ForgotPasswordController.php（パスワードのリセットのリンク送信画面） │ ├── LoginController.php（ログイン画面） │ ├── RegisterController.php（会員登録画面） │ └── ResetPasswordController.php（パスワードリセット画面） ├── Controller.php └── HomeController.php（ログイン後のホーム画面） 最後に以下のコマンドを実行して、先のパスワードのリセットのリンク送信画面から発行されるＥメールので使用されるHTMLのテンプレートを作成作成します。\nphp artisan vendor:publish 日本語化 さて、ここからが日本語化の作業です。\nまず、config/app.phpの編集から。\nconfig/app.php ... \u0026#39;timezone\u0026#39; =\u0026gt; \u0026#39;UTC\u0026#39;, \u0026#39;locale\u0026#39; =\u0026gt; \u0026#39;en\u0026#39;, ... を\nconfig/app.php ... \u0026#39;timezone\u0026#39; =\u0026gt; \u0026#39;Asia/Tokyo\u0026#39;, \u0026#39;locale\u0026#39; =\u0026gt; \u0026#39;ja\u0026#39;, ... と変えて保存します。\ntimezone\nこれは、通常、プログラム内の日時設定のタイムゾーンとして使用されるもので、PHPの以下の関数で使用されます。\ndate_default_timezone_set()\nここで設定すれば、後はLaravelが面倒みてくれます。\n日本時間の場合は、Aisa/Tokyoの設定だけで十分。\nlocale\nresources/langで言語のファイルが以下のように存在します。これらは、Laravelのプロジェクトでバリデーションのエラーメッセージなどを定義しています。\nresources/lang └── en/ ├── auth.php ├── pagination.php ├── passwords.php └── validation.php デフォルトの設定では、英語のenのディレクトリしかありません。日本語の翻訳を作成するには、上で設定したjaと同じ名前のディレクトリをそこに作成します。以下の実行でディレクトリごとコピーしてください。\n$ cp -pr en ja バリデーションに関しては、見米氏のバリデーション（１）Validatorファサードのextend　を参照してください。\n次は、ユーザー認証画面などで使用されるブレードファイルの翻訳です。\n私のLaravelの日本語レポジトリでは、以下は、すべて翻訳してあります。\nresources/views ├── auth │ ├── login.blade.php │ ├── passwords │ │ ├── email.blade.php │ │ └── reset.blade.php │ └── register.blade.php ├── errors │ └── 503.blade.php ├── home.blade.php ├── layouts │ └── app.blade.php ├── vendor │ ├── notifications │ │ ├── email.blade.php │ │ └── email-plain.blade.php │ └── pagination │ ├── bootstrap-4.blade.php │ ├── default.blade.php │ ├── simple-bootstrap-4.blade.php │ └── simple-default.blade.php └── welcome.blade.php 翻訳とは関係ありませんが、app.blade.phpのレイアウトで参照されている、app.cssとapp.jsには、url()を入れて、プロジェクトがインストールされるディレクトリが変わっても参照されるようにしてあります。\nパスワードリセットで送信されるＥメールの翻訳 ここまで来ても、残念ながら、パスワードを忘れたときに送信される、パスワードリセットを含むＥメールの内容がまだ翻訳されていません。なぜなら、本文がハードコードされているからです。\nこれはちょっと頭をひねりましたが、多分以下が最小の変更で対応できると思います。\nまず、\nvendor/laravel/framework/src/Illuminate/AuthのディレクトリからResetPassword.phpとCanResetPassword.phpのファイルを以下の場所にコピーします。\napp/Auth ├── Notifications/ │ └── ResetPassword.php └── Passwords/ └── CanResetPassword.php 次に、以下のようにファイルを編集します。app/User.phpのファイルも変更必要です。\napp/Auth/Notifications/ResetPassword.php namespace App\\Auth\\Notifications; use Illuminate\\Notifications\\Notification; use Illuminate\\Notifications\\Messages\\MailMessage; class ResetPassword extends Notification { ... /** * Build the mail representation of the notification. * * @param mixed $notifiable * @return \\Illuminate\\Notifications\\Messages\\MailMessage */ public function toMail($notifiable) { return (new MailMessage) -\u0026gt;subject(\u0026#39;パスワードリセット\u0026#39;) -\u0026gt;greeting(\u0026#39;パスワードリセット\u0026#39;) -\u0026gt;line(\u0026#39;パスワードリセットリンクの送信のリクエストがありました。\u0026#39;) -\u0026gt;action(\u0026#39;リセットパスワード\u0026#39;, url(\u0026#39;password/reset\u0026#39;, $this-\u0026gt;token)) -\u0026gt;line(\u0026#39;リクエストされていなかったら、無視してください。\u0026#39;); } } app/Auth/Passwords/CanResetPassword.php namespace App\\Auth\\Passwords; use App\\Auth\\Notifications\\ResetPassword as ResetPasswordNotification; trait CanResetPassword { /** * Get the e-mail address where password reset links are sent. * * @return string */ public function getEmailForPasswordReset() { return $this-\u0026gt;email; } /** * Send the password reset notification. * * @param string $token * @return void */ public function sendPasswordResetNotification($token) { $this-\u0026gt;notify(new ResetPasswordNotification($token)); } } app/User.hpp namespace App; use Illuminate\\Notifications\\Notifiable; use Illuminate\\Foundation\\Auth\\User as Authenticatable; use App\\Auth\\Passwords\\CanResetPassword; class User extends Authenticatable { use Notifiable; use CanResetPassword; ... 認証のroutesの設定 日本語化とは関係ないですが、私がこうした方がわかりやすいと思ったことです。\nオリジナルのroutes.phpは、\nroutes/web.php Route::get(\u0026#39;/\u0026#39;, function () { return view(\u0026#39;welcome\u0026#39;); }); Auth::routes(); Route::get(\u0026#39;/home\u0026#39;, \u0026#39;HomeController@index\u0026#39;); とシンプルですが、Auth::routes()で認証のrouteが隠されてしまって不透明。\nということで、私のLaravelの日本語レポジトリでは、以下のように編集しました。\nroutes/web.php // 以下は、Auth::routes()の中身を移したもの。将来において変更が可能なように // Authentication Routes... Route::get(\u0026#39;login\u0026#39;, \u0026#39;Auth\\LoginController@showLoginForm\u0026#39;)-\u0026gt;name(\u0026#39;login\u0026#39;); Route::post(\u0026#39;login\u0026#39;, \u0026#39;Auth\\LoginController@login\u0026#39;); Route::post(\u0026#39;logout\u0026#39;, \u0026#39;Auth\\LoginController@logout\u0026#39;)-\u0026gt;name(\u0026#39;logout\u0026#39;); // Registration Routes... Route::get(\u0026#39;register\u0026#39;, \u0026#39;Auth\\RegisterController@showRegistrationForm\u0026#39;)-\u0026gt;name(\u0026#39;register\u0026#39;); Route::post(\u0026#39;register\u0026#39;, \u0026#39;Auth\\RegisterController@register\u0026#39;); // Password Reset Routes... Route::get(\u0026#39;password/reset\u0026#39;, \u0026#39;Auth\\ForgotPasswordController@showLinkRequestForm\u0026#39;); Route::post(\u0026#39;password/email\u0026#39;, \u0026#39;Auth\\ForgotPasswordController@sendResetLinkEmail\u0026#39;); Route::get(\u0026#39;password/reset/{token}\u0026#39;, \u0026#39;Auth\\ResetPasswordController@showResetForm\u0026#39;); Route::post(\u0026#39;password/reset\u0026#39;, \u0026#39;Auth\\ResetPasswordController@reset\u0026#39;); Route::get(\u0026#39;/home\u0026#39;, \u0026#39;HomeController@index\u0026#39;); 最後に Laravelの最新バージョンは今週に5.3から5.4となりました。今回説明した（前回に作成したLaravelの日本語レポジトリも）はバージョン5.3をベースにしたものです。しかし、バージョン5.4での日本語レポジトリ作成もいくつかファイルは違いますが、基本的には、最初のcomposerの実行で、5.3.の代わりに、5.4.と指定すれば、同じような手順で作成できます。\n","date":"2017-01-30T09:55:52+09:00","permalink":"https://www.larajapan.com/2017/01/30/laravel%E3%81%AE%E6%97%A5%E6%9C%AC%E8%AA%9E%E3%83%AC%E3%83%9D%E3%82%B8%E3%83%88%E3%83%AA%E3%81%AE%E4%BD%9C%E6%88%90/","title":"Laravelの日本語レポジトリの作成（Laravel 5.3）"},{"content":"Laravelにおいて新規のプロジェクト作成はとても簡単。コマンドラインでいくつかのコマンドを実行をちょちょいとすれば完了。しかし、インストールされるのは英語のプロジェクト。テンプレートやメッセージの翻訳をいちいちしなければ日本語のプロジェクトにはならない。\nここのプロセスを簡単にと、Laravelバージョン5.3をもとに、開発者のために日本語化したレポジトリを作成してみました。\nこのレポジトリには、\nデフォルトのユーザー認証の機能：会員登録、パスワードリセット、会員ログイン 日本語に翻訳されたデフォルトのテンプレートとＥメールメッセージ 日本語に翻訳されたデフォルトの入力エラーメッセージ デバッグのためのDebugbarツール ウェブ解析ツールGoogle Analyticsのトラッキングスクリプト 以上を含みます。\nさらに、今回は、実際動作するデモとして以下に用意しました。 https://larajapan.lotsofbytes.com/larajapan\nさて、このレポジトリのインストールは以下の手順で簡単にできます。\nレポジトリのインストール SSHを利用しているなら、以下をコマンドラインで実行してレポジトリをインストールします。\n$ git clone -b 5.3 git@github.com:lotsofbytes/larajapan.git あるいは、Httpsを使用するなら、以下を実行します。\n$ git clone -b 5.3 https://github.com/lotsofbytes/larajapan.git 実行後は、larajapanのディレクトリが作成されますが、お好きな名前に改名してもらってＯＫです。\nその後、\ncd larajapan それから、ファイルのパーミッションを与えるべく以下を実行。\n$ chmod -R a+w storage インストール後は、以下を実行してください。\n$ composer install 以下も必要かもしれません。\n$ composer update .envの編集 .env.example をコピーして、.env　を作成し編集して以下のように設定します。*****の部分を適切な値に変更してください。\nAPP_ENV=local APP_KEY= APP_DEBUG=true APP_LOG_LEVEL=debug DB_CONNECTION=mysql DB_DATABASE=***** DB_USERNAME=***** DB_PASSWORD=***** MAIL_DRIVER=sendmail\nその後、以下を実行して.env内のAPP_KEYを更新します。\n$ php artisan key:generate APP_DEBUG=trueこれによりDebugbarが画面下方に表示されます。Debugbarに関しては、Debugbarで楽々デバッグも読んでください。\nまた、 ANALYTICS=UA-XXXXXXのようにサイトのためにGoogleから取得したコードを設定すれば、Google Analyticsでウェブでのユーザーの動向が追跡できます。\nDBを作成 .envで指定したDBを作成。\n$ echo \u0026#39;CREATE DATABASE larajapan CHARACTER SET utf8\u0026#39; | mysql -u root -p php artisan migrate ウェブサーバーの立ち上げ 最後に以下を実行して、ウェブサーバーを立ち上げると、\n$ php artisan serve 以下のアドレスでブラウザーからアクセスできます。\nhttp://localhost:8000\n以上です。\n上の設定は私の開発環境Fedora LinuxとAmazon Linux OSで、5.6のバージョンのPHPで、DBにはMysqlあるいはMaria DBを使用して動作確認しています。しかし、皆さんの環境ではいろいろ異なることがあると思います。問題や指摘があれば、ご連絡ください。\n","date":"2017-01-24T12:16:53+09:00","permalink":"https://www.larajapan.com/2017/01/24/laravel%E3%81%AE%E6%97%A5%E6%9C%AC%E8%AA%9E%E3%83%AC%E3%83%9D%E3%82%B8%E3%83%88%E3%83%AA/","title":"Laravelの日本語レポジトリ（Laravel 5.3）"},{"content":"前回では、Laravel Collective Remoteを利用して、sftpでファイルをアップロード・ダウンロードする話をしました。それだけで事はほとんど足りるのですが、１つ困ったことがありました。\nsftpを使用するのは、たいてい自社のサーバーとではなく、他社とのサーバーとです。注文データを取得してくるのも、こちらのデータをアップロードするのも。御存知のように、sftpはsshと同じプロトコルであり、sshが使用できるならsftpも使用可能。しかし、先のような状況だと、セキュリティのために、sftpは使用できるけれど、sshは使用できないようにサーバーが設定されています。また、sftpでは、勝手に他のディレクトリへ行ったりできないように、閲覧できるディレクトリを特定して、jailします。\nこのような制限された設定となると、例えば以下のように、lsコマンドの実行が不可能となり、ファイルのリストさえ取ってくることできなくなります。\nPsy Shell v0.7.2 (PHP 5.6.26 — cli) by Justin Hileman \u0026gt;\u0026gt;\u0026gt; use SSH; =\u0026gt; null \u0026gt;\u0026gt;\u0026gt; SSH::into(\u0026#39;acme\u0026#39;)-\u0026gt;run([\u0026#39;ls data\u0026#39;]); [foo@acme] (acme) This service allows sftp connections only. .. かと言って、\n\u0026gt;\u0026gt;\u0026gt; SSH::into(\u0026#39;acme\u0026#39;)-\u0026gt;list(\u0026#39;data\u0026#39;); PHP Fatal error: Call to undefined method Collective\\Remote\\Connection::list() のように、listのコマンドがあるわけでもありません。\nいろいろ、探ってみると、\nLaravel Collective Remoteは、\nhttps://github.com/LaravelCollective/remote/blob/5.3/src/SecLibGateway.php\nでは、\n以下のphpseclibというパッケージを使用しています。\nhttps://github.com/phpseclib/phpseclib/blob/master/phpseclib/Net/SFTP.php\nこれらを参考にすると、\n\u0026gt;\u0026gt;\u0026gt;SSH::into(\u0026#39;acme\u0026#39;)-\u0026gt;getGateway()-\u0026gt;getConnection()-\u0026gt;nlist(\u0026#39;data\u0026#39;); =\u0026gt; [ \u0026#34;base-invoice.csv\u0026#34;, \u0026#34;base-product.csv\u0026#34;, \u0026#34;.\u0026#34;, ] .. というように、nlistを実行できます。\n他にも、chdirとも実行できるようです。オープンソースのおかげでこういう問題自分で解決できます。\n","date":"2016-12-25T01:48:48+09:00","permalink":"https://www.larajapan.com/2016/12/25/%E3%83%91%E3%83%96%E3%83%AA%E3%83%83%E3%82%AF%E3%82%AD%E3%83%BC%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%97%E3%81%A6sftp-2-sftponly%E3%81%AE%E7%92%B0%E5%A2%83%E3%81%A7/","title":"パブリックキーを使用してsftp (2) sftponlyの環境で"},{"content":"中規模のＥＣを営む私のお客さんのところでは、自社製品を持ち出荷するゆえに、自社のウェブで販売するのみではなく、他社でのウェブサイトでも製品が売られています。となると、そこからも注文データが来ます。\nその発注データは、ウェブサービスを使用したAPIを使用して取得、というようなものではなく、彼らが生成した注文データをCSVファイルとして指定のサーバーに置かれ、それを毎日sftpでダウンロードして、システム内に取り込みます。\nまた、逆に自社サイトで販売した注文情報を、出荷や解析の目的で他のサーバーにsftpでアップロードというケースもあります。\nここで重要なのは、パブリックキーを使用したsftpのコミュニケーションが必要なことです。\nそこで登場するのが、以前会員編集フォーム紹介した、Laravel Collectiveです。\nLaravel Collectiveのインストール 最初に、以下をコマンドラインで実行します。\n$ composer require \u0026#34;laravelcollective/remote\u0026#34;:\u0026#34;^5.3.0\u0026#34; この実行で、必要なライブラリがインストールされ、composer.jsonが更新されます。\n次に、config/app.phpのファイルをエディタで開き、以下を追加します。\n\u0026#39;providers\u0026#39; =\u0026gt; [ // ... Collective\\Remote\\RemoteServiceProvider::class, // ... ], \u0026#39;aliases\u0026#39; =\u0026gt; [ // ... \u0026#39;SSH\u0026#39; =\u0026gt; Collective\\Remote\\RemoteFacade::class, // ... ], 設定 SSHのパッケージがインストールされたところで、次は設定です。Laravel用に開発されたパッケージでは、たいていはパッケージ独自の設定ファイルを、config/ディレクトリに作成します。\n以下を、コマンドラインで実行してください。\n$ php artisan vendor:publish --provider=\u0026#34;Collective\\Remote\\RemoteServiceProvider\u0026#34; この実行により、config/remote.phpの設定ファイルが作成されます。\nエディターで、そのファイルを開き、acmeエントリーを追加します。acmeでなくても、名前はなんでもよいです。\nconfig/remote.php \u0026#39;default\u0026#39; =\u0026gt; \u0026#39;production\u0026#39;, \u0026#39;connections\u0026#39; =\u0026gt; [ \u0026#39;production\u0026#39; =\u0026gt; [ \u0026#39;host\u0026#39; =\u0026gt; \u0026#39;\u0026#39;, \u0026#39;username\u0026#39; =\u0026gt; \u0026#39;\u0026#39;, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;\u0026#39;, \u0026#39;key\u0026#39; =\u0026gt; \u0026#39;\u0026#39;, \u0026#39;keytext\u0026#39; =\u0026gt; \u0026#39;\u0026#39;, \u0026#39;keyphrase\u0026#39; =\u0026gt; \u0026#39;\u0026#39;, \u0026#39;agent\u0026#39; =\u0026gt; \u0026#39;\u0026#39;, \u0026#39;timeout\u0026#39; =\u0026gt; 10, ], \u0026#39;acme\u0026#39; =\u0026gt; [ \u0026#39;host\u0026#39; =\u0026gt; env(\u0026#39;ACME_HOST\u0026#39;), // sftp先 \u0026#39;username\u0026#39; =\u0026gt; env(\u0026#39;ACME_USERNAME\u0026#39;), // ログイン名 \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;\u0026#39;, // \u0026#39;key\u0026#39; =\u0026gt; base_path(env(\u0026#39;ACME_KEY\u0026#39;)), // パブリックキーファイルのパス名\u0026#39;, \u0026#39;keytext\u0026#39; =\u0026gt; \u0026#39;\u0026#39;, \u0026#39;keyphrase\u0026#39; =\u0026gt; \u0026#39;\u0026#39;, \u0026#39;agent\u0026#39; =\u0026gt; \u0026#39;\u0026#39;, \u0026#39;timeout\u0026#39; =\u0026gt; 10, ], ], 見ての通り、このファイルにはデフォルトとして、すでにproductionがありますが、それには触らず、ここではacmeの配列を新設しました。開発サイトでのテストも考えて、いろいろな情報をここで設定できるのは便利です。\n今回は、バージョン管理にプライベートの値が入らないように、.envで指定するようにしました。特に、大きい値のパブリックキーを含むファイルを、keyで指定できるのも便利です。このファイルは、.gitignoreで必ずバージョン管理から排除が必要ですね。\nkeyphraseは、パブリックキーを生成したときに指定したパスワードを入れますが、パスワード指定なしで作成することが多いです。それでも十分セキュアであるのでここでは空とします。\n.envでは、以下のような変数を追加します。\n.env .. ACME_HOST=foo.google.com ACME_USERNAME=foo ACME_KEY=config/keys/test.pem .. 以上でテスト可能となります。もし、sftp先でsftpのみで設定されていないなら、\n$ php artisan tinker を実行して、接続テストができます。以下では、sshでacmeに接続して、lsコマンドを実行しました。\nPsy Shell v0.7.2 (PHP 5.6.25 — cli) by Justin Hileman \u0026gt;\u0026gt;\u0026gt; use SSH; =\u0026gt; null \u0026gt;\u0026gt;\u0026gt; SSH::into(\u0026#39;acme\u0026#39;)-\u0026gt;run([\u0026#39;ls\u0026#39;]); [foo@acme] (acme) data.csv =\u0026gt; null \u0026gt;\u0026gt;\u0026gt; sftpでファイルのダウンロード・アップロード ここまで来たら、あとは簡単です。プログラムの中で以下を実行するだけです。\nファイルのダウンロードは、\nSSH::info(\u0026#39;acme\u0026#39;)-\u0026gt;get(\u0026#39;data.csv\u0026#39;, storage_path(\u0026#39;data.csv\u0026#39;)); ファイルのアップロードは、\nSSH::info(\u0026#39;acme\u0026#39;)-\u0026gt;put(storage_path(\u0026#39;data.csv\u0026#39;), \u0026#39;data.csv\u0026#39;); あたかも、コマンドラインで実行するような軽さですね。\n","date":"2016-12-19T10:06:19+09:00","permalink":"https://www.larajapan.com/2016/12/19/%E3%83%91%E3%83%96%E3%83%AA%E3%83%83%E3%82%AF%E3%82%AD%E3%83%BC%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%97%E3%81%A6sftp/","title":"パブリックキーを使用してsftp"},{"content":"Laravelのバージョンが5になってから、グローバルスコープを持つ変数の設定は、.envとconfig/のディレクトリの設定ファイルで、実用的しかも綺麗にまとまりました。変数の使用も、config(\u0026lsquo;app.url\u0026rsquo;)と、プログラムのでどこでも簡単に取得できます。\nまた、DBの設定などのデフォルト設定ファイル以外にも、独自の設定ファイルも作成できます。\n例えば、\nconfig/local.phpというファイルを作成して、\nconfig/local.php return [ \u0026#39;convert\u0026#39; =\u0026gt; \u0026#39;/usr/bin/convert\u0026#39;, // 画像変換のプログラムのパス名 \u0026#39;items_per_page\u0026#39; =\u0026gt; 50, // 1ページのアイテム数 \u0026#39;max_upload_size\u0026#39; =\u0026gt; 20, // 最大アップロードの画像サイズ（ＭＢ） \u0026#39;company\u0026#39; =\u0026gt; env(\u0026#39;LOCAL_COMPANY\u0026#39;, \u0026#39;Lots of Bytes\u0026#39;), // 会社名 ]; とすれば、config(\u0026rsquo;local.company\u0026rsquo;)と参照できます。\nしかし、これらのグローバル変数設定の問題は、プログラムとして設定ファイルあるいは.envでハードコードしなければならないことです。\nグローバル変数をDBに値を保存して、管理画面で以下のように値を編集できて、プログラムの各所で使用できるようにするには、どうしたらよいのでしょうか？\n幸いにも、config()の関数は、取得だけでなく書き込みも可能なのです。\nconfig([\u0026#39;global.admin_email\u0026#39; =\u0026gt; \u0026#39;abc@gmail.com\u0026#39;]); というように実行すれば、config(\u0026lsquo;global.admin\u0026rsquo;)でabc@gmail.comを返すことができます。\nglobal.とプリフィックスしたのは、単に他の変数と区別やグループ分けがしたいがためです。\nということは、ＤＢからデータを読んでプログラムの実行時の最初に、configして入れしまえば良いわけです。\nこのための適切な場所は、プログラムの初期に実行される、app/Providers/AppServiceProvider.phpの中です。\napp/Providers/AppServiceProvider.php namespace App\\Providers; use Illuminate\\Support\\ServiceProvider; use DB; class AppServiceProvider extends ServiceProvider { /** * Bootstrap any application services. * * @return void */ public function boot() { $setting = DB::table(\u0026#39;setting\u0026#39;)-\u0026gt;pluck(\u0026#39;value\u0026#39;, \u0026#39;key\u0026#39;); config([\u0026#39;setting\u0026#39; =\u0026gt; $setting]); } ... } DBテーブルsettingの中身は、以下とすると、\n+------------+---------------------+---------------------+------------------+-------------------+ | setting_id | created_at | updated_at | key | value | +------------+---------------------+---------------------+------------------+-------------------+ | 1 | 2016-12-03 01:09:51 | 2016-12-03 01:09:51 | email_admin | admin@gmail.com | | 2 | 2016-12-03 01:09:51 | 2016-12-03 01:09:51 | email_from | from@gmail.com | | 3 | 2016-12-03 01:09:51 | 2016-12-03 01:09:51 | site_name | ララジャパン | +------------+---------------------+---------------------+------------------+-------------------+ 例えば、config(\u0026lsquo;setting.site_name\u0026rsquo;)の実行では、ララジャパンの値が返ってきます。\n","date":"2016-12-04T04:07:20+09:00","permalink":"https://www.larajapan.com/2016/12/04/%E3%82%B0%E3%83%AD%E3%83%BC%E3%83%90%E3%83%AB%E5%A4%89%E6%95%B0%E3%82%92%E3%83%80%E3%82%A4%E3%83%8A%E3%83%9F%E3%83%83%E3%82%AF%E3%81%AB%E7%AE%A1%E7%90%86/","title":"グローバル変数をダイナミックに管理"},{"content":"Laravel 5.3に更新して、Eloquentのモデルの設定において嬉しいこと発見しました。\nLaravelのEloquentでは、指定のDBテーブルにおいて、作成日時と編集日時に、規定のcreated_atとupdated_atの項目名が使用されているなら、いちいち、\nuse Users; use Carbon\\Carbon; $user = new Users; ... $user-\u0026gt;created_at = $user-\u0026gt;updated_at = Carbon::now(); $user-\u0026gt;save(); というようなことを、DBレコードの追加や編集の際に、書かなくとも自動で作成日時と編集日時に値を入れてくれます。\n私のケースでは、既存のプロジェクトのDBにおいて、作成日時と編集日時には、date_created、date_modifiedと違う名前を使用していて、今まで「いちいち」コードで指定していました。\nしかし、Laravel 5.3では、以下のようにモデルの定数(const)を指定することで、項目名を指定できるようになりました。\nnamespace App\\Models; use Illuminate\\Database\\Eloquent\\Model; class Company extends Model { protected $table = \u0026#39;company\u0026#39;; protected $primaryKey = \u0026#39;company_id\u0026#39;; public $timestamps = true; //デフォルトではtrueなので、指定する必要はない const CREATED_AT = \u0026#39;date_created\u0026#39;; const UPDATED_AT = \u0026#39;date_modified\u0026#39;; .. これは大変便利です。\nもう１つ便利なことで最近見つけたことは、最初の例で使用したCarbonは、Laravelをインストールしたら一緒にインストールされるパッケージですが、使い勝手あります。\n例えば、DBに記録した日時から現在までの「経過日数」を計算するには、\n$days_past = (new Carbon($user-\u0026gt;updated_at))-\u0026gt;diff(Carbon::now())-\u0026gt;days; 簡単でわかりやすいですね。これをphpでやろうとすると、文字列から秒数に変換してなどと大変です。\n最後に、日時の使用で忘れてならないのは、必ずアプリの設定ファイルで、タイムゾーンを設定すること。\nconfig/app.php .. \u0026#39;timezone\u0026#39; =\u0026gt; env(\u0026#39;APP_TIMEZONE\u0026#39;, \u0026#39;Asia/Tokyo\u0026#39;), .. ","date":"2016-11-26T10:12:28+09:00","permalink":"https://www.larajapan.com/2016/11/26/laravel-5-3-%E3%82%BF%E3%82%A4%E3%83%A0%E3%82%B9%E3%82%BF%E3%83%B3%E3%83%97%E3%81%AEdb%E9%A0%85%E7%9B%AE%E5%90%8D%E3%81%AE%E6%8C%87%E5%AE%9A/","title":"Laravel 5.3 タイムスタンプのDB項目名の指定"},{"content":"ユーザー認証（１１）Laravel 5.2 ログインの記録で、ログイン成功後の処理に関して説明しました。\nしかし、前回のログインのRemember Meのポストのための調査で、この「Remember Me」がオンになっているときは、先のログイン成功後の処理は、最初のログインのときだけしか実行しないことを見つけました。\nつまり、最悪のケース、5年間ログイン成功後の処理は実行されません。同じブラウザを使用してもＩＰアドレスが変わることがあるし、記録としても不十分となり不都合です。\nさて、どう解決したらよいでしょうか？\nこれにはLaravelのイベントのメカニズムを利用します。イベントの仕組みは、イベントとリスナーの２つのクラスの定義から構成され、イベントのクラスのオブジェクトをプログラムの中でコール（ファイヤー）することでリスナーのオブジェクトのアクションが実行されます。\n例えば、ログインが成功したときに希望するアクション（ログインの記録）を実行したいときは、ログイン成功のイベントとそのリスナーを作成します。\nありがたいことに、ユーザー認証に関してのイベントは、すでにIlluminiate\\Auth\\Eventsで定義されており、Illuminiate\\Auth\\SessionGuardのクラス内のメソッドで要所要所でファイヤーされています。\nイベントの種類としては、以下が揃っています。\nAttempting ログインを試みたとき Authenticated ログイン成功後のセッションにアクセスするとき Failed ログインが失敗したとき Lockout ログイン連続失敗でロックアウトされたとき Login ログインが成功したとき Logout ログアウトしたとき Registered 会員登録完了したとき 今回は、Loginのイベントが使えそうです。\nコードをチェックしたところ、Remember Meがオンのときも、このイベントをファイヤーします。\n最初にログイン成功したときだけでなく、デフォルトでは2時間アイドル後にセッションが切れるときや、ブラウザを閉じて再度オープンするときなど、再度ログインが必要なときにRemember Meの情報を使用して自動ログインするときにも、このイベントがファイヤーされます。\nということで、イベントのクラスはすでにあるので、このイベントに対応するリスナークラスを作成して登録すれば作業終了です。\nログイン成功のイベントLoginに対して、リスナーを作成してみましょう。逆になりますが、まず登録から始めます。\n登録は、\napp/Providers/EventServiceProvider.php namespace App\\Providers; use Illuminate\\Support\\Facades\\Event; use Illuminate\\Foundation\\Support\\Providers\\EventServiceProvider as ServiceProvider; class EventServiceProvider extends ServiceProvider { /** * The event listener mappings for the application. * * @var array */ protected $listen = [ \u0026#39;Illuminate\\Auth\\Events\\Login\u0026#39; =\u0026gt; [ \u0026#39;App\\Listeners\\LoginListener\u0026#39;, ], ]; /** * Register any events for your application. * * @return void */ public function boot() { parent::boot(); // } } $listenの変数に、イベントに対応するクラスを指定するだけです。\n次にリスナーのクラスを作成しますが、先の登録が完了していれば、便利なことに以下をコマンドラインで実行するだけで作成してくれます。\n$ php artisan event:generate もちろん、以下でも作成可能です。\n$ php artisan make:listener LoginListener --event=Illuminate\\Auth\\Events\\Login 作成された、リスナーを以下のように編集します。\napp/Listener/LoginListener namespace App\\Listeners; use Illuminate\\Auth\\Events\\Login; use Illuminate\\Queue\\InteractsWithQueue; use Illuminate\\Contracts\\Queue\\ShouldQueue; use Illuminate\\Http\\Request; use App\\UserLog; class LoginListener { /** * Create the event listener. * * @return void */ public function __construct(Request $request) { $this-\u0026gt;request = $request; } /** * Handle the event. * * @param Illuminate\\Auth\\Events\\Login $event * @return void */ public function handle(Login $event) { $user = $event-\u0026gt;user; $ip = $this-\u0026gt;request-\u0026gt;ip(); $agent = $this-\u0026gt;request-\u0026gt;header(\u0026#39;User-Agent\u0026#39;); UserLog::create([ \u0026#39;user_id\u0026#39; =\u0026gt; $user-\u0026gt;id, \u0026#39;ip\u0026#39; =\u0026gt; $ip, \u0026#39;hostname\u0026#39; =\u0026gt; gethostbyaddr($ip), \u0026#39;agent\u0026#39; =\u0026gt; $agent ]); } } これで、ユーザーがログインをしたときや、セッションが期限切れで失って自動ログインされるときに、ログインの記録のレコードを作成してくれます。\n","date":"2016-11-20T07:20:23+09:00","permalink":"https://www.larajapan.com/2016/11/20/%E3%83%AD%E3%82%B0%E3%82%A4%E3%83%B3%E6%88%90%E5%8A%9F%E3%81%AE%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88/","title":"ログイン成功のイベント"},{"content":"Laravel 5.3になって、認証の部分が変りました。それに関していっぱい紹介したいことありますが、それは将来でのポストとして、今回は、「ログインのRemember Me」に関して学んだことを紹介します。\n以下の画面のようにログインには、\n「Remember Me」あるいは「次から入力を省略」、ログインのときにオンとすると次回から毎回毎回ログインする必要ないよという便利な機能。巷ではどこでも見かけます。\nLaravelでも、認証のパッケージのインストール、\n$ php artisan make:auth を実行すると、デフォルトでついてくる機能です。\nさて、この「Remember Me」でどうやってログインを要らなくするのか、そのメカニズムを探ってみましょう。\nまず、「Remember Me」をクリックしてログイン成功すると、以下のように２つのクッキーを作成します。\nlaravel_session 通常のセッションのためのクッキー。期限は、 config/session.php ```php .. 'lifetime' =\u003e 120, 'expire_on_close' =\u003e true, .. 'cookie' =\u003e 'laravel_session', .. ``` lifetimeあるいはexpires_on_closeの値で決定されます。 デフォルトでは、前者が2時間（120分）、後者は、fale。後者をtrueとするとブラウザをクローズしたときに期限切れとなります。ちなみに、このクッキーの名前は、cookieの値で変更可能です。\nremember_web_59ba36addc2b2f9401580f014c7f58ea4e30989d こちらは、「Remember Me」のためのクッキーで、Laravelではログインした日から5年間と期限はハードコードされています。つまり、同じブラウザを使用し続けるなら、ログオフしない限り5年間ログインなしでアクセス可能です。 このクッキーが使用されるシナリオは簡単にはこうです。\nログイン後、2時間アイドルが続いたらあるいはブラウザを閉じたら、現在のセッションは無効になります。\nセッションが無効となると、ユーザーの認証が不可能となります。なぜなら、セッションの中に含まれていたユーザーのIDの取得が不可能となるからです。そうなるとログインの画面に遷移して、再度ログインしなければなりません。\nしかし、「Remember Me」をオンとしてログインしていたなら、この時点で、まだ期限までたっぷり時間がある「Remember Me」のクッキーを見に行きます。\nこのクッキーの中には、暗号化されたユーザーIDが入っているので、それを非暗号化して再度セッションを作成します。これにより、再度ログインをすることなしに、ログインした状態をキープすることが可能となるのです。\n","date":"2016-11-13T11:04:23+09:00","permalink":"https://www.larajapan.com/2016/11/13/laravel-5-3-remember-me/","title":"ログインのRemember Me"},{"content":"配列のDistinctバリデーション 前回の例題とした商品オプションに、オプション名に同じ値が入ることを禁じるバリデーションを追加します。\nこれには、配列プレースホルダー .* ともに laravel 5.2 で追加された distinct バリデーションが利用できます。よく考えられていますね。\n$rules = [ \u0026#39;option_id\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;option_name.*\u0026#39; =\u0026gt; \u0026#39;required_with:option_id.*|distinct\u0026#39;, \u0026#39;unit_price.*\u0026#39; =\u0026gt; \u0026#39;required_with:option_id.*|integer|min:0\u0026#39;, \u0026#39;inventory.*\u0026#39; =\u0026gt; \u0026#39;required_with:option_id.*|integer|min:0\u0026#39;, ]; 空の入力に対するdistinct？ ところで、ここまで「オプション名」は必須入力であることを前提としてきましたが、商品オプションが１つだけなら「オプション名」は空のまま「販売単価」と「在庫数」だけ入力できるようにしたい、という要望が現場から上がってくるかもしれません。\nもう少し仕様追求すると、商品オプションが２つ以上であっても、先頭（とは限らないかもしれません）の１つは空でよいかもしれません。\nこのために空の入力が１個まで可というバリデーションルールを追加するとロジックが複雑化します。required_with を外したとき distinct が空文字列もあわせて重複チェックしてくれれば話が簡単ですね。こんなふうに・・・\n３つの入力欄のうち２つを空にした場合のリクエストは次のようになります。\nArray ( [option_name] =\u0026gt; Array ( [11] =\u0026gt; [12] =\u0026gt; [13] =\u0026gt; オレンジ ) option_name のキー 11 と 12 は同じ値（空文字列）なのだから重複チェックにかかりそうなものです。\nしかしこのバリデーションはそんな都合よく機能しません。\ndistinct バリデーションは、option_name 全体ではなく、配列の値それぞれに働きます。キー 11 の値は配列内で重複してるか？ キー 12 の値は・・・と。\nそして、バリデーションの原則は値が入力された項目にのみ働きます。このケースでは、そもそもキー 11 と 12 は調査されることなく、キー 13 の値が配列内で重複してるかだけが調べられ、重複なしと判定されるのです。\nそう、リクエストが空でも働くのは、required 系バリデーションだけでした。\n暗黙の拡張 implicitRules ここまでくると次の展開が読めましたか？\nこの課題は解決には、distinct が required と同様に 空リクエストに対してもバリデーションが働く 必要があり、これを実現するのが 暗黙の拡張 ルールです。\nこの暗黙の拡張を「暗黙の必須」と読んでしまうと理解を間違えます。空リクエストに対するバリデーション、それ以上でも以下でもありません。\n暗黙の拡張ルールを作るには、Validator::extendImplicit() メソッドを使います。通常の拡張ルールとして登録されると同時に、暗黙の拡張ルールの名前を収めた配列 $implicitRules にルール名が追加されます。\nValidator::extendImplicit(\u0026#39;foo\u0026#39;, function($attribute, $value, $parameters, $validator) { return $value == \u0026#39;foo\u0026#39;; }); ということは、Validator を継承して作成した customValidaor クラスでは、直接 $implicitRules にルール名を追加することで暗黙の拡張を作ることができます。\n以下は、空文字列を含んだ重複判定ができる distinct_with_blank バリデーションの作成例です。\nclass CustomValidator extends \\Illuminate\\Validation\\Validator { public function __construct($translator, $data, $rules, $messages = []) { parent::__construct($translator, $data, $rules, $messages); // 暗黙の拡張に追加 $this-\u0026gt;implicitRules[] = \u0026#39;DistinctWithBlank\u0026#39;; } /** * 空文字列も含む重複判定 distinct_with_blank * @param string $attribute * @param string $value * @param array $parameters * @return true */ public function validateDistinctWithBlank($attribute, $value, $parameters) { // 標準distinctを呼ぶだけ return parent::validateDistinct($attribute, $value, $parameters); } これを用いて option_name.* から required_with を外すと、最初のルール定義は次のようになります。無事に、オプション名の空は１つだけ許可されるようになりました。\n$rules = [ \u0026#39;option_id\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;option_name.*\u0026#39; =\u0026gt; \u0026#39;distinct_with_blank\u0026#39;, \u0026#39;unit_price.*\u0026#39; =\u0026gt; \u0026#39;required_with:option_id.*|integer|min:0\u0026#39;, \u0026#39;inventory.*\u0026#39; =\u0026gt; \u0026#39;required_with:option_id.*|integer|min:0\u0026#39;, ]; この他にも、DB上の重複をチェックする unique に対し、空入力でも機能する unique_with_blank も状況によっては（DB定義がNULL OKではないなど）ニーズがあるかもしれませんね。\n","date":"2016-11-01T21:51:12+09:00","permalink":"https://www.larajapan.com/2016/11/01/%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3-9-%E9%85%8D%E5%88%97%E3%81%AEdistinct%E3%81%A8%E6%9A%97%E9%BB%99%E3%81%AE%E6%8B%A1%E5%BC%B5/","title":"バリデーション (9) 配列のDistinctと暗黙の拡張"},{"content":"laravel 5.2 で、バリデーションルールに配列を表す .* というプレースホルダーが使えるようになりました。\n例えば、次のような商品オプションのバリデーションを考えます。\n入力行の追加UIやドラッグ＆ドロップによるソートはjQueryなどで実装することにします（laravel から離れるので解説は省きます）\nこの場合、項目数がいくつになるかわからないので、エレメントの属性名を name=\u0026ldquo;option_name[{{$id}}]\u0026quot; などとし、配列を返すように作りますよね。\nリクエストは次のようなものになるでしょう。配列のキーは編集データに依存しますので不定です（ここでは仮に 11, 12, 13 としました）\nprint_r($request-\u003eall()); Array ( [option_id] =\u0026gt; Array ( [11] =\u0026gt; Y [12] =\u0026gt; Y [13] =\u0026gt; Y ) [option_name] =\u0026gt; Array ( [11] =\u0026gt; ホワイト [12] =\u0026gt; ブラック [13] =\u0026gt; オレンジ ) [unit_price] =\u0026gt; Array ( [11] =\u0026gt; 1000 [12] =\u0026gt; 1200 [13] =\u0026gt; 980 ) [inventory] =\u0026gt; Array ( [11] =\u0026gt; 30 [12] =\u0026gt; 20 [13] =\u0026gt; 15 ) ) 最低必要なバリデーションとしては、「保存」が少なくとも１つチェックされることと、「保存」がチェックされた行は必須入力となること、そして「販売単価」と「在庫数」の数値判定です。\n苦労していた配列のバリデーションですが、5.2 からは次のようなルール定義が可能になったのです。\n$rules = [ \u0026#39;option_id\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;option_name.*\u0026#39; =\u0026gt; \u0026#39;required_with:option_id.*\u0026#39;, \u0026#39;unit_price.*\u0026#39; =\u0026gt; \u0026#39;required_with:option_id.*|integer|min:0\u0026#39;, \u0026#39;inventory.*\u0026#39; =\u0026gt; \u0026#39;required_with:option_id.*|integer|min:0\u0026#39;, ]; $messages = [ \u0026#39;option_id.required\u0026#39; =\u0026gt; \u0026#39;保存するレコードを少なくとも１つ選択してください\u0026#39;, ]; 「オプション名」、「販売単価」、「在庫数」のルール定義には属性名に .* を加えることで、配列の値それぞれにバリデーションが適用されるようになります。\n「保存」がチェックされた行だけ入力が必要なので、required_with の引数も option_id.* となります。一方で「保存」の必須チェックは配列全体の required です。\n配列部分のエラーは array_dot() による配列ドット記法で返ってきますので、Bladeテンプレートのエラーメッセージ表示は次のような書き方になります。\n{{ $errors-\u0026gt;first(\u0026#34;unit_price.$id\u0026#34;) }} laravel 4 や 5.1 以前での配列バリデーション 後出しジャンケンでの自慢話となってしまいますが、私たちのプロジェクトでは lavavel 4 のころから配列に対するバリデーションを、5.2 と同じルールの書き方で処理してきました。\nもともと laravel のバリデーションでは、リクエストは配列のまま処理されるでのはなく、array_dot によるドット記法の一次元データに変換されて内部処理されていました。\n例えば次のように、配列のキーが固定されてる入力フォームの場合ならば、\n\u0026lt;input type=\u0026#34;text\u0026#34; name=\u0026#34;name[last]\u0026#34; value=\u0026#34;{{ old(\u0026#39;name.last\u0026#39;) }}\u0026#34;\u0026gt; \u0026lt;input type=\u0026#34;text\u0026#34; name=\u0026#34;name[first]\u0026#34; value=\u0026#34;{{ old(\u0026#39;name.first\u0026#39;) }}\u0026#34;\u0026gt; Array ( [name] =\u0026gt; Array ( [last] =\u0026gt; 山田 [first] =\u0026gt; 太郎 ) ) 以下のようにドット記法でルールを定義することで、配列データもバリデーションできたのです。\n$rules = [ \u0026#39;name.last\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;name.first\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, ]; 最初に、「5.2 で .* というプレースホルダーに対応した」と書き、配列バリデーションそのものに対応したと書かなかった意味がここにあります。 このプレースホルダーの変換を自分で対応すれば、5.1 以前や 4 でも同様の処理ができるわけです。\n定義するルール：\n$rules = [ \u0026#39;option_id\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;option_name.*\u0026#39; =\u0026gt; \u0026#39;required_with:option_id.*\u0026#39;, \u0026#39;unit_price.*\u0026#39; =\u0026gt; \u0026#39;required_with:option_id.*|integer|min:0\u0026#39;, \u0026#39;inventory.*\u0026#39; =\u0026gt; \u0026#39;required_with:option_id.*|integer|min:0\u0026#39;, ]; 変換後のルール：\n$rules = [ \u0026#39;option_id\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;option_name.11\u0026#39; =\u0026gt; \u0026#39;required_with:option_id.11\u0026#39;, \u0026#39;option_name.12\u0026#39; =\u0026gt; \u0026#39;required_with:option_id.12\u0026#39;, \u0026#39;option_name.13\u0026#39; =\u0026gt; \u0026#39;required_with:option_id.13\u0026#39;, \u0026#39;unit_price.11\u0026#39; =\u0026gt; \u0026#39;required_with:option_id.11|integer|min:0\u0026#39;, \u0026#39;unit_price.12\u0026#39; =\u0026gt; \u0026#39;required_with:option_id.12|integer|min:0\u0026#39;, \u0026#39;unit_price.13\u0026#39; =\u0026gt; \u0026#39;required_with:option_id.13|integer|min:0\u0026#39;, \u0026#39;inventory.11\u0026#39; =\u0026gt; \u0026#39;required_with:option_id.11|integer|min:0\u0026#39;, \u0026#39;inventory.12\u0026#39; =\u0026gt; \u0026#39;required_with:option_id.12|integer|min:0\u0026#39;, \u0026#39;inventory.13\u0026#39; =\u0026gt; \u0026#39;required_with:option_id.13|integer|min:0\u0026#39;, ]; この変換は次のような関数で対応できます。ベースコントローラかトレイトに入れて、バリデーション直前にルールの変換を挿入してみてください。\n/** * 配列バリデーションルールの変換 * @param \\Illuminate\\Http\\Request $request * @param array $rules * @param array $messages */ public static function setArrayRules($request, \u0026amp;$rules, \u0026amp;$messages) { foreach($rules as $field =\u0026gt; $rule) { // field文字列は foo.* か？ if (!preg_match(\u0026#39;/^(.+)\\.\\\\*$/\u0026#39;, $field, $m)) continue; // fooはリクエストに存在して配列か？ $name = $m[1]; if (!($req = $request-\u0026gt;get($name)) || !is_array($req)) continue; foreach(array_keys($req) as $i) { // rulesに foo.$i を複写 $rules[\u0026#34;$name.$i\u0026#34;] = str_replace(\u0026#39;*\u0026#39;, $i, $rule); // messagesにfoo.$i.barがあれば複写 foreach($messages as $key =\u0026gt; $message) { if (!preg_match(\u0026#34;/^$name\\.\\\\*\\.(.+)$/\u0026#34;, $key, $m)) continue; $messages[\u0026#34;$name.$i.{$m[1]}\u0026#34;] = $message; } } unset($rules[$field]); } } p.s.\n上で紹介した setArrayRules() では、次のような配列の記述には対応してません。もし必要とするならばご自身で考えてみてくださいね。\nBladeソース:\n\u0026lt;input type=\u0026#34;text\u0026#34; name=\u0026#34;option[{{$id}}][name]\u0026#34; value=\u0026#34;old(\u0026#34;opton.$id.name\u0026#34;)\u0026#34;\u0026gt; バリデーションルール:\n\u0026#39;option.*.name\u0026#39; =\u0026gt; \u0026#39;required_with:option_id.*\u0026#39;, ","date":"2016-10-23T10:47:45+09:00","permalink":"https://www.larajapan.com/2016/10/23/%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3-8-%E9%85%8D%E5%88%97%E3%82%92%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%99%E3%82%8B/","title":"バリデーション (8) 配列をバリデーションする"},{"content":"以前に、Route::resourceの便利さを紹介しました。\nroutesを使いこなす（１）resourceを使う\nroutesを使いこなす（２）resourceを使いこなす\nまた、名前付きrouteがもたらす便宜さも紹介しました。\nroutesを使いこなす（４）routeを名付ける\nしかし、Laravel 5.3のバージョンアップで「ちょっと、それはないよ」みたいな問題が出てきました。\nRoute::resourceでは、名前付きrouteが自動で作成されます。\n例えば、Laravel 5.2では、routes.phpにこう設定すると、\nroutes.php Route::group([\u0026#39;prefix\u0026#39; =\u0026gt; \u0026#39;admin\u0026#39;, \u0026#39;middleware\u0026#39; =\u0026gt; \u0026#39;web\u0026#39;, \u0026#39;namespace\u0026#39; =\u0026gt; \u0026#39;Admin\u0026#39;], function () { Route::resource(\u0026#39;product\u0026#39;, \u0026#39;ProductController\u0026#39;); }); 以下のように名前付きroutesがprefixをもとに設定されます。「Name」の列です。\nphp artisan route:list +--------+-----------+------------------------------+-----------------------+---------------------------------------------------------------+------------+ | Domain | Method | URI | Name | Action | Middleware | +--------+-----------+------------------------------+-----------------------+---------------------------------------------------------------+------------+ | | POST | admin/product | admin.product.store | App\\Http\\Controllers\\Admin\\ProductController@store | web,web | | | GET|HEAD | admin/product | admin.product.index | App\\Http\\Controllers\\Admin\\ProductController@index | web,web | | | GET|HEAD | admin/product/create | admin.product.create | App\\Http\\Controllers\\Admin\\ProductController@create | web,web | | | DELETE | admin/product/{product} | admin.product.destroy | App\\Http\\Controllers\\Admin\\ProductController@destroy | web,web | | | PUT|PATCH | admin/product/{product} | admin.product.update | App\\Http\\Controllers\\Admin\\ProductController@update | web,web | | | GET|HEAD | admin/product/{product} | admin.product.show | App\\Http\\Controllers\\Admin\\ProductController@show | web,web | | | GET|HEAD | admin/product/{product}/edit | admin.product.edit | App\\Http\\Controllers\\Admin\\ProductController@edit | web,web | +--------+-----------+------------------------------+-----------------------+---------------------------------------------------------------+------------+ しかし、おなじ、routes.phpをLaravel 5.3に持っていくと（もちろん、web.phpと改名して、app/Httpからroutes/のディレクトリに移して）、\nphp artisan route:list +--------+-----------+------------------------------+-----------------+---------------------------------------------------------------+------------+ | Domain | Method | URI | Name | Action | Middleware | +--------+-----------+------------------------------+-----------------+---------------------------------------------------------------+------------+ | | POST | admin/product | product.store | App\\Http\\Controllers\\Admin\\ProductController@store | web,web | | | GET|HEAD | admin/product | product.index | App\\Http\\Controllers\\Admin\\ProductController@index | web,web | | | GET|HEAD | admin/product/create | product.create | App\\Http\\Controllers\\Admin\\ProductController@create | web,web | | | DELETE | admin/product/{product} | product.destroy | App\\Http\\Controllers\\Admin\\ProductController@destroy | web,web | | | PUT|PATCH | admin/product/{product} | product.update | App\\Http\\Controllers\\Admin\\ProductController@update | web,web | | | GET|HEAD | admin/product/{product} | product.show | App\\Http\\Controllers\\Admin\\ProductController@show | web,web | | | GET|HEAD | admin/product/{product}/edit | product.edit | App\\Http\\Controllers\\Admin\\ProductController@edit | web,web | +--------+-----------+------------------------------+-----------------+---------------------------------------------------------------+------------+ なんと！ admin.product.store ⇒ product.store　とadmin.が皆消えてしまいました。\nドキュメントを読むと、どうもこのprefixの設定は、resourceの名前付きrouteに反映される意図はなかったとのことで、5.3で修正という訳です。\n以前と同様な、名前付きのrouteとするためには、asを使用して、以下のように設定します。\nroutes.php Route::group([\u0026#39;prefix\u0026#39; =\u0026gt; \u0026#39;admin\u0026#39;, \u0026#39;as\u0026#39; =\u0026gt; \u0026#39;admin.\u0026#39;, \u0026#39;middleware\u0026#39; =\u0026gt; \u0026#39;web\u0026#39;, \u0026#39;namespace\u0026#39; =\u0026gt; \u0026#39;Admin\u0026#39;], function () { Route::resource(\u0026#39;product\u0026#39;, \u0026#39;ProductController\u0026#39;); }); 注意：adminでなく、admin.です。最後にドットが必要。\nあるいは、ひとつひとつを命名するというオプションもあります。しかし、これはちょっと。。\nroutes.php Route::group([\u0026#39;prefix\u0026#39; =\u0026gt; \u0026#39;admin\u0026#39;, \u0026#39;middleware\u0026#39; =\u0026gt; \u0026#39;web\u0026#39;, \u0026#39;namespace\u0026#39; =\u0026gt; \u0026#39;Admin\u0026#39;], function () { Route::resource(\u0026#39;product\u0026#39;, \u0026#39;ProductController\u0026#39;, [\u0026#39;names\u0026#39; =\u0026gt; [\u0026#39;index\u0026#39; =\u0026gt; \u0026#39;admin.product.index\u0026#39;]]); }); ","date":"2016-10-22T04:42:28+09:00","permalink":"https://www.larajapan.com/2016/10/22/laravel-5-3%E3%80%80resource%E3%81%A7%E3%81%AE%E5%90%8D%E5%89%8D%E4%BB%98%E3%81%8Droute%E3%81%AE%E5%A4%89%E6%9B%B4/","title":"Laravel 5.3　resourceでの名前付きrouteの変更"},{"content":"等号を含む日付の最大最小 日付の最大最小 after, before の比較はなぜか等号を含みません。英単語の意味を厳格にプログラム仕様に落とし込んだようですが、実際の使い勝手としては等号を含んでほしかったところです。\nこれを等号を含むように拡張してしまうと、前回の min, max を拡張したのと違って意味が変わってしまいます。 そこで、start, end を新たに作成して追加してみましょう。\n\u0026#39;start_date\u0026#39; =\u0026gt; \u0026#39;date|start:today\u0026#39;, // 今日を含んでそれ以降 \u0026#39;end_date\u0026#39; =\u0026gt; \u0026#39;date|start:start_date\u0026#39;, // 開始日を含んでそれ以降 最低限の追加で済むように、内部で標準バリデーションの after, before を呼び出すようにします。標準が等号を含まないのですから、普通に考えると after に与えられた引数を -1（秒）し、before の引数を +1（秒）するのがわかりやすいでしょう。\nしかし、これらの引数は日付そのものだけではなく、上に示したように比較対象の属性名が与えられることがありますから、加減算をするには属性名から参照先の日付を取得するコードを自分で書かなければなりません。\nそこで発想を逆転して、検査する入力値の日付を +1（秒）して after に、-1（秒）して before に与えることにします。意味を考えると分かりづらいですが、相対的なものなので正負を逆転しただけです。\n作成したバリデーションルールは次のとおりです。\npublic function validateStart($attribute, $value, $parameters) { $value = date(\u0026#39;Y-m-d H:i:s\u0026#39;, strtotime($value.\u0026#39; +1 sec\u0026#39;)); return parent::validateAfter($attribute, $value, $parameters); } public function validateEnd($attribute, $value, $parameters) { $value = date(\u0026#39;Y-m-d H:i:s\u0026#39;, strtotime($value.\u0026#39; -1 sec\u0026#39;)); /* // 比較相手が 日付のみ場合は 23:59:59を補完する if ((($date = $this-\u0026gt;getValue($parameters[0])) || ($date = $parameters[0])) \u0026amp;\u0026amp; !preg_match(\u0026#39;/:/\u0026#39;, $date)) { $parameters[0] = date(\u0026#39;Y-m-d 23:59:59\u0026#39;, strtotime($date)); } */ return parent::validateBefore($attribute, $value, $parameters); } validateEnd() のコメントされたコード部分は、Datetime と Date を比較したときに起きる問題の補正です。\n例えば、DBレコードの販売期間が Datetime で、出荷期間が Date のときなど、異なる日付型の間で比較が必要な場合には有効にしてください。 補正は end のみに必要で、start には必要ありません。\n$tests = [ [\u0026#39;2016-10-10\u0026#39;, \u0026#39;2016-10-10 10:00:00\u0026#39;], [\u0026#39;2016-10-10 10:00:00\u0026#39;, \u0026#39;2016-10-10\u0026#39;], ]; foreach($tests as $i =\u0026gt; $date) { printf(\u0026#34;%d =\u0026gt; %d %d\\n\u0026#34;, $i, ($date[0] \u0026gt;= $date[1]), ($date[0] \u0026lt;= $date[1])); } 結果 0 =\u0026gt; 0 1 // (\u0026#39;2016-10-10\u0026#39; \u0026gt;=\u0026#39; 2016-10-10 10:00:00\u0026#39;) == FALSE は正しい！ 1 =\u0026gt; 1 0 // (\u0026#39;2016-10-10 10:00:00\u0026#39; \u0026lt;= \u0026#39;2016-10-10 23:59:59\u0026#39;に補正が必要 メッセージへの引数の展開 さて、前回、比較対象の属性名を引数に持てるように拡張した min, max において、エラーメッセージの引数部分が、バリデーション言語ファイルの定義通りの日本語に展開されませんでした。 今回追加作成した start, end にいたっては、英文字の属性名にすら展開されません。この問題を解決しましょう。\n今、バリデーション言語ファイルが次のように定義されているとします。\nresources/lang/ja/validation.php\n\u0026#39;after\u0026#39; =\u0026gt; \u0026#39;:attributeが:dateより前\u0026#39;, \u0026#39;before\u0026#39; =\u0026gt; \u0026#39;:attributeが:dateより後\u0026#39;, \u0026#39;max\u0026#39; =\u0026gt; [ \u0026#39;numeric\u0026#39; =\u0026gt; \u0026#39;:attributeが:maxよりも大きい\u0026#39;, ], \u0026#39;min\u0026#39; =\u0026gt; [ \u0026#39;numeric\u0026#39; =\u0026gt; \u0026#39;:attributeが:minよりも小さい\u0026#39;, ], // 追加 \u0026#39;start\u0026#39; =\u0026gt; \u0026#39;:attributeが:dateより前\u0026#39;, \u0026#39;end\u0026#39; =\u0026gt; \u0026#39;:attributeが:dateより後\u0026#39;, // 項目名 \u0026#39;attibutes\u0026#39; =\u0026gt; [ \u0026#39;end_date\u0026#39; =\u0026gt; \u0026#39;終了日\u0026#39;, \u0026#39;start_date\u0026#39; =\u0026gt; \u0026#39;開始日\u0026#39;, \u0026#39;unit_cost\u0026#39; =\u0026gt; \u0026#39;仕入単価\u0026#39;, \u0026#39;unit_price\u0026#39; =\u0026gt; \u0026#39;販売単価\u0026#39;, ], このとき、それぞれのルールに対するエラーメッセージは次のように展開されます。\n区分ルールエラーメッセージ課題 標準'unit_price' =\u003e 'min:0'販売単価が0より小さい 拡張'unit_cost' =\u003e 'max:unit_price'仕入単価がunit_priceより大きい日本語にはならない 標準'end_date' =\u003e 'before:start_date'終了日が開始日より後 追加'end_date' =\u003e 'end:start_date'終了日が:dateより後属性名への展開すらない 属性名を引数に取れるように拡張した min, max で英文字の属性名が展開できるのは、数値が与えられたときの標準の展開がそのまま適用されているためです。 カスタムプレースホルダー エラーメッセージの中の :attribute や :min, :date はプレースホルダーと呼ばれます。\nカスタムのプレースホルダーを追加するには、サービルプロバイダーの boot メソッドの中で、Validator::replacer() メソッドを呼び出します。\nValidator::replacer(\u0026#39;foo\u0026#39;, function($message, $attribute, $rule, $parameters) { return str_replace(...); }); もしくは、Validator を継承した CustomVlidator クラスでは、addReplacer() メソッドか、複数のリプレーサーを配列にしてまとめて登録できる addReplacers() メソッドで登録できます。\n$this-\u0026gt;addReplacer(\u0026#39;foo\u0026#39;, function($message, $attribute, $rule, $parameters) { return str_replace(...); }); リプレーサーの中身は簡単な文字列置換です。\n与えられたエラーメッセージ（$message）に対して、プレースホルダー文字列（\u0026quot;:min\u0026quot; や \u0026quot;:date\u0026quot; など）を引数の文字列（$parameters[0]）に置き換えるコードを記述します。\n引数（$parameters[0]）は与えられた最小値の値や、比較相手の英文字の属性名です。これを日本語に展開するには、バリデーション言語ファイルの $attributes 配列から対応語を参照する getAttribute() メソッドを使います。\n以下、CustomValidator クラスのコンストラクタで min, max, start, end にリプレーサーを登録するサンプルです。共通部分が多いのでクロージャを変数に代入し、setReplacers() でまとめて配列登録しました。\nこれで、エラーメッセージは期待通りすべて日本語に展開されるようになりました。\n\u0026lt;?php namespace App\\Services; class CustomValidator extends \\Illuminate\\Validation\\Validator { public function __construct($translator, $data, $rules, $messages = []) { parent::__construct($translator, $data, $rules, $messages); // 汎用（ルールと同名） $replacer = function($message, $attribute, $rule, $parameters) { return str_replace(\u0026#39;:\u0026#39;.$rule, $this-\u0026gt;getAttribute($parameters[0]), $message); }; // 日付 $date = function($message, $attribute, $rule, $parameters) { return str_replace(\u0026#39;:date\u0026#39;, $this-\u0026gt;getAttribute($parameters[0]), $message); }; $this-\u0026gt;addReplacers([ \u0026#39;min\u0026#39; =\u0026gt; $replacer, \u0026#39;max\u0026#39; =\u0026gt; $replacer, \u0026#39;start\u0026#39; =\u0026gt; $date, \u0026#39;end\u0026#39; =\u0026gt; $date ]); } ","date":"2016-10-18T20:11:25+09:00","permalink":"https://www.larajapan.com/2016/10/18/%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3-7-%E3%82%A8%E3%83%A9%E3%83%BC%E3%83%A1%E3%83%83%E3%82%BB%E3%83%BC%E3%82%B8%E3%81%AE%E3%83%AA%E3%83%97%E3%83%AC%E3%83%BC%E3%82%B5/","title":"バリデーション (7) エラーメッセージのリプレーサー"},{"content":"日付の最大最小 after, before では、引数に値（日付）を与えるだけでなく、比較対象の属性の名前を与えることができます。 むしろ具体的な日付を引数にすることのほうがまれでしょう。\n\u0026#39;date_start\u0026#39; =\u0026gt; \u0026#39;date|before:today\u0026#39;, \u0026#39;date_end\u0026#39; =\u0026gt; \u0026#39;date|after:date_start\u0026#39;, このような使い方は、数値型の最大最小 min, max でも使用したいこともありますよね？\n例えば、次のような関係です。\n\u0026#39;unit_price\u0026#39; =\u0026gt; \u0026#39;required|integer|min:0\u0026#39;, \u0026#39;unit_cost\u0026#39; =\u0026gt; \u0026#39;required|integer|min:0|max:unit_price\u0026#39;, 標準バリデーションの min と max を拡張して、比較対象の属性名を引数に持てるようにしてみましょう。\ngetValue() バリデーションクラスにルールを登録するときの雛形は次のようなものでした。 一見すると引数には、ルールを定義した属性に関する情報しか与えられません。\n/** * 追加バリデーション * @param string $attribute 検査する属性名 * @param string $value 入力された値 * @param array $parameters 引数の配列 * @return boolean */ public function validateHoge($attribute, $value, $parameters) { // } しかし、バリデーションクラスにはリクエストされたすべての属性に関する情報が与えれていて、他の属性に入力された値も getValue() で得ることができます。getValue() の引数は属性名です。\nよって、次のバリデーションは必ず TRUE を返します。\npublic function validateHoge($attribute, $value, $parameters) { return ($value == $this-\u0026gt;getValue($attribute)); } min と max の拡張 min, max の引数は、属性の型が何であろうと必ず数値を取ります。\nそこで、引数が数値（is_numeric）でないときを限定し、引数を属性名とした値が存在するならそれを新たなしきい値としました。\n/** * 最小値 min * @param string $attribute * @param string $value * @param array $parameters\t0 =\u0026gt; 比較する属性名 * @return true */ protected function validateMin($attribute, $value, $parameters) { if (!is_numeric($parameters[0]) \u0026amp;\u0026amp; !is_null($val = $this-\u0026gt;getValue($parameters[0]))) { $parameters[0] = $val; } return parent::validateMin($attribute, $value, $parameters); } /** * 最大値 max * @param string $attribute * @param string $value * @param array $parameters\t0 =\u0026gt; 比較する属性名 * @return true */ protected function validateMax($attribute, $value, $parameters) { if (!is_numeric($parameters[0]) \u0026amp;\u0026amp; !is_null($val = $this-\u0026gt;getValue($parameters[0]))) { $parameters[0] = $val; } return parent::validateMax($attribute, $value, $parameters); } ＊　＊　＊　＊\nさて、拡張した最大最小バリデーションですが、このままではエラーメッセージが与えられた引数の属性名のままとなり、言語ファイルによる置き換えが行われません。\n本来の最大最小が与えられた引数そのものを、メッセージのプレースホルダー :max と置き換えることしか想定していないからです。 この部分の解決については次回にまとめる予定です。\n","date":"2016-10-15T19:30:29+09:00","permalink":"https://www.larajapan.com/2016/10/15/%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3-6-%E4%BB%96%E3%81%AE%E5%B1%9E%E6%80%A7%E3%81%AE%E5%80%A4%E3%82%92%E5%8F%82%E7%85%A7/","title":"バリデーション (6) 他の属性の値を参照"},{"content":"バリデーション言語ファイルを編集するときにお気づきと思いますが、Laravel標準バリデーション Max と Min は（他に Size、Between も）調査対象の属性の型でその挙動を変えます。\n\u0026#39;max\u0026#39; =\u0026gt; [ \u0026#39;numeric\u0026#39; =\u0026gt; \u0026#39;:attributeの値が:maxを超えています\u0026#39;, \u0026#39;file\u0026#39; =\u0026gt; \u0026#39;:attributeのサイズが:max kBを超えています\u0026#39;, \u0026#39;string\u0026#39; =\u0026gt; \u0026#39;:attributeの文字数が:maxを超えています\u0026#39;, \u0026#39;array\u0026#39; =\u0026gt; \u0026#39;:attributeの個数が:maxを超えています\u0026#39;, ], \u0026#39;min\u0026#39; =\u0026gt; [ \u0026#39;numeric\u0026#39; =\u0026gt; \u0026#39;:attributeの値が:minに足りません\u0026#39;, \u0026#39;file\u0026#39; =\u0026gt; \u0026#39;:attributeのサイズが:min kBに足りません\u0026#39;, \u0026#39;string\u0026#39; =\u0026gt; \u0026#39;:attributeの文字数が:minに足りません\u0026#39;, \u0026#39;array\u0026#39; =\u0026gt; \u0026#39;:attributeの個数が:minに足りません\u0026#39;, ], ここで問題となるのは、文字列型が入力した文字数カウントとの比較になることです。文字列比較の大小によるバリデーションは存在してません。\n例えば日付型の属性に対する開始日のつもりで Min を指定しても、文字数判定となり、期待したバリデーションにはなりません。\n\u0026#39;date_start\u0026#39; =\u0026gt; \u0026#39;date|min:\u0026#39;.date(\u0026#39;Y-m-d\u0026#39;), // 開始日判定にはならない 日付の最大値と最小値 日付型の属性の前後関係の判定には、専用の after と before が用意されています。 しかも、引数の文字列は strtotime で処理されるので次のような指定が可能です。\n\u0026#39;date_start\u0026#39; =\u0026gt; \u0026#39;date|after:today\u0026#39;, // 今日より大（明日以降） \u0026#39;date_end\u0026#39; =\u0026gt; \u0026#39;date|befor:+1 year\u0026#39;, // １年以内 または属性名を引数に持つことができるので、期間指定のバリデーションに使うことが出来ます。\n\u0026#39;date_end\u0026#39; =\u0026gt; \u0026#39;datetime|after:date_start\u0026#39;, ただし、after, befor の比較は等号を含みませんので注意が必要です。\n数値型のカスタムバリデーション 例えば1以上の整数（自然数）を判定するカスタムバリデーション natural_number を作成した場合、それを min や max で大小判定したときは、正しく数値として判定されるでしょうか？\n結論を言えば、カスタムバリデーション natural_number は（そのままでは）、入力された値が数値であっても、文字列として判断され、文字数でカウントされてしまいます。\n\u0026#39;quantity\u0026#39; =\u0026gt; \u0026#39;requied|natural_number|max:10\u0026#39;, // 10桁（ 10億）まで可 では、数値として認められるにはどのようにしたら良いのでしょうか。\nvender の Validation.php でコードを追うと、getSize() メソッドにおいて次のように型判定されていることがわかります。\n1入力値が数値（is_numeric）でバリデーションに numeric か integer があれば、その値を返す 2入力値が配列（is_array）なら配列の個数を返す 3入力値が File インスタンスならファイルサイズを kB で返す 4それ以外は文字数（mb_strlen）を返す 数値として判定されるには、入力された値が数値であるかどうかだけでなく、バリデーションに numeric か integer が含まれなければなりません。これでは他のどんなルールであっても数値と判定されることはないのです。\nnumericRules この数値と判断されるためのバリデーションルールは、変数 $numericRules で定義されていました。\nprotected $numericRules = [\u0026#39;Numeric\u0026#39;, \u0026#39;Integer\u0026#39;]; カスタムバリデーションとして数値型のルールを自作した場合には、これにルール名を追加すればよいわけです。\n変数 $numericRules を protected で再定義して上書きするか、コンストラクタで必要分だけ追加します。……再定義する場合は Numeric と Integer を忘れずに。\npublic function __construct($translator, $data, $rules, $messages = []) { parent::__construct($translator, $data, $rules, $messages); $this-\u0026gt;numericRules[] = \u0026#39;NatrualNumber\u0026#39;; } /** * 自然数 natural_number * * @param string $attribute\t検査する属性名 * @param string $value\t検査する値 * @return bool */ public function validateNaturalNumber($attribute, $value) { if ($this-\u0026gt;validateInteger($attribute, $value)) { return ($value \u0026gt; 0); } return false; } ","date":"2016-10-10T19:42:42+09:00","permalink":"https://www.larajapan.com/2016/10/10/%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%A0%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3-5-%E6%9C%80%E5%A4%A7%E5%80%A4%E3%81%A8%E6%9C%80%E5%B0%8F%E5%80%A4/","title":"バリデーション (5) 最大値と最小値"},{"content":"Laravel 標準の Date バリデーションは、次のときに TRUE を返します。\nOR値がPHPの DateTime クラスのインスタンスである AND値が strtotime で理解できる文字列である date_parse で年月日を返す checkdate で年月日が妥当である 文字列の具体例でまとめると以下のようになります。\n文字列バリデーションMySQL保存 例01nowFALSE 例02+1 dayFALSE 例03next ThursdayFALSE 例042016-09-30 + 1dayTRUE× 例0530 September 2016TRUE× 例062016-09-30TRUE◯ 例0720160930TRUE◯ 例082016/09/30TRUE◯ 例0909/30/2016TRUE× 例1009-30-2016FALSE 例1130/09/2016FALSE 例122016-09-30 10:10:00TRUE◯ date_parse を通すため 例01〜例03のような論理指定は弾かれますが、例04のような記述では前半部分が年月日の配列値を返すためバリデーションは通ります。 また、アメリカ式の月日年（例09）が通って、ヨーロッパ式の日月年（例11）は弾かれます（これは環境のタイムゾーンに影響されるでしょう）。\nいずれにせよ、バリデーションに通ってもそのままデータベースに保存できるとは限りません。 データベース保存を前提にするなら、ISO-8601 の \u0026ldquo;YYYY-MM-DD\u0026rdquo; 書式に限定したほうが何かと都合が良いですね。\ndate_format による ISO-8601限定 入力され日付のフォーマットを限定するために date_format が用意されています。引数は date_parse_from_format を通して処理されます。 date と date_format は両方を同時に使用することはできません。\n\u0026#39;date\u0026#39; =\u0026gt; \u0026#39;date_format:Y-m-d\u0026#39;, \u0026#39;datetime\u0026#39; =\u0026gt; \u0026#39;date_format:Y-m-d H:i:s\u0026#39;, Date と Datetime の拡張 データベースの保存フィールドで Date と Datetime を区別し、入力フォームのインターフェースもこの２つしかないならば、date_format を使用せずに、date バリデーションを拡張してしまうこともできます。\n書式を正規表現で限定してからオリジナルのバリデーションを通せばよいでしょう。\n/** * date (without datetime) * * @param string $attribute * @param string $value * @return true */ protected function validateDate($attribute, $value) { // YYYY-MM-DD に限定する if (!preg_match(\u0026#39;/^[0-9]+-[0-9]+-[0-9]+$/\u0026#39;, $value)) return false; return parent::validateDate($attribute, $value); } /** * datetime * * @param string $attribute * @param string $value * @return bool */ public function validateDatetime($attribute, $value) { // YYYY-MM-DD hh:mm に限定する if (!preg_match(\u0026#39;/^[0-9]+-[0-9]+-[0-9]+ ([0-9]+):([0-9]+)/\u0026#39;, $value, $m)) return false; return parent::validateDate($attribute, $value); } validateDete() は標準の置き換えなので protected 宣言ですが validateDatetime() は追加関数として public で宣言しています。\n日付入力のフォームインターフェイス 入力バリデーションとして Date と Datetime を用意したならば、入力フォームもそれに対応しなければなりませんが、HTML5 の日付フォームはブラウザの対応に依存し、非対応な環境を無視したとしてもユーザーインターフェイスが人によって異なりサポートが難しいものです。\n私たちのプロジェクトの管理画面では、Bootstrapベースの SmartAdmin を導入してインターフェイスの統一を図っています。\nSmartAdmin の日付フォームは jQuery UI Datepicker と Bootstrap Timepicker の組み合わせを採用していますが、HTML5の datetime や datetime-local には対応してません。スクリプトを書くことで datetime 属性（厳密には datetime-local 属性）の入力フォームを実現する必要があります。\nbladeソース（の概念）と対応するjQueryスクリプト\n\u0026lt;div class=\u0026#34;input-group-datetime\u0026#34;\u0026gt; \u0026lt;input type=\u0026#34;hidden\u0026#34; name=\u0026#34;date_start\u0026#34; value=\u0026#34;{{ old(\u0026#39;date_start\u0026#39;) }}\u0026#34;\u0026gt; \u0026lt;input class=\u0026#34;datepicker\u0026#34;\u0026gt; \u0026lt;input class=\u0026#34;timepicker\u0026#34;\u0026gt; \u0026lt;/div\u0026gt; $(\u0026#39;.input-group-datetime\u0026#39;).on(\u0026#39;change\u0026#39;, \u0026#39;input:visible\u0026#39;, function() { // 日付または時刻が変更された var $parent = $(this).parents(\u0026#39;.input-group-datetime\u0026#39;), dt = $parent.find(\u0026#39;.datepicker\u0026#39;).val(), tm = $parent.find(\u0026#39;.timepicker\u0026#39;).val(); if (dt \u0026amp;\u0026amp; !tm) { // 時刻が空なら00:00で補完 tm = \u0026#39;00:00\u0026#39;; $parent.find(\u0026#39;.timepicker\u0026#39;).val(tm); } if (!dt) { // 日付が空なら時刻も削除 $parent.find(\u0026#39;.timepicker\u0026#39;).val(\u0026#39;\u0026#39;); } // 日時を合成して値を格納 $parent.find(\u0026#39;:hidden\u0026#39;).val((dt) ? dt + \u0026#39; \u0026#39; + tm + \u0026#39;:00\u0026#39; : \u0026#39;\u0026#39;); }); ","date":"2016-10-10T13:59:04+09:00","permalink":"https://www.larajapan.com/2016/10/10/%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%A0%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3-4-date%E3%81%A8datetime/","title":"バリデーション (4) DateとDatetime"},{"content":"アップロードしたファイルの保存のメソッドがLaravelで5.3で少し変わりました。ここでそれらの情報更新とともに、AmazonのストレージサービスS3にファイルをアップロードする仕方を紹介します。\nまず、準備から、\nパッケージの追加と設定 コマンドラインで以下の実行が必要です。\ncomposer require league/flysystem-aws-s3-v3 ~1.0 これにより、\nconfig/filesystems.php\nの設定ファイルが作成されます。\nreturn [ \u0026#39;default\u0026#39; =\u0026gt; \u0026#39;local\u0026#39;, \u0026#39;cloud\u0026#39; =\u0026gt; \u0026#39;s3\u0026#39;, \u0026#39;disks\u0026#39; =\u0026gt; [ \u0026#39;local\u0026#39; =\u0026gt; [ \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;local\u0026#39;, \u0026#39;root\u0026#39; =\u0026gt; storage_path(\u0026#39;app\u0026#39;), ], \u0026#39;public\u0026#39; =\u0026gt; [ \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;local\u0026#39;, \u0026#39;root\u0026#39; =\u0026gt; storage_path(\u0026#39;app/public\u0026#39;), \u0026#39;visibility\u0026#39; =\u0026gt; \u0026#39;public\u0026#39;, ], \u0026#39;s3\u0026#39; =\u0026gt; [ \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;s3\u0026#39;, \u0026#39;key\u0026#39; =\u0026gt; \u0026#39;AWSのキー\u0026#39;, \u0026#39;secret\u0026#39; =\u0026gt; \u0026#39;秘密のキー\u0026#39;, \u0026#39;region\u0026#39; =\u0026gt; \u0026#39;地域のコード\u0026#39;,　// 日本なら、ap-northeast-1 \u0026#39;bucket\u0026#39; =\u0026gt; \u0026#39;バケット名\u0026#39; ], ], ]; localは、使用しているサーバーのストレージのことです。\nrootは、Laravelをインストールしたディレクトリのサブディレクトリ、storage/appの場所となります。\npublicは、ウェブユーザーにアップロードしたファイルをパブリックに紹介する場所です。\n以下の実行で、public/storageが、storage/app/publicにリンクされます\nphp artisan storage:link 例えば、アップロードされたファイルは、\nstorage/app/public/mario.jpg\nに保存され、\nhttp://localhost/public/storage/mario.jp\nで閲覧できるということです。\ns3のkey, secret, region, bucketの指定は必須です。これらは、Amazonのウェブサービスのコンソールで取得できます。\nこれで設定終わりです。\nファイルのアップロードのプログラム 簡単なファイルのアップロードのプログラムを書いてみます。\nまず、routeの設定から、\nroutes/web.php Route::get(\u0026#39;upload\u0026#39;, \u0026#39;UploadController@create\u0026#39;); Route::post(\u0026#39;upload\u0026#39;, \u0026#39;UploadController@store\u0026#39;); これで、\nhttp://localhost/upload\nにアクセス可能です。\n次にコントローラ、\napp/Http/Controllers/UploadController.php namespace App\\Http\\Controllers; use Illuminate\\Http\\Request; use Storage; class UploadController extends Controller { public function create() { return view(\u0026#39;upload\u0026#39;); } public function store(Request $request) { $filename = $request-\u0026gt;file(\u0026#39;image\u0026#39;)-\u0026gt;getClientOriginalName();　//アップロードしたファイル名を取得 $path = $request-\u0026gt;file(\u0026#39;image\u0026#39;)-\u0026gt;storeAs(\u0026#39;public\u0026#39;, $filename); return back()-\u0026gt;with(\u0026#39;filename\u0026#39; =\u0026gt; $filename); } } storeAs(\u0026lsquo;public\u0026rsquo;, $filename);\nこの最初のパラメータは、ファイルを保存するディレクトリ名です。先のconfig/filesystems.phpの設定で、storage/appがルートのディレクトリゆえに、上のコードではstorage/app/publicにファイルが保存されることになります。\nファイル名がmario.jpgであれば、\nstorage/app/public/mario.jpg\nと保存されます。\nファイルをアップロードするフォームのブレードは、\nupload.blade.php @extends(\u0026#39;layouts.app\u0026#39;) @section(\u0026#39;content\u0026#39;) \u0026lt;div class=\u0026#34;container\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;row\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;col-md-8 col-md-offset-2\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;panel panel-default\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;panel-heading\u0026#34;\u0026gt;Media Upload\u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;panel-body\u0026#34;\u0026gt; \u0026lt;form class=\u0026#34;form-horizontal\u0026#34; role=\u0026#34;form\u0026#34; method=\u0026#34;POST\u0026#34; action=\u0026#34;{{ url(\u0026#39;upload\u0026#39;) }}\u0026#34; enctype=\u0026#34;multipart/form-data\u0026#34;\u0026gt; {{ csrf_field() }} \u0026lt;div class=\u0026#34;form-group\u0026#34;\u0026gt; \u0026lt;label for=\u0026#34;image\u0026#34; class=\u0026#34;col-md-4 control-label\u0026#34;\u0026gt;File\u0026lt;/label\u0026gt; \u0026lt;div class=\u0026#34;col-md-6\u0026#34;\u0026gt; \u0026lt;input id=\u0026#34;image\u0026#34; type=\u0026#34;file\u0026#34; name=\u0026#34;image\u0026#34;\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;form-group\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;col-md-6 col-md-offset-4\u0026#34;\u0026gt; \u0026lt;button type=\u0026#34;submit\u0026#34; class=\u0026#34;btn btn-primary\u0026#34;\u0026gt; Upload \u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/form\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;row\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;col-md-8 col-md-offset-2\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;panel panel-default\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;panel-body\u0026#34;\u0026gt; @if (session(\u0026#39;filename\u0026#39;)) \u0026lt;h4\u0026gt;Local\u0026lt;/h4\u0026gt; \u0026lt;img src=\u0026#34;{!! asset(\u0026#39;storage/\u0026#39;.session(\u0026#39;filename\u0026#39;)) !!}\u0026#34;\u0026gt; @endif \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; @endsection 先の例で、ファイル名が、mario.jpgならば、\nasset(\u0026lsquo;storage/\u0026rsquo;.session(\u0026lsquo;filename\u0026rsquo;))\nは、\nhttp://localhost/public/storage/mario.jpg\nのようになるわけです。\nファイルをアップロードした後の画面はこんな感じです。 Ｓ３に画像をアップロード さて、サーバーにアップした画像を、今度はS3にアップするのですが、これはconfig/filesystems.phpの設定が済んでいれば、本当に簡単です。\nコントローラのstoreメソッドにたったの２行追加するだけです。\npublic function store(Request $request) { $filename = $request-\u0026gt;file(\u0026#39;image\u0026#39;)-\u0026gt;getClientOriginalName(); $path = $request-\u0026gt;file(\u0026#39;image\u0026#39;)-\u0026gt;storeAs(\u0026#39;public\u0026#39;, $filename); $contents = Storage::get(\u0026#39;public/\u0026#39;.$filename); //ファイルを読み取る Storage::disk(\u0026#39;s3\u0026#39;)-\u0026gt;put($filename, $contents, \u0026#39;public\u0026#39;); // Ｓ３にアップ return back()-\u0026gt;with([\u0026#39;filename\u0026#39; =\u0026gt; $filename]); } \u0026gt;put($filename, $contents, \u0026lsquo;public\u0026rsquo;)\nここのpublicに注意してください。これがないと一般には公開されません。\n以下のAWSのコンソールの赤箱の部分がそれにより追加されます。\n先の画面にＳ３から直接画像を表示したいなら、ブレードに以下の変更を。\n\u0026lt;div class=\u0026#34;panel-body\u0026#34;\u0026gt; @if (session(\u0026#39;filename\u0026#39;)) \u0026lt;h4\u0026gt;Local\u0026lt;/h4\u0026gt; \u0026lt;img src=\u0026#34;{!! asset(\u0026#39;storage/\u0026#39;.session(\u0026#39;filename\u0026#39;)) !!}\u0026#34;\u0026gt; \u0026lt;h4\u0026gt;S3\u0026lt;/h4\u0026gt; \u0026lt;img src=\u0026#34;{!! Storage::disk(\u0026#39;s3\u0026#39;)-\u0026gt;url(session(\u0026#39;filename\u0026#39;)) !!}\u0026#34;\u0026gt; @endif \u0026lt;/div\u0026gt; S3の以下のURLが生成されます。\nhttps://s3-us-west-2.amazonaws.com/demo53/mario.jpg\nus-west-2は、設定に使用した地域コードです。日本ならap-northeast-1となります。\n","date":"2016-10-09T04:38:28+09:00","permalink":"https://www.larajapan.com/2016/10/09/laravel-5-3%E3%80%80aws-s3%E3%81%AB%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%92%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%89/","title":"Laravel 5.3　AWS S3にファイルをアップロード"},{"content":"Laravelのバージョン5.3がリリースされてから、かれこれ１ヶ月。使い始めてみました。\n以前のバージョンからバージョン5.*への変更に比べれば、そう注意しなければならない変更はないのだけれど、とっても注意することありました。\nLaravel5.2では、コントローラのコンストラクタでこんなこと可能でした。\nnamespace App\\Http\\Controllers; use Illuminate\\Http\\Request; use Auth; class HomeController extends Controller { protected $options; public function __construct() { $user = Auth::user(); $this-\u0026gt;options = $user-\u0026gt;options; //ログインしたユーザーの設定オプションを取得 } } 認証されたユーザー、つまりログインしたユーザーの認証の情報にアクセスすることが、コントローラのコンストラクタで可能でした。\n同じコントローラの他のメソッドで何回も同じことを行うのは面倒なわけで、また以下のようにすでにroutes.php（5.3では、app/routes/web.phpとなります）の設定で、パスワード保護されているので、認証されたユーザーの情報を共有できる理想の場所というわけです。\nRoute::get(\u0026#39;login\u0026#39;, \u0026#39;Auth\\AuthController@showLoginForm\u0026#39;); Route::post(\u0026#39;login\u0026#39;, \u0026#39;Auth\\AuthController@login\u0026#39;); Route::get(\u0026#39;logout\u0026#39;, \u0026#39;Auth\\AuthController@logout\u0026#39;); Route::group([\u0026#39;middleware\u0026#39; =\u0026gt; \u0026#39;auth\u0026#39;], function() { Route::get(\u0026#39;home\u0026#39;, \u0026#39;HomeController@index\u0026#39;); }); しかし、それは「してはいけないこと」だったのです！Laravel5.3になるやいなや、認証したユーザーのAuth::userがnullを返すようになりました。\nそして、Laravelの作者(Mr. Tayler)の思惑とは逆に、こんなことをやっている開発者はごまんといたわけです。\n以下のやり取りをみていると、\n突如の変更に困ったユーザーとそれに対応する作者たちの会話\n作者は、ユーザー認証はクッキーやセッションがあってこそ成り立つものであり、それが入ってくるのはコントローラのメソッドにおいてのrequestの引数である。それゆえにrequestがないコンストラクタでその情報を取得するのはデザイン上「悪い」と。\nしかし、コンストラクタはいろいろと共有できる便利な場所であり、しかも今までそこで問題なかったのだからね。\n作者もそれを理解して、すぐに以下のような対応をしてくれました。\npublic function __construct() { $this-\u0026gt;middleware(function ($request, $next) { $this-\u0026gt;options = Auth::user()-\u0026gt;options;//ログインしたユーザーの設定オプションを取得 return $next($request); }); } Laravel 5.3.4からの対応です。\n","date":"2016-09-24T08:40:03+09:00","permalink":"https://www.larajapan.com/2016/09/24/laravel-5-3%E3%80%80%E3%82%B3%E3%83%B3%E3%83%88%E3%83%AD%E3%83%BC%E3%83%A9%E3%81%AE%E3%82%B3%E3%83%B3%E3%82%B9%E3%83%88%E3%83%A9%E3%82%AF%E3%82%BF%E3%81%AE%E9%87%8D%E8%A6%81%E3%81%AA%E5%A4%89%E6%9B%B4/","title":"Laravel 5.3　コントローラのコンストラクタの重要な変更"},{"content":"Laravelは非常にたくさんのファイルを起動時に読み込んでいるので、パフォーマンスの改善は以前から感心があります。最近、管理画面だけでrouteの数が300近いプログラムをインストールするにあたり、重たくなることを予想して、簡単にできる範囲でLaravelでのパフォーマンスの改善を調査してみました。\n１．設定ファイルのキャッシュ作成 php artisan config:cache この実行は、configのディレクトリにあるファイルすべてを合わせて１つのキャッシュファイルにします。これにより設定データのローディング時間を速めようということです。\nファイルは、\nbootstrap/cache/config.php\nとして作成されます。\nこの使用において、注意は３点あります。\n注意点１:プログラムにおいてenv()の関数を使用してはいけない\n例えば、\n.envのファイルにおいて、\nAPP_ENV=local と設定していて、\n$path = storage_path().\u0026#39;/\u0026#39;.env(\u0026#39;APP_ENV\u0026#39;).\u0026#39;/logs/test.log\u0026#39;; のように、現在の環境変数の値によりログファイルを置くディレクトリを変えるとすると、\n$pathの値は、\n/var/www/test/storage/local/logs/test.log\nのようになります。\nしかし、php artisan config:cacheを実行すると、env()の値は空となり、\n/var/www/test/storage/logs/test.log\nとなってしまい、意図した場所とは違うことになってしまいます。\nそれゆえに、先のプログラムは以下と変更することが必要です。\n$path = storage_path().\u0026#39;/\u0026#39;.config(\u0026#39;app.env\u0026#39;).\u0026#39;/logs/test.log\u0026#39;; もちろん、config/app.phpなどの設定ファイルの中でのenv()の使用は問題ないです。\n注意点２:ダイナミックに値を設定しているconfigに気をつける\n設定ファイルの値は、\nconfig([\u0026#39;some_setting\u0026#39; =\u0026gt; \u0026#39;some_value\u0026#39;]); のようにダイナミックに値の設定が可能です。例えば、DBからの値をAppServiceProvider::boot()で読み込んでおいて、DBに再度アクセスすることなくプログラムのあちらこちらで使用しようというときなどに便利です。\nところが、php artisan config:cacheの実行時に、AppServiceProvider::boot()が実行されるので、それらの設定もキャッシュファイルに保存されてしまいます。これで、ダイナミックに変わる値が固定されては困ります。\nしかし、ダイナミックに上書きされるので問題はなさそうです。しかし、プログラムの他の部分では問題があるかもしれません。使用には気をつけてください。\n注意点３：.envやconfigのファイルを編集したら、必ずキャッシュを再作成\n最後に忘れてはならないのは、.envやconfig/*.phpのファイルを編集したら、必ずphp artisan config:cacheの実行が必要なことです。それなくしては、せっかくの変更も反映されません。\n２．routeのキャッシュ作成 php artisan route:cache こちらは、config:cacheと違い、この実行で作成されるファイル、\nbootstrap/cache/routes.php\nには、routes.phpファイルを読み込んでLaravelがマップしたデータ構造をbase64_encodeして、serializeした形で収めます。それゆえに、この読み込みとマップの作業を一気に短縮します。とくに、routeの数が大きいプロジェクトではパフォーマンスの改善に期待できる仕組みです。\nそれからconfig:cacheと同様に、routes.phpファイルを編集したら、必ずphp artisan route:cacheの実行が必要です。\n３．共有クラスファイルの最適化 これは、環境変数のAPP_ENVがproductionの値のときにだけに有効なものです。また、設定ファイルに依存するので、設定ファイルをキャッシュするなら、php artisan config:cacheを実行してから以下を実行してください。\nphp artisan optimize この実行で、共有されるクラスファイルを１つのファイルにまとめ、読み込み時間を短縮するのが目的です。以下のファイルを作成します。\nbootstrap/cache/compiled.php\nこのファイルには、デフォルトでは、Laravel関連のファイルが含まれますが、必要なら以下の設定ファイルに追加が可能です。\nbootstrap/cache/compile.php\n改善した？ さて、実際にこれらの最適化でパフォーマンスはどれくらい変わるのでしょうか？\n私の厳密ではないテストでは、DB操作を伴わない計測では１０～３０％の違いがありました。configよりもrouteやoptimizeの方が実際のパフォーマンスにより影響ありました。多分、設定ファイルが小さく数少ないためがその違いと思います。しかし、現実では、DBクエリーやCSS、JS、画像などのダウンロードがはるかに時間がかかるので、まだ私のプロジェクトのスケールではちょっとした改善というところです。スケールがより大きくなるとかなりの差となるかもしれません。\n","date":"2016-09-13T01:59:13+09:00","permalink":"https://www.larajapan.com/2016/09/13/%E3%81%A1%E3%82%87%E3%81%A3%E3%81%A8%E3%81%97%E3%81%9F%E3%83%91%E3%83%95%E3%82%A9%E3%83%BC%E3%83%9E%E3%83%B3%E3%82%B9%E3%81%AE%E6%94%B9%E5%96%84/","title":"ちょっとしたパフォーマンスの改善"},{"content":"このブログを開始してから、もうすでに1年以上。RawのSQLを書いてコードに埋め込む日常から、Eloquentを使用したORMのコードへと日常へと移行しています。Eloquentに関しても、ブログを書き始めた頃からは理解が深まり、洗練されたLaravelのコードを書けるようになってきたこの頃です。\n１年前に書いた「マスアサインメントで一括取り込み」のトピックで、EloquentのModelのクラスの属性fillableとguardedの話、1年の経験で学んだことを含めてここでもう一度説明します。\nまず、話のお膳立てを。\nDBテーブルmemberにおいて以下の項目があるとします。\n+----------------+------------------+------+-----+---------------------+----------------+ | Field | Type | Null | Key | Default | Extra | +----------------+------------------+------+-----+---------------------+----------------+ | member_id | int(10) unsigned | NO | PRI | NULL | auto_increment | | active_flag | char(1) | NO | | NULL | | | name | varchar(255) | NO | | NULL | | | email | varchar(255) | NO | UNI | NULL | | | password | varchar(60) | NO | | NULL | | | memo | varchar(100) | YES | | NULL | | | created_at | timestamp | NO | | 0000-00-00 00:00:00 | | | updated_at | timestamp | NO | | 0000-00-00 00:00:00 | | +----------------+------------------+------+-----+---------------------+----------------+ MemberのModelは以下のような定義になります。\nMember.php ... class Member extends Model { protected $table = \u0026#39;member\u0026#39;; protected $primaryKey = \u0026#39;member_id\u0026#39;; protected $fillable = [\u0026#39;name\u0026#39;, \u0026#39;email\u0026#39;]; ... } $timestampsや$incrementingの定義は必要ないです。両方ともデフォルトでtrueなので。\n入力フォームは、\nで、email, password, nameの項目を入力できます。\n以下のコントローラで、入力フォームから入ってきた値は以下のコードでDBに保存できます。\nMemberController.php ... class MemberController extends Controller { ... public store(Request $request) { $member = new Member; $member-\u0026gt;fill($request-\u0026gt;all())-\u0026gt;save(); ... } ... } しかし、先ほどの$fillable定義により、DBに保存されるのは、emailとnameのみです。実行されるSQLは以下で、他の入力された値は無視されるので、passwordはDBはデフォルトの空のままです。つまり、$fillableは、DBに入力したい値をリストするホワイトリストです。ちなみに、created_at, updated_atの項目は、Eloquentにより自動的に保存時の日時を記録します。\n逆に、DBに入れたくない項目をリストするなら、つまりブラックリストを定義したいなら、$fillableの代わりに、$guardedを使用します。\nMember.php ... class Member extends Model { protected $table = \u0026#39;member\u0026#39;; protected $primaryKey = \u0026#39;member_id\u0026#39;; protected $guarded = [\u0026#39;member_id\u0026#39;, \u0026#39;active_flag\u0026#39;, \u0026#39;password\u0026#39;]; ... } 以上がマスアサイメントの使用の仕方で、意図的あるいは間違って入力フォームから、DBへ保存されるのを防いでくれます。\n$fillableや$guardedの目的を理解したところで、２つ問題。\nまず、\nactive_flagやpasswordなどマスアサインメントで相手にしない項目の値はどうやってDBに保存するのでしょう？\nこれは、通常のオブジェクトの値のアサインメントで行います。\n... public store(Request $request) { $member = new Member; $member-\u0026gt;active_flag = \u0026#39;Y\u0026#39;; //デフォルト $member-\u0026gt;password = bcrypt($request-\u0026gt;password);　//ハッシュ値に変換 $member-\u0026gt;fill($request-\u0026gt;all())-\u0026gt;save(); ... } ... 次に、 入力画面によりDBに入れたい項目が違う場合は、どう$fillableを設定？\n例えば、管理画面で会員の情報を編集する画面。そこでは、会員が有効か無効のフラッグ(active_flag)、そして会員に関するノート(memo)も付加情報としてDBに保存したいです。もちろん、裏で使用するのは、同じMemberのクラスなので、同じ$fillableは使えないですね。\n１つは、先の値のアサイメント使用する方法。\n$member-\u0026gt;active_flag = $request-\u0026gt;active_flag; $member-\u0026gt;memo = $request-\u0026gt;memo; $member-\u0026gt;fill($request-\u0026gt;all())-\u0026gt;save(); しかし、これではもっと項目が増えたら面倒です。\n$fillableを使用するのではなく、先のように$guardedをMemberで定義して、以下のようにコントローラにおいて、独自の$fillableを使用します。\n... public store(Request $request) { $fillable = [\u0026#39;email\u0026#39;, \u0026#39;name\u0026#39;]; $input = array_only($request-\u0026gt;all(), $fillable); $member = new Member; $member-\u0026gt;active_flag = \u0026#39;Y\u0026#39;; $member-\u0026gt;password = Hash::make($request-\u0026gt;password); $member-\u0026gt;fill($input)-\u0026gt;save(); ... } public edit(Member $member, Request $request) { $fillable = [\u0026#39;email\u0026#39;, \u0026#39;name\u0026#39;, \u0026#39;active_flag\u0026#39;, \u0026#39;memo\u0026#39;]; $input = array_only($request-\u0026gt;all(), $fillable); $member-\u0026gt;fill($input)-\u0026gt;save(); ... } ... array_onlyの関数は、Laravelのヘルパー関数です。\n","date":"2016-09-03T08:25:41+09:00","permalink":"https://www.larajapan.com/2016/09/03/%E5%85%A5%E5%8A%9B%E3%81%AE%E3%83%96%E3%83%A9%E3%83%83%E3%82%AF%E3%83%AA%E3%82%B9%E3%83%88%E3%81%A8%E3%83%9B%E3%83%AF%E3%82%A4%E3%83%88%E3%83%AA%E3%82%B9%E3%83%88/","title":"入力のブラックリストとホワイトリスト"},{"content":"ユーザーがアクセスするURLを理解して、必要な関数にマップするのがroutes.phpの基本的な仕事です。\nそれらのURLには、以下のようにいろいろな形があります。\nhttp://localhost/admin/login http://localhost/admin/product/156 http://localhost/admin/product/156/edit さて、上の例の156の数字は、DBテーブルのproductの主キーの値なのですが、Laravelはこの値をどのようにコントローラに取り込むのでしょう？\nまず、前回のroutes.phpの設定を見てみましょう。\nRoute::resource(\u0026#39;product\u0026#39;, \u0026#39;ProductController\u0026#39;); は、\nphp artisan route:list\nの出力では以下のようなマップになります。\n+--------+-----------+------------------------------------+------------------------+-----------------------------------------------------------------------+-----------------+ | Domain | Method | URI | Name | Action | Middleware | +--------+-----------+------------------------------------+------------------------+-----------------------------------------------------------------------+-----------------+ | | POST | admin/product | admin.product.store | App\\Http\\Controllers\\Admin\\ProductController@store | web | | | GET|HEAD | admin/product | admin.product.index | App\\Http\\Controllers\\Admin\\ProductController@index | web | | | GET|HEAD | admin/product/create | admin.product.create | App\\Http\\Controllers\\Admin\\ProductController@create | web | | | GET|HEAD | admin/product/{product} | admin.product.show | App\\Http\\Controllers\\Admin\\ProductController@show | web | | | DELETE | admin/product/{product} | admin.product.destroy | App\\Http\\Controllers\\Admin\\ProductController@destroy | web | | | PUT|PATCH | admin/product/{product} | admin.product.update | App\\Http\\Controllers\\Admin\\ProductController@update | web | | | GET|HEAD | admin/product/{product}/edit | admin.product.edit | App\\Http\\Controllers\\Admin\\ProductController@edit | web | +--------+-----------+------------------------------------+------------------------+-----------------------------------------------------------------------+-----------------+ URIの部分を見てください。\n例えば、7行目の\nadmin/product/{product}\nは、\nProductController@show\nにマップされています。\nそして156のIDは、この{product}の部分に対応します。\nadmin/product/{product}/edit\nも同じことです。\n対応するコントローラのメソッドを見ると、\nProductController.php namespace App\\Http\\Controllers; use Illuminate\\Http\\Request; use App\\Http\\Requests; use App\\Models\\Product; class ProductController extends Controller { /** * 表示画面 * * @param \\App\\Models\\Product $product * @return \\Illuminate\\Http\\Response */ public function show(Product $product) { return $product-\u0026gt;name; //画面に商品名を表示 } ... showのパラメータが、数字でなくモデルのProductのタイプになっていますね。 これは、Laravelが自動的に、IDの数字をもとに、\n$product = Product::find(156); のようなモデルバインディング（紐づけ）操作を行って、Eloquentのオブジェクトを生成して、メソッドの中で使用できるようにしてくれているのです。便利ですね。\nさて、product_id = 156に対応するDBレコードがない場合はどうなるのでしょう？\nSorry, the page you are looking for could not be found. 2/2 NotFoundHttpException in Handler.php line 102: No query results for model [App\\Product]. 1/2 ModelNotFoundException in Builder.php line 290: No query results for model [App\\Product]. 「検索結果が空」の404エラーとなります。\n今度は、上のshowの関数のパラメータ名を以下のように変えたとしたら、どうなるのでしょう？\npublic function show(Product $a_product) { return $a_product_name;//画面は空 } この場合、$a_project_nameにはDBレコードが入らず、単にProjectの新規オブジェクトとなり、画面には何も表示されません。\nつまり、パラメータ名は、{product}とまったく同じ名前である必要があるということです。ミススペルに気をつけましょう。\nさて、今度は、product_idではなく、例えば、skuという商品番号の項目の値でレコードを引っ張ってきたいときはどうするのでしょう？　つまり、\nhttp://localhost/admin/product/ABCDE\nでアクセスしたい。そこでは、ABCDがskuの値とします。\nその場合は、Productのモデルの定義で、\nProduct.php namespace App; use Illuminate\\Database\\Eloquent\\Model; class Product extends Model { protected $table = \u0026#39;product\u0026#39;; protected $primaryKey = \u0026#39;product_id\u0026#39;; public function getRouteKeyName() { return \u0026#39;sku\u0026#39;; } ... } のように、getRouteKeyNameの関数を作成して、skuをリターンすれば、product_idの代わりに以下のようにオブジェクトを作成してくれます。\n$product = Product::where(\u0026#39;sku\u0026#39;, \u0026#39;ABCD\u0026#39;)-\u0026gt;first(); もちろん、skuは、productのDBテーブルで、主キーと同様に１つのレコードを特定するためにユニークなキーを持つ必要あります。\n最後に、\nhttp://localhost/admin/product/156?print=Y の場合、printの値はどう取ってくるのでしょう？\nこれはshowメソッドを以下のように変更して、\nProductController.php ... /** * 表示画面 * * @param \\App\\Models\\Product $product * @param \\Illuminate\\Http\\Request * @return \\Illuminate\\Http\\Response */ public function show(Product $product, Request $request) { $print = $request-\u0026gt;input(\u0026#39;print\u0026#39;); return $print; } ... printの値の取得が可能です。ここ、関数のパラメータの順番はURLでの順番とは関係ありません。逆でも同じ結果となります。先に説明したように、{product}の名前と一致する変数名でLaravelは判断するからです。\n","date":"2016-08-26T12:22:41+09:00","permalink":"https://www.larajapan.com/2016/08/26/routes%E3%82%92%E4%BD%BF%E3%81%84%E3%81%93%E3%81%AA%E3%81%99%EF%BC%88%EF%BC%95%EF%BC%89%E3%83%A2%E3%83%87%E3%83%AB%E3%81%A8%E3%81%AE%E3%83%90%E3%82%A4%E3%83%B3%E3%83%87%E3%82%A3%E3%83%B3%E3%82%B0/","title":"routesを使いこなす（５）モデルとのバインディング"},{"content":"Laravelには最初から利用可能なバリデーションルールが多数存在していますが、そのいくつかは実用にならなかったり、自分好みに挙動を変えたりしたいこともあります。\nCustomValidator クラスは標準 Validator を継承していますので、これらを置き換えて上書きすることが可能です。\nvendor/laravel/framework/src/Illuminate/Validation/Validator.php\nここから関数宣言をコピーして、前回作成した CustomValidator クラスのファイルに貼り付けて編集します。引数はそれぞれの関数によって異なりますし、宣言が public ではなく protected であることに注意してください。\nemail 標準の email バリデーションは実在してるメールアドレスがエラー判定になることがあります。これを解決するために正規表現による判定に置き換えてみたものです。\n/** * email * * @param string $attribute * @param string $value * @return true */ protected function validateEmail($attribute, $value) { return (preg_match(\u0026#34;/^([*+!.\u0026amp;#$|\\\u0026#39;\\\\%\\/0-9a-z^_`{}=?~:-]+)@(([0-9a-z-]+\\.)+[0-9a-z]{2,10})$/i\u0026#34;, $value)); } 管理画面では、メール送信者の指定などで名前付きメールアドレスの入力を容認する場面もあります。ついでに email_with_name を作成しておきましょう。\nクラスメソッドとして関数を書いていますので、内部で他のバリデーションルールを呼び出して使用することができます。 ここでは先に上書きした email を使用するので $this-\u0026gt;validateEmail() で呼び出していますが、標準ルールを呼び出すなら parent::validateEmail() となります。\n/** * email_with_name * * @param string $attribute * @param string $value * @return true */ public function validateEmailWithName($attribute, $value) { if (preg_match(\u0026#39;/\u0026lt;(.*)\u0026gt;$/\u0026#39;, $value, $m) { $value = $m[1]; } return $this-\u0026gt;validateEmail($attribute, $value); } alpha, alpha_dash, alpha_num これらアルファベット系の標準バリデーションは、全角英数字の文字コードを通します。\n半角のasciiコードだけの入力許可に絞るため、これらも正規表現によるバリデーションルールに置き換えます。\n/** * alpah * * @param string $attribute * @param string $value * @return true */ protected function validateAlpha($attribute, $value) { return (preg_match(\u0026#34;/^[a-z]+$/i\u0026#34;, $value)); } /** * alpah_dash * * @param string $attribute * @param string $value * @return true */ protected function validateAlphaDash($attribute, $value) { return (preg_match(\u0026#34;/^[a-z0-9_-]+$/i\u0026#34;, $value)); } /** * alpah_num * * @param string $attribute * @param string $value * @return true */ protected function validateAlphaNum($attribute, $value) { return (preg_match(\u0026#34;/^[a-z0-9]+$/i\u0026#34;, $value)); } ただし、ユーザが入力した全角英数に対してただエラーを返すようなユーザーインターフェースは褒められたものではありませんね。\n入力を受け付けてからphpで半角に変換するという方法もありますが、私たちはjQueryスクリプトで入力前に変換する方法を選択しています。\njQueryスクリプトの例:\nvar toHankaku = function (strVal){ // 半角変換 var halfVal = strVal.replace(/[！-～]/g, function(tmpStr) { // 文字コードをシフト return String.fromCharCode(tmpStr.charCodeAt(0) - 0xFEE0); }); // 文字コードシフトで対応できない文字の変換 halfVal = halfVal.replace(/”/g, \u0026#34;\\\u0026#34;\u0026#34;) .replace(/[ｰー―－‐]/, \u0026#34;-\u0026#34;) .replace(/’/g, \u0026#34;\u0026#39;\u0026#34;) .replace(/‘/g, \u0026#34;`\u0026#34;) .replace(/￥/g, \u0026#34;\\\\\u0026#34;) .replace(/　/g, \u0026#34; \u0026#34;) .replace(/?/g, \u0026#34;~\u0026#34;); return halfVal; }; $(\u0026#34;input.en\u0026#34;).change(function() { var $this = $(this); $this.val(toHankaku($this.val())); }); ","date":"2016-08-15T10:27:51+09:00","permalink":"https://www.larajapan.com/2016/08/15/%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%A0%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3-3-%E6%97%A2%E5%AD%98%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3/","title":"バリデーション (3) 既存バリデーションの置き換え"},{"content":"カスタムバリデーションのクラスを追加するには、サービスプロバイダーで次のようにクラスを登録します。\n追加するクラスの名前や位置はどのようなものでもかまいせん。ここでは、ディレクトリパス app/Services に CustomVaidator.phpを作成し、AppServiceProvider に登録することにします。\napp/Providers/AppServiceProvider.php\n\u0026lt;?php namespace App\\Providers; use Validator; use Illuminate\\Support\\ServiceProvider; use App\\Services\\CustomValidator; class AppServiceProvider extends ServiceProvider { /** * Bootstrap any application services. * * @return void */ public function boot() { Validator::resolver(function($translator, $data, $rules, $messages) { return new CustomValidator($translator, $data, $rules, $messages); }); } /** * Register any application services. * * @return void */ public function register() { // } } 次に、標準の Validator を継承する CustomValidator を作成します。 前回 AppServiceProvier.php に直接登録したバリデーションルールを、こちらに移します。\napp/Service/CustomValidator.php\n\u0026lt;?php namespace App\\Services; class CustomValidator extends \\Illuminate\\Validation\\Validator { /** * kana * * @param string $attribute * @param string $value * @return bool */ public function validateKana($attribute, $value) { // かな、半角空白、全角空白、全角記号を許可 return preg_match(\u0026#34;/^[ぁ-んー ！-＠［-｀｛-～]+$/u\u0026#34;, $value); } } こうして専用のバリデーションクラスを作成すると、それぞれのバリデーションルールは通常のメソッドとして実行できるようになり、ユニットテストの作成も容易になります。\n$bool = CustomValidator::validateKana(\u0026#39;kana\u0026#39;, $request-\u0026gt;get(\u0026#39;kana\u0026#39;)); 前回との重複になりますが、エラーメッセージをバリデーション言語ファイルの「Validation Language Lines」グループに追加します。\n\u0026#39;unique\u0026#39; =\u0026gt; \u0026#39;:attribute に指定された値はすでに存在しています\u0026#39;, \u0026#39;url\u0026#39; =\u0026gt; \u0026#39;:attribute のフォーマットが正しくありません\u0026#39;, // カスタムバリデーション \u0026#39;kana\u0026#39; =\u0026gt; \u0026#39;:attribute は全角のひらがなで入力してください\u0026#39;, これでふりがなバリデーションをどこでも利用できるようになりました。\n\u0026#39;name_kana\u0026#39; =\u0026gt; \u0026#39;required|kana\u0026#39;, 次回から、このクラスにいろいろなルールを登録していきましょう。\nところで、ふりがなに「カタカナ」を強要するサイト設計が多いことに苛立つのは私だけでしょうか？ 無駄にカタカナを入力するとIMEの辞書学習が汚れ、その後の漢字変換のリズムが崩れるのです。名簿などの入力作業をすると誰でも体験できるでしょう。 カタカナルールは昔ながらの紙文化の遺物なのでしょう。手書きならばそのほうが識別しやすいに違いないですが、電子データにその配慮は必要ありませんよね。 ","date":"2016-08-14T11:35:17+09:00","permalink":"https://www.larajapan.com/2016/08/14/%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%A0%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3-2-customvalidator%E3%82%92%E8%BF%BD%E5%8A%A0/","title":"バリデーション (2) CustomValidatorを追加"},{"content":"Laravelでは、routeに名前を付けることができます。いったいそれがどうした？と思いますが、これができることで便利なことが増えます。\nまず、routeの名前の付け方から、\nRoute::get(\u0026#39;admin/ranking/download\u0026#39;, \u0026#39;RankingController@download\u0026#39;); とroutes.phpに指定して、\nphp artisan route:list\nを実行すると、\n+--------+-----------+-------------------------+------------------------+-------------------------------------------------------+ | Domain | Method | URI | Name | Action | +--------+-----------+-------------------------+------------------------+-------------------------------------------------------+ | | GET|HEAD | admin/ranking/download | | App\\Http\\Controllers\\Admin\\RankingController@download | を出力します。3列名のNameの列に注目してください。\n今は空ですね。\nしかし、\nRoute::get(\u0026#39;admin/ranking/download\u0026#39;, \u0026#39;RankingController@download\u0026#39;)-\u0026gt;name(\u0026#39;admin.ranking.download\u0026#39;); とすると、\n+--------+-----------+-------------------------+------------------------+-------------------------------------------------------+ | Domain | Method | URI | Name | Action | +--------+-----------+-------------------------+------------------------+-------------------------------------------------------+ | | GET|HEAD | admin/ranking/download | admin.ranking.download | App\\Http\\Controllers\\Admin\\RankingController@download | admin.ranking.downloadと名前が付きます。\n現在は、URIのスラッシュ(/)をピリオド(.)に置き換えたフォーマットですが、admin:ranking_downloadでも、admin::rankingdownloadでもOKです。\nさて、routeにresourceを使用するときはどうなるのでしょう？\n例えば、\nRoute::resource(\u0026#39;product\u0026#39;, \u0026#39;ProductController\u0026#39;); は、\n+--------+-----------+------------------------------------+------------------------+-----------------------------------------------------------------------+-----------------+ | Domain | Method | URI | Name | Action | Middleware | +--------+-----------+------------------------------------+------------------------+-----------------------------------------------------------------------+-----------------+ | | POST | admin/product | admin.product.store | App\\Http\\Controllers\\Admin\\ProductController@store | web | | | GET|HEAD | admin/product | admin.product.index | App\\Http\\Controllers\\Admin\\ProductController@index | web | | | GET|HEAD | admin/product/create | admin.product.create | App\\Http\\Controllers\\Admin\\ProductController@create | web | | | GET|HEAD | admin/product/{product} | admin.product.show | App\\Http\\Controllers\\Admin\\ProductController@show | web | | | DELETE | admin/product/{product} | admin.product.destroy | App\\Http\\Controllers\\Admin\\ProductController@destroy | web | | | PUT|PATCH | admin/product/{product} | admin.product.update | App\\Http\\Controllers\\Admin\\ProductController@update | web | | | GET|HEAD | admin/product/{product}/edit | admin.product.edit | App\\Http\\Controllers\\Admin\\ProductController@edit | web | +--------+-----------+------------------------------------+------------------------+-----------------------------------------------------------------------+-----------------+ と自動で名前を付けてくれます。\nrouteに名前が付いたところで、どう使用するか見てみましょう。\necho route(\u0026#39;admin.ranking.download\u0026#39;); は、\nhttp://localhost/admin/ranking/download となります。もちろん、localhostはライブ環境では実際のドメイン名となります。\nということは、\necho url(\u0026#39;admin/ranking/download\u0026#39;); と同じになります。\n引数があるときは、\necho route(\u0026#39;admin.product.edit\u0026#39;, 12); echo url(\u0026#39;admin/product\u0026#39;, [12, \u0026#39;edit\u0026#39;]); の出力は、両方とも、\nhttp://localhost/admin/product/12/edit となりますが、名前付きの方がわかりやすいですね。\nこのroute()は、\nreturn redirect()-\u0026gt;route(\u0026#39;admin.product.show\u0026#39;, 12); のようにリダイレクトや、\n{!! Form::open([\u0026#39;route\u0026#39; =\u0026gt; [\u0026#39;admin.product.store\u0026#39;], \u0026#39;method\u0026#39; =\u0026gt; \u0026#39;post\u0026#39;]) !!} のようにテンプレートでも使用可能です。\nもちろん、名前付きのrouteをプログラムで使用するなら、そのrouteのURIを変えても何もプログラムで変更は要らないことになります。便利ですね。\nさらに、URIに比べて比較的扱いやすい文字列なので、名前付きのrouteに対してユーザーの権限をDBで定義すれば、例えば、ログインしたユーザーの役割により、特定のrouteを実行するかどうかの権限の判断にも使用可能です。将来、この連載で扱う予定のトピックです。\n","date":"2016-08-13T08:28:31+09:00","permalink":"https://www.larajapan.com/2016/08/13/routes%E3%82%92%E4%BD%BF%E3%81%84%E3%81%93%E3%81%AA%E3%81%99%EF%BC%88%EF%BC%94%EF%BC%89route%E3%82%92%E5%90%8D%E4%BB%98%E3%81%91%E3%82%8B/","title":"routesを使いこなす（４）routeを名付ける"},{"content":"開発しているプログラムの機能が増えてくると、必然的に定義するrouteの数が増えてきます。特に、マルチ認証ともなると、関わるプレイヤーの分だけで倍増する可能性があります。\n例えば、ECのプロジェクトで、会員と管理者がプレイヤーとすると、ログインだけでも2通りのURIが必要となり、routeの設定は以下のように４つ必要となります。\nroutes.php Route::group([\u0026#39;middleware\u0026#39; =\u0026gt; \u0026#39;web\u0026#39;], function () { Route::get(\u0026#39;user/login\u0026#39;, \u0026#39;User\\Auth\\AuthController@showLoginForm\u0026#39;); Route::post(\u0026#39;user/login\u0026#39;, \u0026#39;User\\Auth\\AuthController@login\u0026#39;); Route::get(\u0026#39;admin/login\u0026#39;, \u0026#39;Admin\\Auth\\AuthController@showLoginForm\u0026#39;); Route::post(\u0026#39;admin/login\u0026#39;, \u0026#39;Admin\\Auth\\AuthController@login\u0026#39;); }); 実際作成されるrouteは、\nphp artisan route:list\nの実行によると、\n+--------+----------+-------------+------+--------------------------------------------------------------+-----------------+ | Domain | Method | URI | Name | Action | Middleware | +--------+----------+-------------+------+--------------------------------------------------------------+-----------------+ | | POST | admin/login | | App\\Http\\Controllers\\Admin\\Auth\\AuthController@login | web,guest:admin | | | GET|HEAD | admin/login | | App\\Http\\Controllers\\Admin\\Auth\\AuthController@showLoginForm | web,guest:admin | | | POST | user/login | | App\\Http\\Controllers\\User\\Auth\\AuthController@login | web,guest | | | GET|HEAD | user/login | | App\\Http\\Controllers\\User\\Auth\\AuthController@showLoginForm | web,guest | +--------+----------+-------------+------+--------------------------------------------------------------+-----------------+ これに他のいくつもの機能を加えていくと、管理が大変になってきます。現在私が管理しているプロジェクトでは、管理画面だけで300近いrouteが存在します。そうなると、タイプミスも増えるでしょうし、一目見て理解もしにくいです。\n管理性を高める１つの方法は、prefixを使用してroutesのgroupを分割します。\n先の例では、userとadminのprefixを用いて、それぞれのプレイヤーに１つのgroupを作成します。\nRoute::group([\u0026#39;prefix\u0026#39; =\u0026gt; \u0026#39;user\u0026#39;, \u0026#39;namespace\u0026#39; =\u0026gt; \u0026#39;User\u0026#39;, \u0026#39;middleware\u0026#39; =\u0026gt; \u0026#39;web\u0026#39;], function () { Route::get(\u0026#39;login\u0026#39;, \u0026#39;Auth\\AuthController@showLoginForm\u0026#39;); Route::post(\u0026#39;login\u0026#39;, \u0026#39;Auth\\AuthController@login\u0026#39;); }); Route::group([\u0026#39;prefix\u0026#39; =\u0026gt; \u0026#39;admin\u0026#39;, \u0026#39;namespace\u0026#39; =\u0026gt; \u0026#39;Admin\u0026#39;, \u0026#39;middleware\u0026#39; =\u0026gt; \u0026#39;web\u0026#39;], function () { Route::get(\u0026#39;login\u0026#39;, \u0026#39;Auth\\AuthController@showLoginForm\u0026#39;); Route::post(\u0026#39;login\u0026#39;, \u0026#39;Auth\\AuthController@login\u0026#39;); }); 行数が少し増えたかもしれませんが、重複部分がなくなってすっきりしただけでなく、グループ分けで理解しやすいコードとなりましたね。さらに、namespaceを使用することで、コントローラのnamespaceも重複も省いています。\nさて、実際生成されるrouteはどうなのでしょう？この変更で変わってしまっては問題です。\nphp artisan route:list\nを実行すると、\n+--------+----------+-------------+------+--------------------------------------------------------------+-----------------+ | Domain | Method | URI | Name | Action | Middleware | +--------+----------+-------------+------+--------------------------------------------------------------+-----------------+ | | POST | admin/login | | App\\Http\\Controllers\\Admin\\Auth\\AuthController@login | web,guest:admin | | | GET|HEAD | admin/login | | App\\Http\\Controllers\\Admin\\Auth\\AuthController@showLoginForm | web,guest:admin | | | POST | user/login | | App\\Http\\Controllers\\User\\Auth\\AuthController@login | web,guest | | | GET|HEAD | user/login | | App\\Http\\Controllers\\User\\Auth\\AuthController@showLoginForm | web,guest | +--------+----------+-------------+------+--------------------------------------------------------------+-----------------+ まったく先と同じです。\n","date":"2016-08-03T23:37:08+09:00","permalink":"https://www.larajapan.com/2016/08/03/routes%E3%82%92%E4%BD%BF%E3%81%84%E3%81%93%E3%81%AA%E3%81%99%EF%BC%88%EF%BC%93%EF%BC%89prefix%E3%81%A7group%E3%82%92%E5%88%86%E5%89%B2/","title":"routesを使いこなす（３）prefixでgroupを分割"},{"content":"resourceを使い始めて、まず思うのは、いつもいつも index, store, create, edit, update, destroyの一式が必要というわけではないことです。\n例えば、以下のようなレポートタイプのコントローラなら、\nindexだけで十分なので、以下のようにonlyを使い他のrouteを無効にします。\nRoute::resource(\u0026#39;ranking\u0026#39;, \u0026#39;RankingController.php\u0026#39;, [\u0026#39;only\u0026#39; =\u0026gt; [\u0026#39;index\u0026#39;]]); また、追加と編集だけで、削除が要らないコントローラなら、exceptを使い、index, store, create, edit, updateだけの使用を可能にします。\nRoute::resource(\u0026#39;no_destroy\u0026#39;, \u0026#39;NoDestroyController.php\u0026#39;, [\u0026#39;except\u0026#39; =\u0026gt; [\u0026#39;destroy\u0026#39;]]); 便利ですね。\nさらに、 index, store, create, edit, update, destroyだけでないrouteが必要なときもあります。\n例えば、先の「売上ランキング」のレポートに、結果をCSVでダウンロードしたいとします。\n今のところ、indexしか使用していないので、storeでも使う？\nしかし、名前がいまひとつ。理想は、ranking/downloadとしたいです。\nこのような場合は、\nRoute::get(\u0026#39;admin/ranking/download\u0026#39;, \u0026#39;RankingController@download\u0026#39;); Route::resource(\u0026#39;admin/ranking\u0026#39;, \u0026#39;RankingController\u0026#39;, [\u0026#39;only\u0026#39; =\u0026gt; [\u0026#39;index\u0026#39;]]); と、独自のrouteをRoute::resourceの前に追加します。\nphp artisan route:list\nの出力は、\n+--------+-----------+-------------------------+------------------------+-------------------------------------------------------+ | Domain | Method | URI | Name | Action | +--------+-----------+-------------------------+------------------------+-------------------------------------------------------+ | | GET|HEAD | admin/ranking | admin.ranking.index | App\\Http\\Controllers\\Admin\\RankingController@index | | | GET|HEAD | admin/ranking/download | | App\\Http\\Controllers\\Admin\\RankingController@download | ","date":"2016-07-28T13:39:45+09:00","permalink":"https://www.larajapan.com/2016/07/28/routes%E3%82%92%E4%BD%BF%E3%81%84%E3%81%93%E3%81%AA%E3%81%99%EF%BC%88%EF%BC%92%EF%BC%89resource%E3%82%92%E4%BD%BF%E3%81%84%E3%81%93%E3%81%AA%E3%81%99/","title":"routesを使いこなす（２）resourceを使いこなす"},{"content":"はじめまして。ブログ主筆khino氏と同じプロジェクトで仕事をしてます。 彼とは別テーマを平行して掲載しますので、これまで順番に読み進めていた方にはちょっと読みにくくなるかもしれませんが、ご容赦ください。\n私の最初のテーマはカスタムバリデーションルールです。 このシリーズでは、Validatorファサードの基本的な使い方から始めて、より複雑なルールの定義の仕方や、laravel 5.2で追加された配列定義のカスタムバリデーションまで紹介する予定です。\nまずは、Validatorファサードのextendを利用する方法から。\nコントローラやモデルに依存しない一般的なルールの追加は、サービスプロバイダーの中で定義するのが一般です。\napp/Providers/AppServiceProvider.php\n\u0026lt;?php namespace App\\Providers; use Validator; use Illuminate\\Support\\ServiceProvider; class AppServiceProvider extends ServiceProvider { /** * Bootstrap any application services. * * @return void */ public function boot() { Validator::extend(\u0026#39;kana\u0026#39;, function($attribute, $value, $parameters, $validator) { // 半角空白、全角空白、全角記号、全角かなを許可 return preg_match(\u0026#34;/^[ぁ-んー ！-＠［-｀｛-～]+$/u\u0026#34;, $value); }); } /** * Register any application services. * * @return void */ public function register() { // } } 例は全角ひらがなだけを許可するバリデーションですが、見たとおり正規表現による判定を行っています。 カスタムバリデーションを使いこなす前に、正規表現を使いこなさなければなりませんが、それはまた別に勉強してくださいね。\nエラーメッセージはバリデーション言語ファイルに追加しておきましょう。追加するのは「Validation Language Lines」のグループの末尾です。\nresouces/lang/ja/validation.php に追加\n\u0026#39;unique\u0026#39; =\u0026gt; \u0026#39;:attribute に指定された値はすでに存在しています\u0026#39;, \u0026#39;url\u0026#39; =\u0026gt; \u0026#39;:attribute のフォーマットが正しくありません\u0026#39;, // カスタムバリデーション \u0026#39;kana\u0026#39; =\u0026gt; \u0026#39;:attribute は全角のひらがなで入力してください\u0026#39;, 「Custom Validation Language Lines」のグループは、属性名とルールの組み合わせでメッセージを独自のものにすると言う意味での「カスタム」です。\n\u0026#39;custom\u0026#39; =\u0026gt;[ \u0026#39;password\u0026#39; =\u0026gt; [ \u0026#39;alpha_num\u0026#39; =\u0026gt; \u0026#39;パスワードは10文字以上40文字以下の英数字で入力してください\u0026#39;, \u0026#39;between\u0026#39; =\u0026gt; \u0026#39;パスワードは10文字以上40文字以下の英数字で入力してください\u0026#39;, ], ], また、言語ファイル末尾の「Custom Validation Attributes」に属性名の訳を追加しておきましょう。メッセージ内の「:attribute」の部分に用いられるようになります。\n\u0026#39;attributes\u0026#39; =\u0026gt;[ \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;名前\u0026#39;, \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;メールアドレス\u0026#39;, ], また、モデルやコントローラに依存して、そこでしか使用しないバリデーションなら、次のように フォームリクエスト で定義することも可能です。\nしかし、この位置ではユニットテストが作りにくくなるので、次回で説明するようにカスタムバリデーションクラスを作成して、すべてのバリデーションを１つにまとめて管理するほうが良いように思います。\napp/Http/Requests/MemberRequest.php\n\u0026lt;?php namespace App\\Http\\Requests; use Validator; use App\\Http\\Requests\\Request; use App\\Member; class MemberRequest extends Request { public function authorize() { return true; } public function rules() { Validator::extend(\u0026#39;kana\u0026#39;, function($attribute, $value, $parameters, $validator) { // 半角空白、全角空白、全角記号、全角かなを許可 return preg_match(\u0026#34;/^[ぁ-んー ！-＠［-｀｛-～]+$/u\u0026#34;, $value); }); $member = Member::find($this-\u0026gt;member_id); switch($this-\u0026gt;method()) { case \u0026#39;POST\u0026#39;: return [ \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;required|email|unique:member,email\u0026#39;, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;required|min:6|max:20|confirmed\u0026#39;, \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;kana\u0026#39; =\u0026gt; \u0026#39;required|kana\u0026#39; ]; case \u0026#39;PUT\u0026#39;: case \u0026#39;PATCH\u0026#39;: return [ \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;required|email|unique:member,email,\u0026#39;.$member-\u0026gt;member_id.\u0026#39;,member_id\u0026#39;, \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;kana\u0026#39; =\u0026gt; \u0026#39;required|kana\u0026#39; ]; } } public function attributes() { return [ \u0026#39;email\u0026#39;\t=\u0026gt; \u0026#39;メールアドレス\u0026#39;, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;パスワード\u0026#39;, \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;名前\u0026#39;, \u0026#39;kana\u0026#39; =\u0026gt; \u0026#39;ふりがな\u0026#39; ]; } public function messages() { return [ \u0026#39;kana\u0026#39;\t=\u0026gt; \u0026#39;:attribute は全角ひらがなで入力してください\u0026#39;, ]; } } ","date":"2016-07-19T10:28:50+09:00","permalink":"https://www.larajapan.com/2016/07/19/%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%A0%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3-1-validator%E3%83%95%E3%82%A1%E3%82%B5%E3%83%BC%E3%83%89%E3%81%AEextend/","title":"バリデーション（１）Validatorファサードのextend"},{"content":"私が開発・管理しているプロジェクトのひとつは、もともとは過去に人気があったCodeIgniterで書かれたもの。過去２年の間に、それをLaravelのバージョン４で書き直し、さらに更新して現在はLaravelのバージョン5.2となっています。\nそれゆえに、最初のLaravelを使っての書き換えは、Laravelを勉強しながらの書き換えで、知らないことが多く、Route::controllerを多用していました。\nコントローラは以下のように作成して、\nProductController.php class ProductController extends Controller { ... public function getAdd() {...} public function postAdd() {...} public function getEdit(Product $product) {..} public function postEdit(Product $product) {..} ... } メソッドには、getやpostのプリフィックスが必要です。 次は、routeを設定します。\nroutes.php ... Route::controller(\u0026#39;product\u0026#39;, \u0026#39;ProductController.php\u0026#39;); ... これを\nphp artisan route::list\nで実行して見るとこんな感じに出力されます。\n+--------+--------------------------------+------------------------------------------------------------+-----------------------+-----------------------------------------------------------------------+-----------------+ | Domain | Method | URI | Name | Action | Middleware | +--------+--------------------------------+------------------------------------------------------------+-----------------------+-----------------------------------------------------------------------+-----------------+ | | GET|HEAD | admin/product/add/{one?}/{two?}/{three?}/{four?}/{five?} | | App\\Http\\Controllers\\Admin\\ProductController@getAdd | web | | | POST | admin/product/add/{one?}/{two?}/{three?}/{four?}/{five?} | | App\\Http\\Controllers\\Admin\\ProductController@postAdd | web | | | GET|HEAD | admin/product/edit/{one?}/{two?}/{three?}/{four?}/{five?} | | App\\Http\\Controllers\\Admin\\ProductController@getEdit | web | | | POST | admin/product/edit/{one?}/{two?}/{three?}/{four?}/{five?} | | App\\Http\\Controllers\\Admin\\ProductController@postEdit | web | | | GET|HEAD|POST|PUT|PATCH|DELETE | admin/product/{_missing} | | App\\Http\\Controllers\\Admin\\ProductController@missingMethod | web | さて、ここでの問題は、デフォルトでは、URIが、\nadmin/product/edit/{one?}/{two?}/{three?}/{four?}/{five?}\nとなりgetEditの関数のパラメータがrouteに反映されないことです。\nうえは、\nadmin/product/{product}/edit\nとあるべきです。\nこれを正しくするには、\nroutes.php ... Route::model(\u0026#39;product\u0026#39;, \u0026#39;App\\Models\\Product\u0026#39;); ... Route::get(\u0026#39;product/add\u0026#39;, \u0026#39;ProductController@getAdd\u0026#39;); Route::post(\u0026#39;product/add\u0026#39;, \u0026#39;ProductController@getAdd\u0026#39;); Route::get(\u0026#39;product/{product}/edit\u0026#39;, \u0026#39;ProductController@getEdit\u0026#39;); Route::post(\u0026#39;product/{product}/edit\u0026#39;, \u0026#39;ProductController@getEdit\u0026#39;); ... と、やたらroutes.phpにおいての記述が多くなってしまいます。ウェブのアプリのほとんどは、DBでのレコードの閲覧、作成、編集、削除などの定型です。簡単にならないものでしょうか？\nそこで、Route::resourceの登場です。\nちなみに、Route::controllerは、バージョン5.1まではLaravelのマニュアルに説明がありましたが、現バージョン5.2ではなくなりました（コードではまた対応はしているようです）。\nもうRoute::controllerはリタイヤが近づいているということで、Route::resourceに切り替えるちょうど良い機会です。\nまず、以下のコマンドを実行してひな形を作成します。\nphp artisan make:controller ProductController \u0026ndash;resource\n作成されたファイルを編集して、\nProductController.php namespace App\\Http\\Controllers; use Illuminate\\Http\\Request; use App\\Http\\Requests; use App\\Models\\Product; class ProductController extends Controller { /** * 検索表示 * * @return \\Illuminate\\Http\\Response */ public function index() { // } /** * 新規作成画面 * * @return \\Illuminate\\Http\\Response */ public function create() { // } /** * DBレコード新規作成 * * @param \\Illuminate\\Http\\Request $request * @return \\Illuminate\\Http\\Response */ public function store(Request $request) { // } /** * 表示画面 * * @param \\App\\Models\\Product $product * @return \\Illuminate\\Http\\Response */ public function show(Product $product) { // } /** * 編集画面 * * @param \\App\\Models\\Product $product * @return \\Illuminate\\Http\\Response */ public function edit(Product $product) { // } /** * DBレコード編集 * * @param \\Illuminate\\Http\\Request $request * @param \\App\\Models\\Product $product * @return \\Illuminate\\Http\\Response */ public function update(Request $request, Product $product) { // } /** * DBレコード削除 * * @param \\App\\Models\\Product $product * @return \\Illuminate\\Http\\Response */ public function destroy(Product $product) { // } } Route::Controllerと比較すると、resourceのメソッドは、\ngetAdd → create postAdd → store getEdit → edit postEdit → update となります。\nroutesを設定すると、\nroutes.php ... Route::resource(\u0026#39;product\u0026#39;, \u0026#39;ProductController.php\u0026#39;); ... php artisan route:list\nの出力は、\n+--------+-----------+------------------------------------+-----------------------+-----------------------------------------------------------------------+-----------------+ | Domain | Method | URI | Name | Action | Middleware | +--------+-----------+------------------------------------+-----------------------+-----------------------------------------------------------------------+-----------------+ | | POST | admin/product | admin.product.store | App\\Http\\Controllers\\Admin\\ProductController@store | web | | | GET|HEAD | admin/product | admin.product.index | App\\Http\\Controllers\\Admin\\ProductController@index | web | | | GET|HEAD | admin/product/create | admin.product.create | App\\Http\\Controllers\\Admin\\ProductController@create | web | | | GET|HEAD | admin/product/{product} | admin.product.show | App\\Http\\Controllers\\Admin\\ProductController@show | web | | | PUT|PATCH | admin/product/{product} | admin.product.update | App\\Http\\Controllers\\Admin\\ProductController@update | web | | | DELETE | admin/product/{product} | admin.product.destroy | App\\Http\\Controllers\\Admin\\ProductController@destroy | web | | | GET|HEAD | admin/product/{product}/edit | admin.product.edit | App\\Http\\Controllers\\Admin\\ProductController@edit | web | | URIも自動で設定され、さらに名前付きのroute (named route)も設定されていますね。\nさて、admin.product.updateのPUTやPATCHはどうbladeのフォームで対応するのでしょう？HTMLののactionにはGETとPOST以外見たことありませんね。\nもちろん、簡単です。\n\u0026lt;form method=\u0026#34;POST\u0026#34; action=\u0026#34;admin/product/{{$product-\u0026gt;product_id}}\u0026#34;\u0026gt; {!! method_field(\u0026#39;put\u0026#39;) !!} . . . \u0026lt;/form\u0026gt; method_field()は、Laravelのヘルパー関数で、以下と同じです。\n\u0026lt;input type=\u0026#34;hidden\u0026#34; name=\u0026#34;_method\u0026#34; value=\u0026#34;PUT\u0026#34;\u0026gt; そして、以前紹介した、Laravel Collectiveのフォームを使用するなら、\n{!! Form::open([\u0026#39;route\u0026#39; =\u0026gt; [\u0026#39;admin.product.update\u0026#39;, $product-\u0026gt;product_id], \u0026#39;method\u0026#39; =\u0026gt; \u0026#39;put\u0026#39;]) !!} .. {!! Form::close !!} ","date":"2016-07-18T04:46:59+09:00","permalink":"https://www.larajapan.com/2016/07/18/routes%E3%82%92%E4%BD%BF%E3%81%84%E3%81%93%E3%81%AA%E3%81%99%EF%BC%88%EF%BC%91%EF%BC%89resource%E3%82%92%E4%BD%BF%E3%81%86/","title":"routesを使いこなす（１）resourceを使う"},{"content":"Laravel以前は、ほぼコードにSQL文を埋め込んでいたので、Eloquentよりクエリビルダーの方が馴染みあります。特に複数のDBテーブルをjoinした検索などには。\nしかし、各Modelにおいてリレーションを定義していると、それを使用しないのがもったいないように思えてきます。\nクエリビルダーでできることをEloquentではどうやるのか、興味ありありになってきました。\n前回と同じ検索、親子関係のテーブルをリレーションを使ってできるか考えてみましょう。\n前回と同様に、商品productと商品画像product_imageの親子関係、つまり、１対多の関係があるとします。 その関係をモデルで定義するには、以下。一応名前は、product_imagesと複数形にしてありますが、関数名のコンフリクトがないなら単数形でもOKです（要はプログラム内でどちらかに統一すること）。\nProduct.php namespace App; use Illuminate\\Database\\Eloquent\\Model; class Product extends Model { ... public function product_images() { return $this-\u0026gt;hasMany(\u0026#39;App\\ProductImage\u0026#39;); } } Laravelのマニュアルによると、hasが親子のテーブルをjoinしてくれるようです。\n$products = Product::has(\u0026#39;product_images\u0026#39;)-\u0026gt;get(); この実行は以下のようなSQL文となります。\nselect * from `product` where exists (select * from `product_image` where `product_image`.`product_id` = `product`.`product_id`) ちょっと通常のjoinとは違いますね。\nしかし、商品画像のレコードを１つでも持つ商品は、これ使えそうですね。そうなら、検索値で絞るとすると、\n$input = [ \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;商品名\u0026#39;, \u0026#39;mime\u0026#39; =\u0026gt; \u0026#39;image/gif\u0026#39; ]; $products = Product::has(\u0026#39;product_images\u0026#39;) -\u0026gt;where(\u0026#39;name\u0026#39;, $input[\u0026#39;name\u0026#39;]) -\u0026gt;where(\u0026#39;mime\u0026#39;, $input[\u0026#39;mime\u0026#39;]) -\u0026gt;get(); しかし、これを実行するとエラーとなります。なぜなら、whereは両方とも、Productに対しての条件となり、mimeの項目名が、productに存在しないというエラーとなります。\n正しくやるには、前回のクエリビルダーで使用したwhereInのようなものが必要です。\n$input = [ \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;商品名\u0026#39;, \u0026#39;mime\u0026#39; =\u0026gt; \u0026#39;image/gif\u0026#39; ]; $products = Product::where(\u0026#39;name\u0026#39;, $input[\u0026#39;name\u0026#39;]) -\u0026gt;whereHas(\u0026#39;product_images\u0026#39;, function($query) use($input) { $query-\u0026gt;where(\u0026#39;mime\u0026#39;, $input[\u0026#39;mime\u0026#39;]); })-\u0026gt;get(); 今回は、クエリビルダーと違って、whereInを使うではなく、whereHasとなります。また、whereHasゆえに、Product::hasは要らなくなります。ちょっと慣れが必要ですね。\nこれを実行すると、SQL文は以下のようになります。\nselect * from `product` where `name` = \u0026#39;Produt\u0026#39; and exists (select * from `product_image` where `product_image`.`product_id` = `product`.`product_id` and `mime` = \u0026#39;image/gif\u0026#39;) ","date":"2016-07-09T09:52:13+09:00","permalink":"https://www.larajapan.com/2016/07/09/%E8%A6%AA%E5%AD%90%E9%96%A2%E4%BF%82%E3%81%AE%E3%83%86%E3%83%BC%E3%83%96%E3%83%AB%E3%81%A7%E3%81%AE%E3%82%AF%E3%82%A8%E3%83%AA%E3%83%BC%E3%81%AE%E4%BD%9C%E6%88%90%EF%BC%88eloquent%E7%B7%A8%EF%BC%89/","title":"親子関係のテーブルでのクエリーの作成（Eloquent編）"},{"content":"いつもの例を使いますと、商品productと商品画像product_imageの親子関係、つまり、１対多の関係があるとして、これに対して検索画面を作成するとします。\n検索画面では、商品名だけでなく、商品画像のMIMEも検索項目として、検索できるようにします。つまり、親のテーブルの項目（商品名）でなく、子のテーブルの項目（MIME)も指定可能とします。\nこの場合、すぐに思いつくのは、以下のようにjoinして、その検索結果を表示です。\n//検索値 $input[] = [ \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;商品名\u0026#39;, \u0026#39;mime\u0026#39; =\u0026gt; \u0026#39;image/gif\u0026#39; ]; $products = DB::table(\u0026#39;product\u0026#39;) -\u0026gt;join(\u0026#39;product_image\u0026#39;, \u0026#39;product.product_id\u0026#39;, \u0026#39;=\u0026#39;, \u0026#39;product_image.product_id\u0026#39;) -\u0026gt;where(\u0026#39;product.name\u0026#39;, $input[\u0026#39;name\u0026#39;]) -\u0026gt;where(\u0026#39;product_image.mime\u0026#39;, $input[\u0026#39;mime\u0026#39;]) -\u0026gt;get(); しかし、これでは検索結果の各行は、商品画像のレコードとなってしまいます。１商品に対して、複数のGIF画像があるときは、複数分商品が表示されます。今回は、該当する商品のみを表示したいです。\nとなると、\n$products = DB::table(\u0026#39;product\u0026#39;) -\u0026gt;join(\u0026#39;product_image\u0026#39;, \u0026#39;product.product_id\u0026#39;, \u0026#39;=\u0026#39;, \u0026#39;product_image.product_id\u0026#39;) -\u0026gt;where(\u0026#39;product.name\u0026#39;, $input[\u0026#39;name\u0026#39;]) -\u0026gt;where(\u0026#39;product_image.mime\u0026#39;, $input[\u0026#39;mime\u0026#39;]) -\u0026gt;groupBy(\u0026#39;product.product_id\u0026#39;) -\u0026gt;get(); あるいは、joinでなくwhereInを使用して、\n$products = DB::table(\u0026#39;product\u0026#39;) -\u0026gt;where(\u0026#39;product.name\u0026#39;, $input[\u0026#39;name\u0026#39;]) -\u0026gt;whereIn(\u0026#39;product.product_id\u0026#39;, function($query) use($input) { $query-\u0026gt;from(\u0026#39;product_image\u0026#39;)-\u0026gt;select(\u0026#39;product_id\u0026#39;)-\u0026gt;where(\u0026#39;mime\u0026#39;, $input[\u0026#39;mime\u0026#39;]); }) -\u0026gt;get(); $query-\u0026gt;tableでなく、$query-\u0026gt;fromというところがちょいとややこしいですね。しかし、親のレコードだけを引き出すという点では、joinを使用するよりわかりやすいです。\n実効すると、このSQL文は以下のようになります。\nselect * from `product` where `name` = \u0026#39;商品名\u0026#39; and `product`.`product_id` in (select `product_id` from `product_image` where `mime` = \u0026#39;image/gif\u0026#39;) ","date":"2016-06-25T08:46:54+09:00","permalink":"https://www.larajapan.com/2016/06/25/%E8%A6%AA%E5%AD%90%E9%96%A2%E4%BF%82%E3%81%AE%E3%83%86%E3%83%BC%E3%83%96%E3%83%AB%E3%81%A7%E3%81%AE%E3%82%AF%E3%82%A8%E3%83%AA%E3%83%BC%E3%81%AE%E4%BD%9C%E6%88%90/","title":"親子関係のテーブルでのクエリーの作成（クエリビルダー編）"},{"content":"Eloquentのcount()の関数を使用して、DBのレコード数を数える作業はよく起こります。\n例えば、前回の画像の件では、商品productのレコード１に対して商品画像product_imageレコードが複数あるという、１対多の関係。そこでは、商品を削除するときに商品画像がないかをチェックする必要あります。画像のレコードがあるなら、削除を拒否あるいはユーザーに削除してよいか尋ねるということになります。\nこの場合は、商品画像があるかないかは、count()するのが一番。しかし、Eloquentではいろいろなカウントのコードの仕方があります。\n今回は、これを説明するために、コントローラに特別にメソッドを作成してみました。\nProductController.php namespace App\\Http\\Controllers\\User; use App\\Http\\Controllers\\Controller; use App\\Product; use App\\ProductImage; class ProductController extends Controller { public function getCount(Product $product) { $count1 = ProductImage::where(\u0026#39;product_id\u0026#39;, $product-\u0026gt;product_id)-\u0026gt;get()-\u0026gt;count(); $count2 = ProductImage::where(\u0026#39;product_id\u0026#39;, $product-\u0026gt;product_id)-\u0026gt;count(); $count3 = $product-\u0026gt;product_images-\u0026gt;count(); $count4 = $product-\u0026gt;product_images()-\u0026gt;count(); return sprintf(\u0026#34;\u0026lt;pre\u0026gt;count1 = %d\\ncount2 = %d\\ncount3 = %d\\ncount4 = %d\\n\u0026lt;/pre\u0026gt;\u0026#34;, $count1, $count2, $count3, $count4); } } 最初の、$count1は、whereでproduct_imageのレコードを絞ってgetして、それらのレコードをcount()します。しかし、getしたのはIlluminate\\Database\\Eloquent\\Collectionのオブジェクトであり、取得したレコードのデータすべてが入っています。２，３のレコード数なら問題ないけれど、１０００とかあれば、それだけのデータがメモリーに入るわけで、単にレコード数が必要なのにとんでもない浪費です。\nそれに比べて次の、$count2は、SQLクエリのCOUNT(*)を使用するので、取得するのは、まさに１つのカウント数だです。$count1とは大きな違いです。\nさて、Productのモデルには、\nProduct.php namespace App; use Illuminate\\Database\\Eloquent\\Model; class Product extends Model { ... public function product_images() { return $this-\u0026gt;hasMany(\u0026#39;App\\ProductImage\u0026#39;); } } ProductImageとのリレーションが定義されています。\nこれを利用したのが、先の$count3です。コードすっきりしましたね。しかし、ここ注意してください。この取得のしかたは、先の$count1とまったく同じなのです。つまり、必要なレコードをすべて含んだCollectionを作成してから、それをカウント。\nこれを正しくクエリで実行してもらうのが、$count4です。ちょっとした違いですね。product_images-\u0026gt;count()かproduct_images()-\u0026gt;count()か。\n実際、これらの実行がどうなっているかは、実行したクエリを見ればわかります。以下は、Debugbarの結果です。\nカウント数は皆同じですが、２と４では、select count(*)ですが、１と３は、select *となっていますね。\n","date":"2016-06-19T03:56:11+09:00","permalink":"https://www.larajapan.com/2016/06/19/eloquent%E3%81%A7%E3%82%AB%E3%82%A6%E3%83%B3%E3%83%88%E3%81%99%E3%82%8B%E3%81%A8%E3%81%8D%E3%81%AE%E6%B3%A8%E6%84%8F/","title":"Eloquentでカウントするときの注意"},{"content":"以前、マルチ認証の説明で以下のような、routes.phpを掲載しました（ユーザー認証（１０）Laravel 5.2 マルチ認証）。\nRoute::group([\u0026#39;middleware\u0026#39; =\u0026gt; \u0026#39;guest:users\u0026#39;], function() { Route::get(\u0026#39;login\u0026#39;, \u0026#39;user\\AuthController@getLogin\u0026#39;); Route::post(\u0026#39;login\u0026#39;, \u0026#39;user\\AuthController@postLogin\u0026#39;); Route::get(\u0026#39;signup\u0026#39;, \u0026#39;user\\SignupController@getSignup\u0026#39;); Route::post(\u0026#39;signup\u0026#39;, \u0026#39;user\\SignupController@postSignup\u0026#39;); Route::get(\u0026#39;password/email\u0026#39;, \u0026#39;user\\PasswordController@getEmail\u0026#39;); Route::post(\u0026#39;password/email\u0026#39;, \u0026#39;user\\PasswordController@postEmail\u0026#39;); Route::get(\u0026#39;password/reset/{token}\u0026#39;, \u0026#39;user\\PasswordController@getReset\u0026#39;); Route::post(\u0026#39;password/reset\u0026#39;, \u0026#39;user\\PasswordController@postReset\u0026#39;); }); ここ、user\\AuthControllerとか、user\\SignupControllerとか、namespaceのuser\u0026lt;/code\u0026gt;がいつも繰り返されていて、面倒だなあと思いませんでしたか？\n賢くなるものです。最近、ここnamespaceを使用して、user\u0026lt;/code\u0026gt;を削除することが可能なこと見つけました。\nRoute::group([\u0026#39;middleware\u0026#39; =\u0026gt; \u0026#39;guest:users\u0026#39;, \u0026#39;namespace\u0026#39; =\u0026gt; \u0026#39;user\u0026#39;], function() { Route::get(\u0026#39;login\u0026#39;, \u0026#39;AuthController@getLogin\u0026#39;); Route::post(\u0026#39;login\u0026#39;, \u0026#39;AuthController@postLogin\u0026#39;); Route::get(\u0026#39;signup\u0026#39;, \u0026#39;SignupController@getSignup\u0026#39;); Route::post(\u0026#39;signup\u0026#39;, \u0026#39;SignupController@postSignup\u0026#39;); Route::get(\u0026#39;password/email\u0026#39;, \u0026#39;PasswordController@getEmail\u0026#39;); Route::post(\u0026#39;password/email\u0026#39;, \u0026#39;PasswordController@postEmail\u0026#39;); Route::get(\u0026#39;password/reset/{token}\u0026#39;, \u0026#39;PasswordController@getReset\u0026#39;); Route::post(\u0026#39;password/reset\u0026#39;, \u0026#39;PasswordController@postReset\u0026#39;); }); すっきりしましたね。\nまた、php artisan route:listの実行でrouteをチェックしても、変更の前後ではまったく変わりません。\n","date":"2016-06-13T08:37:02+09:00","permalink":"https://www.larajapan.com/2016/06/13/namespace%E3%82%92%E6%8C%87%E5%AE%9A%E3%81%97%E3%81%A6routes%E3%81%99%E3%81%A3%E3%81%8D%E3%82%8A/","title":"namespaceを指定してroutesすっきり"},{"content":"以前に紹介した、Debugbar。\n私には、もうなくてはならないものになりました。対象の画面で実行されたDBのクエリーはすべて見ることができるし、セッションの中身の値も確認できる。\nそして、もうひとつ、プログラムの中で自分が見たいという変数をdebug()のヘルパー関数で、以下のように使用すれば、\nclass UserController extends Controller { public function getUpload() { return view(\u0026#39;user/upload\u0026#39;); } public function postUpload(Request $request) { debug($request-\u0026gt;all()); $file = $request-\u0026gt;file(\u0026#39;file\u0026#39;); $filename = $file-\u0026gt;getClientOriginalName(); $request-\u0026gt;file(\u0026#39;file\u0026#39;)-\u0026gt;move(public_path(\u0026#39;images\u0026#39;), $filename); } } 以下の画面のように、バーで表示してくれます。\n行をクリックすれば、詳細を表示します。\n","date":"2016-06-06T03:25:24+09:00","permalink":"https://www.larajapan.com/2016/06/06/debugbar%E3%81%A7%E3%83%87%E3%83%90%E3%83%83%E3%82%B0/","title":"Debugbarで楽々デバッグ"},{"content":"前回は、画像をパブリックに表示する方法を説明しましたが、今回は画像をプライベートに表示する方法です。\nいくつか方法があります。\nまず、前回のようにアップロードをパブリックの場所に保存して、特定のユーザーだけに表示のためのURLを教える。\nしかし、DBから自動発行されるproduct_image_idを画像ファイル名に使用するなら、URLを操作することで他のファイルも見れてしまいます。\nそうなら、画像のURLをわかりにくいように変えて、他の画像のURLを予想しにくくすることも可能です。\n例えば、235.jpgとは見せずに、1f3870be274f6c49b3e31a0c6728957f.jpgにするとか。\nそれは、md5()を利用することで簡単に可能です。\npublic function filename() { $ext = \u0026#39;jpg\u0026#39;; switch($this-\u0026gt;mime) { case \u0026#39;image/jpeg\u0026#39;: case \u0026#39;image/jpg\u0026#39;: $ext = \u0026#34;jpg\u0026#34;; break; case \u0026#39;image/png\u0026#39;: $ext = \u0026#34;png\u0026#34;; break; case \u0026#39;image/gif\u0026#39;: $ext = \u0026#34;gif\u0026#34;; break; } return sprintf(\u0026#34;%d.%s\u0026#34;, md5($this-\u0026gt;product_image_id), $ext); } よりセキュアにするには、DBに保存するときに、uniqid()あるいは、openssl-random-pseudo-bytes()を使用してランダムな値を生成して、その値をファイル名として保存するとか。要するに、IDのように連続な番号とはならないので、容易にファイル名を予測できないようにすることです。\nしかし、究極は、ファイルをパブリックから見れない場所に保存して、それを表示する方法です。\n例えば、storage/images/product/1.jpgのように、パブリックから見れないstorageのディレクトリに画像をアップロードするようにして、見せるときには、ログインしたユーザーと関連ある画像だけを、そのユーザーに表示する。\nこの場合は、パブリックに保存されている画像と違い、固定のURLを通してウェブサーバーに画像の表示を任せることはできません。逆に、あたかもウェブサーバーが画像ファイルを読んでデータをストリームするという作業と同じことをプログラムで行います。header()を使用すれば、そう難しいことではありません。\nProductController.php namespace App\\Http\\Controllers\\User; use Illuminate\\Http\\Request; use Log; use App\\Http\\Requests; use App\\Http\\Controllers\\Controller; use App\\Product; use App\\ProductImage; class ProductController extends Controller { public function getImage(Product $product) { return view(\u0026#39;user/product_image\u0026#39;, compact(\u0026#39;product\u0026#39;)); } public function downloadImage(ProductImage $product_image) { //@TODO ここで、認証したユーザーに画像を表示していいかどうかをチェック。 //そうでないなら、空の画像を表示 $filename = $product_image-\u0026gt;filename(); header(\u0026#34;Content-type: $product_image-\u0026gt;mime name=$filename\u0026#34;); header(\u0026#34;Content-Disposition: attachment; filename=$filename\u0026#34;); header(\u0026#34;Content-Length: \u0026#34;.@filesize($product_image-\u0026gt;path)); header(\u0026#34;Expires: 0\u0026#34;); @readfile($product_image-\u0026gt;path); exit; } } 上で使用されるテンプレートは、\nuser/product_image.blade.php @extends(\u0026#39;user.layouts.app\u0026#39;) @section(\u0026#39;content\u0026#39;) \u0026lt;div class=\u0026#34;container\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;row\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;col-md-8 col-md-offset-2\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;panel panel-default\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;panel-heading\u0026#34;\u0026gt;アップロードしたファイルを表示\u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;panel-body\u0026#34;\u0026gt; \u0026lt;div\u0026gt; @foreach ($product-\u0026gt;product_images as $image) \u0026lt;img src=\u0026#34;{{ url(\u0026#39;/user/product_image\u0026#39;, $image-\u0026gt;product_image_id) }}\u0026#34;\u0026gt; @endforeach \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; @endsection のようになります。\nroutes.phpは、以下のように認証保護された中でコールされます。\nRoute::group([\u0026#39;prefix\u0026#39; =\u0026gt; \u0026#39;user\u0026#39;, \u0026#39;middleware\u0026#39; =\u0026gt; \u0026#39;web\u0026#39;], function () { Route::get(\u0026#39;login\u0026#39;, \u0026#39;User\\Auth\\AuthController@showLoginForm\u0026#39;); Route::post(\u0026#39;login\u0026#39;, \u0026#39;User\\Auth\\AuthController@login\u0026#39;); Route::get(\u0026#39;logout\u0026#39;, \u0026#39;User\\Auth\\AuthController@logout\u0026#39;); .. Route::group([\u0026#39;middleware\u0026#39; =\u0026gt; \u0026#39;auth:user\u0026#39; ], function () { Route::get(\u0026#39;home\u0026#39;, \u0026#39;User\\HomeController@index\u0026#39;); .. Route::get(\u0026#39;product/{product}/image\u0026#39;, \u0026#39;User\\ProductController@getImage\u0026#39;); Route::get(\u0026#39;product_image/{product_image}\u0026#39;, \u0026#39;User\\ProductController@downloadImage\u0026#39;); }); }); ","date":"2016-05-29T07:38:21+09:00","permalink":"https://www.larajapan.com/2016/05/29/%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AE%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%89%EF%BC%888%EF%BC%89%E3%83%97%E3%83%A9%E3%82%A4%E3%83%99%E3%83%BC%E3%83%88%E3%81%AB%E7%94%BB%E5%83%8F/","title":"ファイルのアップロード（8）プライベートに画像を表示"},{"content":"アップロードしてサーバーに保存した画像ファイルを表示するには、いくつか方法があります。\n前回のように、誰もが見れるパブリックな場所（public/images/product）にファイルを保存したなら、\nProductImage.php namespace App; use Illuminate\\Database\\Eloquent\\Model; class ProductImage extends Model { protected $table = \u0026#39;product_image\u0026#39;; protected $primaryKey = \u0026#39;product_image_id\u0026#39;; public $incrementing = true; public $timestamps = true; protected $fillable = [ \u0026#39;product_id\u0026#39;, \u0026#39;mime\u0026#39; ]; protected $baseUri = \u0026#39;images/product\u0026#39;; public function getUrlAttribute() { return url($this-\u0026gt;baseUri, $this-\u0026gt;filename()); } public function storeImage($file) { $file-\u0026gt;move(public_path($this-\u0026gt;baseUri), $this-\u0026gt;filename()); } public function filename() { $ext = \u0026#39;jpg\u0026#39;; switch($this-\u0026gt;mime) { case \u0026#39;image/jpeg\u0026#39;: case \u0026#39;image/jpg\u0026#39;: $ext = \u0026#34;jpg\u0026#34;; break; case \u0026#39;image/png\u0026#39;: $ext = \u0026#34;png\u0026#34;; break; case \u0026#39;image/gif\u0026#39;: $ext = \u0026#34;gif\u0026#34;; break; } return sprintf(\u0026#34;%d.%s\u0026#34;, $this-\u0026gt;product_image_id, $ext); } と、getUrlAttribute()のアクセサーを作成して、\nproduct_image.blade.php @extends(\u0026#39;user.layouts.app\u0026#39;) @section(\u0026#39;content\u0026#39;) \u0026lt;div class=\u0026#34;container\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;row\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;col-md-8 col-md-offset-2\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;panel panel-default\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;panel-heading\u0026#34;\u0026gt;File Upload\u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;panel-body\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;form-group{{ $errors-\u0026gt;has(\u0026#39;file\u0026#39;) ? \u0026#39; has-error\u0026#39; : \u0026#39;\u0026#39; }}\u0026#34;\u0026gt; \u0026lt;div\u0026gt; \u0026lt;form method=\u0026#34;POST\u0026#34; action=\u0026#34;/demo/public/admin/product/{{ $product-\u0026gt;product_id }}/image\u0026#34; class=\u0026#34;dropzone\u0026#34; id=\u0026#34;imageUpload\u0026#34; enctype=\u0026#34;multipart/form-data\u0026#34;\u0026gt; {{ csrf_field() }} \u0026lt;/form\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div\u0026gt; @foreach ($product-\u0026gt;product_images as $image)　// １商品に対して複数の画像を表示 \u0026lt;img src=\u0026#34;{!! $image-\u0026gt;url !!}\u0026#34;\u0026gt; @endforeach \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; @endsection @section(\u0026#39;script\u0026#39;) Dropzone.options.imageUpload = { dictDefaultMessage: \u0026#39;アップロードするファイルをここへドロップしてください\u0026#39;, maxFiles: 10, acceptedFiles: \u0026#39;.jpg,.jpeg,.gif,.png\u0026#39;, maxFilesize: 5, // 5 MB init: function () { this.on(\u0026#39;queuecomplete\u0026#39;, function () {　//　アップロードがすべて完了したら、画面を更新してアップロードした画像を表示 location.reload();　}); } } @endsection のように、$image-\u0026gt;urlとしてテンプレートでコールして、/demo/public/images/product/4.pngのような値を生成し画像をブラウザに表示します。\nまた、以下のEloquentのRelationshipを使用して、１商品に対して複数アップロードした画像を表示させています。\nnamespace App; use Illuminate\\Database\\Eloquent\\Model; class Product extends Model { protected $table = \u0026#39;product\u0026#39;; protected $primaryKey = \u0026#39;product_id\u0026#39;; public $incrementing = true; public $timestamps = true; protected $fillable = [ \u0026#39;name\u0026#39; ]; public function product_images() { return $this-\u0026gt;hasMany(\u0026#39;App\\ProductImage\u0026#39;); } } さて、以上は、管理者が管理画面からアップロードした画像を、同画面で表示する例ですが、ユーザー画面でも$image-\u0026gt;urlを使用すれば、誰にでも同じ画像を見せることが可能です。\nしかし、アップロードしたファイルを誰からも見えない場所に保存して、例えば、認証したユーザーしか見れないという制限が必要なら、どうしましょう？\n","date":"2016-05-22T03:34:31+09:00","permalink":"https://www.larajapan.com/2016/05/22/%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AE%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%89%EF%BC%88%EF%BC%97%EF%BC%89%E3%83%91%E3%83%96%E3%83%AA%E3%83%83%E3%82%AF%E3%81%AB%E7%94%BB%E5%83%8F/","title":"ファイルのアップロード（７）パブリックに画像を表示"},{"content":"画像のファイルのアップロードの基本を学んだところで、少し実践的なことを考えてみましょう。\n例えば、ECサイトなら、販売する商品画像を管理画面でアップロードします。しかし、重複となるかもしれないので、アップロードした画像のファイル名で保存することはできません。\nそこで、DBが自動発行する商品のIDあるいは商品番号を利用してファイル名を変えてサーバーに保存します。\n例えば、Nikeの靴.jpgというファイル名のファイルをアップするなら、その商品のIDが234なら、234.jpgとしてサーバーに保存します。\nもう１つ考慮必要なことは、アップする画像はいつも、JPEGの形式とは限らないことです。GIFかもしれませんし、PNGのフォーマットかもしれません。もちろん、手元でJPEGに変換してからアップもできますが、サーバーで違う画像フォーマットに対応できるならそれに越したことありません。\nこれらのフォーマットの違いの情報を対応するには、アップしたMIMEの情報あるいはファイルの拡張子をDBに保存する必要あります。\nまず、DBの設計から始めましょう。ここで必要なのは以下の２つのテーブル、\nproduct +------------+------------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +------------+------------------+------+-----+---------+----------------+ | product_id | int(10) unsigned | NO | PRI | NULL | auto_increment | | name | varchar(255) | NO | | NULL | | | created_at | timestamp | YES | | NULL | | | updated_at | timestamp | YES | | NULL | | +------------+------------------+------+-----+---------+----------------+ product_image +------------------+------------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +------------------+------------------+------+-----+---------+----------------+ | product_image_id | int(10) unsigned | NO | PRI | NULL | auto_increment | | product_id | int(11) | NO | MUL | NULL | | | mime | varchar(255) | NO | | NULL | | | created_at | timestamp | YES | | NULL | | | updated_at | timestamp | YES | | NULL | | +------------------+------------------+------+-----+---------+----------------+ 一応、productとproduct_imageのテーブルの関係は、１対多の関係となります。つまり、商品１に対して複数の画像を持つことが可能。\nmigrationを作成して、以下のように作成してテーブルを作成します。\nuse Illuminate\\Database\\Schema\\Blueprint; use Illuminate\\Database\\Migrations\\Migration; class CreateProductTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create(\u0026#39;product\u0026#39;, function (Blueprint $table) { $table-\u0026gt;increments(\u0026#39;product_id\u0026#39;); $table-\u0026gt;string(\u0026#39;name\u0026#39;); $table-\u0026gt;timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::drop(\u0026#39;product\u0026#39;); } } class CreateProductImageTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create(\u0026#39;product_image\u0026#39;, function (Blueprint $table) { $table-\u0026gt;increments(\u0026#39;product_image_id\u0026#39;); $table-\u0026gt;integer(\u0026#39;product_id\u0026#39;); $table-\u0026gt;string(\u0026#39;mime\u0026#39;); $table-\u0026gt;timestamps(); $table-\u0026gt;index(\u0026#39;product_id\u0026#39;); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::drop(\u0026#39;product_image\u0026#39;); } } 次は管理画面でのルートの作成。\n... Route::group([\u0026#39;prefix\u0026#39; =\u0026gt; \u0026#39;admin\u0026#39;, \u0026#39;middleware\u0026#39; =\u0026gt; \u0026#39;web\u0026#39;], function () { ... Route::get(\u0026#39;product/{product}/image\u0026#39;, \u0026#39;Admin\\ProductController@getImage\u0026#39;); Route::post(\u0026#39;product/{product}/image\u0026#39;, \u0026#39;Admin\\ProductController@postImage\u0026#39;); Route::resource(\u0026#39;product\u0026#39;, \u0026#39;Admin\\ProductController\u0026#39;); ... }); ... CRUD(Create, Read, Update, Delete)のオペレーションは、Route::resource(\u0026lsquo;product\u0026rsquo;, ..に任せて、画像に関しては、product/{product}/imageのURIを使用して、それらのメソッドはすべてProductControllerに収めます。今回は、前回と違って管理画面での作業のことに注意してください。\nそれらの定義は、\nProductController.php namespace App\\Http\\Controllers\\Admin; use Illuminate\\Http\\Request; use App\\Http\\Requests; use App\\Http\\Controllers\\Controller; use App\\Product; use App\\ProductImage; class ProductController extends Controller { public function getImage(Product $product) { return view(\u0026#39;admin/product_image\u0026#39;, compact(\u0026#39;product\u0026#39;)); } public function postImage(Request $request, Product $product) { $file = $request-\u0026gt;file(\u0026#39;file\u0026#39;); $image = new ProductImage; $image-\u0026gt;product_id = $product-\u0026gt;product_id; $image-\u0026gt;mime = $file-\u0026gt;getClientMimeType(); $image-\u0026gt;save(); // product_imageにレコードを作成 $image-\u0026gt;storeImage($file);　// アップロードしたファイルを移動 } } となり前回とほぼ同じようなコードです。しかし、前回と違って、product_imageのテーブルに、アップロードしたファイルのMIME情報を入れてレコードを作成しています。\nProductImage.php namespace App; use Illuminate\\Database\\Eloquent\\Model; class ProductImage extends Model { protected $table = \u0026#39;product_image\u0026#39;; protected $primaryKey = \u0026#39;product_image_id\u0026#39;; public $incrementing = true; public $timestamps = true; protected $fillable = [ \u0026#39;product_id\u0026#39;, \u0026#39;mime\u0026#39; ]; public function storeImage($file) { $file-\u0026gt;move(public_path(\u0026#39;images/product\u0026#39;), $this-\u0026gt;filename()); } public function filename() { $ext = \u0026#39;jpg\u0026#39;; switch($this-\u0026gt;mime) { case \u0026#39;image/jpeg\u0026#39;: case \u0026#39;image/jpg\u0026#39;: $ext = \u0026#34;jpg\u0026#34;; break; case \u0026#39;image/png\u0026#39;: $ext = \u0026#34;png\u0026#34;; break; case \u0026#39;image/gif\u0026#39;: $ext = \u0026#34;gif\u0026#34;; break; } return sprintf(\u0026#34;%d.%s\u0026#34;, $this-\u0026gt;product_image_id, $ext); } } filenameでは、レコードで自動発行されたproduct_image_idとMIMEをもとにした拡張子を合わせて移動先のファイル名を作成します。\nコントローラにより使用されるテンプレートも少し違います。\n@extends(\u0026#39;user.layouts.app\u0026#39;) @section(\u0026#39;content\u0026#39;) \u0026lt;div class=\u0026#34;container\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;row\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;col-md-8 col-md-offset-2\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;panel panel-default\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;panel-heading\u0026#34;\u0026gt;File Upload\u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;panel-body\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;form-group{{ $errors-\u0026gt;has(\u0026#39;file\u0026#39;) ? \u0026#39; has-error\u0026#39; : \u0026#39;\u0026#39; }}\u0026#34;\u0026gt; \u0026lt;div\u0026gt; \u0026lt;form method=\u0026#34;POST\u0026#34; action=\u0026#34;/demo/public/admin/product/{{ $product-\u0026gt;product_id }}/image\u0026#34; class=\u0026#34;dropzone\u0026#34; id=\u0026#34;imageUpload\u0026#34; enctype=\u0026#34;multipart/form-data\u0026#34;\u0026gt; {{ csrf_field() }} \u0026lt;/form\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; @endsection @section(\u0026#39;script\u0026#39;) Dropzone.options.imageUpload = { dictDefaultMessage: \u0026#39;アップロードするファイルをここへドロップしてください\u0026#39;, maxFiles: 10, acceptedFiles: \u0026#39;.jpg,.jpeg,.gif,.png\u0026#39;, maxFilesize: 5, // 5 MB } @endsection ファイルのアップロードの結果は以下のようにレコードの生成となります。\n+------------------+------------+------------+---------------------+---------------------+ | product_image_id | product_id | mime | created_at | updated_at | +------------------+------------+------------+---------------------+---------------------+ | 1 | 1 | image/jpeg | 2016-05-15 19:46:24 | 2016-05-15 19:46:24 | | 2 | 1 | image/gif | 2016-05-15 19:46:24 | 2016-05-15 19:46:24 | | 3 | 1 | image/png | 2016-05-15 19:46:24 | 2016-05-15 19:46:24 | +------------------+------------+------------+---------------------+---------------------+ ","date":"2016-05-17T00:02:38+09:00","permalink":"https://www.larajapan.com/2016/05/17/%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AE%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%89%EF%BC%88%EF%BC%96%EF%BC%89%E7%95%B0%E3%81%AA%E3%82%8B%E7%94%BB%E5%83%8F%E3%83%95%E3%82%A9%E3%83%BC/","title":"ファイルのアップロード（６）異なる画像フォーマットの対応"},{"content":"Dropzone.jsを使用する利点は、ファイルアップロードの途中経過を表示するようになりUIが良くなるだけでありません。１画面で複数の画像ファイルを一度にアップすることができます。\nもちろん、以下のように複数のファイルのアップロードは、Dropzone.jsを使用しなくても可能です。\n\u0026lt;form method=\u0026#34;POST\u0026#34; action=\u0026#34;/demo/public/user/upload\u0026#34; class=\u0026#34;form-horizontal\u0026#34; role=\u0026#34;form\u0026#34; enctype=\u0026#34;multipart/form-data\u0026#34;\u0026gt; {{ csrf_field() }} ... \u0026lt;input type=\u0026#34;file\u0026#34; name=\u0026#34;file1\u0026#34;\u0026gt; \u0026lt;input type=\u0026#34;file\u0026#34; name=\u0026#34;file2\u0026#34;\u0026gt; ... \u0026lt;/form\u0026gt; しかし、以下のポストで説明したように、\nファイルのサイズの制限・制限なしでも制限ある\nウェブサーバーによる制限により、１フォームでアップロードできるファイルの数は、すべてのファイルを足したファイルのサイズになります。\n例えば、\nphp.ini post_max_size = 2M upload_max_filesize = 1M の設定なら、１フォームでは、最大１Mのファイルが２つまでしかアップロード可能でありません。\nしかし、Dropzone.jsを使用すれば、ajaxを使用するので、それぞれのファイルアップロードは、１フォームで１つのファイルのアップロードとなり、この例では、1Mファイルをいくつでもアップロード可能となります。\nそして、この複数のファイルのアップロードに対応するプログラムの変更といえば、\nまず、フロントエンド、\nDropzone.jsでアップロード途中経過を表示\nとまったく同じです。変更はありません。\nアップロード時、複数のファイルを選択してドラッグするか、ファイルダイアログで複数のファイルを選択して実行すれば以下のようにアップされます。 バックエンドは、\nUserController.php namespace App\\Http\\Controllers\\User; use Illuminate\\Http\\Request; use App\\Http\\Requests; use App\\Http\\Controllers\\Controller; use Validator; class UserController extends Controller { ... public function getUpload() { return view(\u0026#39;user/upload\u0026#39;); } public function postUpload(Request $request) { $file = $request-\u0026gt;file(\u0026#39;file\u0026#39;); $filename = $file-\u0026gt;getClientOriginalName(); $file-\u0026gt;move(public_path(\u0026#39;images\u0026#39;), $filename) } } と、アップしたファイル名で、/public/imagesのディレクトリに保存するように変更します。\n同時にアップするファイル数を制限したいなら、\n\u0026lt;script type=\u0026#34;text/javascript\u0026#34;\u0026gt; Dropzone.options.imageUpload = { dictDefaultMessage: \u0026#39;アップロードするファイルをここへドロップしてください\u0026#39;, acceptedFiles: \u0026#39;.jpg, .jpeg\u0026#39;, maxFilesize: 5 // 5MBまで maxFiles: 2 // ファイルは２つまでアップロード可能 } \u0026lt;/script\u0026gt; と、Dropzone.jsの設定で、maxFilesを指定します。\nこのように３番目のファイルは、アップロード不可となります。\n","date":"2016-05-09T05:20:42+09:00","permalink":"https://www.larajapan.com/2016/05/09/%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AE%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%89%EF%BC%88%EF%BC%95%EF%BC%89%E8%A4%87%E6%95%B0%E3%81%AE%E7%94%BB%E5%83%8F%E3%83%95%E3%82%A1%E3%82%A4/","title":"ファイルのアップロード（５）複数の画像ファイルをアップロード"},{"content":"前回のDropzone.jsを使用したファイルアップロードのフロントエンドに対して、サーバーサイドのバックエンドをLaravelでプログラムします。つまり、アップロードされたファイルを受け取るプログラムです。\nまず、routes.phpにルートの追加となりますが、前回の設定を見てみますと、\nDropzoneを使用したファイルアップロード \u0026lt;div class=\u0026#34;form-group{{ $errors-\u0026gt;has(\u0026#39;file\u0026#39;) ? \u0026#39; has-error\u0026#39; : \u0026#39;\u0026#39; }}\u0026#34;\u0026gt; \u0026lt;div\u0026gt; \u0026lt;form method=\u0026#34;POST\u0026#34; action=\u0026#34;/demo/public/user/upload\u0026#34; class=\u0026#34;dropzone\u0026#34; id=\u0026#34;imageUpload\u0026#34; enctype=\u0026#34;multipart/form-data\u0026#34;\u0026gt; {{ csrf_field() }} \u0026lt;/form\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; のパラメータのactionで指定している/demo/public/user/uploadがルートとなります。\nとなると、routes.phpでは、\n... Route::get(\u0026#39;upload\u0026#39;, \u0026#39;User\\UserController@getUpload\u0026#39;); Route::post(\u0026#39;upload\u0026#39;, \u0026#39;User\\UserController@postUpload\u0026#39;); ... のようになります。getの方は前回の画面を表示するとして、postは、サーバーでアップされるファイルの処理となります。\nUserController.php namespace App\\Http\\Controllers\\User; use Illuminate\\Http\\Request; use App\\Http\\Requests; use App\\Http\\Controllers\\Controller; use Validator; class UserController extends Controller { ... public function getUpload() { return view(\u0026#39;user/upload\u0026#39;); } public function postUpload(Request $request) { ... $request-\u0026gt;file(\u0026#39;file\u0026#39;)-\u0026gt;move(public_path(\u0026#39;images\u0026#39;), \u0026#39;test.jpg\u0026#39;); } } ファイルをアップロードすると、phpは、/tmpのディレクトリに一時的なファイルを作成します。上の２９行目では、それをpublic/imagesのディレクトリにtest.jpgと命名してファイルを保存します。\n最後に、上のpostUpload()の関数の最後において、通常あるredirect()文がないことに気づきましたか？ この関数は、Dropzone.jsによりajaxでコールされるので、画面を更新するためのredirect()は要らないのです。\n","date":"2016-05-03T13:50:00+09:00","permalink":"https://www.larajapan.com/2016/05/03/%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AE%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%89%EF%BC%88%EF%BC%94%EF%BC%89%E3%83%90%E3%83%83%E3%82%AF%E3%82%A8%E3%83%B3%E3%83%89/","title":"ファイルのアップロード（４）バックエンド"},{"content":"今まで話したファイルアップロードは、基本でもっとも簡単にプログラムできるものです。\nしかし、アップロードするファイルのサイズが大きく、アップロードに時間がかかるようになると、送信ボタンを押してからじ～っと何も起きない画面を見ているのは、退屈でもありちょっと心配ですね。これ、ちゃんと動いているのかなと。\n要は、「アップロード中です」とか「８０％アップロード完了」したとか、しかもそれをビジュアルで伝えてくれれば最高です。それを行ってくれるのが今回紹介するツール、Dropzone.jsです。\n「アップロードするファイルをここへドラッグしてください」にファイルをドラッグすると、\nこのように、アップロードの画像ファイルのサムナイルの中にアップロードの途中経過をバーで表示してくれます。\nファイル完了時は、\n必要な設定は、\nhttps://github.com/enyo/dropzone/archive/master.zip\nよりダウンロードして解凍してから、assets/jsのディレクトリに入れて、画面のテンプレートを以下のように編集します。\nDropzoneを使用したファイルアップロード \u0026lt;div class=\u0026#34;form-group{{ $errors-\u0026gt;has(\u0026#39;file\u0026#39;) ? \u0026#39; has-error\u0026#39; : \u0026#39;\u0026#39; }}\u0026#34;\u0026gt; \u0026lt;div\u0026gt; \u0026lt;form method=\u0026#34;POST\u0026#34; action=\u0026#34;/demo/public/user/upload\u0026#34; class=\u0026#34;dropzone\u0026#34; id=\u0026#34;imageUpload\u0026#34; enctype=\u0026#34;multipart/form-data\u0026#34;\u0026gt; {{ csrf_field() }} \u0026lt;/form\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; 気づきましたか、 や がないことに。\nちなみにデフォルトのファイル変数名はfileですが、設定変更できます。\nそして、レイアウトのテンプレートを以下のように編集します。\n... \u0026lt;head\u0026gt; ... \u0026lt;link href=\u0026#34;{{ url(\u0026#39;assets/css/dropzone/dropzone.min.css\u0026#39;) }}\u0026#34; rel=\u0026#34;stylesheet\u0026#34; type=\u0026#34;text/css\u0026#34;\u0026gt; ... \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; ... \u0026lt;script src=\u0026#34;{{ url(\u0026#39;assets/js/dropzone/dropzone.min.js\u0026#39;) }}\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script type=\u0026#34;text/javascript\u0026#34;\u0026gt; Dropzone.options.imageUpload = { dictDefaultMessage: \u0026#39;アップロードするファイルをここへドロップしてください\u0026#39;, acceptedFiles: \u0026#39;.jpg, .jpeg\u0026#39;, maxFilesize: 5 // 5MBまで } \u0026lt;/script\u0026gt; ... 上の設定では、ファイルをドロップする場所のメッセージ、アップロードを許すMIMEあるいは拡張子（jpegのみ）、最大のファイルのサイズ（5MBまで）としています。他にもいろいろな設定があります。以下を参照にしてください。\nhttp://www.dropzonejs.com/#configuration\n将来は、これらの設定を使用した複雑な例を紹介する予定です。\n","date":"2016-04-17T08:01:28+09:00","permalink":"https://www.larajapan.com/2016/04/17/%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AE%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%89%EF%BC%88%EF%BC%93%EF%BC%89dropzone-js%E3%81%A7%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%89/","title":"ファイルのアップロード（３）Dropzone.jsでアップロード途中経過を表示"},{"content":"ファイルのアップロードと言っても、なんのフォーマットのファイルでもアップをしてよいというわけではありません。たいていは画像ファイルのアップロードになりますが、それでもGIFはアップしてもらいたくない、JPEGだけが欲しいとか。\nそのときは、バリデーションを使用してアップしたファイルのMIMEの情報をもとにプログラムで制限します。\nまず、jpegだけのファイルをOKとしましょう。\nUserController ... class UserController extends Controller { ... public function postUpload(Request $request) { $validator = Validator::make($request-\u0026gt;all(), [ \u0026#39;file\u0026#39; =\u0026gt; \u0026#39;required|max:10240|mimes:jpeg\u0026#39; ]); if ($validator-\u0026gt;fails()) { return back()-\u0026gt;withInput()-\u0026gt;withErrors($validator); } return redirect(\u0026#39;user/upload\u0026#39;); } } ここ、jpgでなくjpegであることに注意してください。ファイルの拡張子はたいてい、.jpgですが、ユーザーのマシンのOSが送信するMIMIEは、image/jpegです。\n複数の画像フォーマットを許すなら、\nUserController ... class UserController extends Controller { ... public function postUpload(Request $request) { $validator = Validator::make($request-\u0026gt;all(), [ \u0026#39;file\u0026#39; =\u0026gt; \u0026#39;required|max:10240|mimes:jpeg,gif,png\u0026#39; ]); if ($validator-\u0026gt;fails()) { return back()-\u0026gt;withInput()-\u0026gt;withErrors($validator); } return redirect(\u0026#39;user/upload\u0026#39;); } } と指定します。\n","date":"2016-04-02T09:05:53+09:00","permalink":"https://www.larajapan.com/2016/04/02/%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AE%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%89%EF%BC%88%EF%BC%92%EF%BC%89%E7%94%BB%E5%83%8F%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%A0%E3%81%91/","title":"ファイルのアップロード（２）画像ファイルだけをアップ"},{"content":"入力フォーム画面でのファイルのアップロードは、他のテキストの入力と違っていろいろなことを考慮する必要があり、開発はそう簡単ではありません。PHPのマニュアルでも、説明のためだけに一章を費やしています。\nもちろん、Laravelを使うことで開発はかなり簡単になるのだけれど、注意する点やアップしたファイルをどう使用するか．．．などたくさんのトピックあります。以下、いくつか興味あるトピックをリストしました。\nファイルのサイズの制限：制限なしでも制限ある ファイルのMIMEタイプの制限：画像ファイルだけをアップ ファイルのアップの進行状況表示\t：いつファイルのアップが終わるのかな ファイルのウィルスの検出：そんなこと可能かな アップしたファイルをメールで送信：届かないのはどうして アップしたファイルをダウンロード：MIMEからファイルの拡張子を作成 アップしたファイルを配信：Amazonのウェブサービスを使用する ファイルのサイズの制限：制限なしでも制限ある\nまず、ファイルアップロードのフォームをプログラムしましょう。\nフォーム \u0026lt;form method=\u0026#34;POST\u0026#34; action=\u0026#34;/demo/public/user/upload\u0026#34; class=\u0026#34;form-horizontal\u0026#34; role=\u0026#34;form\u0026#34; enctype=\u0026#34;multipart/form-data\u0026#34;\u0026gt; {{ csrf_field() }} \u0026lt;div class=\u0026#34;form-group{{ $errors-\u0026gt;has(\u0026#39;file\u0026#39;) ? \u0026#39; has-error\u0026#39; : \u0026#39;\u0026#39; }}\u0026#34;\u0026gt; \u0026lt;label class=\u0026#34;col-md-4 control-label\u0026#34;\u0026gt;File\u0026lt;/label\u0026gt; \u0026lt;div class=\u0026#34;col-md-6\u0026#34;\u0026gt; \u0026lt;input type=\u0026#34;file\u0026#34; name=\u0026#34;file\u0026#34;\u0026gt; @if ($errors-\u0026gt;has(\u0026#39;file\u0026#39;)) \u0026lt;span class=\u0026#34;help-block\u0026#34;\u0026gt; \u0026lt;strong\u0026gt;{{ $errors-\u0026gt;first(\u0026#39;file\u0026#39;) }}\u0026lt;/strong\u0026gt; \u0026lt;/span\u0026gt; @endif \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;form-group\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;col-md-6 col-md-offset-4\u0026#34;\u0026gt; \u0026lt;input class=\u0026#34;btn btn-primary\u0026#34; type=\u0026#34;submit\u0026#34; value=\u0026#34;Upload\u0026#34;\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/form\u0026gt; このフォームのコントローラで、アップするファイルの制限を課します。\nUserController namespace App\\Http\\Controllers\\User; use Illuminate\\Http\\Request; use App\\Http\\Requests; use App\\Http\\Controllers\\Controller; use App\\User; use Auth; use Validator; class UserController extends Controller { ... public function getUpload() { return view(\u0026#39;user/upload\u0026#39;); } public function postUpload(Request $request) { $validator = Validator::make($request-\u0026gt;all(), [ \u0026#39;file\u0026#39; =\u0026gt; \u0026#39;required|max:10240\u0026#39; ]); if ($validator-\u0026gt;fails()) { return back()-\u0026gt;withInput()-\u0026gt;withErrors($validator); } return redirect(\u0026#39;user/upload\u0026#39;); } } \u0026lsquo;file\u0026rsquo; =\u0026gt; \u0026lsquo;required|max:10240\u0026rsquo;ここ大事です。maxで指定する最大のファイルのサイズの単位は、バイトではなくキロバイト(KByte)です。つまり、10240は。10メガバイト(10M)となります。\nさて、これでファイルをアップロードしてみましょう。８MBのファイルをアップしてみます。しかし、送信ボタンを押した後、TokenMismatchExceptionのエラーとなってしまいます。\nどうしてアップできないのでしょう？\n制限しているのはプログラムだけではなく、サーバーのPHPの設定でも制限があるからなのです。Unix系のサーバーなら、/etc/php.iniにおいて以下の２か所を編集する必要あります。8Mを20Mとでも設定しましょう。\nphp.ini ... post_max_size = 8M upload_max_filesize = 8M ... これで、ウェブサーバーを再スタートしてみてください。\n１つ残る疑問は、どうしてエラーがPHPの制限のエラーでなくTokenMismatchExceptionのエラーなのでしょう。これは、制限を超えることにより、送信した入力の値が空となり、Laravelが期待するCSRF（クロスサイトリクエストフォージェリ）のトークンの値が入って来なかったからです。\n","date":"2016-03-26T05:29:40+09:00","permalink":"https://www.larajapan.com/2016/03/26/%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AE%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%89%EF%BC%88%EF%BC%91%EF%BC%89%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AE%E3%82%B5%E3%82%A4%E3%82%BA/","title":"ファイルのアップロード（１）ファイルのサイズの制限：制限なしでも制限ある"},{"content":"今回は人気のドロップダウンスクリプト、Select2を紹介します。\n通常のドロップダウンリストは、\n通常のドロップダウン \u0026lt;div class=\u0026#34;form-group{{ $errors-\u0026gt;has(\u0026#39;category\u0026#39;) ? \u0026#39; has-error\u0026#39; : \u0026#39;\u0026#39; }}\u0026#34;\u0026gt; \u0026lt;label class=\u0026#34;col-md-4 control-label\u0026#34;\u0026gt;分類\u0026lt;/label\u0026gt; \u0026lt;div class=\u0026#34;col-md-6\u0026#34;\u0026gt; {!! Form::select(\u0026#39;category\u0026#39;, [\u0026#39;犬\u0026#39;, \u0026#39;猫\u0026#39;, \u0026#39;猿\u0026#39;], null, [\u0026#39;class\u0026#39; =\u0026gt; \u0026#39;form-control\u0026#39;]) !!} \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; とテンプレートで指定すると、\nと表示。\nこれに、\n複数選択 \u0026lt;div class=\u0026#34;form-group{{ $errors-\u0026gt;has(\u0026#39;category\u0026#39;) ? \u0026#39; has-error\u0026#39; : \u0026#39;\u0026#39; }}\u0026#34;\u0026gt; \u0026lt;label class=\u0026#34;col-md-4 control-label\u0026#34;\u0026gt;分類\u0026lt;/label\u0026gt; \u0026lt;div class=\u0026#34;col-md-6\u0026#34;\u0026gt; {!! Form::select(\u0026#39;category[]\u0026#39;, [\u0026#39;犬\u0026#39;, \u0026#39;猫\u0026#39;, \u0026#39;猿\u0026#39;], null, [\u0026#39;class\u0026#39; =\u0026gt; \u0026#39;form-control\u0026#39;, \u0026#39;multiple\u0026#39; =\u0026gt; \u0026#39;multiple\u0026#39;]) !!} \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; と\u0026lsquo;multiple\u0026rsquo; =\u0026gt; \u0026lsquo;multiple\u0026rsquo;を追加すると、\nと複数選択に変わります。\nしかし、この複数選択にSelect2を使用すれば、\nのように、ドロップダウンから複数選択して、最初の行に選択した値を表示してくれます。また、そこでXをクリックすることで削除もできます。\n必要な設定は、\nまず、\nhttps://github.com/select2/select2/archive/master.zip\nよりダウンロードして解凍してから、レイアウトのテンプレートを以下のように編集します。\nSelect2の設定 ... \u0026lt;head\u0026gt; ... \u0026lt;link href=\u0026#34;{{ url(\u0026#39;assets/css/select2/select2.min.css\u0026#39;) }}\u0026#34; rel=\u0026#34;stylesheet\u0026#34; type=\u0026#34;text/css\u0026#34;\u0026gt; ... \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; ... \u0026lt;script src=\u0026#34;{{ url(\u0026#39;assets/js/select2/select2.min.js\u0026#39;) }}\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script src=\u0026#34;{{ url(\u0026#39;assets/js/select2/ja.js\u0026#39;) }}\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script type=\u0026#34;text/javascript\u0026#34;\u0026gt; $(document).ready(function() { $(\u0026#34;.js-multiple\u0026#34;).select2({ width: \u0026#39;resolve\u0026#39; }); }); \u0026lt;/script\u0026gt; ... そして、画面のテンプレートを、\nSelect2を使用した複数選択 \u0026lt;div class=\u0026#34;form-group{{ $errors-\u0026gt;has(\u0026#39;category\u0026#39;) ? \u0026#39; has-error\u0026#39; : \u0026#39;\u0026#39; }}\u0026#34;\u0026gt; \u0026lt;label class=\u0026#34;col-md-4 control-label\u0026#34;\u0026gt;分類\u0026lt;/label\u0026gt; \u0026lt;div class=\u0026#34;col-md-6\u0026#34;\u0026gt; {!! Form::select(\u0026#39;category[]\u0026#39;, [\u0026#39;犬\u0026#39;, \u0026#39;猫\u0026#39;, \u0026#39;猿\u0026#39;], null, [\u0026#39;class\u0026#39; =\u0026gt; \u0026#39;form-control js-multiple\u0026#39;, \u0026#39;multiple\u0026#39; =\u0026gt; \u0026#39;multiple\u0026#39;]) !!} \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; と編集します。\nSelect２には他にもいろいろな機能があります。\nhttps://select2.github.io/examples.html\n","date":"2016-03-19T03:42:52+09:00","permalink":"https://www.larajapan.com/2016/03/19/select2-%E3%82%BB%E3%83%AC%E3%82%AF%E3%83%88%E8%A4%87%E6%95%B0%E9%81%B8%E6%8A%9E/","title":"Select2 セレクト複数選択"},{"content":"今回は、前回作成した会員編集フォームを、Laravel CollectiveのForm \u0026amp; HTMLを使用して書き換えてみます。\nこのForm \u0026amp; HTMLは、Laravelのバージョン4.2には含まれていました。その後、Lumenの登場によりLaravelから分けて、Laravel Collectiveからの配布となりました。Laravel Collectiveではその他にも、Annotation, Remote(SSH)など有用なパッケージが含まれています。\nまず、Form \u0026amp; HTMLのインストールから。\ncomposer.jsonを編集します。\ncomposer.json ... \u0026#34;require\u0026#34;: { \u0026#34;php\u0026#34;: \u0026#34;\u0026gt;=5.5.9\u0026#34;, \u0026#34;laravel/framework\u0026#34;: \u0026#34;5.2.*\u0026#34;, \u0026#34;barryvdh/laravel-debugbar\u0026#34;: \u0026#34;^2.1\u0026#34;, \u0026#34;laravelcollective/html\u0026#34;: \u0026#34;5.2.*\u0026#34; }, ... その後以下をコマンドで実行。\n$ composer update\n次は、config/app.phpを編集します。\n\u0026#39;providers\u0026#39; =\u0026gt; [ ... Collective\\Html\\HtmlServiceProvider::class, ... ], \u0026#39;aliases\u0026#39; =\u0026gt; [ ... \u0026#39;Form\u0026#39; =\u0026gt; Collective\\Html\\FormFacade::class, \u0026#39;Html\u0026#39; =\u0026gt; Collective\\Html\\HtmlFacade::class, ... ], これでインストール完了です。\n前回のテンプレートは、以下のように変わります。ハイライトの部分が変更した部分です。\nprofile.blade.php @extends(\u0026#39;user.layouts.app\u0026#39;) @section(\u0026#39;content\u0026#39;) \u0026lt;div class=\u0026#34;container\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;row\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;col-md-8 col-md-offset-2\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;panel panel-default\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;panel-heading\u0026#34;\u0026gt;Profile\u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;panel-body\u0026#34;\u0026gt; {!! Form::open([\u0026#39;url\u0026#39; =\u0026gt; url(\u0026#39;/user/profile\u0026#39;), \u0026#39;class\u0026#39; =\u0026gt; \u0026#39;form-horizontal\u0026#39;, \u0026#39;role\u0026#39; =\u0026gt; \u0026#39;form\u0026#39;]) !!} \u0026lt;div class=\u0026#34;form-group{{ $errors-\u0026gt;has(\u0026#39;name\u0026#39;) ? \u0026#39; has-error\u0026#39; : \u0026#39;\u0026#39; }}\u0026#34;\u0026gt; \u0026lt;label class=\u0026#34;col-md-4 control-label\u0026#34;\u0026gt;名前\u0026lt;/label\u0026gt; \u0026lt;div class=\u0026#34;col-md-6\u0026#34;\u0026gt; {!! Form::text(\u0026#39;name\u0026#39;, $user-\u0026gt;name, [\u0026#39;class\u0026#39; =\u0026gt; \u0026#39;form-control\u0026#39;]) !!} @if ($errors-\u0026gt;has(\u0026#39;name\u0026#39;)) \u0026lt;span class=\u0026#34;help-block\u0026#34;\u0026gt; \u0026lt;strong\u0026gt;{{ $errors-\u0026gt;first(\u0026#39;name\u0026#39;) }}\u0026lt;/strong\u0026gt; \u0026lt;/span\u0026gt; @endif \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;form-group{{ $errors-\u0026gt;has(\u0026#39;email\u0026#39;) ? \u0026#39; has-error\u0026#39; : \u0026#39;\u0026#39; }}\u0026#34;\u0026gt; \u0026lt;label class=\u0026#34;col-md-4 control-label\u0026#34;\u0026gt;Eメール\u0026lt;/label\u0026gt; \u0026lt;div class=\u0026#34;col-md-6\u0026#34;\u0026gt; {!! Form::email(\u0026#39;email\u0026#39;, $user-\u0026gt;email, [\u0026#39;class\u0026#39; =\u0026gt; \u0026#39;form-control\u0026#39;]) !!} @if ($errors-\u0026gt;has(\u0026#39;email\u0026#39;)) \u0026lt;span class=\u0026#34;help-block\u0026#34;\u0026gt; \u0026lt;strong\u0026gt;{{ $errors-\u0026gt;first(\u0026#39;email\u0026#39;) }}\u0026lt;/strong\u0026gt; \u0026lt;/span\u0026gt; @endif \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;form-group\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;col-md-6 col-md-offset-4\u0026#34;\u0026gt; {!! Form::submit(\u0026#39;保存\u0026#39;, [\u0026#39;class\u0026#39; =\u0026gt; \u0026#39;btn btn-primary\u0026#39;]) !!} \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; {!! Form::close() !!} \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; @endsection デザイナーが編集できるテンプレートというより、どちらかというと開発者向け？と思われるかもしれません。しかし、例えば、前回で追加したクロスサイトリクエストフォージェリ対策の{!! csrf_field() !!}は自動的に生成されるし、タイプする量は減りますね。\nまた、以下のようにドロップダウンのデフォルト選択ではとても便利です。\n前回のフォームでは、以下のように選択値のそれぞれでチェックする必要ありますが、\n\u0026lt;select name=\u0026#34;category\u0026#34;\u0026gt; \u0026lt;option value=\u0026#34;\u0026#34;\u0026gt;選択してください\u0026lt;/option\u0026gt; \u0026lt;option value=\u0026#34;1\u0026#34; {{ (old(\u0026#34;category\u0026#34;) == \u0026#39;1\u0026#39;) ? \u0026#34;selected\u0026#34;:\u0026#34;\u0026#34; }}\u0026gt;犬\u0026lt;/option\u0026gt; \u0026lt;option value=\u0026#34;2\u0026#34; {{ (old(\u0026#34;category\u0026#34;) == \u0026#39;2\u0026#39;) ? \u0026#34;selected\u0026#34;:\u0026#34;\u0026#34; }}\u0026gt;猫\u0026lt;/option\u0026gt; \u0026lt;option value=\u0026#34;3\u0026#34; {{ (old(\u0026#34;category\u0026#34;) == \u0026#39;3\u0026#39;) ? \u0026#34;selected\u0026#34;:\u0026#34;\u0026#34; }}\u0026gt;猿\u0026lt;/option\u0026gt; \u0026lt;/select\u0026gt; これをFormで書き換えると、一行で終わってしまいます。\n{!! Form::select(\u0026#39;category\u0026#39;, [\u0026#39;1\u0026#39; =\u0026gt; \u0026#39;犬\u0026#39;, \u0026#39;2\u0026#39; =\u0026gt; \u0026#39;猫\u0026#39;, \u0026#39;3\u0026#39; =\u0026gt; \u0026#39;猿\u0026#39;], old(\u0026#39;category\u0026#39;), [\u0026#39;placeholder\u0026#39; =\u0026gt; \u0026#39;選択してください\u0026#39;]) !!} ","date":"2016-03-12T06:47:48+09:00","permalink":"https://www.larajapan.com/2016/03/12/%E4%BC%9A%E5%93%A1%E7%B7%A8%E9%9B%86%E3%83%95%E3%82%A9%E3%83%BC%E3%83%A0-laravel-collective/","title":"会員編集フォーム  - Laravel Collective"},{"content":"ウェブのプログラミングでなんといっても難しいのはフォーム画面のプログラムです。テキスト入力、ドロップダウン、ファイルのアップロード機能、さらにjqueryやangularなどのフロントエンドのjavascriptも入れば、無限の可能性があります。\n以前紹介したLaravelの付録の会員認証サンプルの会員登録画面は、php artisan make:authコマンドで自動作成されますが、ログイン後に会員がEメールアドレスや名前を編集する画面は作成されません。\n今回はこの編集画面を作成してみます。\nまず、新規のコントローラ作成します。\nphp artisan make:controller User/UserController\nを実行して、それを以下のように編集し、\nUserController.php namespace App\\Http\\Controllers\\User; use Illuminate\\Http\\Request; use App\\Http\\Requests; use App\\Http\\Controllers\\Controller; use App\\User; use Auth; use Validator; class UserController extends Controller { protected $user; public function __construct() { $this-\u0026gt;middleware(\u0026#39;auth:user\u0026#39;); // 認証 $this-\u0026gt;user = Auth::guard(\u0026#39;user\u0026#39;)-\u0026gt;user(); } public function getProfile() { return view(\u0026#39;user/profile\u0026#39;)-\u0026gt;with([\u0026#39;user\u0026#39; =\u0026gt; $this-\u0026gt;user]); } public function postProfile(Request $request) { $validator = Validator::make($request-\u0026gt;all(), [ \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;required|max:255\u0026#39;, \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;required|email|max:255|unique:users,email,\u0026#39;.$this-\u0026gt;user-\u0026gt;id ]); //　エラーチェック if ($validator-\u0026gt;fails()) { return back()-\u0026gt;withInput()-\u0026gt;withErrors($validator); } // データを更新 $this-\u0026gt;user-\u0026gt;update([ \u0026#39;name\u0026#39; =\u0026gt; $request-\u0026gt;input(\u0026#39;name\u0026#39;), \u0026#39;email\u0026#39; =\u0026gt; $request-\u0026gt;input(\u0026#39;email\u0026#39;) ]); return redirect(\u0026#39;user/home\u0026#39;); } } Validationでは、emailのチェックは、$this-\u0026gt;user-\u0026gt;idを除くusersのレコードを対象に他に同じEメールが使われていないかチェックします。\n次に、以下のようなテンプレートを作成します。\nprofile.blade.php @extends(\u0026#39;user.layouts.app\u0026#39;) @section(\u0026#39;content\u0026#39;) \u0026lt;div class=\u0026#34;container\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;row\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;col-md-8 col-md-offset-2\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;panel panel-default\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;panel-heading\u0026#34;\u0026gt;Profile\u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;panel-body\u0026#34;\u0026gt; \u0026lt;form class=\u0026#34;form-horizontal\u0026#34; role=\u0026#34;form\u0026#34; method=\u0026#34;POST\u0026#34; action=\u0026#34;{{ url(\u0026#39;/user/profile\u0026#39;) }}\u0026#34;\u0026gt; {!! csrf_field() !!} \u0026lt;div class=\u0026#34;form-group{{ $errors-\u0026gt;has(\u0026#39;name\u0026#39;) ? \u0026#39; has-error\u0026#39; : \u0026#39;\u0026#39; }}\u0026#34;\u0026gt; \u0026lt;label class=\u0026#34;col-md-4 control-label\u0026#34;\u0026gt;名前\u0026lt;/label\u0026gt; \u0026lt;div class=\u0026#34;col-md-6\u0026#34;\u0026gt; \u0026lt;input type=\u0026#34;text\u0026#34; class=\u0026#34;form-control\u0026#34; name=\u0026#34;name\u0026#34; value=\u0026#34;{{ old(\u0026#39;name\u0026#39;, $user-\u0026gt;name) }}\u0026#34;\u0026gt; @if ($errors-\u0026gt;has(\u0026#39;name\u0026#39;)) \u0026lt;span class=\u0026#34;help-block\u0026#34;\u0026gt; \u0026lt;strong\u0026gt;{{ $errors-\u0026gt;first(\u0026#39;name\u0026#39;) }}\u0026lt;/strong\u0026gt; \u0026lt;/span\u0026gt; @endif \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;form-group{{ $errors-\u0026gt;has(\u0026#39;email\u0026#39;) ? \u0026#39; has-error\u0026#39; : \u0026#39;\u0026#39; }}\u0026#34;\u0026gt; \u0026lt;label class=\u0026#34;col-md-4 control-label\u0026#34;\u0026gt;Eメール\u0026lt;/label\u0026gt; \u0026lt;div class=\u0026#34;col-md-6\u0026#34;\u0026gt; \u0026lt;input type=\u0026#34;email\u0026#34; class=\u0026#34;form-control\u0026#34; name=\u0026#34;email\u0026#34; value=\u0026#34;{{ old(\u0026#39;email\u0026#39;, $user-\u0026gt;email) }}\u0026#34;\u0026gt; @if ($errors-\u0026gt;has(\u0026#39;email\u0026#39;)) \u0026lt;span class=\u0026#34;help-block\u0026#34;\u0026gt; \u0026lt;strong\u0026gt;{{ $errors-\u0026gt;first(\u0026#39;email\u0026#39;) }}\u0026lt;/strong\u0026gt; \u0026lt;/span\u0026gt; @endif \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;form-group\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;col-md-6 col-md-offset-4\u0026#34;\u0026gt; \u0026lt;button type=\u0026#34;submit\u0026#34; class=\u0026#34;btn btn-primary\u0026#34;\u0026gt; 保存 \u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/form\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; @endsection ここ、old()のLaravelの関数が使用されていますが、すでに存在する値を編集するので、デフォルトとしてDBからの値が入っていることに注意してください。\n\u0026lt;input type=\u0026#34;text\u0026#34; class=\u0026#34;form-control\u0026#34; name=\u0026#34;name\u0026#34; value=\u0026#34;{{ old(\u0026#39;name\u0026#39;, $user-\u0026gt;name) }}\u0026#34;\u0026gt; コントローラでは、validationエラーが発生して、現在の画面に戻る必要があります。そのときは、withInput()がセッションに入力の値を保存して、back()で現在の画面にリダイレクトします。リダイレクト後には、old()は、デフォルトでなくセッションに保存された入力された値を表示します。\n最後に、routes.phpを以下のように編集します。\nroutes.php Route::group([\u0026#39;prefix\u0026#39; =\u0026gt; \u0026#39;user\u0026#39;, \u0026#39;middleware\u0026#39; =\u0026gt; \u0026#39;web\u0026#39;], function () { ... Route::get(\u0026#39;profile\u0026#39;, \u0026#39;User\\UserController@getProfile\u0026#39;); Route::post(\u0026#39;profile\u0026#39;, \u0026#39;User\\UserController@postProfile\u0026#39;); }); 以下のように、認証のミドルウェアをコントローラから移してもOKです。\nroutes.php Route::group([\u0026#39;prefix\u0026#39; =\u0026gt; \u0026#39;user\u0026#39;, \u0026#39;middleware\u0026#39; =\u0026gt; \u0026#39;web\u0026#39;], function () { ... Route::group([\u0026#39;middleware\u0026#39; =\u0026gt; \u0026#39;auth:user\u0026#39; ], function () { Route::get(\u0026#39;home\u0026#39;, \u0026#39;User\\HomeController@index\u0026#39;); Route::get(\u0026#39;profile\u0026#39;, \u0026#39;User\\UserController@getProfile\u0026#39;); Route::post(\u0026#39;profile\u0026#39;, \u0026#39;User\\UserController@postProfile\u0026#39;); }); }); ","date":"2016-03-05T07:36:54+09:00","permalink":"https://www.larajapan.com/2016/03/05/%E4%BC%9A%E5%93%A1%E7%B7%A8%E9%9B%86%E3%83%95%E3%82%A9%E3%83%BC%E3%83%A0/","title":"会員編集フォーム"},{"content":"新規のLaravelのプロジェクトは、以下の実行で作成できます。\n$ laravel new blog /blogのディレクトリが作成され、そこには以下のようなディレクトリやファイルが作成されます。\napp/ bootstrap/ config/ database/ public/ resources/ storage/ tests/ vendor/ artisan composer.json composer.lock gulpfile.js package.json phpunit.xml readme.md server.php /publicのディレクトリがウェブサイトのルートとなる仮定で、その他のディレクトリはユーザーからはアクセスできないという仮定なのですが、\nしかし、現実は使用するサーバーの都合により、このpublicのディレクトリを違う場所に移動する必要があります。\n例えば、\npublic\nを\n/usr/www/demo/webdocs\nに、\nその他のディレクトリやファイルは、ウェブがアクセスできない以下に\n/usr/www/demo/blog\nこの変更に必要な設定は、Laravelではとても簡単です。\npublic/index.php、移動後では、/usr/www/demo/webdocs/index.phpを編集するだけです。\nindex.php ... require __DIR__.\u0026#39;/../bootstrap/autoload.php\u0026#39;; ... $app = require_once __DIR__.\u0026#39;/../bootstrap/app.php\u0026#39;; .. を\nindex.php ... require \u0026#39;/usr/www/demo/blog/bootstrap/autoload.php\u0026#39;; ... $app = \u0026#39;/usr/www/demo/blog/bootstrap/app.php\u0026#39;; .. と書き換えるだけです。\nしかし、ここで１つ問題は、Laravel定義のpublic_path関数が返す値が、\n/usr/www/demo/blog/public\nとなります。\nこれを修正するには、\nindex.php ... require \u0026#39;/usr/www/demo/blog/bootstrap/autoload.php\u0026#39;; ... $app = \u0026#39;/usr/www/demo/blog/bootstrap/app.php\u0026#39;; $app-\u0026gt;bind(\u0026#39;path.public\u0026#39;, function() { return __DIR__; }); .. と、path.publicを設定する必要あります。 それにより、public_path関数が返す値は、\n/usr/www/demo/blog/webdocs\nとなります。\n","date":"2016-02-27T07:53:43+09:00","permalink":"https://www.larajapan.com/2016/02/27/public%E3%81%AE%E3%83%87%E3%82%A3%E3%83%AC%E3%82%AF%E3%83%88%E3%83%AA%E3%82%92%E7%A7%BB%E5%8B%95%E3%81%99%E3%82%8B/","title":"publicのディレクトリを移動する"},{"content":"Laravelのマニュアルは、ほどよい説明で気に入っています。長い説明でポイントがつかめなく困ることはそうありません。理解には何回か読むことも必要ですが。\nしかし、ときには掲載されるサンプルコードがよく使用されるような例ではなく、逆に混乱してしまうことあります。\n例えば、Eloquent Mutatorsの以下のサンプルコード。\nnamespace App; use Illuminate\\Database\\Eloquent\\Model; class User extends Model { public function getFirstNameAttribute($value) { return ucfirst($value); } } getFirstNameAttributeを上のように定義すると、\n$user = App\\User::find(1); $firstName = $user-\u0026gt;first_name; のように使えると。\nucfirstの関数は、文字列の最初の文字を大文字にする関数なので、もとのDBテーブルuserのfirst_nameの項目の値、つまり$user-\u0026gt;first_nameが変換されるのだな、とは推測できます。\nしかし、元の項目名もfirst_nameであるし、変換した値にアクセスするのもfirst_nameで、同じ変数名でややこしく思いませんか？\nテンプレートで使用するなら、こちらの方が混乱せずに管理性が高いです。 {{ ucfirst($user-\u0026gt;first_name) }}\nアクセッサーの便利さの説明には、違うサンプルコードを使用した方が良いと思います。\n例えば、\npublic function getNameAttribute() { return $this-\u0026gt;first_name.\u0026#39; \u0026#39;.$this-\u0026gt;last_name; } この例では、nameという項目はDBテーブルに存在しないという仮定です。\n$user = App\\User::find(1); echo $user-\u0026gt;name; $user-\u0026gt;first_nameが「Kenji」で、$user-\u0026gt;last_nameが「Hino」なら、「Kenji Hino」が表示されます。つまり、モデルに新規の属性を作成するのは簡単ということが、このサンプルからわかります。\n注意してもらいたいのは、関数名にパラメータがないことです。最初の例では、$valueがありました。そのパラメータは、関数名で指定される（この場合、FisrtName =\u0026gt; first_name）変数の値をとってくるという意味ですね。\nさて、この新規の属性name、他のDBから入ってくる属性と違って生成するオブジェクトに自動的にふくまれるというわけではありません。\n$ php artisan tinker Psy Shell v0.6.1 (PHP 5.6.13 — cli) by Justin Hileman \u0026gt;\u0026gt;\u0026gt; use App\\User; =\u0026gt; false \u0026gt;\u0026gt;\u0026gt; $user = User::find(1); =\u0026gt; App\\User {#847 id: 1, created_at: \u0026#34;2015-04-21 11:56:46\u0026#34;, updated_at: \u0026#34;2016-01-29 14:17:54\u0026#34;, first_name: \u0026#34;Kenji\u0026#34;, last_name: \u0026#34;Hino\u0026#34;, } \u0026gt;\u0026gt;\u0026gt; $user-\u0026gt;name; =\u0026gt; \u0026#34;Kenji Hino\u0026#34; \u0026gt;\u0026gt;\u0026gt; $user-\u0026gt;toArray() =\u0026gt; [ \u0026#34;id\u0026#34; =\u0026gt; 1, \u0026#34;created_at\u0026#34; =\u0026gt; \u0026#34;2015-04-21 11:56:46\u0026#34;, \u0026#34;updated_at\u0026#34; =\u0026gt; \u0026#34;2016-01-29 14:17:54\u0026#34;, \u0026#34;first_name\u0026#34; =\u0026gt; \u0026#34;Kenji\u0026#34;, \u0026#34;last_name\u0026#34; =\u0026gt; \u0026#34;Hino\u0026#34;, ] \u0026gt;\u0026gt;\u0026gt; 最初のオブジェクトの属性としては、nameは入ってきませんね。しかし、$user-\u0026gt;nameでは値が返ってきます。しかし、toArrayやtoJsonでは入ってきません。\nこれは、パフォーマンスのためにLaravelの意図的な仕様です。毎回毎回必要なものでもありませんね。\nしかし、必要なときにはどうしたらよいのでしょう？\nclass User extends Model { protected $appends = [\u0026#39;name\u0026#39;]; ... と$appendsに新規属性を指定すると、\n\u0026gt;\u0026gt;\u0026gt; $user-\u0026gt;toArray() =\u0026gt; [ \u0026#34;id\u0026#34; =\u0026gt; 1, \u0026#34;created_at\u0026#34; =\u0026gt; \u0026#34;2015-04-21 11:56:46\u0026#34;, \u0026#34;updated_at\u0026#34; =\u0026gt; \u0026#34;2016-01-29 14:17:54\u0026#34;, \u0026#34;first_name\u0026#34; =\u0026gt; \u0026#34;Kenji\u0026#34;, \u0026#34;last_name\u0026#34; =\u0026gt; \u0026#34;Hino\u0026#34;, \u0026#34;name\u0026#34; =\u0026gt; \u0026#34;Kenji Hino\u0026#34;, ] \u0026gt;\u0026gt;\u0026gt; name入ってきますね。\n","date":"2016-02-20T08:22:11+09:00","permalink":"https://www.larajapan.com/2016/02/20/eloquent%E3%81%AE%E3%82%A2%E3%82%AF%E3%82%BB%E3%83%83%E3%82%B5%E3%83%BC%E3%81%AE%E8%AA%AC%E6%98%8E%E4%BE%8B/","title":"Eloquentのアクセッサーの説明例"},{"content":"マルチ認証のトピックが続いていますが、同じブラウザを使用して「会員」と「管理者」の両方にログインしたらどうなるのでしょう？\nログインはブラウザのクッキーを使用して、サーバーのセッションと繋がっています。Laravelはそれぞれに違うセッション、違うクッキーを使用するのでしょうか？それとも同じセッションで違う情報を保持するのでしょうか？興味ありませんか？\nということで、便利なツールの紹介とともに、認証のセッションがどうなっているかチェックしてみましょう。\nここで紹介するツールはDebugarというLaravelのためのツールです。\nこれをインストールすると、画面下にDebugarの情報パネルが表示され、実行したLaravelの様々な情報を見ることができます。\n例えば、以下は会員のログイン成功直後の画面ですが、実行したDBのクエリーを見ることができます。DBテーブルusersに対するクエリーがあります。\nこのツールのインストールはとても簡単です（GitHubにリポあります）。\nまず、以下を実行します。\ncomposer require barryvdh/laravel-debugbar composer.jsonが更新されライブラリがダウンロードされインストールされます。\n次に、config/app.phpに以下を追加します。\n... \u0026#39;providers\u0026#39; =\u0026gt; [ ... Barryvdh\\Debugbar\\ServiceProvider::class, ], \u0026#39;aliases\u0026#39; =\u0026gt; [ ... \u0026#39;Debugbar\u0026#39; =\u0026gt; Barryvdh\\Debugbar\\Facade::class, ], ]; これで完了です。\n先の会員ログインを実行では、DBのクエリーの情報を見ることができましたが、セッションはどうなのでしょう？\n赤丸の部分がセッションの情報を見るボタンです。そして赤の四角の部分が、会員が認証されていることを示す情報です。login-user..の部分です。\nさて、同じブラウザでもう１つタブを作成して今度は、管理画面にログインしてみましょう。以下が、ログイン成功後のセッション値です。\n値が増えていますね。しかし、今度は、login-admin..と違う名前となっていますね。\nつまり、セッションの中でguardの値をセッションの変数名として使い分けているわけです。\n","date":"2016-02-12T08:26:57+09:00","permalink":"https://www.larajapan.com/2016/02/12/%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E8%AA%8D%E8%A8%BC%EF%BC%88%EF%BC%91%EF%BC%93%EF%BC%89debugbar/","title":"ユーザー認証（１４）Debugbar"},{"content":"ユーザー認証（１０）Laravel 5.2 マルチ認証では、会員と管理者に対して異なるＤＢテーブルをもとに認証を設定しました。\nまた、\n前回では、違うHasherの使用を試みました。\n今回は、マルチ認証のときに異なるHasherを用いるケースについて考えてみましょう。そうたくさん起こるケースでないかもしれませんが、私のクライアントのシステムでは実際に起こるケースです。１つのシステムにおいて、「会員」と「管理者」と「店舗管理者」が存在し、それぞれの認証は異なるHasherを使用しています。特に「管理者」は複数のシステムで共有するもので、その認証のためのサーバーが違うマシンに存在します。\nまず直面する問題は、Laravelの5.2のマルチ認証では、このような状況にはシンプルに対応できないことです。\nconfig/auth.phpのprovidersでは、それぞれのproviderにおいてhasherの設定がないのです。\nconfig/auth.php .. \u0026#39;providers\u0026#39; =\u0026gt; [ \u0026#39;users\u0026#39; =\u0026gt; [ \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;eloquent\u0026#39;, \u0026#39;model\u0026#39; =\u0026gt; App\\User::class, ], \u0026#39;admin_users\u0026#39; =\u0026gt; [ \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;eloquent\u0026#39;, \u0026#39;model\u0026#39; =\u0026gt; App\\AdminUser::class, ], ], .. また、前回のように、グローバルでHasherを変えることもできません。\n「会員」の認証のときには、デフォルトのHasherを使用し、「管理者」の認証のときには、違うHasherを使用できるのが理想です。\nまず、Authのサービスがどう初期化されているか追跡してみましょう。\nAuthServiceProviderで、ユーザー認証のサービスauthが登録されます。そこでは、AuthManagerのクラスが使用されます。\nAuthServiceProvider.php protected function registerAuthenticator() { $this-\u0026gt;app-\u0026gt;singleton(\u0026#39;auth\u0026#39;, function ($app) { $app[\u0026#39;auth.loaded\u0026#39;] = true; return new AuthManager($app); }); $this-\u0026gt;app-\u0026gt;singleton(\u0026#39;auth.driver\u0026#39;, function ($app) { return $app[\u0026#39;auth\u0026#39;]-\u0026gt;guard(); }); } AuthManagerでは、config/auth.phpの設定を読み込み、providerを作成します。その作成は、CreatesUserProvidersで行われます。\nAuthManger.php .. public function createSessionDriver($name, $config) { $provider = $this-\u0026gt;createUserProvider($config[\u0026#39;provider\u0026#39;]); .. providerのdriverは、先のconfig/auth.phpではeloquentと設定されているので、以下のメソッドでオブジェクトが作成されます。\nCreatesUserProviders.php .. protected function createEloquentProvider($config) { return new EloquentUserProvider($this-\u0026gt;app[\u0026#39;hash\u0026#39;], $config[\u0026#39;model\u0026#39;]); } .. やっとたどり着きましたね。そうproviderの作成時に、グローバルのHasher app[\u0026lsquo;hash\u0026rsquo;]がパラメとして渡されているのです。\nここを変えることができれば、認証のHasherを変えることできるのです。変更するには、新規の認証のためのdriverを使用することも可能です。しかし、以下のEloquentUserProviderを見ると、オブジェクトが作成された後でもHasherを変えることが可能のようです。\nEloquesntUserProvider.php ... /** * Sets the hasher implementation. * * @param \\Illuminate\\Contracts\\Hashing\\Hasher $hasher * @return $this */ public function setHasher(HasherContract $hasher) { $this-\u0026gt;hasher = $hasher; return $this; } ... ここまで理解すると、あとはそう難しくはありません。\nまずは、config/auth.phpにおいて、どのHasherのサービスを使用するか指定しましょう。\n... \u0026#39;providers\u0026#39; =\u0026gt; [ \u0026#39;users\u0026#39; =\u0026gt; [ \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;eloquent\u0026#39;, \u0026#39;model\u0026#39; =\u0026gt; App\\User::class, \u0026#39;hasher\u0026#39; =\u0026gt; Illuminate\\Hashing\\BcryptHasher::class ], \u0026#39;admin_users\u0026#39; =\u0026gt; [ \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;eloquent\u0026#39;, \u0026#39;model\u0026#39; =\u0026gt; App\\AdminUser::class, \u0026#39;hasher\u0026#39; =\u0026gt; App\\Services\\MD5Hasher::class ], ], ... 上のhasherはまったくLaravelのコードでは使用されませんが、今回の目的でHasherを指定するにはベストの場所と思いませんか！\n次に、管理者のAuthControllerにおいて、\nAuthController.php ... class AuthController extends Controller { use AuthenticatesAndRegistersUsers, ThrottlesLogins { getCredentials as getCredentialsTrait; } protected $redirectTo = \u0026#39;/admin/home\u0026#39;; protected $guard = \u0026#39;admin\u0026#39;; protected $redirectAfterLogout = \u0026#39;admin/login\u0026#39;;　//ログイン後のリダイレクト先 protected $username = \u0026#39;email\u0026#39;; //　DBテーブルのログインに使用される項目 protected $registerView = \u0026#39;admin.auth.register\u0026#39;;　// 登録に使用されるテンプレート protected $loginView = \u0026#39;admin.auth.login\u0026#39;;　//　ログインに使用されるテンプレート protected $hasher; public function __construct() { $hasher_class = config(\u0026#39;auth.providers.admin_users.hasher\u0026#39;); $this-\u0026gt;hasher = new $hasher_class; \\Auth::guard($this-\u0026gt;guard)-\u0026gt;getProvider()-\u0026gt;setHasher($this-\u0026gt;hasher); $this-\u0026gt;middleware(\u0026#39;guest:admin\u0026#39;, [\u0026#39;except\u0026#39; =\u0026gt; \u0026#39;logout\u0026#39;]); } ... 会員と違うテンプレートを用意するために、テンプレートの場所を指定していることにも注意してください。すべて変数の指定で可能です。\nPasswordControllerにおいても同様な設定をします。\nPasswordsController class PasswordController extends Controller { use ResetsPasswords; protected $redirectTo = \u0026#39;/admin/home\u0026#39;;　// ログイン後のリダイレクト先 protected $guard = \u0026#39;admin\u0026#39;; protected $linkRequestView = \u0026#39;admin.auth.passwords.email\u0026#39;;　// パスワードのリセットリンクを送信してもらう画面のテンプレート protected $resetView = \u0026#39;admin.auth.passwords.reset\u0026#39;;　// パスワードリセット画面のテンプレート protected $subject = \u0026#39;管理者のパスワードリセット\u0026#39;; //　送信メールの件名 public function __construct() { $hasher_class = config(\u0026#39;auth.providers.admin_users.hasher\u0026#39;); \\Auth::guard($this-\u0026gt;guard)-\u0026gt;getProvider()-\u0026gt;setHasher(new $hasher_class); $this-\u0026gt;middleware(\u0026#39;guest:admin\u0026#39;); } } 最後に、管理者へのパスワードリセットのメールのテンプレートは、以下のように、config/auth.phpで可能です。\nconfig/auth.php ... \u0026#39;passwords\u0026#39; =\u0026gt; [ \u0026#39;users\u0026#39; =\u0026gt; [ \u0026#39;provider\u0026#39; =\u0026gt; \u0026#39;users\u0026#39;, \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;user.auth.emails.password\u0026#39;, \u0026#39;table\u0026#39; =\u0026gt; \u0026#39;password_resets\u0026#39;, \u0026#39;expire\u0026#39; =\u0026gt; 60, ], \u0026#39;admin_users\u0026#39; =\u0026gt; [ \u0026#39;provider\u0026#39; =\u0026gt; \u0026#39;admin_users\u0026#39;, \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;admin.auth.emails.password\u0026#39;, \u0026#39;table\u0026#39; =\u0026gt; \u0026#39;admin_password_resets\u0026#39;, \u0026#39;expire\u0026#39; =\u0026gt; 60, ], ], ]; ","date":"2016-02-06T11:46:16+09:00","permalink":"https://www.larajapan.com/2016/02/06/%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E8%AA%8D%E8%A8%BC%EF%BC%88%EF%BC%91%EF%BC%93%EF%BC%89laravel-5-2-hash%E3%82%92%E8%A4%87%E6%95%B0%E4%BD%BF%E7%94%A8%E3%81%99%E3%82%8B/","title":"ユーザー認証（１３）Laravel 5.2 Hashを複数使用する"},{"content":"Hasherとは、パスワードからHashの作成に使用される関数です。さて、Hashとはなんぞや？\n例えば、パスワードをtesttestとします。これをHasherに与えると、 $2y$10$CE4R5SS6f5g4Rd0fgYRbneoeCOYbE0S2xfaYNC7i41CLysQ8TRUPO のような文字列を生成します。これがHashです。\nHashは暗号化と異なり、非暗号化はできる機能はありません。つまり、 $2y$10$CE4R5SS6f5g4Rd0fgYRbneoeCOYbE0S2xfaYNC7i41CLysQ8TRUPO から、もとのパスワードtesttestを解読できる機能はありません。\nこの特性を活かしてユーザー認証の機能のセキュリティを高めます。\nまず会員登録時に入力したパスワードをHashした値をDBに保存します。そのままのテキストでは保存しません。そして、ログイン時に入力されたパスワードをHashして、DBに保存されたHashされたパスワードとマッチするかチェックします。マッチするなら認証OKです。\nこのHashする関数(Hasher)にはいろいろな種類があります。PHPでは、一昔前までは、\nmd5\nを使用していました。md5は、必ず同じ値を返すので、Hashされたパスワードのマッチは、お互いの文字列を比較するだけです。\nしかし、php5.5からは、よりセキュアな以下の関数の使用が薦められています。\npassword_hash\nこの関数が返すHashの値は、与えられる値が同じでも返す値が変わります。それゆえに、Hashされたパスワードのマッチにはもう１つの関数、\npassword_verify\nを使用します。以下のように。\n$password = \u0026#39;testtest\u0026#39;; $hashed = password_hash($password, PASSWORD_DEFAULT); $result = password_verify($password, $hashed); echo $result ? \u0026#39;認証成功\u0026#39; : \u0026#39;認証失敗\u0026#39;; Laravelも基本的に同様な関数を使用しています。\nBcryptHasher\nさて、新規にLaravelを使用してプログラムを書くならまったくこれで問題ありません。しかし、既存のプログラムをLaravelに書き換えるとき、例えば、既存のプログラムがパスワードのHashにmd5を使用しているなら、DBに保存されているパスワードでは、LaravelのHasherでは誰もログインが不可能となってしまいます。\nこれに対応するには、LaravelのHasherを取り換える必要があります。HasherはLaravelではプログラムを通して１種類しか使用できない仕組みなので、HasherのProviderを作成して取り換えることになります。\nまず、新規のHasherを作成します。\napp/Services/MD5Hahser.php namespace App\\Services; class MD5Hasher implements \\Illuminate\\Contracts\\Hashing\\Hasher { public function make($value, array $options = []) { return md5($value); } public function check($value, $hashedValue, array $options = []) { return (md5($value) == $hashedValue); } public function needsRehash($hashedValue, array $options = []) { return false; } } \\Illuminate\\Contracts\\Hashing\\Hasherで定められている型をもとに、必要な関数を定義します。\n次にこのHasherを登録するサービスプロバイダーを作成します。\napp/Providers/MD5HasherServiceProvider.php namespace App\\Providers; use Illuminate\\Support\\ServiceProvider; use App\\Services\\MD5Hasher; class MD5HasherServiceProvider extends ServiceProvider { protected $defer = true; public function register() { $this-\u0026gt;app-\u0026gt;singleton(\u0026#39;hash\u0026#39;, function() { return new MD5Hasher; }); } public function provides() { return [\u0026#39;hash\u0026#39;]; } } そして、config/app.phpを編集して、Hasherをサービスプロバイダーを入れ替えます。\nconfig/app.php ... \u0026#39;providers\u0026#39; =\u0026gt; [ /* * Laravel Framework Service Providers... */ ... // Illuminate\\Hashing\\HashServiceProvider::class, //コメントして App\\Providers\\MD5HasherServiceProvider::class,　//登録する ... 最後に、以下をコマンドラインで実行します。\ncomposer dump-autoload ","date":"2016-01-31T06:26:48+09:00","permalink":"https://www.larajapan.com/2016/01/31/%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E8%AA%8D%E8%A8%BC%EF%BC%88%EF%BC%91%EF%BC%92%EF%BC%89laravel-5-2-hasher%E3%82%92%E5%A4%89%E3%81%88%E3%82%8B/","title":"ユーザー認証（１２）Laravel 5.2 Hasherを変える"},{"content":"「ユーザー認証」のポストは、もうすでに１１回目になりました。Taylorくんのプログラムは、宝石がいっぱい詰まっているから、ソースコードを見ているといろいろ発見あります。\n例えば、私はユーザー認証成功後にログインの記録が欲しいです。つまり、ユーザーがどのIPからどのブラウザあるいはどのOSでアクセスしたかをDBに記録したいのです。ソースコードを見ているとそのことをあたかも考慮しているメカニズムが存在することに気づきます。今回はそれをどう利用するかを紹介します。\nまず、ユーザー認証（９）Laravel 5.2 コンポーネント自動作成で紹介したコマンドで作成されるファイルの１つ、AuthController.phpを編集することになります。しかし、以下を見てわかるようにトレイトをバンバン使っているので、そのファイルの中身はほとんど空です。\napp/Http/Controllers/Auth/AuthController.php namespace App\\Http\\Controllers\\Auth; use App\\User; use Validator; use App\\Http\\Controllers\\Controller; use Illuminate\\Foundation\\Auth\\ThrottlesLogins; use Illuminate\\Foundation\\Auth\\AuthenticatesAndRegistersUsers; class AuthController extends Controller { use AuthenticatesAndRegistersUsers, ThrottlesLogins; protected $redirectTo = \u0026#39;/\u0026#39;; public function __construct() { $this-\u0026gt;middleware(\u0026#39;guest\u0026#39;, [\u0026#39;except\u0026#39; =\u0026gt; \u0026#39;logout\u0026#39;]); } protected function validator(array $data) { return Validator::make($data, [ \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;required|max:255\u0026#39;, \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;required|email|max:255|unique:users\u0026#39;, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;required|confirmed|min:6\u0026#39;, ]); } protected function create(array $data) { return User::create([ \u0026#39;name\u0026#39; =\u0026gt; $data[\u0026#39;name\u0026#39;], \u0026#39;email\u0026#39; =\u0026gt; $data[\u0026#39;email\u0026#39;], \u0026#39;password\u0026#39; =\u0026gt; bcrypt($data[\u0026#39;password\u0026#39;]), ]); } } さて、どうすればよいでしょう？\n上の１１行目のトレイトのファイルを遡って辿ります。\nAuthenticatesAndRegistersUsers ↓ AuthenticatesUsers\nこのファイルで、postLoginは、ログイン情報を送信したときに呼ばれる関数、その定義の中では、loginをコールしています。\nvendor/laravel/framework/src/Illuminate/Foundation/Auth/AuthenticatesUsers.php ... public function postLogin(Request $request) { return $this-\u0026gt;login($request); } ... public function login(Request $request) { $this-\u0026gt;validate($request, [ $this-\u0026gt;loginUsername() =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, ]); $throttles = $this-\u0026gt;isUsingThrottlesLoginsTrait(); if ($throttles \u0026amp;\u0026amp; $this-\u0026gt;hasTooManyLoginAttempts($request)) { return $this-\u0026gt;sendLockoutResponse($request); } $credentials = $this-\u0026gt;getCredentials($request); if (Auth::guard($this-\u0026gt;getGuard())-\u0026gt;attempt($credentials, $request-\u0026gt;has(\u0026#39;remember\u0026#39;))) { return $this-\u0026gt;handleUserWasAuthenticated($request, $throttles); } if ($throttles) { $this-\u0026gt;incrementLoginAttempts($request); } return $this-\u0026gt;sendFailedLoginResponse($request); } ... protected function handleUserWasAuthenticated(Request $request, $throttles) { if ($throttles) { $this-\u0026gt;clearLoginAttempts($request); } if (method_exists($this, \u0026#39;authenticated\u0026#39;)) { return $this-\u0026gt;authenticated($request, Auth::guard($this-\u0026gt;getGuard())-\u0026gt;user()); } return redirect()-\u0026gt;intended($this-\u0026gt;redirectPath()); } ... loginの定義ではattempt()（２１行目）で認証を実行しています。そしてそれが成功なら、handleUserWasAuthenticatedをコールします。\n今度は、36行目見てください。ここのifは、このクラスにauthenticatedというメソッドが定義されているなら、それを実行しますよ！という意味です。\n先のAuthController.phpに以下のメソッドを追加します。以下のハイライトされた 部分です。\napp/Http/Controllers/Auth/AuthController.php namespace App\\Http\\Controllers\\Auth; use App\\User; use Validator; use App\\Http\\Controllers\\Controller; use Illuminate\\Foundation\\Auth\\ThrottlesLogins; use Illuminate\\Foundation\\Auth\\AuthenticatesAndRegistersUsers; use App\\UserLog; use Illuminate\\Http\\Request; class AuthController extends Controller { use AuthenticatesAndRegistersUsers, ThrottlesLogins; protected $redirectTo = \u0026#39;/\u0026#39;; public function __construct() { $this-\u0026gt;middleware(\u0026#39;guest\u0026#39;, [\u0026#39;except\u0026#39; =\u0026gt; \u0026#39;logout\u0026#39;]); } protected function validator(array $data) { return Validator::make($data, [ \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;required|max:255\u0026#39;, \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;required|email|max:255|unique:users\u0026#39;, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;required|confirmed|min:6\u0026#39;, ]); } protected function create(array $data) { return User::create([ \u0026#39;name\u0026#39; =\u0026gt; $data[\u0026#39;name\u0026#39;], \u0026#39;email\u0026#39; =\u0026gt; $data[\u0026#39;email\u0026#39;], \u0026#39;password\u0026#39; =\u0026gt; bcrypt($data[\u0026#39;password\u0026#39;]), ]); } public function authenticated(Request $request, User $user) { UserLog::add($request, $user);//ここでログを作成 return redirect()-\u0026gt;intended($this-\u0026gt;redirectPath()); } } これで、ログインが成功するたびに、このauthenticatedがコールされログインの履歴をDBに作成します。\n以下はそこで使用されるモデルUserLogです。ホスト名を得るために、ミューテーターを使用します。\napp/UserLog.php namespace App; use Illuminate\\Database\\Eloquent\\Model; use Illuminate\\Http\\Request; class UserLog extends Model { protected $table = \u0026#39;user_log\u0026#39;; protected $primaryKey = null; public $incrementing = false; public $timestamps = true; public function setHostnameAttribute($ip) { $this-\u0026gt;attributes[\u0026#39;hostname\u0026#39;] = gethostbyaddr($ip); } public static function add(Request $request, User $user) { $userLog = new static; $userLog-\u0026gt;user_id = $user-\u0026gt;id; $userLog-\u0026gt;ip = $request-\u0026gt;getClientIp(); $userLog-\u0026gt;hostname = $userLog-\u0026gt;ip; // ミューテーターを使用 $userLog-\u0026gt;save(); } } user_logのDBテーブルを作成することをお忘れずに。\nauthenticatedには、ログインの履歴を作成するだけでなく、会員の１周年記念のためのお祝いのメッセージや、それにまつわる特典の提供とか、いろいろなコードを含むことが考えられます。\n","date":"2016-01-24T03:17:25+09:00","permalink":"https://www.larajapan.com/2016/01/24/%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E8%AA%8D%E8%A8%BC%EF%BC%88%EF%BC%91%EF%BC%91%EF%BC%89laravel-5-2-%E3%83%AD%E3%82%B0%E3%82%A4%E3%83%B3%E3%81%AE%E8%A8%98%E9%8C%B2/","title":"ユーザー認証（１１）Laravel 5.2 ログインの記録"},{"content":"マルチ認証と言っても、複数のステップでユーザーを認証するわけでもなく、ちょっとピンと来ないですね。\n例えばECシステムにおいて、ユーザー画面での会員ログインと、管理画面での管理者のログインがそれぞれ別に必要とします。どちらもログインはEメールとは限らないし、片方でログインしたらもう片方でも認証となるとも限りません。つまり、ログインするユーザーの種類や場所が複数必要となる状況が多々あります。それに対応する機能が、マルチ認証です。\nLaravelの5.1までは、マルチ認証は対応していなく、以下のようなパッケージをインストールして使用していました。\nLaravel4.2対応のLaravel Multi Auth\nLaravel5.1対応のMultiAuth for Laravel 5.1\nしかし、5.2からはLaravelの基本仕様となっています。さすが、Taylorくん！\n今回はこの機能を見てみましょう。\nまず、デフォルトでインストールされるconfig/auth.phpの中身の解析。\nLaravel 5.1では、\nreturn [ /* デフォルトの認証ドライバー */ \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;eloquent\u0026#39;, /* 認証に使用されるモデル */ \u0026#39;model\u0026#39; =\u0026gt; App\\User::class, /* 認証に使用されるDBテーブル。ここでは、dirverがdatabaseでないので関係ない */ \u0026#39;table\u0026#39; =\u0026gt; \u0026#39;users\u0026#39;, /* パスワードリセットの設定 */ \u0026#39;password\u0026#39; =\u0026gt; [ \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;emails.password\u0026#39;,　// resources/views/emails/passwordをリンク送信メールのテンプレートとする \u0026#39;table\u0026#39; =\u0026gt; \u0026#39;password_resets\u0026#39;, // パスワードリセットのトークンの情報を保存するDBテーブル \u0026#39;expire\u0026#39; =\u0026gt; 60, // トークンは60分で期限切れ ], ]; これがLaravel5.2では、\nreturn [ /* 認証のデフォルト設定 */ \u0026#39;defaults\u0026#39; =\u0026gt; [ \u0026#39;guard\u0026#39; =\u0026gt; \u0026#39;web\u0026#39;, \u0026#39;passwords\u0026#39; =\u0026gt; \u0026#39;users\u0026#39;, ], /* 認証のガードを定義 */ \u0026#39;guards\u0026#39; =\u0026gt; [ \u0026#39;web\u0026#39; =\u0026gt; [ \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;session\u0026#39;, \u0026#39;provider\u0026#39; =\u0026gt; \u0026#39;users\u0026#39;, ], \u0026#39;api\u0026#39; =\u0026gt; [ \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;token\u0026#39;, \u0026#39;provider\u0026#39; =\u0026gt; \u0026#39;users\u0026#39;, ], ], /* 認証のプロバイダー */ \u0026#39;providers\u0026#39; =\u0026gt; [ \u0026#39;users\u0026#39; =\u0026gt; [ \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;eloquent\u0026#39;, \u0026#39;model\u0026#39; =\u0026gt; App\\User::class, ], // \u0026#39;users\u0026#39; =\u0026gt; [ // \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;database\u0026#39;, // \u0026#39;table\u0026#39; =\u0026gt; \u0026#39;users\u0026#39;, // ], ], /* パスワードリセットの設定 */ \u0026#39;passwords\u0026#39; =\u0026gt; [ \u0026#39;users\u0026#39; =\u0026gt; [ \u0026#39;provider\u0026#39; =\u0026gt; \u0026#39;users\u0026#39;, \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;auth.emails.password\u0026#39;, \u0026#39;table\u0026#39; =\u0026gt; \u0026#39;password_resets\u0026#39;, \u0026#39;expire\u0026#39; =\u0026gt; 60, ], ], ]; となりました。違いは、guardsとprovidersの導入です。\nちょっとこれではわかりにくいので、先の例を使って、ECサイトを想像してもらって、ショッピングをするユーザ画面と、サイトを管理する管理者画面があり、どちらもログインで認証が必要と仮定しましょう。ユーザー画面では買い物かごをチェックアウトするには、会員のログインが必要とします。\nとなると必要な設定は以下にようになります。\nreturn [ /* 認証のデフォルト設定 */ \u0026#39;defaults\u0026#39; =\u0026gt; [ \u0026#39;guard\u0026#39; =\u0026gt; \u0026#39;users\u0026#39;, \u0026#39;passwords\u0026#39; =\u0026gt; \u0026#39;users\u0026#39;, ], /* 認証のガードを定義 */ \u0026#39;guards\u0026#39; =\u0026gt; [ \u0026#39;users\u0026#39; =\u0026gt; [ \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;session\u0026#39;, \u0026#39;provider\u0026#39; =\u0026gt; \u0026#39;users_provider\u0026#39;, ], \u0026#39;admin_users\u0026#39; =\u0026gt; [ \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;session\u0026#39;, \u0026#39;provider\u0026#39; =\u0026gt; \u0026#39;admin_users_provider\u0026#39;, ], ], /* 認証のプロバイダー */ \u0026#39;providers\u0026#39; =\u0026gt; [ \u0026#39;users_provider\u0026#39; =\u0026gt; [ \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;eloquent\u0026#39;, \u0026#39;model\u0026#39; =\u0026gt; App\\User::class, ], \u0026#39;admin_users_provider\u0026#39; =\u0026gt; [ \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;eloquent\u0026#39;, \u0026#39;model\u0026#39; =\u0026gt; App\\AdminUser::class, ], ], /* パスワードリセットの設定 */ \u0026#39;passwords\u0026#39; =\u0026gt; [ \u0026#39;users\u0026#39; =\u0026gt; [ \u0026#39;provider\u0026#39; =\u0026gt; \u0026#39;users_provider\u0026#39;, \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;auth.emails.password\u0026#39;, \u0026#39;table\u0026#39; =\u0026gt; \u0026#39;password_resets\u0026#39;, \u0026#39;expire\u0026#39; =\u0026gt; 60, ], ], ]; ガードには、会員ログインのためのusersと、管理者ログインのためのadmin_usersの２つを定義します。どちらもセッションを使って、ログイン後の画面をプロテクトします。また、それらのプロバイダーで定義されているように、会員のUsersと管理者のAdminUsersのエロクエントモデルが認証のための情報提供元となります。\nguardsとprovidersの概念を導入することにより、今までの１つだけの認証のメカニズムを複数としたわけです。\nさて、次はこの設定の使用です。このファイル以外で使用するのは、ガード名だけですから簡単です。\nガードを指定する場所はプログラムの中でいくつかありますが、以下のようにapp/Http/routes.phpで使用されるのが一番明確と思います。\n例えば、ユーザー画面では、\nRoute::group([\u0026#39;middleware\u0026#39; =\u0026gt; \u0026#39;guest:users\u0026#39;], function() { Route::get(\u0026#39;login\u0026#39;, \u0026#39;user\\AuthController@getLogin\u0026#39;); Route::post(\u0026#39;login\u0026#39;, \u0026#39;user\\AuthController@postLogin\u0026#39;); Route::get(\u0026#39;signup\u0026#39;, \u0026#39;user\\SignupController@getSignup\u0026#39;); Route::post(\u0026#39;signup\u0026#39;, \u0026#39;user\\SignupController@postSignup\u0026#39;); Route::get(\u0026#39;password/email\u0026#39;, \u0026#39;user\\PasswordController@getEmail\u0026#39;); Route::post(\u0026#39;password/email\u0026#39;, \u0026#39;user\\PasswordController@postEmail\u0026#39;); Route::get(\u0026#39;password/reset/{token}\u0026#39;, \u0026#39;user\\PasswordController@getReset\u0026#39;); Route::post(\u0026#39;password/reset\u0026#39;, \u0026#39;user\\PasswordController@postReset\u0026#39;); }); Route:: group([\u0026#39;prefix\u0026#39; =\u0026gt; \u0026#39;member\u0026#39;, \u0026#39;middleware\u0026#39; =\u0026gt; \u0026#39;auth:users\u0026#39;], function() { Route::get(\u0026#39;index\u0026#39;, \u0026#39;user\\MemberController@getIndex\u0026#39;); Route::get(\u0026#39;password\u0026#39;, \u0026#39;user\\MemberController@getPassword\u0026#39;); Route::post(\u0026#39;password\u0026#39;, \u0026#39;user\\MemberController@postPassword\u0026#39;); Route::get(\u0026#39;profile\u0026#39;, \u0026#39;user\\MemberController@getProfile\u0026#39;); Route::post(\u0026#39;profile\u0026#39;, \u0026#39;user\\MemberController@postProfile\u0026#39;); Route::get(\u0026#39;logout\u0026#39;, \u0026#39;user\\AuthController@getLogout\u0026#39;); }); 以前、ユーザー認証（４）認証でページを保護で説明したように、\nミドルウェアとして、guestとauthが使われます。しかし、前回と違って、guest:usersのようにガード名を指定することが必要です。指定がないなら、auth.phpのデフォルトのセクションで指定したガードが自動的に使われます。\nちなみに、管理者側では、\nRoute::group([\u0026#39;prefix\u0026#39; =\u0026gt; \u0026#39;admin\u0026#39;, \u0026#39;middleware\u0026#39; =\u0026gt; \u0026#39;guest:admin_users\u0026#39;], function() { Route::get(\u0026#39;login\u0026#39;, \u0026#39;admin\\AuthController@getLogin\u0026#39;); Route::post(\u0026#39;login\u0026#39;, \u0026#39;admin\\AuthController@postLogin\u0026#39;); }); Route:: group([\u0026#39;prefix\u0026#39; =\u0026gt; \u0026#39;admin\u0026#39;, \u0026#39;middleware\u0026#39; =\u0026gt; \u0026#39;auth:admin_users\u0026#39;], function() { Route::get(\u0026#39;logout\u0026#39;, \u0026#39;admin\\AuthController@getLogout\u0026#39;); Route::get(\u0026#39;index\u0026#39;, \u0026#39;admin\\HomeController@getIndex\u0026#39;)-\u0026gt;name(\u0026#39;admin.home\u0026#39;); .. こんな感じです。\nAuthControllerは、ユーザ画面と管理画面では、前回紹介した自動作成使われるもののコピーを編集する必用あります。\nユーザ画面では、\n... class AuthController extends BaseController { protected $guard = \u0026#39;users\u0026#39;; protected $redirectTo = \u0026#39;user/member/index\u0026#39;; // ログイン後のリダイレクト先 protected $redirectAfterLogout = \u0026#39;user/login\u0026#39;; // ログアウト後のリダイレクト先 protected $username = \u0026#39;email\u0026#39;; // ログインとなるDBの項目名 protected $maxLoginAttempts = 5; // ログインスロットルとなるまで最高のログイン失敗回数 protected $lockoutTime = 60; // ログインスロットルとなってからの待ち秒数 use AuthenticatesAndRegistersUsers, ThrottlesLogins; public function showLoginForm() { return view(\u0026#39;user.login\u0026#39;);　//テンプレートの場所を変える } .. 管理画面では、\n... class AuthController extends BaseController { protected $guard = \u0026#39;admin_users\u0026#39;; protected $redirectTo = \u0026#39;admin/index\u0026#39;; // ログイン後のリダイレクト先 protected $redirectAfterLogout = \u0026#39;admin/login\u0026#39;; // ログアウト後のリダイレクト先 protected $username = \u0026#39;login\u0026#39;; // ログインとなるDBの項目名 protected $maxLoginAttempts = 5; // ログインスロットルとなるまで最高のログイン失敗回数 protected $lockoutTime = 60; // ログインスロットルとなってからの待ち秒数 use AuthenticatesUsers, ThrottlesLogins; public function showLoginForm() { $form = new \\stdClass(); $form-\u0026gt;login = Form::text(\u0026#39;login\u0026#39;, \u0026#39;\u0026#39;, [\u0026#39;size\u0026#39; =\u0026gt; 20, \u0026#39;maxlength\u0026#39; =\u0026gt; 20, \u0026#39;class\u0026#39; =\u0026gt; \u0026#39;en\u0026#39;, \u0026#39;autofocus\u0026#39; =\u0026gt; \u0026#39;autofocus\u0026#39;]); $form-\u0026gt;password = Form::password(\u0026#39;password\u0026#39;, [\u0026#39;size\u0026#39; =\u0026gt; 40, \u0026#39;maxlength\u0026#39; =\u0026gt; 20, \u0026#39;class\u0026#39; =\u0026gt; \u0026#39;en\u0026#39;]); return view(\u0026#39;admin.login\u0026#39;)-\u0026gt;with(compact(\u0026#39;form\u0026#39;)); //テンプレートの場所を変える } .. となります。認証が２つとなると、テンプレートなどいろいろな指定が必要となることに注意してください。上の例では、会員のログインは、emailですが、管理者のログインは、emailでなくてもよい文字列という仮定です。\n最後に、ログイン後にログインしたユーザーの情報がほしいときは、今まで、\nAuth::user()\nでしたが、マルチ認証となると、\nAuth::guard(\u0026lsquo;users\u0026rsquo;)-\u0026gt;user() Auth::guard(\u0026lsquo;admin_users\u0026rsquo;)-\u0026gt;user()\nと明確にガード名を指定する必用があります。\n","date":"2016-01-18T04:00:49+09:00","permalink":"https://www.larajapan.com/2016/01/18/%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E8%AA%8D%E8%A8%BC%EF%BC%88%EF%BC%91%EF%BC%90%EF%BC%89laravel-5-2-%E3%83%9E%E3%83%AB%E3%83%81%E8%AA%8D%E8%A8%BC/","title":"ユーザー認証（１０）Laravel 5.2 マルチ認証"},{"content":"先日、開発完了のLaravelのプログラムをインストールするときに、私だけのためにメンテナンス画面に裏口があったらいいなと思いました。\nLaravelでは、\nphp artisan down\nこのコマンドの実行で、すべてのアクセスを以下のようなメンテナンス画面にできます。そのモードに切り替えて、DBの変更とかなどのインストールの作業を誰にも邪魔されずにするのです。\nしかし、インストール後には、できる限りの動作テストを行いたいのですが、自分も含めて誰もアクセスできない状態は不都合です。せめて私や関係者だけ、つまりそれらのIPだけからは通常に見れるようにしたいのです。\n私がフレームワークなしの時代に開発したプログラムでは、すべてのプログラムに共通で最初に読み込まれるPHPファイルに、以下のようなコードで、私だけが閲覧できるようにしていました。\n$allow = array(\u0026#39;xxx.xxx.xxx.xxx\u0026#39;); if (isset($_SERVER)) // ウェブのアクセスのみ { if (isset($_SERVER[\u0026#34;REMOTE_ADDR\u0026#34;]) \u0026amp;\u0026amp; !in_array($_SERVER[\u0026#34;REMOTE_ADDR\u0026#34;], $allow)) { include_once \u0026#34;maintenance.php\u0026#34;; exit; } } 普段はこれらはコメントしておいて、メンテナンスのときにコメントを解除するのです。xxx.xxx.xxx.xxxは私の固定IPですが、そのIPだけが、メンテ画面を含むmaintenance.phpの実行をしないのです。\nこれと同じことをLaravelでできないでしょうか？\nちょっと調べたところ、以下のファイルにおいて、\napp/Http/Kernel.php class Kernel extends HttpKernel { /** * The application\u0026#39;s global HTTP middleware stack. * * @var array */ protected $middleware = [ \\Illuminate\\Foundation\\Http\\Middleware\\CheckForMaintenanceMode::class, \\App\\Http\\Middleware\\EncryptCookies::class, \\Illuminate\\Cookie\\Middleware\\AddQueuedCookiesToResponse::class, \\Illuminate\\Session\\Middleware\\StartSession::class, \\Illuminate\\View\\Middleware\\ShareErrorsFromSession::class, \\App\\Http\\Middleware\\VerifyCsrfToken::class, ]; ... vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/CheckForMaintenanceMode.php\nこのファイルがミドルウェアとして、メンテナンスのモードかどうかをチェックしているらしい。\nということで、そのファイルをコピーして、\napp/Http/Middleware/CheckForMaintenanceMode.php\nとして、その中味を以下のように編集します。\napp/Http/Middleware/CheckForMaintenanceMode.php namespace App\\Http\\Middleware; use Closure; use Illuminate\\Contracts\\Foundation\\Application; use Symfony\\Component\\HttpKernel\\Exception\\HttpException; class CheckForMaintenanceMode { /** * The application implementation. * * @var \\Illuminate\\Contracts\\Foundation\\Application */ protected $app; protected $allow = [\u0026#39;xxx.xxx.xxx.xxx\u0026#39;]; /** * Create a new middleware instance. * * @param \\Illuminate\\Contracts\\Foundation\\Application $app * @return void */ public function __construct(Application $app) { $this-\u0026gt;app = $app; } /** * Handle an incoming request. * * @param \\Illuminate\\Http\\Request $request * @param \\Closure $next * @return mixed */ public function handle($request, Closure $next) { if ($this-\u0026gt;app-\u0026gt;isDownForMaintenance()) { if (!in_array($request-\u0026gt;getClientIp(), $allow)) { throw new HttpException(503); } } return $next($request); } } 基本的には、先のコードと同様なメカニズムです。\nそして先のKernel.phpを\napp/Kernel.php ... protected $middleware = [ \\App\\Http\\Middleware\\CheckForMaintenanceMode::class, \\App\\Http\\Middleware\\EncryptCookies::class, \\Illuminate\\Cookie\\Middleware\\AddQueuedCookiesToResponse::class, \\Illuminate\\Session\\Middleware\\StartSession::class, \\Illuminate\\View\\Middleware\\ShareErrorsFromSession::class, \\App\\Http\\Middleware\\VerifyCsrfToken::class, ]; ... と編集する。簡単な変更ですが有用です。\nちなみに、メンテナンスの解除をするには、\nphp artisan up\nこれを実行するのみなのですが、ちょっとメカニズムに興味ありますね。\n調べたところ、downでは、\nstorage/framework/down\nという空のファイルを作成されます。そしてupの実行ではこのファイルが削除されます。意外とシンプルですね。\n最後に、メンテナンスの画面は以下のファイルを編集します。\nresources/views/errors/503.blade.php\n","date":"2016-01-10T07:17:25+09:00","permalink":"https://www.larajapan.com/2016/01/10/%E3%83%A1%E3%83%B3%E3%83%86%E3%83%8A%E3%83%B3%E3%82%B9%E7%94%BB%E9%9D%A2%E3%81%AE%E8%A3%8F%E5%8F%A3/","title":"メンテナンス画面の裏口"},{"content":"Laravelの5.2が登場してきました。いくつか興味ある変更がありますが、ユーザ認証に関してはスタート地点が身近になりました。\n以下のコマンドを実行するだけで、\nphp artisan make:auth\n以下を自動的に作成してくれます。\n画面とEメールのテンプレート ログイン画面 ユーザー登録画面 パスワードリセット情報送信画面 パスワードリセットのEメール パスワードリセット画面 ホーム画面（ログイン後） ホームコントローラ（HomeController.php) ルータの設定の変更 (routes.php) データベースを用意して、.envの設定ファイルを編集すれば、以下のように。\n画面は、Bootstrapを使用していて、ちょっと編集すればすぐに実践で使えそうです。\n","date":"2015-12-28T07:09:22+09:00","permalink":"https://www.larajapan.com/2015/12/28/%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E8%AA%8D%E8%A8%BC%EF%BC%88%EF%BC%99%EF%BC%89laravel-5-2-%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%E8%87%AA%E5%8B%95%E4%BD%9C%E6%88%90/","title":"ユーザー認証（９）Laravel 5.2 コンポーネント自動作成"},{"content":"まだまだ続くユーザ認証。今まではいわば基礎編みたいなもので、教科書通りの紹介。今回は、実践として、こういうときはどうする？みたいな応用編です。\nまずはDBテーブルusersの構成を見るところから。\n+----------------+------------------+------+-----+---------------------+----------------+ | Field | Type | Null | Key | Default | Extra | +----------------+------------------+------+-----+---------------------+----------------+ | id | int(10) unsigned | NO | PRI | NULL | auto_increment | | active_flag | char(1) | NO | | | | | name | varchar(255) | NO | | NULL | | | email | varchar(255) | NO | UNI | NULL | | | password | varchar(60) | NO | | NULL | | | remember_token | varchar(100) | YES | | NULL | | | created_at | timestamp | NO | | 0000-00-00 00:00:00 | | | updated_at | timestamp | NO | | 0000-00-00 00:00:00 | | +----------------+------------------+------+-----+---------------------+----------------+ ごく簡単な構成です。しかし、実際にはユーザーはすでに退会したかとか、問題があるユーザーをブロックする必要がある、のような理由で、有効・無効のフラッグをつけるのが一般的です。\nということで、active_flagとしてusersのフィールドを追加してみましょう。このactive_flagは、char(1)として有効はY、無効はNの値とします。\nmigrationを作成して、以下のように編集して実行。\nuse Illuminate\\Database\\Schema\\Blueprint; use Illuminate\\Database\\Migrations\\Migration; class AddActiveFlagToUsers extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::table(\u0026#39;users\u0026#39;, function ($table) { $table-\u0026gt;char(\u0026#39;active_flag\u0026#39;, 1)-\u0026gt;after(\u0026#39;id\u0026#39;); }); } ... } さて、今度は認証のプログラムの変更です。ユーザーのログインとパスワードがマッチかつユーザーが有効のときだけに認証を成功としたいです。さて、どこを変更すればよいのでしょうか？\nこれは以下のAuthController.phpにトレイトで定義されているメソッドの追加となります。\nnamespace App\\Http\\Controllers\\Auth; use App\\User; use Validator; use App\\Http\\Controllers\\Controller; use Illuminate\\Foundation\\Auth\\ThrottlesLogins; use Illuminate\\Foundation\\Auth\\AuthenticatesAndRegistersUsers; use Illuminate\\Http\\Request; class AuthController extends Controller { use AuthenticatesAndRegistersUsers, ThrottlesLogins { getCredentials as getCredentialsTrait; } public function __construct() { $this-\u0026gt;middleware(\u0026#39;guest\u0026#39;, [\u0026#39;except\u0026#39; =\u0026gt; \u0026#39;getLogout\u0026#39;]); $this-\u0026gt;maxLoginAttempts = 5; $this-\u0026gt;lockoutTime = 60; } protected function getCredentials(Request $request) { $credentials = $this-\u0026gt;getCredentialsTrait($request); $credentials[\u0026#39;active_flag\u0026#39;] = \u0026#39;Y\u0026#39;; return $credentials; } ... getCredentialsは、AuthenticateAndRegistsUsersトレイトの中で使用されているAuthenticatesUsersのトレイト。以下を参照してください。\nユーザー認証（３）ログイン・ログアウト\nここで同じ名前のメソッドを使用して、クレデンシャルにユーザーが有効、つまり　active_flag =\u0026gt; \u0026lsquo;Y\u0026rsquo;という条件を追加したいです。 しかし、ここではまず、すでに定義されているgetCredentialsの関数をコールしてからの追加としたい。問題は、\nこれでは、再帰となるし、\n$credentials = $this-\u0026gt;getCredentials($request); スタテックのメソッドでも、継承したクラスのメソッドでもないのでこうともできない。\n$credentials = parent::getCredentials($request); それゆえに、\nuse AuthenticatesAndRegistersUsers, ThrottlesLogins { getCredentials as getCredentialsTrait; } として、オリジナルのメソッド名を改名して、\n$credentials = $this-\u0026gt;getCredentialsTrait($request); となったわけです。\n","date":"2015-12-27T12:33:42+09:00","permalink":"https://www.larajapan.com/2015/12/27/%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E8%AA%8D%E8%A8%BC%EF%BC%88%EF%BC%98%EF%BC%89%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E3%81%AE%E6%9C%89%E5%8A%B9%E3%83%BB%E7%84%A1%E5%8A%B9%E3%82%92%E8%80%83%E6%85%AE/","title":"ユーザー認証（８）ユーザーの有効・無効を考慮"},{"content":"前回話したログインのスロットル機能。もちろん手動、つまりブラウザ、で確認はできます。しかし、手動ではいちいち面倒ですね。自動で動作を確認できたらもっと良いです。そこで登場するのがユニットテスト。難しくはないです。チェックしたいのは、\nパスワードが正しいときのログインが成功するかのテスト パスワードを間違えて、失敗回数がデフォルトの５回となったときに、スロットされるかのテスト その後デフォルトの６０秒待って、パスワードが正しいときに成功するかのテスト 他にもいくつか考えられますが、最低限は以上としてテストを作成してみましょう。\n注意：パスワードには理解しやすいように日本語としていますが、実際は英数字が一般的です。\nclass LoginTest extends TestCase { public function testLoginSuccess() { $this-\u0026gt;visit(\u0026#39;/auth/login\u0026#39;) -\u0026gt;type(\u0026#39;test@gmail.com\u0026#39;, \u0026#39;email\u0026#39;) -\u0026gt;type(\u0026#39;正しいパスワード\u0026#39;, \u0026#39;password\u0026#39;) -\u0026gt;press(\u0026#39;保存\u0026#39;) -\u0026gt;see(\u0026#39;ホームページ\u0026#39;); } public function testLoginThrottle() { for($i=1;$i \u0026lt;=5; $i++) { $this-\u0026gt;visit(\u0026#39;/auth/login\u0026#39;) -\u0026gt;type(\u0026#39;test@gmail.com\u0026#39;, \u0026#39;email\u0026#39;) -\u0026gt;type(\u0026#39;間違ったパスワード\u0026#39;, \u0026#39;password\u0026#39;) -\u0026gt;press(\u0026#39;ログイン\u0026#39;) -\u0026gt;see(\u0026#39;Eメールとパスワードにマッチするレコードがありません。\u0026#39;); } $this-\u0026gt;visit(\u0026#39;/auth/login\u0026#39;) -\u0026gt;type(\u0026#39;test@gmail.com\u0026#39;, \u0026#39;email\u0026#39;) -\u0026gt;type(\u0026#39;間違ったパスワード\u0026#39;, \u0026#39;password\u0026#39;) -\u0026gt;press(\u0026#39;ログイン\u0026#39;) -\u0026gt;see(\u0026#39;ログインの失敗回数が設定を超えました。次回のログインまで６０秒お待ちください。\u0026#39;); sleep(10); $this-\u0026gt;visit(\u0026#39;/auth/login\u0026#39;) -\u0026gt;type(\u0026#39;test@gmail.com\u0026#39;, \u0026#39;email\u0026#39;) -\u0026gt;type(\u0026#39;正しいパスワード\u0026#39;, \u0026#39;password\u0026#39;) -\u0026gt;press(\u0026#39;ログイン\u0026#39;) -\u0026gt;see(\u0026#39;ログインの失敗回数が設定を超えました。\u0026#39;); sleep(50); $this-\u0026gt;visit(\u0026#39;/auth/login\u0026#39;) -\u0026gt;type(\u0026#39;test@gmail.com\u0026#39;, \u0026#39;email\u0026#39;) -\u0026gt;type(\u0026#39;正しいパスワード\u0026#39;, \u0026#39;password\u0026#39;) -\u0026gt;press(\u0026#39;ログイン\u0026#39;) -\u0026gt;see(\u0026#39;ホームページ\u0026#39;); } } 最初のテスト、testLoginSuccessは、正しいパスワードを入力したときのテストで、その後「ホームページ」という文が入ったページへ行くとする仮定。\n次のtestLoginThrottleは、スロットルされるまでの失敗回数の確認とスロットル解除の確認のテスト。まず間違ったパスワードを入れて５回失敗させます。画面には、「Eメールとパスワードにマッチするレコードがありません」のエラーメッセージが表示されます。その後、さらに失敗すると、今度はログインをスロットルしたメッセージが表示されます。ここから６０秒経過しないと次のログインができません。確認のために１０秒後に正しいパスワードでログインしてみましょう。そこでは、さらに「ログインの失敗回数が設定を超えました。」を含むエラーメッセージがでます。残りの秒数は誤差が出ると思うので、その部分の確認はしません。そして、５０秒後（つまり、スロットル開始から６０秒後）には、正しいパスワードでログインが成功となるはずです。\nさて、ここで注意です。Laravelに含まれるphpunitのテストの設定ファイルは、以下の内容です。\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;phpunit backupGlobals=\u0026#34;false\u0026#34; backupStaticAttributes=\u0026#34;false\u0026#34; bootstrap=\u0026#34;bootstrap/autoload.php\u0026#34; colors=\u0026#34;true\u0026#34; convertErrorsToExceptions=\u0026#34;true\u0026#34; convertNoticesToExceptions=\u0026#34;true\u0026#34; convertWarningsToExceptions=\u0026#34;true\u0026#34; processIsolation=\u0026#34;false\u0026#34; stopOnFailure=\u0026#34;false\u0026#34;\u0026gt; \u0026lt;testsuites\u0026gt; \u0026lt;testsuite name=\u0026#34;Application Test Suite\u0026#34;\u0026gt; \u0026lt;directory\u0026gt;./tests/\u0026lt;/directory\u0026gt; \u0026lt;/testsuite\u0026gt; \u0026lt;/testsuites\u0026gt; \u0026lt;filter\u0026gt; \u0026lt;whitelist\u0026gt; \u0026lt;directory suffix=\u0026#34;.php\u0026#34;\u0026gt;app/\u0026lt;/directory\u0026gt; \u0026lt;/whitelist\u0026gt; \u0026lt;/filter\u0026gt; \u0026lt;php\u0026gt; \u0026lt;env name=\u0026#34;APP_ENV\u0026#34; value=\u0026#34;testing\u0026#34;/\u0026gt; \u0026lt;env name=\u0026#34;CACHE_DRIVER\u0026#34; value=\u0026#34;array\u0026#34;/\u0026gt; \u0026lt;env name=\u0026#34;SESSION_DRIVER\u0026#34; value=\u0026#34;array\u0026#34;/\u0026gt; \u0026lt;env name=\u0026#34;QUEUE_DRIVER\u0026#34; value=\u0026#34;sync\u0026#34;/\u0026gt; \u0026lt;/php\u0026gt; \u0026lt;/phpunit\u0026gt; 前回話したように、ログイン失敗回数や現在スロットルされていることなどの情報は、デフォルトではファイルを使用して行っています。しかし、テストでは CACHE_DRIVERは配列設定となっています。私のテストでは、そのためか上のテストはスロットル期間終了後に成功とはなりませんでした。どうも残りの秒数のカウントが負となっていしまい、うまくいかない。\nこの設定をphpunit.xmlでarrayからfileあるいはdatabaseに変更とすると、うまく行きました。\n","date":"2015-12-20T07:30:44+09:00","permalink":"https://www.larajapan.com/2015/12/20/%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E8%AA%8D%E8%A8%BC%EF%BC%88%EF%BC%97%EF%BC%89%E3%82%B9%E3%83%AD%E3%83%83%E3%83%88%E3%83%AB%E3%81%AE%E3%83%A6%E3%83%8B%E3%83%83%E3%83%88%E3%83%86%E3%82%B9%E3%83%88/","title":"ユーザー認証（７）スロットルのユニットテスト"},{"content":"スロットルと聞くと、どうしてもバイクのアクセルを想像してしまいます。どちらかというとスピードを出すために。スロットルは正確にはスピードを抑圧する意味で、「スロットル全開」というと、抑制なしで最高のスピードを出すということになります。\nここでのスロットルは、パスワードを意図的に変えて不正にログインしようという試み、たいていはプログラムによる機械的な攻撃、を抑える目的のセキュリティ対策です。具体的には、例えば過去５分間に５回ログインを失敗すると、次回のログインには１０分間待たなければならないという仕組みです。もちろんそれですべて防御できるとは言えないですが、少なくとも抑制にはなります。\n嬉しいことにこれも、Laravelに機能があります。\nスロットル機能はログインで行われるので、以前に説明したログインのプログラムを再度見てみましょう。\nまず、AuthControler。\napp/Http/Controllers/Auth/AuthController.php namespace App\\Http\\Controllers\\Auth; use App\\User; use Validator; use App\\Http\\Controllers\\Controller; use Illuminate\\Foundation\\Auth\\ThrottlesLogins; use Illuminate\\Foundation\\Auth\\AuthenticatesAndRegistersUsers; class AuthController extends Controller { use AuthenticatesAndRegistersUsers, ThrottlesLogins; ... ThrottlesLoginsのトレイトがありますね。そして、AuthenticatesAndRegistersUsersのトレイトの定義で使用されている、AuthenticatesUsersのトレイト、以前の紹介ではスロットルの部分を省略したので、今度はそこの部分を含めて見てみましょう。以下のコードのコメントを見てください。\nvendors/framework/src/Illuminate/Foundation/Auth/AuthenticateUsers.php namespace Illuminate\\Foundation\\Auth; use Illuminate\\Http\\Request;ろぐ use Illuminate\\Support\\Facades\\Auth; use Illuminate\\Support\\Facades\\Lang; trait AuthenticatesUsers { use RedirectsUsers; ... public function postLogin(Request $request) { $this-\u0026gt;validate($request, [ $this-\u0026gt;loginUsername() =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, ]); // ThrottlesLoginsは、上のAuthControllerのトレイトして使われているから、$throttleには値を含みます。 $throttles = $this-\u0026gt;isUsingThrottlesLoginsTrait(); //ここで指定のログインの失敗回数を超えているなら、 //画面にエラーメッセージを表示して次のログインを指定の時間待たせます。 if ($throttles \u0026amp;\u0026amp; $this-\u0026gt;hasTooManyLoginAttempts($request)) { return $this-\u0026gt;sendLockoutResponse($request); } //入力の値でログインを試みます。 $credentials = $this-\u0026gt;getCredentials($request); if (Auth::attempt($credentials, $request-\u0026gt;has(\u0026#39;remember\u0026#39;))) { return $this-\u0026gt;handleUserWasAuthenticated($request, $throttles); } //ログインが失敗なら、ログインの失敗回数を１増やします。 if ($throttles) { $this-\u0026gt;incrementLoginAttempts($request); } return redirect($this-\u0026gt;loginPath()) -\u0026gt;withInput($request-\u0026gt;only($this-\u0026gt;loginUsername(), \u0026#39;remember\u0026#39;)) -\u0026gt;withErrors([ $this-\u0026gt;loginUsername() =\u0026gt; $this-\u0026gt;getFailedLoginMessage(), ]); } ... } というようにログインの失敗回数を追跡しています。\nさて、何回まで失敗とか失敗回数を超過したら何分ログインを許さないかはどこで指定されているのでしょう。今度はトレイトのThrottleLoginsを見てみましょう。\nvendors/framework/src/Illuminate/Foundation/Auth/ThrottleLogins.php namespace Illuminate\\Foundation\\Auth; use Illuminate\\Http\\Request; use Illuminate\\Cache\\RateLimiter; use Illuminate\\Support\\Facades\\Lang; trait ThrottlesLogins { //ここです、失敗回数をチェックしているのは。 protected function hasTooManyLoginAttempts(Request $request) { return app(RateLimiter::class)-\u0026gt;tooManyAttempts( $request-\u0026gt;input($this-\u0026gt;loginUsername()).$request-\u0026gt;ip(), $this-\u0026gt;maxLoginAttempts(), $this-\u0026gt;lockoutTime() / 60 ); } ... protected function maxLoginAttempts() { return property_exists($this, \u0026#39;maxLoginAttempts\u0026#39;) ? $this-\u0026gt;maxLoginAttempts : 5; } protected function lockoutTime() { return property_exists($this, \u0026#39;lockoutTime\u0026#39;) ? $this-\u0026gt;lockoutTime : 60; } } 最初のhasTooManyLoginAttemptsのメソッドでは、maxLoginAttemptsとlockoutTimeのメソッドがコールされていますね。\nそれぞれ、ここのクラスのインスタンスで、maxLoginAttemptsとlockoutTimeの変数が定義されているなら、その値を使いますが、定義なしでは、最高５回、ログインロックは６０秒という設定です。\nまた、失敗回数は、ログイン＋IPアドレスの値がキーとなっています。つまり、同じログインで同じIPからログインを試みて連続５回失敗したら、１分のログインロックとなるということです。\n最後に、これらの失敗回数の追跡やロックアウトの保持のデータは、どこで管理しているのでしょう。\n先のコードで以下の参照をみてください。\nuse Illuminate\\Cache\\RateLimiter;\nCacheとして管理しているようです。Laravelでは、Cacheはデフォルトでファイルで管理しています。しかし、ファイルではなくデータベースや高速なメムキャッシュでのデータの管理も可能です。\nconfig/cache.phpにおいて設定するか、.envにおいて、\nCACHE_DRIVER=database\nと設定することも可能です。データベースと設定するなら、以下のようにmigrationを通して、\nuse Illuminate\\Database\\Schema\\Blueprint; use Illuminate\\Database\\Migrations\\Migration; class CreateCacheTable extends Migration { public function up() { Schema::create(\u0026#39;cache\u0026#39;, function($table) { $table-\u0026gt;string(\u0026#39;key\u0026#39;)-\u0026gt;unique(); $table-\u0026gt;text(\u0026#39;value\u0026#39;); $table-\u0026gt;integer(\u0026#39;expiration\u0026#39;); }); } public function down() { // } } としてcacheテーブルを作成してください。ここで以下のようにデータベースの中身を見ながらテストをすると、実際どのようなレコードが作成されるかわかります。\n例えば、ログインに失敗すると、以下のようなレコードが作成されます。\nmysql\u0026gt; select * from cache\\G; *************************** 1. row *************************** key: laraveltest@gmail.com66.87.77.84 value: eyJpdiI6IlJQN1Awck9BVzlTWXdnMk4wb0ZOMnc9PSIsInZhb... expiration: 1449946110 *************************** 2. row *************************** key: laraveltest@gmail.com66.87.77.84:lockout value: eyJpdiI6Im5pMmg0dm1iREl0TFIwNkkzZTV1V2c9PSIsInZhb... expiration: 1449946259 2 rows in set (0.00 sec) 最初のレコードは、失敗回数を数えているレコードです。キー(key)として、Eメールアドレス＋IPアドレスとなっています。キーの値（value）は暗号化されています。回数が設定を超えると、２番目のレコードが作成されます。キーの最後に:lockoutが追加されていることに注意してください。こちらのレコードの期限は、設定のブロック時間（デフォルトは６０秒）を経過した日時となっています。期限が過ぎてアクセスしたときに、これらのレコードは削除されて再度、失敗回数を追跡してきます。\n","date":"2015-12-13T03:58:30+09:00","permalink":"https://www.larajapan.com/2015/12/13/%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E8%AA%8D%E8%A8%BC%EF%BC%88%EF%BC%96%EF%BC%89%E3%83%AD%E3%82%B0%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%AD%E3%83%83%E3%83%88%E3%83%AB/","title":"ユーザー認証（６）ログインスロットル"},{"content":"ユーザーログインがあるなら、パスワードを忘れることがあるのは当然。忘れたらなら、通常はログイン（たいていはEメール）を入力して、パスワードのリセットのリンクを受け取り、リンク先の画面で新規のパスワードを設定します。新しいパスワードを作成して送信してくるサイトもあります。しかし、良く利用するサイトなら、やはり自分が覚えられるパスワードを設定したいです。\nそう、この機能もLaravelで提供しています。\nまずは、この機能に必要なものをリスト。\nパスワードリセットリンクを送信してもらう画面 パスワードリセットリンクを含むメールのテンプレート 送信するパスワードリセットリンクの有効期限を設定し保持するDBテーブル パスワードリセットリンク先の画面で、新規パスワードを設定する画面 以上です。\nパスワードリセットリンクを送信してもらう画面 routes.phpの設定から見てみましょう。\nRoute::get(\u0026#39;password/email\u0026#39;,\u0026#39;Auth\\PasswordController@getEmail\u0026#39;); Route::post(\u0026#39;password/email\u0026#39;, \u0026#39;Auth\\PasswordController@postEmail\u0026#39;); PasswordController.phpは、AuthController.phpと同様にlaravelインストール時に app/Http/Controllers/Authに存在します。\n中身は以下にあるように、これまたAuthController.phpと同様にトレイトで構成されているのでほぼ空状態。\nnamespace App\\Http\\Controllers\\Auth; use App\\Http\\Controllers\\Controller; use Illuminate\\Foundation\\Auth\\ResetsPasswords; class PasswordController extends Controller { use ResetsPasswords; public function __construct() { $this-\u0026gt;middleware(\u0026#39;guest\u0026#39;); } } ミドルウェアguestが使用されているので、すでにログインしているなら使用できません。\nテンプレートは、reources/views/auth/password.blade.phpのファイルとします。\n\u0026lt;form method=\u0026#34;POST\u0026#34; action=\u0026#34;{!! url() !!}/password/email\u0026#34;\u0026gt; {!! csrf_field() !!} \u0026lt;div\u0026gt; Eメール \u0026lt;input type=\u0026#34;email\u0026#34; name=\u0026#34;email\u0026#34; value=\u0026#34;{{ old(\u0026#39;email\u0026#39;) }}\u0026#34;\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div\u0026gt; \u0026lt;button type=\u0026#34;submit\u0026#34;\u0026gt;パスワードリセットリンクを送信\u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/form\u0026gt; これで画面の表示までは完了。次は、送信ボタンを押したときの処理。以下の３つ作業が必要です。\n入力バリデーション。Eメールのフォーマットのチェックだけでなく、ユーザーとしてそのEメールがDBに存在するかのチェック ユニークなトークンを発行し、EメールとともにDBに保存 パスワードリセットの画面のURLとトークンを合わせてリンクとして、ユーザーのEメールに送信 この３つの作業が行われているか、PasswordControllerで使用されるトレイトのResetsPasswordsを見てみましょう。\nvendor/laravel/framework/src/Illuminate/Foundation/Auth/ResetPasswords.php namespace Illuminate\\Foundation\\Auth; use Illuminate\\Http\\Request; use Illuminate\\Mail\\Message; use Illuminate\\Support\\Facades\\Auth; use Illuminate\\Support\\Facades\\Password; use Symfony\\Component\\HttpKernel\\Exception\\NotFoundHttpException; trait ResetsPasswords { public function getEmail() { return view(\u0026#39;auth.password\u0026#39;); } public function postEmail(Request $request) { $this-\u0026gt;validate($request, [\u0026#39;email\u0026#39; =\u0026gt; \u0026#39;required|email\u0026#39;]); $response = Password::sendResetLink($request-\u0026gt;only(\u0026#39;email\u0026#39;), function (Message $message) { $message-\u0026gt;subject($this-\u0026gt;getEmailSubject()); }); switch ($response) { case Password::RESET_LINK_SENT: return redirect()-\u0026gt;back()-\u0026gt;with(\u0026#39;status\u0026#39;, trans($response)); case Password::INVALID_USER: return redirect()-\u0026gt;back()-\u0026gt;withErrors([\u0026#39;email\u0026#39; =\u0026gt; trans($response)]); } } ... postEmailがあり画面の入力を受け取り処理しています。入力したEメールのバリデーションがあります。しかしここでは、その他の作業は皆、Password::sendRsetLinkに任せています。もうちょっと追及してみましょう。\nPassword::sendResetLinkは、PasswordのクラスはFacadeで、実際は以下のPasswordBrokerのクラスが使用されています。sendResetLinkの定義がありますね。\nvendor/laravel/framework/src/Illuminate/Auth/Passwors/PasswordBroker.php namespace Illuminate\\Auth\\Passwords; use Closure; use UnexpectedValueException; use Illuminate\\Contracts\\Auth\\UserProvider; use Illuminate\\Contracts\\Mail\\Mailer as MailerContract; use Illuminate\\Contracts\\Auth\\PasswordBroker as PasswordBrokerContract; use Illuminate\\Contracts\\Auth\\CanResetPassword as CanResetPasswordContract; class PasswordBroker implements PasswordBrokerContract { protected $tokens; protected $users; protected $mailer; protected $emailView; protected $passwordValidator; public function __construct(TokenRepositoryInterface $tokens, UserProvider $users, MailerContract $mailer, $emailView) { $this-\u0026gt;users = $users; $this-\u0026gt;mailer = $mailer; $this-\u0026gt;tokens = $tokens; $this-\u0026gt;emailView = $emailView; } public function sendResetLink(array $credentials, Closure $callback = null) { $user = $this-\u0026gt;getUser($credentials); if (is_null($user)) { return PasswordBrokerContract::INVALID_USER; } $token = $this-\u0026gt;tokens-\u0026gt;create($user); $this-\u0026gt;emailResetLink($user, $token, $callback); return PasswordBrokerContract::RESET_LINK_SENT; } public function emailResetLink(CanResetPasswordContract $user, $token, Closure $callback = null) { $view = $this-\u0026gt;emailView; return $this-\u0026gt;mailer-\u0026gt;send($view, compact(\u0026#39;token\u0026#39;, \u0026#39;user\u0026#39;), function ($m) use ($user, $token, $callback) { $m-\u0026gt;to($user-\u0026gt;getEmailForPasswordReset()); if (! is_null($callback)) { call_user_func($callback, $m, $user, $token); } }); } ... $this-\u0026gt;getUserでDBテーブルusersに入力したEメールのレコードを取得して、$this-\u0026gt;tokens-\u0026gt;createでトークンを作成しDBに保存し、$this-\u0026gt;emailResetLinkでパスワードリセットリンクを含むメールを送信します。マッチするDBレコードが存在しないならエラーコードを返して画面にエラーを表示となります。\nここにおいてメール送信に関していくつか。\nまず、メール送信が行われるので、.envにおいてMAIL_DRIVERなどの設定が必要です。 さらに、config/mail.phpにおいて、fromの設定が必要です。\n... \u0026#39;from\u0026#39; =\u0026gt; [\u0026#39;address\u0026#39; =\u0026gt; null, \u0026#39;name\u0026#39; =\u0026gt; null], ... 上のnullには、例えば、\u0026lsquo;support@gmail.com\u0026rsquo;、\u0026lsquo;サポート\u0026rsquo;のような具体的な値に置き換える必要あります。\n次に、送信メールのテンプレートをresources/views/emails/password.blade.phpとして作成する必要あります。\n以下のリンクをクリックして、パスワードのリセットができます。 {{ url(\u0026#39;password/reset/\u0026#39;.$token) } パスワードリセットリンク先の画面で、新規パスワードを設定する画面 さて、次はこのリンク先のパスワードリセット画面です。\nまずは、routes.phpの設定から、\nRoute::get(\u0026#39;password/reset/{token}\u0026#39;, \u0026#39;Auth\\PasswordController@getReset\u0026#39;); Route::post(\u0026#39;password/reset\u0026#39;, \u0026#39;Auth\\PasswordController@postReset\u0026#39;); こちらもまた、PasswordControllerですね。\nテンプレートは、reources/views/auth/reset.blade.phpのファイルとします。hiddenのtokenを忘れなく。\n\u0026lt;form method=\u0026#34;POST\u0026#34; action=\u0026#34;{!! url() !!}/password/reset\u0026#34;\u0026gt; {!! csrf_field() !!} \u0026lt;input type=\u0026#34;hidden\u0026#34; name=\u0026#34;token\u0026#34; value=\u0026#34;{{ $token }}\u0026#34;\u0026gt; \u0026lt;div\u0026gt; Eメール \u0026lt;input type=\u0026#34;email\u0026#34; name=\u0026#34;email\u0026#34; value=\u0026#34;{{ old(\u0026#39;email\u0026#39;) }}\u0026#34;\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div\u0026gt; パスワード \u0026lt;input type=\u0026#34;password\u0026#34; name=\u0026#34;password\u0026#34;\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div\u0026gt; パスワードの確認 \u0026lt;input type=\u0026#34;password\u0026#34; name=\u0026#34;password_confirmation\u0026#34;\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div\u0026gt; \u0026lt;button type=\u0026#34;submit\u0026#34;\u0026gt;パスワードをリセット\u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/form\u0026gt; さて、この画面でボタンをクリックしたときの処理は、\n入力バリデーション。Eメールやパスワードの値チェック。EメールがDBに存在するかのチェック DBのパスワードの値を暗号化して更新 でしょうか？見てみましょう。\nまずは、先ほど出てきたトレイトのResetPasswords.php\nvendor/laravel/framework/src/Illuminate/Foundation/Auth/ResetPasswords.php ... public function getReset($token = null) { if (is_null($token)) { throw new NotFoundHttpException; } return view(\u0026#39;auth.reset\u0026#39;)-\u0026gt;with(\u0026#39;token\u0026#39;, $token); } .. public function postReset(Request $request) { $this-\u0026gt;validate($request, [ \u0026#39;token\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;required|email\u0026#39;, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;required|confirmed|min:6\u0026#39;, ]); $credentials = $request-\u0026gt;only( \u0026#39;email\u0026#39;, \u0026#39;password\u0026#39;, \u0026#39;password_confirmation\u0026#39;, \u0026#39;token\u0026#39; ); $response = Password::reset($credentials, function ($user, $password) { $this-\u0026gt;resetPassword($user, $password); }); switch ($response) { case Password::PASSWORD_RESET: return redirect($this-\u0026gt;redirectPath())-\u0026gt;with(\u0026#39;status\u0026#39;, trans($response)); default: return redirect()-\u0026gt;back() -\u0026gt;withInput($request-\u0026gt;only(\u0026#39;email\u0026#39;)) -\u0026gt;withErrors([\u0026#39;email\u0026#39; =\u0026gt; trans($response)]); } } ... 入力バリデーションには、トークンが必須ですね。そして、Password::resetで処理を行い、その結果$responseが成功なら、指定のページにリダイレクト。エラーなら、同画面でエラー表示。\nPassword::resetの中身ですね、問題は。\nこちらも、先のPasswordResetBroker.phpでコードされています。\nvendor/laravel/framework/src/Illuminate/Auth/Passwors/PasswordResetBroker.php ... public function reset(array $credentials, Closure $callback) { $user = $this-\u0026gt;validateReset($credentials); if (! $user instanceof CanResetPasswordContract) { return $user; } $pass = $credentials[\u0026#39;password\u0026#39;]; call_user_func($callback, $user, $pass); $this-\u0026gt;tokens-\u0026gt;delete($credentials[\u0026#39;token\u0026#39;]); return PasswordBrokerContract::PASSWORD_RESET; } protected function validateReset(array $credentials) { if (is_null($user = $this-\u0026gt;getUser($credentials))) { return PasswordBrokerContract::INVALID_USER; } if (! $this-\u0026gt;validateNewPassword($credentials)) { return PasswordBrokerContract::INVALID_PASSWORD; } if (! $this-\u0026gt;tokens-\u0026gt;exists($user, $credentials[\u0026#39;token\u0026#39;])) { return PasswordBrokerContract::INVALID_TOKEN; } return $user; } public function validateNewPassword(array $credentials) { list($password, $confirm) = [ $credentials[\u0026#39;password\u0026#39;], $credentials[\u0026#39;password_confirmation\u0026#39;], ]; if (isset($this-\u0026gt;passwordValidator)) { return call_user_func( $this-\u0026gt;passwordValidator, $credentials) \u0026amp;\u0026amp; $password === $confirm; } return $this-\u0026gt;validatePasswordWithDefaults($credentials); } ... resetでは、$this-\u0026gt;validateResetで、ユーザーレコードの有無。新規パスワードのバリデーション、そしてトークンの存在と有効期限内かのチェックを行い、callbackでDBレコードのパスワードの更新を行います。そして最後に使用したトークンの削除となります。なるほど、削除しないと再度のリセットが可能になるので必要なわけですね。\n","date":"2015-12-06T07:47:28+09:00","permalink":"https://www.larajapan.com/2015/12/06/%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E8%AA%8D%E8%A8%BC%EF%BC%88%EF%BC%95%EF%BC%89%E3%83%91%E3%82%B9%E3%83%AF%E3%83%BC%E3%83%89%E3%83%AA%E3%82%BB%E3%83%83%E3%83%88/","title":"ユーザー認証（５）パスワードリセット"},{"content":"ユーザー認証の目的は、ユーザー本人であることを確認し、ユーザーのプライベートの情報を他に見られることを防ぐことです。\nLaravelでは、アプリにおけるすべてのルートを設定するroutes.phpのファイルにおいて、ミドルウェアを利用して保護するページを指定します。\napp/Http/routes.php Route::get(\u0026#39;/home\u0026#39;, [\u0026#39;middleware\u0026#39; =\u0026gt; \u0026#39;auth\u0026#39;, function () { return view(\u0026#39;auth.home\u0026#39;); }]); 上の例では、/homeを保護するために、ミドルウェアのauthを使用しています。authは、以下のKernel.phpで登録されているサービスです。\napp/Http/Kernel.php namespace App\\Http; use Illuminate\\Foundation\\Http\\Kernel as HttpKernel; class Kernel extends HttpKernel { protected $middleware = [ ... ]; protected $routeMiddleware = [ \u0026#39;auth\u0026#39; =\u0026gt; \\App\\Http\\Middleware\\Authenticate::class, \u0026#39;auth.basic\u0026#39; =\u0026gt; \\Illuminate\\Auth\\Middleware\\AuthenticateWithBasicAuth::class, \u0026#39;guest\u0026#39; =\u0026gt; \\App\\Http\\Middleware\\RedirectIfAuthenticated::class, ]; } そこでバインドされているクラスは、以下のAuthenticate.phpで定義されています。\napp/Http/Middleware/Authenticate.php namespace App\\Http\\Middleware; use Closure; use Illuminate\\Contracts\\Auth\\Guard; class Authenticate { protected $auth; public function __construct(Guard $auth) { $this-\u0026gt;auth = $auth; } public function handle($request, Closure $next) { if ($this-\u0026gt;auth-\u0026gt;guest()) { if ($request-\u0026gt;ajax()) { return response(\u0026#39;Unauthorized.\u0026#39;, 401); } else { return redirect()-\u0026gt;guest(\u0026#39;auth/login\u0026#39;); } } return $next($request); } } handle()内での、$this-\u0026gt;auth-\u0026gt;guest()は、ユーザーがゲスト、つまり認証されていないユーザーであるかどうかをチェックします。認証されていないなら、auth/loginにリダイレクトして、ログイン画面が表示されます。\nroutes.phpでのページ保護は、Route::groupで行うこともできます。例えば、以下のように複数のルートをまとめることができます。わかりやすいですね。\nRoute:: group([\u0026#39;prefix\u0026#39; =\u0026gt; \u0026#39;member\u0026#39;, \u0026#39;middleware\u0026#39; =\u0026gt; \u0026#39;auth\u0026#39;], function() { Route::get(\u0026#39;index\u0026#39;, \u0026#39;MemberController@getIndex\u0026#39;); Route::get(\u0026#39;password\u0026#39;, \u0026#39;MemberController@getPassword\u0026#39;); Route::post(\u0026#39;password\u0026#39;, MemberController@postPassword\u0026#39;); Route::get(\u0026#39;profile\u0026#39;, \u0026#39;MemberController@getProfile\u0026#39;); Route::post(\u0026#39;profile\u0026#39;, \u0026#39;MemberController@postProfile\u0026#39;); Route::get(\u0026#39;logout\u0026#39;, \u0026#39;MemberController@getLogout\u0026#39;); }); さらに、認証の保護しないページにおいて、すでに認証されているならスキップしてリダイレクトさせることも可能です。 以下では、ログインや登録画面にアクセスしたときにすでにログインしているなら、/homeにリダイレクトされます。ミドルウェアにおいて、guestが使用されていることに注意してください。このミドルウェアも先のKernel.phpで登録されています。\nRoute::group([\u0026#39;middleware\u0026#39; =\u0026gt; \u0026#39;guest\u0026#39;], function() { Route::get(\u0026#39;login\u0026#39;, \u0026#39;LoginController@getLogin\u0026#39;); Route::post(\u0026#39;login\u0026#39;, \u0026#39;LoginController@postLogin\u0026#39;); Route::get(\u0026#39;signup\u0026#39;, \u0026#39;SignupController@getSignup\u0026#39;); Route::post(\u0026#39;signup\u0026#39;, \u0026#39;SignupController@postSignup\u0026#39;); }); これらのミドルウェアは、routes.phpだけでなくコントローラの中でも実行できます。例えば、前回まで使用してきたAuthController.php。\nnamespace App\\Http\\Controllers\\Auth; use App\\User; use Validator; use App\\Http\\Controllers\\Controller; use Illuminate\\Foundation\\Auth\\ThrottlesLogins; use Illuminate\\Foundation\\Auth\\AuthenticatesAndRegistersUsers; class AuthController extends Controller { use AuthenticatesAndRegistersUsers, ThrottlesLogins; public function __construct() { $this-\u0026gt;middleware(\u0026#39;guest\u0026#39;, [\u0026#39;except\u0026#39; =\u0026gt; \u0026#39;getLogout\u0026#39;]); } ... } guestのミドルウェアをコンストラクタで使用することにより、ログアウト(getLogout)以外のメソッド、つまり会員登録（getRegisterとpostRegister）とログイン（getLoginとpostLogin)のメソッドにおいて、すでにログインしているなら/homeにリダイレクトします。\n","date":"2015-11-29T07:46:38+09:00","permalink":"https://www.larajapan.com/2015/11/29/%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E8%AA%8D%E8%A8%BC%EF%BC%88%EF%BC%94%EF%BC%89%E8%AA%8D%E8%A8%BC%E3%81%A7%E3%83%9A%E3%83%BC%E3%82%B8%E3%82%92%E4%BF%9D%E8%AD%B7/","title":"ユーザー認証（４）認証でページを保護"},{"content":"前回のユーザー登録では、登録後は自動的に認証され、ユーザーはあたかもすでにログインしたような状態となります。\nしかし、デフォルトの設定の２時間のアイドルを過ぎると、ログアウトされてしまいます。そうなると必要なのはユーザー認証のためのログイン画面です。\nログイン画面に必要なのは、\napp/Http/Controllers/AuthController.php\nのコントローラ。前回にはこのコントローラは、登録画面に使われましたが、コントローラ中の以下のトレイトの使用により、\nuse AuthenticatesAndRegistersUsers\nさらに、\nそのトレイトの定義で使われているトレイト、\nuse AuthenticatesUsers\nにより、ログインが機能します。その定義を見てみましょう。以下のコードは説明のために、ログインスロットルなどの部分を省いています。\nnamespace Illuminate\\Foundation\\Auth; use Illuminate\\Http\\Request; use Illuminate\\Support\\Facades\\Auth; use Illuminate\\Support\\Facades\\Lang; trait AuthenticatesUsers { use RedirectsUsers; // ログイン画面の表示 public function getLogin() { // app/resources/views/auth/authenticate.blade.phpがあるならそれを使用 if (view()-\u0026gt;exists(\u0026#39;auth.authenticate\u0026#39;)) { return view(\u0026#39;auth.authenticate\u0026#39;);　} // authenticate.blade.phpがないなら、login.blade.phpを使用 return view(\u0026#39;auth.login\u0026#39;); } //　ログインボタンを押したら以下を実行 public function postLogin(Request $request) { // 入力バリデーション $this-\u0026gt;validate($request, [ $this-\u0026gt;loginUsername() =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, ]); $credentials = $this-\u0026gt;getCredentials($request); if (Auth::attempt($credentials, $request-\u0026gt;has(\u0026#39;remember\u0026#39;))) { //　認証成功後の処理：リダイレクトとか return $this-\u0026gt;handleUserWasAuthenticated($request); } //　認証失敗なら、ログイン画面にエラーを表示 return redirect($this-\u0026gt;loginPath()) -\u0026gt;withInput($request-\u0026gt;only($this-\u0026gt;loginUsername(), \u0026#39;remember\u0026#39;)) -\u0026gt;withErrors([ $this-\u0026gt;loginUsername() =\u0026gt; $this-\u0026gt;getFailedLoginMessage(), ]); } // 認証成功後の処理。目的の画面にリダイレクト protected function handleUserWasAuthenticated(Request $request) { if (method_exists($this, \u0026#39;authenticated\u0026#39;)) { return $this-\u0026gt;authenticated($request, Auth::user()); } return redirect()-\u0026gt;intended($this-\u0026gt;redirectPath()); } // ユーザーのクレデンシャル、つまりemailとpasswordをゲット protected function getCredentials(Request $request) { return $request-\u0026gt;only($this-\u0026gt;loginUsername(), \u0026#39;password\u0026#39;); } //　ログアウト、つまりクッキーの削除。そしてリダイレクト。デフォルトはルートディレクトリへ public function getLogout() { Auth::logout(); return redirect(property_exists($this, \u0026#39;redirectAfterLogout\u0026#39;) ? $this-\u0026gt;redirectAfterLogout : \u0026#39;/\u0026#39;); } ... postLogin()では、入力バリデーションの後に、Auth::attemptが実行されます。そこでは、config/auth.phpで指定したドライバー（Eloquent)を使用して、DBにEメールと暗号化されたパスワードがマッチするかどうかチェックして結果を返します。マッチしたなら、そこでセッションを作成して認証された事実を残します。認証関連のイベントでフックされているコードもそこで実行されます。\nその後に実行される、handleUserWasAuthenticated()では、redirect()-\u0026gt;intended()により、アクセスを試みた初期のURLへリダイレクトします。\n例えば、認証以前に、\nhttp://localhost/demo-auth/public/password/edit\nへアクセスを試みると、まず認証のためにログイン画面へリダイレクトされて、ログイン成功後には、上のURLへ自動的に移動します。デフォルトでは、$this-\u0026gt;redirectPath()へ移行します。\nログアウトは、ログインのようにボタンとかは必要とせずに、現在ログインしているならログアウトのURLにアクセスするだけで、ログインのセッションを無効とします。get Login()のAuth::logout()がその処理を行います。\n上記のコントローラで使用されるテンプレート、login.blade.phpには、以下のようなフォームが含まれます。\n\u0026lt;form method=\u0026#34;POST\u0026#34; action=\u0026#34;/auth/login\u0026#34;\u0026gt; {!! csrf_field() !!} \u0026lt;div\u0026gt; Eメール \u0026lt;input type=\u0026#34;email\u0026#34; name=\u0026#34;email\u0026#34; value=\u0026#34;{{ old(\u0026#39;email\u0026#39;) }}\u0026#34;\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div\u0026gt; パスワード \u0026lt;input type=\u0026#34;password\u0026#34; name=\u0026#34;password\u0026#34; id=\u0026#34;password\u0026#34;\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div\u0026gt; \u0026lt;input type=\u0026#34;checkbox\u0026#34; name=\u0026#34;remember\u0026#34;\u0026gt; 次回から入力を省略 \u0026lt;/div\u0026gt; \u0026lt;div\u0026gt; \u0026lt;button type=\u0026#34;submit\u0026#34;\u0026gt;ログイン\u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/form\u0026gt; さて、これでログイン画面は動作しますが、このフォームで使用される「次回から入力を省略」のチェックボックスは何でしょう？\nここがオフでは、ログインのセッションは２時間で時間切れとなり、再度ログインが必要となります。ここをオンとしてログインが成功となると現在の設定では５年間は、ログインなしで認証が必要な画面へのアクセスが可能となります。Laravelではセキュリティを上げるために、ここがオンのときはトークンを作成してDBに保存し、クッキーの値とマッチするかのチェックを行っています。\nさて、ユーザーの認証の部分ができたところで、次回は、どうやってLaravelがこの認証の情報をもとにプライベート画面を保護するかを見てみましょう。\n","date":"2015-11-22T06:58:31+09:00","permalink":"https://www.larajapan.com/2015/11/22/%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E8%AA%8D%E8%A8%BC%EF%BC%88%EF%BC%93%EF%BC%89%E3%83%AD%E3%82%B0%E3%82%A4%E3%83%B3%E3%83%BB%E3%83%AD%E3%82%B0%E3%82%A2%E3%82%A6%E3%83%88/","title":"ユーザー認証（３）ログイン・ログアウト"},{"content":"ユーザーの登録は、重複回避ーDB重複エラーで、一部分紹介しました。ここでは、一部と言わず全部をカバーしてみましょう。しかも、ララベル5.1がサンプルとして提供するプログラムをもとに。\nララベルが提供するユーザー認証のサンプルは、ララベルの作者、テイラーが作成したプログラムでありますが、すべてのパーツが揃っていて、親切な説明がなされているというものではありません。コントローラで使用するブレイドのファイルもないし、ブレイドのファイル名やパス名もドキュメントにはありません。また、プログラムでは、トレイトが多用されているために、まず参照されているファイルを見て、次の参照ファイルを見て、さらに次へ・・という、理解には探偵作業が必要です。ちょっと腰を据えて取り組んでみましょう。\nまず、最前線のファイルのリストから、\napp/Http/routes.php app/Http/Controllers/Auth/AuthController.php そう、これだけなのです。もちろんブレイドファイルも作成する必要ありますが、これだけでユーザーの登録とログイン画面ができてしまいます。それぞれ中身を見てみましょう。\nまず、routes.php。これは、画面でアクセスするプログラムの回路図のようなもので、ここでURLとコントローラを結びつけます。\napp/Http/routes.php // 会員登録 Route::get(\u0026#39;auth/register\u0026#39;, \u0026#39;Auth\\AuthController@getRegister\u0026#39;); Route::post(\u0026#39;auth/register\u0026#39;, \u0026#39;Auth\\AuthController@postRegister\u0026#39;); // ログイン・ログアウト Route::get(\u0026#39;auth/login\u0026#39;, \u0026#39;Auth\\AuthController@getLogin\u0026#39;); Route::post(\u0026#39;auth/login\u0026#39;, \u0026#39;Auth\\AuthController@postLogin\u0026#39;); Route::get(\u0026#39;auth/logout\u0026#39;, \u0026#39;Auth\\AuthController@getLogout\u0026#39;); //会員登録後、ログイン後に飛ばされるホーム画面 Route::get(\u0026#39;/home\u0026#39;, function () { return view(\u0026#39;auth.home\u0026#39;); }); 次に、AuthController.php。このひとつで会員登録とログインをやってしまいます。いったいどうやっているのか見てみましょう。（オリジナルの英語のコメントはスペースのために省いています）\napp/Http/Controllers/Auth/AuthController.php namespace App\\Http\\Controllers\\Auth; use App\\User; use Validator; use App\\Http\\Controllers\\Controller; use Illuminate\\Foundation\\Auth\\ThrottlesLogins; use Illuminate\\Foundation\\Auth\\AuthenticatesAndRegistersUsers; class AuthController extends Controller { use AuthenticatesAndRegistersUsers, ThrottlesLogins; public function __construct() { // ログアウト以外は、guestのミドルウェアを通す。 // ここですでに認証されているかどうかを判断。 $this-\u0026gt;middleware(\u0026#39;guest\u0026#39;, [\u0026#39;except\u0026#39; =\u0026gt; \u0026#39;getLogout\u0026#39;]); } //　入力チェックのルーチン protected function validator(array $data) { return Validator::make($data, [ \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;required|max:255\u0026#39;, \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;required|email|max:255|unique:users\u0026#39;, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;required|confirmed|min:6\u0026#39;, ]); } //　DBにレコードを作成。 protected function create(array $data) { return User::create([ \u0026#39;name\u0026#39; =\u0026gt; $data[\u0026#39;name\u0026#39;], \u0026#39;email\u0026#39; =\u0026gt; $data[\u0026#39;email\u0026#39;], \u0026#39;password\u0026#39; =\u0026gt; bcrypt($data[\u0026#39;password\u0026#39;]), ]); } } これを最初に見て思うのは、routes.phpで使用されているコントローラのメソッドはどこにある？？\ngetRegister postRegister getLogin postLogin getLogout\nこれらのメソッドのことです。もしかして、Controllerのクラスを継承しているので、Controller.phpで定義されている？ しかし、そちらはもっと短い（以下）！\napp/Http/Controller.php namespace App\\Http\\Controllers; use Illuminate\\Foundation\\Bus\\DispatchesJobs; use Illuminate\\Routing\\Controller as BaseController; use Illuminate\\Foundation\\Validation\\ValidatesRequests; use Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests; abstract class Controller extends BaseController { use AuthorizesRequests, DispatchesJobs, ValidatesRequests; } それならいったいどこで？と探偵作業の始まりです。\nとなると、AuthController.phpの１３行目のトレイト？\nuse AuthenticatesAndRegistersUsers\n見てみましょう。\nvendor/laravel/framework/src/Illuminate/Foundation/Auth/AuthenticatesAndRegistersUsers.php namespace Illuminate\\Foundation\\Auth; trait AuthenticatesAndRegistersUsers { use AuthenticatesUsers, RegistersUsers { AuthenticatesUsers::redirectPath insteadof RegistersUsers; } } またしてもトレイト！\nuse AuthenticateUsers, RegisterUsers\nRegisterUsers.phpを見てみましょう（ログイン関連は、AuthenticateUsers.phpですがそれは次回に）。\nvendor/laravel/framework/src/Illuminate/Foundation/Auth/RegisterUsers.php namespace Illuminate\\Foundation\\Auth; use Illuminate\\Http\\Request; use Illuminate\\Support\\Facades\\Auth; trait RegistersUsers { use RedirectsUsers; public function getRegister() { return view(\u0026#39;auth.register\u0026#39;); } public function postRegister(Request $request) { $validator = $this-\u0026gt;validator($request-\u0026gt;all()); if ($validator-\u0026gt;fails()) { $this-\u0026gt;throwValidationException( $request, $validator ); } Auth::login($this-\u0026gt;create($request-\u0026gt;all())); return redirect($this-\u0026gt;redirectPath()); } } これですね、探していたのは！\ngetRegisterでは、auth.registerがブレードの場所ということわかります。つまり、\nresources/views/auth/register.blade.php\nを作成すれば、会員登録の画面ができあがりです。\npostRegisterの関数では、先にAuthController.phpで定義した、validatorとcreateがここで参照されていますね。\nAuth::login($this-\u0026gt;create($request-\u0026gt;all()));\nここでは、DBレコードを作成して返される、Userのインスタンスをもとに、ログインも行っていしまいます。つまり、登録したら自動ログインして、ホームにリダイレクトということです。\nしかし、いったいどこへリダイレクト？\nそれも、もちろんRedirectUsersのトレイトなのです。\nvendor/laravel/framework/src/Illuminate/Foundation/Auth/RedirectUsers.php namespace Illuminate\\Foundation\\Auth; trait RedirectsUsers { public function redirectPath() { if (property_exists($this, \u0026#39;redirectPath\u0026#39;)) { return $this-\u0026gt;redirectPath; } return property_exists($this, \u0026#39;redirectTo\u0026#39;) ? $this-\u0026gt;redirectTo : \u0026#39;/home\u0026#39;; } } デフォルトは、/homeですね。また、Authocontrollerにおいて、redirectToの上書きも可能ということです。\n最後に、登録画面のブレードファイルには、以下のようなHTMLがあればOKです。\n\u0026lt;form method=\u0026#34;post\u0026#34; action=\u0026#34;auth/register\u0026#34;\u0026gt; {!! csrf_field() !!} \u0026lt;div\u0026gt; ログイン: \u0026lt;input type=\u0026#34;email\u0026#34; name=\u0026#34;email\u0026#34; value=\u0026#34;{{ old(\u0026#39;email\u0026#39;) }}\u0026#34; required autofocus\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div\u0026gt; パスワード: \u0026lt;input type=\u0026#34;password\u0026#34; name=\u0026#34;password\u0026#34; required\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div\u0026gt; パスワードの確認: \u0026lt;input type=\u0026#34;password\u0026#34; name=\u0026#34;password_confirmation\u0026#34; required\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div\u0026gt; 名前: \u0026lt;input type=\u0026#34;text\u0026#34; name=\u0026#34;name\u0026#34; value=\u0026#34;{{ old(\u0026#39;name\u0026#39;) }}\u0026#34; required\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div\u0026gt; \u0026lt;button type=\u0026#34;submit\u0026#34;\u0026gt;保存\u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/form\u0026gt; 次回は、ログインの方を深く見てみましょう。\n","date":"2015-11-15T10:39:11+09:00","permalink":"https://www.larajapan.com/2015/11/15/%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E8%AA%8D%E8%A8%BC%EF%BC%88%EF%BC%92%EF%BC%89%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E3%81%AE%E7%99%BB%E9%8C%B2/","title":"ユーザー認証（２）ユーザーの登録"},{"content":"ララベルを使用したユーザー認証のプログラムを紹介していきます。\nユーザー認証と言っても、いろいろ考えることがたくさんあります。\n認証の対象としては、フロントエンドとして会員認証のためのログインがあるし、フロントエンドがあればもちろんバックエンドがあり、そこでは会員と違う管理者を認証する必要もあります。また、最近ではFacebookやTwitterなどのソーシャルウェブサイトのログインを自分のウェブサイトの会員認証のために利用することも可能です。\nまた、ユーザー認証に関わる機能としては、\nユーザーの登録 ユーザーのログイン ユーザーのプライベート情報の保護 パスワードを忘れたときにパスワードのリセット ユーザーのログアウト パスワードの編集 ユーザーログインの不正攻撃対策 認証したユーザーのアクセス権限管理 と、いろいろあります。どれもアプリの開発には必要な機能です。\n嬉しいことに、ララベルでは、画面のテンプレートファイル（ブレード）以外は、これらの機能をカバーするプログラムがほとんど揃っています。今回からこれらの機能を見ていきましょう。\nアプリの設定 まず、このためにララベルのプロジェクトを作成します。新プロジェクトの名前は、demo-auth。以下をコマンドラインで実行してください。\nlaravel new demo-auth\nあるいは、\n~/.composer/vendor/bin/laravel new demo-auth\n次にデータベースを作成してください。mysql, sqlite、pgsqlなどお好きなものでDBを作成してください。\nDB作成後は、設定ファイルの編集です。まず、サンプルの設定ファイルを改名して、この環境でのアプリの設定ファイルとします。\nmv .env.example .env\n.envのファイルで編集が必要なのは今のところ、以下のＤＢ設定の部分です。\nDB_HOST=localhost DB_DATABASE=データベースの名前 DB_USERNAME=データベースユーザーの名前 DB_PASSWORD=データベースのパスワード その後、以下を実行してください。\nphp artisan key:generate\nそれにより、.envのファイルの\nAPP_KEY=SomeRandomString\nの「SomeRandomString」に値が「s0IWRnfPlNAiPl7Bwl1MX8V7i14loYyK」のようなランダム値に変更されます。この値は、アプリのすべての暗号化に使用されるので、一度設定したら２度と変えないように。\nこれで環境の準備は完了です。\nDBテーブルの作成 この作業には、すでにdemo-authディレクトリで作成された以下のファイルが重要となります。これらをまず見てみましょう。\napp/User.php config/auth.php まず、２のconfig/auth.php\nreturn [ // ドライバーにはエロクエントを使用します。オプションとしては、\u0026#39;database\u0026#39;も可能です。 \u0026#39;driver\u0026#39; =\u0026gt; \u0026#39;eloquent\u0026#39;, // ドライバーでエロクエントを使用するなら、そのクラスをここで指定。ファイルは app/User.phpです。 // ::classは、namespaceとともにクラス名を返します。 \u0026#39;model\u0026#39; =\u0026gt; App\\User::class, // ドライバーがdatabaseならテーブル名を指定。 \u0026#39;table\u0026#39; =\u0026gt; \u0026#39;users\u0026#39;, // パスワードを忘れたときにリセットするための情報。 // emailは、views/emails/password.blade.phpを送信するメールとテンプレート（作成必要あり）とします。 // DBテーブルは、password_resetsを使います。 // リセットのトークンは発行されてから６０分間で時間切れとなります。 \u0026#39;password\u0026#39; =\u0026gt; [ \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;emails.password\u0026#39;, \u0026#39;table\u0026#39; =\u0026gt; \u0026#39;password_resets\u0026#39;, \u0026#39;expire\u0026#39; =\u0026gt; 60, ], ]; 今回はドライバーにエロクエントを使用するので、モデルファイルのapp/User.phpを見てみましょう。\n\u0026lt;?php namespace App; use Illuminate\\Auth\\Authenticatable; use Illuminate\\Database\\Eloquent\\Model; use Illuminate\\Auth\\Passwords\\CanResetPassword; use Illuminate\\Foundation\\Auth\\Access\\Authorizable; use Illuminate\\Contracts\\Auth\\Authenticatable as AuthenticatableContract; use Illuminate\\Contracts\\Auth\\Access\\Authorizable as AuthorizableContract; use Illuminate\\Contracts\\Auth\\CanResetPassword as CanResetPasswordContract; class User extends Model implements AuthenticatableContract, AuthorizableContract, CanResetPasswordContract { // このクラスの関数の定義は、以下のtraitを使用しています。 use Authenticatable, Authorizable, CanResetPassword; // 「users」が使用するDBテーブルとなります。 protected $table = \u0026#39;users\u0026#39;; // これらが入力フィールドという仮定。 protected $fillable = [\u0026#39;name\u0026#39;, \u0026#39;email\u0026#39;, \u0026#39;password\u0026#39;]; // JSONで排除するフィールド名 protected $hidden = [\u0026#39;password\u0026#39;, \u0026#39;remember_token\u0026#39;]; } ということで、DBテーブルとして必要なのは、usersとpassword_resetsの２つとなります。実際のプロジェクトで、DBテーブル名や、テーブルのフィールド名に違いがあれば、これらが編集必要なファイルですね。\nさて、それらのテーブルを定義しているのが、以下のファイル。\ndatabase/migrations/2014_10_12_000000_create_users_table.php database/migrations/2014_10_12_100000_create_password_resets_table.php それらは以下をコマンドラインで実行して作成します。\nphp artisan migrate\n下準備は、以上で完了です。次は、会員登録画面、ログイン画面を作成します。\n","date":"2015-11-09T05:52:53+09:00","permalink":"https://www.larajapan.com/2015/11/09/%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E8%AA%8D%E8%A8%BC%EF%BC%88%EF%BC%91%EF%BC%89%E4%B8%8B%E6%BA%96%E5%82%99/","title":"ユーザー認証（１）下準備"},{"content":"今回は、「入力の空白文字のトリム」の最後です。\n以下のようなフォームでは、入力の変数に配列を使用することできます。\n\u0026lt;form\u0026gt; ... \u0026lt;div\u0026gt;あなたの趣味を以下に記入してください：\u0026lt;/div\u0026gt; \u0026lt;ul\u0026gt; \u0026lt;li\u0026gt;\u0026lt;input type=\u0026#34;text\u0026#34; name=\u0026#34;hobby[]\u0026#34; value=\u0026#34;\u0026#34;\u0026gt;\u0026lt;/li\u0026gt; \u0026lt;li\u0026gt;\u0026lt;input type=\u0026#34;text\u0026#34; name=\u0026#34;hobby[]\u0026#34; value=\u0026#34;\u0026#34;\u0026gt;\u0026lt;/li\u0026gt; ... \u0026lt;/form\u0026gt; これらがプログラムには、\nhobby[0] = \u0026#34;映画鑑賞\u0026#34;; hobby[1] = \u0026#34;　読書\u0026#34;; hobby[2] = \u0026#34;テニス　\u0026#34;; .. という形で入ってきます。\nしかし、前回のミドルウェアの定義では、このような配列には対応できませんので、以下のように書き直しました。\nnamespace App\\Http\\Middleware; use Closure; class TrimInput { /** * Handle an incoming request. * * @param \\Illuminate\\Http\\Request $request * @param \\Closure $next * @return mixed */ public function handle($request, Closure $next) { $input = $request-\u0026gt;all(); $request-\u0026gt;merge(self::trim($input)); return $next($request); } public static function trim($value) { if (is_array($value)) { $value = array_map([\u0026#39;self\u0026#39;, \u0026#39;trim\u0026#39;], $value); } elseif (is_string($value)) { $value = preg_replace(\u0026#39;/(^\\s+)|(\\s+$)/u\u0026#39;, \u0026#39;\u0026#39;, $value); } return $value; } } 一見複雑そうに見えますが、それほどでもありません。まず、回帰を使用するためにトリムの関数を分けました。クラスメソッドとしています。他でも使用するなら自身のライブラリのメソッドとしてもよいです。そのトリムのメソッドでは、パラメが配列のときには回帰を使用して配列の個々の値をトリムします。\nこれに伴い、ユニットテストも書き換えました。\nuse Illuminate\\Foundation\\Testing\\WithoutMiddleware; use Illuminate\\Foundation\\Testing\\DatabaseMigrations; use Illuminate\\Foundation\\Testing\\DatabaseTransactions; class SignupTest extends TestCase { use DatabaseTransactions; /** * @dataProvider providerTrimpInput */ public function testTrimpInput2($data, $expected) { $input = [ \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;success@gmail.com\u0026#39;, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;testtest\u0026#39;, \u0026#39;password_confirmation\u0026#39; =\u0026gt; \u0026#39;testtest\u0026#39;, \u0026#39;last_name\u0026#39; =\u0026gt; $data[\u0026#39;last_name\u0026#39;], \u0026#39;first_name\u0026#39; =\u0026gt; \u0026#39;太郎\u0026#39;, \u0026#39;hobby\u0026#39; =\u0026gt; $data[\u0026#39;hobby\u0026#39;] ]; $this-\u0026gt;visit(\u0026#39;/signup\u0026#39;) -\u0026gt;submitForm(\u0026#39;保存\u0026#39;, $input) -\u0026gt;see(\u0026#39;会員登録完了\u0026#39;); $member = \\App\\Member::where(\u0026#39;email\u0026#39;, \u0026#39;success@gmail.com\u0026#39;)-\u0026gt;first(); $this-\u0026gt;assertEquals($expected[\u0026#39;last_name\u0026#39;], $member-\u0026gt;last_name); $this-\u0026gt;assertEquals($expected[\u0026#39;hobby\u0026#39;], unserialize($member-\u0026gt;hobbies)); } public function providerTrimpInput() { return [ [ //半角スペース [\u0026#39;last_name\u0026#39; =\u0026gt; \u0026#39; 山田 \u0026#39;, \u0026#39;hobby\u0026#39; =\u0026gt; [\u0026#39; 映画鑑賞 \u0026#39;, \u0026#39; 読書 \u0026#39;, \u0026#39;\u0026#39;]], [\u0026#39;last_name\u0026#39; =\u0026gt; \u0026#39;山田\u0026#39;, \u0026#39;hobby\u0026#39; =\u0026gt; [\u0026#39;映画鑑賞\u0026#39;, \u0026#39;読書\u0026#39;, \u0026#39;\u0026#39;]], ], [ //全角スペース [\u0026#39;last_name\u0026#39; =\u0026gt; \u0026#39;　山田　\u0026#39;, \u0026#39;hobby\u0026#39; =\u0026gt; [\u0026#39;　映画鑑賞　\u0026#39;, \u0026#39;　読書　\u0026#39;, \u0026#39;\u0026#39;]], [\u0026#39;last_name\u0026#39; =\u0026gt; \u0026#39;山田\u0026#39;, \u0026#39;hobby\u0026#39; =\u0026gt; [\u0026#39;映画鑑賞\u0026#39;, \u0026#39;読書\u0026#39;, \u0026#39;\u0026#39;]], ], ]; } } 前回のテストでは、\n$this-\u0026gt;visit(\u0026#39;/signup\u0026#39;) -\u0026gt;type(\u0026#39;success@gmail.com\u0026#39;, \u0026#39;email\u0026#39;) ... -\u0026gt;press(\u0026#39;保存\u0026#39;) -\u0026gt;see(\u0026#39;会員登録完了\u0026#39;) のように、入力をシミュレートするのに、type()の関数を使用しましたが、今回のテストでは、配列の入力は残念ながらtype()では対応できません。そこで、type()とpress()をまとめた、submitForm()を使っています。\nデータプロバイダーの方も、last_nameとhobbyのおいてそれぞれ値を変えることができるように、インデックスをつけました。\nさて、これで前回の全角スペースとともに配列値でのトリムも可能となりました。空白文字をトリムするミドルウェアの完成です。\n実行できるコードはこのリンクから、コード。タグは、2015-10-19となっています。\ngit fetch \u0026amp;\u0026amp; git checkout -b 2015-10-19\n","date":"2015-11-01T06:38:36+09:00","permalink":"https://www.larajapan.com/2015/11/01/%E5%85%A5%E5%8A%9B%E3%81%AE%E7%A9%BA%E7%99%BD%E6%96%87%E5%AD%97%E3%81%AE%E3%83%88%E3%83%AA%E3%83%A0%EF%BC%88%EF%BC%93%EF%BC%89%E9%85%8D%E5%88%97%E3%81%AE%E5%85%A5%E5%8A%9B%E5%80%A4/","title":"入力の空白文字のトリム（３）配列の入力値"},{"content":"前回の「空白文字の入力をトリムする」の続きです。\nまず、動作確認を簡単にするために、ユニットテストを作成しましょう。\n入力画面のユニットテストをもとに、いろいろなデータをテストできるように、データプロバイダーを使用します。\nuse Illuminate\\Foundation\\Testing\\WithoutMiddleware; use Illuminate\\Foundation\\Testing\\DatabaseMigrations; use Illuminate\\Foundation\\Testing\\DatabaseTransactions; class SignupTest extends TestCase { use DatabaseTransactions; /** * @dataProvider providerTrimpInput */ public function testTrimpInput($last_name, $expected) { $this-\u0026gt;visit(\u0026#39;/signup\u0026#39;) -\u0026gt;type(\u0026#39;success@gmail.com\u0026#39;, \u0026#39;email\u0026#39;) -\u0026gt;type(\u0026#39;testtest\u0026#39;, \u0026#39;password\u0026#39;) -\u0026gt;type(\u0026#39;testtest\u0026#39;, \u0026#39;password_confirmation\u0026#39;) -\u0026gt;type($last_name, \u0026#39;last_name\u0026#39;) -\u0026gt;type(\u0026#39;太郎\u0026#39;, \u0026#39;first_name\u0026#39;) -\u0026gt;press(\u0026#39;保存\u0026#39;) -\u0026gt;see(\u0026#39;会員登録完了\u0026#39;); $member = \\App\\Member::where(\u0026#39;email\u0026#39;, \u0026#39;success@gmail.com\u0026#39;)-\u0026gt;first(); $this-\u0026gt;assertEquals($expected, $member-\u0026gt;last_name); } public function providerTrimpInput() { return [ [ \u0026#39;山田\u0026#39;, \u0026#39;山田\u0026#39;], [ \u0026#39; 山田 \u0026#39;, \u0026#39;山田\u0026#39;], //半角スペース [ \u0026#39;　山田　\u0026#39;, \u0026#39;山田\u0026#39;], //全角スペース ]; } } ユニットテストのデータプロバイダーは、テストとデータを分けることにより、１つのテストでいろいろなデータのテストが可能となります。必要なのは、コメントのphpDocの@dataProviderでデータを供給する関数名を指定することと、そのデータ供給の関数を作成することです。この例では３つのデータを用意することにより、３回テストが実行されます。\nテスト実行結果は、以下です。\nPHPUnit 4.8.9 by Sebastian Bergmann and contributors. ..F Time: 360 ms, Memory: 26.50Mb There was 1 failure: 1) SignupTest::testTrimpInput with data set #2 (\u0026#39;　山田　\u0026#39;, \u0026#39;山田\u0026#39;) Failed asserting that two strings are equal. --- Expected +++ Actual @@ @@ -\u0026#39;山田\u0026#39; +\u0026#39;　山田　\u0026#39; /vol1/usr/www/shop/webdocs/demo/tests/SignupTest.php:27 FAILURES! Tests: 3, Assertions: 12, Failures: 1. ２番目の半角のスペースのデータはトリムされ成功となり、３番目の全角スペースが前後にあるデータでは失敗となります。\nさて、全角スペースはどう対応しましょうか？\n再度、trim関数の仕様を見てみましょう。\nstring trim ( string $str [, string $character_mask = \u0026#34; \\t\\n\\r\\0\\x0B\u0026#34; ] ) この関数は str の最初および最後から空白文字を取り除き、 取り除かれた文字列を返します。 2番目のパラメータを指定しない場合、 trim()は以下の文字を削除します。 \u0026#34; \u0026#34; (ASCII 32 (0x20)), 通常の空白。 \u0026#34;\\t\u0026#34; (ASCII 9 (0x09)), タブ。 \u0026#34;\\n\u0026#34; (ASCII 10 (0x0A)), リターン。 \u0026#34;\\r\u0026#34; (ASCII 13 (0x0D)), 改行。 \u0026#34;\\0\u0026#34; (ASCII 0 (0x00)), NULバイト \u0026#34;\\x0B\u0026#34; (ASCII 11 (0x0B)), 垂直タブ そう、２番目のパラメータに全角スペースを入れてあげればよいですね。\ntrim($val, \u0026#39; \\t\\n\\r\\0\\x0B　\u0026#39;);//全角スペースは最後の空白文字 しかし、これはテストしたところうまくいきませんでした。全角のスペースが入る値には対応するようですが、半角の文字列では文字列によっては空白文字でないものもトリムしてしまいます。trimの関数はユニコード対応でないようですね。それならば、mb_trimと思いますが、残念ながらこれが存在しません。\nいろいろ調べたところ、preg_replaceが使えそうです。\nパターンは、\n\u0026rsquo;/(^\\s+)|(\\s+$)/u\u0026rsquo;\n\\sは空白文字のエスケープシーケンスであり、ドキュメントでは以下の記述があります。\nエスケープシーケンス\n空白文字とは HT (9)、LF (10)、FF (12)、CR (13)、スペース (32) のことです。 しかし、ロケールを指定したマッチングを行った場合には、128から255までのコードポイントの文字 (たとえば NBSP (A0)) も空白文字とみなされる可能性があります。\nということで、trimで削除する空白文字とともにユニコードの全角スペースも削除してくれます。\nミドルウェアを書き直すと、\napp/Http/Middleware/TrimInput.php namespace App\\Http\\Middleware; use Closure; class TrimInput { /** * Handle an incoming request. * * @param \\Illuminate\\Http\\Request $request * @param \\Closure $next * @return mixed */ public function handle($request, Closure $next) { $input = $request-\u0026gt;all(); $trimmed = []; foreach($input as $key =\u0026gt; $val) { $trimmed[$key] = preg_replace(\u0026#39;/(^\\s+)|(\\s+$)/u\u0026#39;, \u0026#39;\u0026#39;, $value); } $request-\u0026gt;merge($trimmed); return $next($request); } } 先のユニットテストの結果は？\nPHPUnit 4.8.9 by Sebastian Bergmann and contributors. ... Time: 359 ms, Memory: 26.50Mb OK (3 tests, 12 assertions) 成功です！\n次回は、配列の入力値の個々の値の空白文字のトリムの対応です。\n","date":"2015-10-24T09:50:38+09:00","permalink":"https://www.larajapan.com/2015/10/24/%E5%85%A5%E5%8A%9B%E3%81%AE%E7%A9%BA%E7%99%BD%E6%96%87%E5%AD%97%E3%82%92%E3%83%88%E3%83%AA%E3%83%A0%EF%BC%88%EF%BC%92%EF%BC%89%E5%85%A8%E8%A7%92%E3%82%B9%E3%83%9A%E3%83%BC%E3%82%B9/","title":"入力の空白文字をトリム（２）全角スペース"},{"content":"フォームから入力されてくる文字でやっかいなのは、文字列の前後にユーザーのタイプミスで入れられる空白文字。英語では半角のスペース、日本語では全角のスペース。\nこれらの空白文字を削除してくれる関数はすでにPHPにはあります。\nhttp://php.net/manual/ja/function.trim.php\nさて、この関数をララベルのどこで使用するのが適切でしょうか？\nまず考えるのは、コントローラ。バリデーションの前に、以下のようにループで空白文字を削除。\npublic function postSignup(Request $request) { ... $trimmed = []; foreach($request-\u0026gt;all as $key =\u0026gt; $val) { $trimmed[$key] = trim($val); } $request-\u0026gt;merge($trimmed); $this-\u0026gt;validate($request, $rules, $messages); ... しかし、どこのコントローラでもこれを行うのは面倒です。フォームリクエストという手もありますね。しかし、これもコントローラ１つに対してフォームリクエスト１つを作成すれば、同じ手間。もっとグローバルで対応する手はない？\nそこで登場するのは、HTTPミドルウェアです。HTTPミドルウェアは、フォームなどから入力されてくる値をフィルターするためのプログラムです。\nフィルターは、\napp/Http/Middleware\nのディレクトリに存在します。すでに以下のファイルが用意されていて、それぞれのフィルターのカスタマイズを行うことができます。\nAuthenticate.php　ユーザーの認証のため EncryptCookies.php クッキーの暗号化のため RedirectIfAuthenticate.php すでに認証されているならリダイレクトするため VerifyCsrfToken.php　セッション乗っ取りを防ぐため\nここに新たなミドルウェアとして空白文字のトリムを入れましょう。\napp/Http/Middleware/TrimInput.php namespace App\\Http\\Middleware; use Closure; class TrimInput { /** * Handle an incoming request. * * @param \\Illuminate\\Http\\Request $request * @param \\Closure $next * @return mixed */ public function handle($request, Closure $next) { $input = $request-\u0026gt;all(); $trimmed = []; foreach($input as $key =\u0026gt; $val) { $trimmed[$key] = trim($val); } $request-\u0026gt;merge($trimmed); return $next($request); } } まず、簡単な対応として、入力されたそれぞれの値に対して半角の空白文字のトリムを実行します。クラス名はファイル名と同様にTrimInputと命名します。既存のクラスを継承する必要はなく、handle()の関数を定義するのみです。\n入力の値は、$requestのパラメータからオブジェクトとして取得して、それをトリムした文字列で上書きします。そして、最後には次のフィルターの関数を上書きされたリクエストとともにコールします。\nしかし、残念ながらこれだけでは実行されません。以下のファイルにおいて、登録する必要あります。\napp/Http/Kernel.php namespace App\\Http; use Illuminate\\Foundation\\Http\\Kernel as HttpKernel; class Kernel extends HttpKernel { /** * The application\u0026#39;s global HTTP middleware stack. * * @var array */ protected $middleware = [ \\Illuminate\\Foundation\\Http\\Middleware\\CheckForMaintenanceMode::class, \\App\\Http\\Middleware\\EncryptCookies::class, \\Illuminate\\Cookie\\Middleware\\AddQueuedCookiesToResponse::class, \\Illuminate\\Session\\Middleware\\StartSession::class, \\Illuminate\\View\\Middleware\\ShareErrorsFromSession::class, \\App\\Http\\Middleware\\VerifyCsrfToken::class, \\App\\Http\\Middleware\\TrimInput::class, ]; /** * The application\u0026#39;s route middleware. * * @var array */ protected $routeMiddleware = [ \u0026#39;auth\u0026#39; =\u0026gt; \\App\\Http\\Middleware\\Authenticate::class, \u0026#39;auth.basic\u0026#39; =\u0026gt; \\Illuminate\\Auth\\Middleware\\AuthenticateWithBasicAuth::class, \u0026#39;guest\u0026#39; =\u0026gt; \\App\\Http\\Middleware\\RedirectIfAuthenticated::class, ]; } TrimInputは21行目に登録されています。先にリストした他のミドルウェアも入っていますね。\nさて、これだけは十分ではありません。いくつか、対応すべき問題があります。\nまず、フォームでは日本語の入力を期待するのに、全角スペースの対応ができていません。\nまた、フォームなどから入力される値は、たいていが文字列ですがＰＨＰでは、配列の[]を使用することで以下のような同型の複数行フォームの入力も可能です。\n\u0026lt;form\u0026gt; ... \u0026lt;div\u0026gt;あなたの趣味を以下に記入してください：\u0026lt;/div\u0026gt; \u0026lt;ul\u0026gt; \u0026lt;li\u0026gt;\u0026lt;input type=\u0026#34;text\u0026#34; name=\u0026#34;hobby[]\u0026#34; value=\u0026#34;\u0026#34;\u0026gt;\u0026lt;/li\u0026gt; \u0026lt;li\u0026gt;\u0026lt;input type=\u0026#34;text\u0026#34; name=\u0026#34;hobby[]\u0026#34; value=\u0026#34;\u0026#34;\u0026gt;\u0026lt;/li\u0026gt; ... \u0026lt;/form\u0026gt; この配列での入力値の対応も先のコードでは無理です。そして、動作確認はどうしましょう。空白文字ゆえに画面での確認は難しいです。フォームのユニットテストが必要ですね。これらを含めて次回に仕上げましょう。\n","date":"2015-10-19T09:32:19+09:00","permalink":"https://www.larajapan.com/2015/10/19/%E5%85%A5%E5%8A%9B%E3%81%AE%E7%A9%BA%E7%99%BD%E6%96%87%E5%AD%97%E3%82%92%E3%83%88%E3%83%AA%E3%83%A0%EF%BC%88%EF%BC%91%EF%BC%89-%E3%83%9F%E3%83%89%E3%83%AB%E3%82%A6%E3%82%A7%E3%82%A2/","title":"入力の空白文字をトリム（１） ミドルウェア"},{"content":"更新：以下のbitbucketのレポは削除されました！新規の記事を閲覧ください。\n今まで、ちょびちょびコードを掲載してきましたが、実際にテストできるコードを全公開します。\n私が使用しているバージョンコントロールは、git（ギット）です。gitと言えば、もちろんgithum.com（ギットハブ）が有名。しかし、彼らの無料アカウントはプライベートのプロジェクト数に制限があるために、私はユーザー数の制限がありプライベートのプロジェクト数の制限がないbitbucket.org（ビットバケット）を使用しています。また、ビットバケットは日本語版にもなります（まだベータ版ですが）。\nまず、実行できるコードはこのリンクから、コード。\n英語画面は、言語のドロップダウンで日本語に変換できます。\n完全な日本語翻訳とは言えないですが、十分。\nファイルのダウンロード 次は、以下をコマンドラインで実行してください。ウェブにアクセスできるディレクトリで。\ngit clone https://bitbucket.org/lotsofbytes/larajapan-code.git\n実行したディレクトリの真下にlarajapan-codeのサブディレクトリが作成され、そこへレポジトリのファイルがダウンロードされます。このサブディレクトリは、好きな名前に改名して問題ありません。\nしかし、そのダウンロードでは、masterのブランチとなっているので、前回のコードを設定するには、以下を実行。\ngit fetch \u0026amp;\u0026amp; git checkout -b 2015-09-30\nディレクトリのパーミッションを変えます。storageはログやセッションやキャッシュなどが保存されるディレクトリです。\nchmod -R 777 storage\n次はコンポーザー（composer)が必要です。システムに存在しないなら、rootユーザーで以下を実行してインストールできます。\ncurl -sS https://getcomposer.org/installer | php mv composer.phar /usr/local/bin/composer その後、git checkoutをしたディレクトリで、以下を実行。\ncomposer install\nアプリの設定 さて、次は設定ファイルの編集です。まず、サンプルの設定ファイルをコピーして、この環境でのアプリの設定ファイルとします。\ncp .env.example .env\nこのファイルで編集が必要なのは今のところ、以下のＤＢ設定の部分です。\nDB_HOST=localhost DB_DATABASE=データベースの名前 DB_USERNAME=データベースユーザーの名前 DB_PASSWORD=データベースのパスワード その後、以下を実行してくさい。\nphp artisan key:generate\nそれにより、.envのファイルの\nAPP_KEY=SomeRandomString の「SomeRandomString」に値が「s0IWRnfPlNAiPl7Bwl1MX8V7i14loYyK」のような値に変更されます。この値は、アプリのすべての暗号化に使用されるので、一度設定したら２度と変えないように。\n次は、前回で使用したＤＢテーブルの作成です。以下を実行すると、memberのテーブルがＤＢに作成されます。\nphp artisan migrate\nさて、これで準備は完了です。\n以下のようなURLにアクセスすると、会員登録画面が表示されます。localhostの部分は使用している環境により変わりますので、注意を。\nhttp://localhost/larajapan-code/public/signup\n","date":"2015-10-08T07:40:29+09:00","permalink":"https://www.larajapan.com/2015/10/08/%E6%8E%B2%E8%BC%89%E3%82%B3%E3%83%BC%E3%83%89%E3%81%AE%E5%AE%9F%E8%A1%8C/","title":"掲載コードの実行"},{"content":"「重複回避-DB重複エラーを利用」で使用した以下のコントローラのメソッドをここでもう一度掲載します。\npublic function postSignup(Request $request) { $rules = [ \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;required|email|unique:member,email\u0026#39;, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;required|min:6|max:20|confirmed\u0026#39;, \u0026#39;first_name\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;last_name\u0026#39; =\u0026gt; \u0026#39;required\u0026#39; ]; $messages = [ \u0026#39;email.unique\u0026#39; =\u0026gt; \u0026#34;Eメールアドレスはすでに使用されています\u0026#34; ]; $this-\u0026gt;validate($request, $rules, $messages); try { $member = Member::create($request-\u0026gt;all()); } catch(IlluminateDatabaseQueryException $e) { $errorCode = $e-\u0026gt;errorInfo[1]; if($errorCode == 1062) //重複エラーをここでキャッチ { return back()-\u0026gt;withInput()-\u0026gt;withErrors([\u0026#39;email\u0026#39; =\u0026gt; \u0026#34;Eメールアドレスはすでに使用されています\u0026#34;]); } } return \u0026#34;登録完了\u0026#34;; } フォームでの入力により送信された値は、このメソッドのパラメータ変数$requestに収められます。それらをバリデーションして、エラーがあれば表示、なければ新規の会員のDBレコードを作成。さらに、タイミングエラーにより逃れた重複エラーも発生するならキャッチ。まとまってとてもわかりやすいです。これはこれで十分と思います。\nしかし、このコントローラのコードがより複雑になったときに、たとえば、新規会員、編集、パスワードの編集と会員関連のバリデーションを一箇所に集めて管理性を高めたいときはどうしましょう？\nもちろん、１つのコントローラでpostSignup()のようにそれぞれのメソッドで対応するのも１つのやり方、しかし、会員のすべての処理を１つのコントローラで対応することはできません。例えば、ユーザーはエンドユーザーだけでなく管理者も会員処理が必要となります。その場合は少なくともユーザーと管理者に別々のコントローラを持つことになるでしょう。そうなら、モデルのMember.phpに入れるのも１つの手です。\nここでは、それらとまったく違う手法、フォームリクエストを紹介します。ララベル5.1以降が必要です。\nフォームリクエストは、まずapp/Http/Requests/MemberRequest.phpのファイルを作成して、以下のように先のコントローラのバリデーション部分を移行します。\nnamespace AppHttpRequests; use AppHttpRequestsRequest; class MemberRequest extends Request { public function authorize() { return true; } public function rules() { return [ \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;required|email|unique:member,email\u0026#39;, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;required|min:6|max:20|confirmed\u0026#39;, \u0026#39;first_name\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;last_name\u0026#39; =\u0026gt; \u0026#39;required\u0026#39; ]; } } これにより先のコントローラは以下のようにシンプルになります。\npublic function postSignup(MemberRequest $request) { try { $member = Member::create($request-\u0026gt;all()); } catch(IlluminateDatabaseQueryException $e) { $errorCode = $e-\u0026gt;errorInfo[1]; if($errorCode == 1062) //重複エラーをここでキャッチ { return back()-\u0026gt;withInput()-\u0026gt;withErrors([\u0026#39;email\u0026#39; =\u0026gt; \u0026#34;Eメールアドレスはすでに使用されています\u0026#34;]); } } return \u0026#34;登録完了\u0026#34;; } パラメータにおいて、$requestのタイプが、RequestからMemberRequestに変わったことに注意してください。\nさて、validateの関数のコールはどこへ行ったのでしょうね？\nvalidateの関数のコールは、MemberRequestのオブジェクトが生成されるとともに、その中で実行されます。そしてエラーがあれば画面にエラーを表示し、エラーがなければ、postSignup()の残りのコードを処理します。\n今回はシンプルな例でしたが、将来はより複雑な例を紹介しましょう。\n","date":"2015-09-30T11:40:55+09:00","permalink":"https://www.larajapan.com/2015/09/30/%E3%83%95%E3%82%A9%E3%83%BC%E3%83%A0%E3%83%AA%E3%82%AF%E3%82%A8%E3%82%B9%E3%83%88%EF%BC%9A%E3%82%B3%E3%83%B3%E3%83%88%E3%83%AD%E3%83%BC%E3%83%A9%E3%81%8B%E3%82%89%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC/","title":"フォームリクエスト：コントローラからバリデーションを分離"},{"content":"ユニットテスト（PHPの場合は、phpunit）を使い始めて、２，３年。その重要さは理解しているものの、つい最近まで、コードの大変さによりなかなか多用はしていませんでした。\nデータベース絡みや入力画面絡みのテスト、書くのはやっかいです。しかし、ウェブのアプリの開発には、データベースや入力画面があって当然。ここのテストを自動化をせずにいったいどこでする？とも思う。\nしかし、どうしてやっかいなのでしょう、これら？\n例えば、前回の会員登録の件では、テストとして考えられるのは、\nまずは、エラーを出して確認するテストたち：\n必須の項目に値がないときにエラーとなるか。 EメールにEメールでないものを入力したときにエラーとなるか。 すでに登録した会員のEメールを入れたときにエラーとなるか（重複チェック）。 そして、エラーがなく、会員登録成功したときの確認のテストも必要です。 前者のエラーを出して確認するテストは、コントローラーでなくモデルに入れてテストが可能かもしれません。ララベルのバリデーションはすでに一貫しているし、カスタマイズのバリデーションも同様にテスト可能です。\nしかし、それらユニットテストは個々のバリデーションをテストするだけで、それら集合した入力画面に対してのファンクショナルテストやアクセプタンステストは難しいです。あたかもユーザーが入力するようなテストです。\nさらに、後者のエラーが出ない成功の確認のテストは、実際にDBにレコードが作成されてしまいます。つまり１回成功テストすると、次回は重複でエラーとなり成功のテストができなくなります。もちろん、毎回レコードを削除するという手段もありますが（これはDBテストとして将来に紹介します）、違うDBを用意するとかお膳立てがかなり面倒です。\nもちろん、ここで、ララベルがそのフレームワークを活かしたユニットテストの登場です（注意：ララベル5.1での前提です）。\nまずは、Eメールアドレスの重複による失敗のテストです。\nclass SignupTest extends TestCase { public function testSignupFail() { $this-\u0026gt;visit(\u0026#39;/signup\u0026#39;) -\u0026gt;type(\u0026#39;dup@gmail.com\u0026#39;, \u0026#39;email\u0026#39;) -\u0026gt;type(\u0026#39;testtest\u0026#39;, \u0026#39;password\u0026#39;) -\u0026gt;type(\u0026#39;testtest\u0026#39;, \u0026#39;password_confirmation\u0026#39;) -\u0026gt;type(\u0026#39;山田\u0026#39;, \u0026#39;last_name\u0026#39;) -\u0026gt;type(\u0026#39;太郎\u0026#39;, \u0026#39;first_name\u0026#39;) -\u0026gt;press(\u0026#39;保存\u0026#39;) -\u0026gt;dontSee(\u0026#39;会員登録完了\u0026#39;); } } このテストは、ユーザーが以下のような画面を経験したと同じ状況をテストします。すでにDBにdup@gmail.comが存在していると仮定です。\n16行目の \u0026ldquo;dontSee(\u0026lsquo;会員登録完了\u0026rsquo;);\u0026rdquo; では、エラーになるために成功時の「会員登録完了」の文字が画面には見えないよ、つまりdon\u0026rsquo;t seeということです。\n今度は、成功時のテストですが、\nuse IlluminateFoundationTestingWithoutMiddleware; use IlluminateFoundationTestingDatabaseMigrations; use IlluminateFoundationTestingDatabaseTransactions; class SignupTest extends TestCase { use DatabaseTransactions; public function testSignupSuccess() { $this-\u0026gt;visit(\u0026#39;/signup\u0026#39;) -\u0026gt;type(\u0026#39;success@gmail.com\u0026#39;, \u0026#39;email\u0026#39;) -\u0026gt;type(\u0026#39;testtest\u0026#39;, \u0026#39;password\u0026#39;) -\u0026gt;type(\u0026#39;testtest\u0026#39;, \u0026#39;password_confirmation\u0026#39;) -\u0026gt;type(\u0026#39;山田\u0026#39;, \u0026#39;last_name\u0026#39;) -\u0026gt;type(\u0026#39;太郎\u0026#39;, \u0026#39;first_name\u0026#39;) -\u0026gt;press(\u0026#39;保存\u0026#39;) -\u0026gt;see(\u0026#39;会員登録完了\u0026#39;); } } 先の失敗の例と違って、今度はdon\u0026rsquo;t seeでなくsee(\u0026lsquo;会員登録完了\u0026rsquo;)となっています。\nしかし会員登録完了なのにDBのレコードは作成されません。そう、７行目のuse DatabaseTransactionsの宣言により、レコードの作成を試みるもののDBのトランザクション機能を使用してわざと変更をロールバックさせています。これにより作成あるいは変更されたデータを手動で戻さずに、何回でもテストの実行が可能となるわけです。もちろん、トランザクション機能があるDB、mysqlならinnodbの使用でないとできないことです。\n先の２つテストを合わせて、実行すると、「OK」という結果です。\n$ phpunit tests/SignupTest PHPUnit 4.8.2 by Sebastian Bergmann and contributors. .. Time: 313 ms, Memory: 27.25Mb OK (2 tests, 7 assertions) ","date":"2015-09-21T03:18:35+09:00","permalink":"https://www.larajapan.com/2015/09/21/%E5%85%A5%E5%8A%9B%E7%94%BB%E9%9D%A2%E3%81%AE%E3%83%A6%E3%83%8B%E3%83%83%E3%83%88%E3%83%86%E3%82%B9%E3%83%88/","title":"入力画面のユニットテスト"},{"content":"前回においては入力バリデーションでの重複回避を紹介しました。\n小規模なサイトではこれだけで重複回避は十分かもしれません。しかし、以下の状況では、どうなるでしょう？\nユーザーAさんとBさんがいたとして、どちらも、ほとんど同時にtest@gmail.comのログインで登録を試みます。もちろんそんなことは滅多に起こらないのですが、たまたまBさんは間違って自分のEメールアドレスをAさんと同じものとタイプしたとします。それから、test@gmail.comでの会員はまだDBには存在しないという仮定です。\nAさんは、\n10時20分00秒：入力完了して送信（登録ボタンを押す） 10時20分05秒：入力バリデーション無事通過！ 10時20分15秒：無事にDBに登録成功！\nBさんも、５秒遅れて、\n10時20分05秒：入力完了して送信（登録ボタンを押す） 10時20分10秒：入力バリデーション無事通過！　（もちろん実際はこんなに遅く物事は進行しませんが、経過を理解してもらうために）\nあれあれ、まだAさんのレコードはDBにないから、入力バリデーション効きませんね。\nこのままだと重複のレコードになってしまいますね。どうしましょう？\n通常、会員の登録のDBテーブルでは、同じID、ここではEメールの重複はないよ、ということで、プライマリーキーあるいはユニークキーというものを設定します。\n例えば、以下はmysqlでのDB設定ですが、「Unique Key member_unique (`email)」がDBに重複のレコードが作成されるのを防ぎます。\nCREATE TABLE `member` ( `member_id` int(11) NOT NULL AUTO_INCREMENT, `created_at` timestamp NOT NULL DEFAULT \u0026#39;0000-00-00 00:00:00\u0026#39;, `updated_at` timestamp NOT NULL DEFAULT \u0026#39;0000-00-00 00:00:00\u0026#39;, `email` varchar(100) NOT NULL DEFAULT \u0026#39;\u0026#39;, `password` varchar(255) NOT NULL DEFAULT \u0026#39;\u0026#39;, PRIMARY KEY (`member_id`), UNIQUE KEY `member_unique` (`email`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; このDBの設定のおかげで、\n10時20分15秒：DBの重複エラーとなり、Bさんのレコードは作成されず重複のレコードの作成は回避されます。\nここの部分、プログラムでは前回の入力バリデーションと合わせて以下のようになります。\npublic function postSignup(Request $request) { $rules = [ \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;required|email|unique:member,email\u0026#39;, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;required|min:6|max:20|confirmed\u0026#39;, \u0026#39;first_name\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;last_name\u0026#39; =\u0026gt; \u0026#39;required\u0026#39; ]; $messages = [ \u0026#39;email.unique\u0026#39; =\u0026gt; \u0026#34;Eメールアドレスはすでに使用されています\u0026#34; ]; $this-\u0026gt;validate($request, $rules, $messages); try { $member = Member::create($request-\u0026gt;all()); } catch(IlluminateDatabaseQueryException $e) { $errorCode = $e-\u0026gt;errorInfo[1]; if($errorCode == 1062) //重複エラーをここでキャッチ { return back()-\u0026gt;withInput()-\u0026gt;withErrors([\u0026#39;email\u0026#39; =\u0026gt; \u0026#34;Eメールアドレスはすでに使用されています\u0026#34;]); } } return \u0026#34;登録完了\u0026#34;; } これで重複は完全に回避されます。\nこれなら、入力バリデーションの重複はチェックはもう要らないのでは？\nそれを削除をしてもOKと思います。しかし、ルールの記述としてプログラムに残るのは良いかもしれません。将来はDBレベルの重複エラーもフレームワークが面倒みてくれるかもしれません。\n","date":"2015-09-14T02:46:41+09:00","permalink":"https://www.larajapan.com/2015/09/14/%E9%87%8D%E8%A4%87%E5%9B%9E%E9%81%BF-db%E9%87%8D%E8%A4%87%E3%82%A8%E3%83%A9%E3%83%BC%E3%82%92%E5%88%A9%E7%94%A8/","title":"重複回避 - DB重複エラーを利用"},{"content":"GmailのEメールアドレス、Facebookのログイン、銀行の口座番号、などなど・・世の中どこに行っても必要なIDの情報。この情報に重複があったら困りますね。\nこのIDとなる情報を登録するプロセス、まずは入力バリデーション、次にDBでレコード作成時の重複DBエラーを利用しての２ステップで重複を回避します。\n今回は、入力バリデーションを使って：\npublic function postSignup(Request $request) { $rules = [ \u0026#39;email\u0026#39; =\u0026gt; \u0026#39;required|email|unique:member,email\u0026#39;, \u0026#39;password\u0026#39; =\u0026gt; \u0026#39;required|min:6|max:20|confirmed\u0026#39;, \u0026#39;first_name\u0026#39; =\u0026gt; \u0026#39;required\u0026#39;, \u0026#39;last_name\u0026#39; =\u0026gt; \u0026#39;required\u0026#39; ]; $messages = [ \u0026#39;email.unique\u0026#39; =\u0026gt; \u0026amp;quot;Eメールアドレスはすでに使用されています\u0026amp;quot; ]; $this-\u0026gt;validate($request, $rules, $messages); $member = Member::create($request-\u0026gt;all()); } 重複をチェックしてくれるのは、unique:member,emailのルールです。「DBテーブル member の email には同じ情報があってはいけません」というルールです。\nすでにDBに存在するEメールを入力したなら、DBに値を保存せずに、入力画面へ戻りエラー「Eメールアドレスはすでに使用されています」を表示してくれます。\nそれを行ってくれるのが次の1行：\n$this-\u0026gt;validate($request, $rules, $messages); しかし、シンプルすぎて逆にわかりにくいかもしれません。そう思うなら、ララベルバージョン４のように、以下とも書くことできます。\n$validator = Validator::make($request-\u0026gt;all(), $rules, $messages) if ($validator-\u0026gt;fails()) { return back()-\u0026gt;withErrors($validator)-\u0026gt;$withInput(); } さて、重複回避は、このチェックだけで十分でしょうか？\n次回は、DBレベルでの重複回避の紹介をします。\n","date":"2015-09-08T09:41:02+09:00","permalink":"https://www.larajapan.com/2015/09/08/%E9%87%8D%E8%A4%87%E5%9B%9E%E9%81%BF%EF%BC%8D%E5%85%A5%E5%8A%9B%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%A7%E5%9B%9E%E9%81%BF/","title":"重複回避－入力バリデーションで回避"},{"content":"Laravelでは、同じことを成し遂げるのにいくつか違う方法で行うことできます。これを便利かと思うかややこしいかと思うかは、人それぞれですが、\n例えば、前回のマスアサインメントの紹介をしたときの以下のコード、\nclass SignupController extends Controller { public function postSignup(Request $request) { $member = new Member; $member-\u0026gt;fill($request-\u0026gt;all())-\u0026gt;save(); } } これ以下のようにも書けるのですよ。\nインスタンスを作成するクラス・メソッドで、コンパクトに。\nclass SignupController extends Controller { public function postSignup(Request $request) { $member = Member::create($request-\u0026gt;all()); } } あるいは、パラメータでインスタンスを作成してまって、値を入れてセーブ。\nclass SignupController extends Controller { public function postSignup(Request $request, Member $member) { $member-\u0026gt;create($request-\u0026gt;all()); } } これら皆同じくDBにレコードを作成します。\n","date":"2015-08-31T07:55:40+09:00","permalink":"https://www.larajapan.com/2015/08/31/%E3%81%82%E3%81%82%E3%82%82%E8%A8%80%E3%81%88%E3%81%9F%E3%82%8A%E3%81%93%E3%81%86%E3%82%82%E8%A8%80%E3%81%88%E3%81%9F%E3%82%8A/","title":"ああも言えたりこうも言えたり"},{"content":"ララベルのマニュアルで紹介されている一般的なDBレコードの作成方法は、\nclass Member extends Model { protected $table = \u0026#39;member\u0026#39;; protected $primary_key = \u0026#39;member_id\u0026#39;; protected $timestamps = true; } use App\\Models\\Member; $member = new Member(); $member-\u0026gt;name = \u0026#39;山田太郎\u0026#39;; $member-\u0026gt;email = \u0026#39;tyamada@gmail.com\u0026#39;; $member-\u0026gt;password = \u0026#39;password\u0026#39;; //　ここもちろん実際は暗号化して $member-\u0026gt;save(); Memberのインスタンスを作成し、ちまちまとそれぞれの項目を埋めて、最後にセーブ。\nしかし、ウェブのアプリでは、コントローラーにおいて、ユーザーの入力がたくさんの項目でいっぺんに入ってくるので、\nnamespace App\\Http\\Controllers; use App\\Models\\Member; use Illuminate\\Http\\Request; use App\\Http\\Controllers\\Controller; class SignupController extends Controller { public function postSignup(Request $request) { $member = new Member; $member-\u0026gt;fill($request-\u0026gt;all())-\u0026gt;save(); } } と、先のようにひとつひとつの項目に値を割り当てずに、一気に保存することできます。いわゆる、一括割り当てのマスアサインメント(Mass Assignment)です。\nしかし、このコード、ちょっと問題があります。\n上のコードでは、フォームからポストされる項目名がすべてが、クラスMemberのDBテーブルにあると仮定して、ララベルは一括割り当てを試みます。\nたとえば、以下のようなフォームでは、\n\u0026lt;form method=\u0026#34;post\u0026#34; action=\u0026#34;signup\u0026#34;\u0026gt; ログイン: \u0026lt;input type=\u0026#34;email\u0026#34; name=\u0026#34;email\u0026#34; value=\u0026#34;{{ old(\u0026#39;email\u0026#39;) }}\u0026#34; required autofocus\u0026gt; パスワード: \u0026lt;input type=\u0026#34;password\u0026#34; name=\u0026#34;password\u0026#34; required\u0026gt; パスワードの確認: \u0026lt;input type=\u0026#34;password\u0026#34; name=\u0026#34;password_confirmation\u0026#34; required\u0026gt; 名前: \u0026lt;input type=\u0026#34;text\u0026#34; name=\u0026#34;name\u0026#34; value=\u0026#34;{{ old(\u0026#39;name\u0026#39;) }}\u0026#34; required\u0026gt; \u0026lt;button type=\u0026#34;submit\u0026#34;\u0026gt;保存\u0026lt;/button\u0026gt; \u0026lt;/form\u0026gt; 以下の項目が入力項目名ですが、\nemail password password_confirmation name\npassword_confirmationは、通常、パスワード確認のための入力項目でDBには存在しない項目です。その値をDBに入れようとするところでエラーとなります。\nどうしましょう？\nエロクエント（Eloquent）では、どの値をDBに入れてよいか、どれを入れていけないかのルールの設定が可能です。$fillableあるいは$guardedのどちらかの変数の定義がその目的で使用されます。前者は、ホワイトリスト（入力OKの項目のリスト）、後者はブラックリスト（入力禁止の項目のリスト）です。\nclass Product extends Model { protected $table = \u0026#39;member\u0026#39;; protected $primary_key = \u0026#39;member_id\u0026#39;; protected $timestamps = true; protected $fillable = [\u0026#39;email\u0026#39;, \u0026#39;password\u0026#39;, \u0026#39;name\u0026#39;]; } こうすれば、エラーがなくDBに値を無事に入れることができます。\n$fillableの代わりに、以下でも良いですね。\nprotected $guarded = [\u0026#39;password_confirmation\u0026#39;]; ","date":"2015-08-19T09:31:52+09:00","permalink":"https://www.larajapan.com/2015/08/19/%E3%83%9E%E3%82%B9%E3%82%A2%E3%82%B5%E3%82%A4%E3%83%B3%E3%83%A1%E3%83%B3%E3%83%88%E3%81%A7%E4%B8%80%E6%8B%AC%E5%8F%96%E3%82%8A%E8%BE%BC%E3%81%BF/","title":"マスアサインメントで一括取り込み"},{"content":"前回の投稿「ダーティーなレコード」で、DBレコードが更新されたかどうか、簡単に実験してチェックできるツールを紹介しましょう。\nララベルにはコマンドで実行できる強力なツール、その名もアーティザン(Artisan)があります。このアーティザン、Macを含めてUnix系の開発者には便利でこのうえないツール。しかも、複数のコマンドがあるわけでなく、１つのコマンドでオプションを変えるだけでいろいろなことができる優れもの。\n$ php artisan list とタイプすればたくさんのオプションのヘルプがでてきます。\nさて、この中でとてもユニークなのが、今回紹介するTinker\n$ php artisan tinker Psy Shell v0.4.4 (PHP 5.5.25 - cli) by Justin Hileman \u0026gt;\u0026gt;\u0026gt; まず、Eloquentのモデルを利用してレコードを取ってきましょう.\n\u0026gt;\u0026gt;\u0026gt; use App\\Models\\Product =\u0026gt; false \u0026gt;\u0026gt;\u0026gt; $product = Product::find(1) =\u0026gt; \u0026lt;App\\Models\\Product #000000000db510bc000000004b81eec1\u0026gt; { product_id: 1, created_at: \u0026#34;2015-04-25 12:22:10\u0026#34;, updated_at: \u0026#34;2015-08-05 13:15:25\u0026#34;, name: \u0026#34;test product\u0026#34;, price: \u0026#34;1000\u0026#34; } \u0026gt;\u0026gt;\u0026gt; 次に、価格（price）を更新してみます。\n\u0026gt;\u0026gt;\u0026gt; $product-\u0026gt;price = \u0026#34;2000\u0026#34; =\u0026gt; \u0026#34;2000\u0026#34; \u0026gt;\u0026gt;\u0026gt; $porduct-\u0026gt;isDirty() =\u0026gt; true \u0026gt;\u0026gt;\u0026gt; $product-\u0026gt;save() =\u0026gt; true \u0026gt;\u0026gt;\u0026gt; $product =\u0026gt; \u0026lt;App\\Models\\Product #000000000ee7a5b70000000066692faa\u0026gt; { product_id: 1, created_at: \u0026#34;2015-04-25 12:22:10\u0026#34;, updated_at: \u0026#34;2015-08-11 09:33:23\u0026#34;, name: \u0026#34;test product\u0026#34;, price: \u0026#34;2000\u0026#34; } \u0026gt;\u0026gt;\u0026gt; isDirty()は変更されたのでもちろんtrue、また update_atも更新時の日時に更新されています。\n今までのDBのクエリの履歴を見たいなら、\n\u0026gt;\u0026gt;\u0026gt; DB::getQueryLog() =\u0026gt; [ [ \u0026#34;query\u0026#34; =\u0026gt; \u0026#34;select * from `product` where `product`.`product_id` = ? limit 1\u0026#34;, \u0026#34;bindings\u0026#34; =\u0026gt; [ 3 ], \u0026#34;time\u0026#34; =\u0026gt; 0.56 ], [ \u0026#34;query\u0026#34; =\u0026gt; \u0026#34;update `product` set `updated_at` = ?, `price` = ? where `product_id` = ?\u0026#34;, \u0026#34;bindings\u0026#34; =\u0026gt; [ \u0026#34;2015-08-11 09:33:23\u0026#34;, \u0026#34;2000\u0026#34;, 3 ], \u0026#34;time\u0026#34; =\u0026gt; 3.13 ] ] \u0026gt;\u0026gt;\u0026gt; さて、今度は同じ値を入れてみましょう。\n\u0026gt;\u0026gt;\u0026gt; $product-\u0026gt;price = \u0026#34;2000\u0026#34; =\u0026gt; \u0026#34;2000\u0026#34; \u0026gt;\u0026gt;\u0026gt; $product-\u0026gt;isDirty() =\u0026gt; false \u0026gt;\u0026gt;\u0026gt; $product-\u0026gt;save() =\u0026gt; true \u0026gt;\u0026gt;\u0026gt; $product =\u0026gt; \u0026lt;App\\Models\\Product #000000000ee7a5b70000000066692faa\u0026gt; { product_id: 1, created_at: \u0026#34;2015-04-25 12:22:10\u0026#34;, updated_at: \u0026#34;2015-08-11 09:33:23\u0026#34;, name: \u0026#34;test product\u0026#34;, price: \u0026#34;2000\u0026#34; } \u0026gt;\u0026gt;\u0026gt; レコードではダーティーではなく(false)、またsave()しているにもかかわらず、update_atの値にも変化ありませんね。\nここで、DB::geQueryLog()を実行すれば、さきとまったく同じ履歴が出てきます。つまり、DBの処理は何もなかったを証明します。\nティンカー便利ですね。ブラウザを使わなくとも、ユニットテストを書かなくても、ここでテストができるのが便利です。\n追記：\n今回のティンカーは、バージョン5.1のララベルを使用しています。他のバージョンでは、コマンドのプロンプトのシンボルが違ったりや行末のセミコロンの必須となったります。ご注意を。\n","date":"2015-08-12T12:44:26+09:00","permalink":"https://www.larajapan.com/2015/08/12/%E3%82%A2%E3%83%BC%E3%83%86%E3%82%A3%E3%82%B6%E3%83%B3%E3%81%AE%E3%83%86%E3%82%A3%E3%83%B3%E3%82%AB%E3%83%BC/","title":"アーティザンのティンカー"},{"content":"フォームの画面でのユーザーのインプットをもとに既存のDBレコードを更新する際、もし何もデータ更新なしに「保存」ボタンを押されたらDBレコードを更新する？\n更新あります！\nもちろん、Javascriptで「保存」ボタンを無効にすることも可能。少なくともブラウザレベルでは。\nしかしサーバーレベルではどう判断？\n現在のデータとインプットデータを項目ごとに１つずつ比較？\nそんな面倒なことを裏で自動的にEloquentは行ってくれます。\n例えば、\nclass Product extends Model { protected $table = \u0026#39;product\u0026#39;; protected $primary_key = \u0026#39;product_id\u0026#39;; protected $timestamps = true; } use App\\Product; $product = Product::find(1); //product_id = 1　のレコードをゲット $product-\u0026gt;price = 2000;　// まったく以前と同じ値段に $product-\u0026gt;update(); update()をコールしているのだけれど、何もデータに変更ないので、ここ裏ではDBの更新はありません（updated_atのタイムスタンプをチェックしてみてください）。賢いですね！\nしかし、ここに例えば、この更新をもとに他のDBのレコードを更新することがプログラムされていたら？\n$product-\u0026gt;update(); update_other_table(); 必要もないのにそちらも更新されては困ります。\n更新されたかどうかを判断するには、\nif($product-\u0026gt;isDirty()) { $product-\u0026gt;update(); update_other_table(); } と簡単にチェックできます。ここisDirty()はDBにレコードが保存される前にチェックする必要あります。つまり、update()の前に。\nさらに、１つの項目だけが変更されたかどうかをチェックするには、\nif($product-\u0026gt;isDirty(\u0026#39;price\u0026#39;)) { update_other_table(); } 素晴らしいですね。\nちなみに、この「ダーティ」。ダーティハリー、ダーティダンシングのムービーの「ダーティ」と同じです。\nhttp://dictionary.goo.ne.jp/leaf/jn2/132268/m0u/\n汚いの意味。\nでもここでの意味は、「データが更新された」の意味。\n","date":"2015-08-05T12:13:26+09:00","permalink":"https://www.larajapan.com/2015/08/05/%E3%83%80%E3%83%BC%E3%83%86%E3%82%A3%E3%81%AA%E3%83%AC%E3%82%B3%E3%83%BC%E3%83%89/","title":"ダーティなレコード"}]