20211202のPHPに関する記事は9件です。

Azure App Service - Web Apps(Windows)に ImageMagick + Imagick + Ghostscript を導入

はじめに ImageMagick は言語毎に異なるモジュールや依存関係の複雑さから yum 等のパッケージ管理の仕組みを利用できる Linux 環境での導入・利用が望ましいようです。 Windows 環境への導入は、既に稼働中のサービスへの追加機能開発で必要など、必要に迫られた場合に限るのが無難かと思われます。 本記事は、必要に迫られ四苦八苦 1 しながら ImageMagick 等を導入した記録です。 本記事に掲載された内容によって生じた損害等の一切の責任を負いかねますのでご了承ください。 環境 Azure App Service - Web Apps Windows IIS 10.0 PHP 7.3 導入手順 ImageMagick と Imagick のダウンロード phpinfo() で Architecture と PHP Extension Build の値を確認する。 Architecture x86 PHP Extension Build API20180731,NTS,VC15 今回は上記であった為 VC15 かつ x86 の ImageMagick と PHP 7.3 で NTS(Non Thread Safe)の VC15 かつ x86 の Imagick を ダウンロードする。 https://windows.php.net/downloads/pecl/deps/ ImageMagick-7.0.7-11-vc15-x86.zip https://windows.php.net/downloads/pecl/releases/imagick/3.5.1/ php_imagick-3.5.1-7.3-nts-vc15-x86.zip ImageMagick のインストール ImageMagick-7.0.7-11-vc15-x86.zip を展開し bin フォルダの中身を全て D:\home\site\ImageMagick\ へアップロードする。 Imagick のインストール php_imagick-3.5.1-7.3-nts-vc15-x86.zip を展開し php_imagick.dll を D:\home\site\ext\ へアップロードする。 他技術ブログ等で ImageMagick のインストール工程で配置した bin 内 CORE_RL_* ファイルを上書きする手順が紹介されているが、バイナリを比較したところファイル内容が同一であった為、上書き不要と判断した。 ImageMagick 環境変数 PATH の設定 D:\home\site\ へ以下の applicationHost.xdt ファイルをアップロードする。 home\site\applicationHost.xdt <?xml version="1.0"?> <configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform"> <system.webServer> <runtime xdt:Transform="InsertIfMissing"> <environmentVariables xdt:Transform="InsertIfMissing"> <add name="PATH" value="%PATH%;d:\home\site\ImageMagick" xdt:Locator="Match(name)" xdt:Transform="InsertIfMissing" /> </environmentVariables> </runtime> </system.webServer> </configuration> アプリケーション設定 Azure ポータルから App Service > 設定 > 構成 > アプリケーション設定 > 新しいアプリケーション設定 以下 3つの設定を追加する。 名前 値 MAGICK_CODER_MODULE_PATH D:\home\site\ImageMagick MAGICK_HOME D:\home\site\ImageMagick PHP_EXTENSIONS D:\home\site\ext\php_imagick.dll ※デプロイスロットの設定 はチェックしない 設定追加後、保存(アプリケーションの再起動)を行う。 インストール確認 phpinfo() で ImageMagick + Imagick が読み込まれていることを確認する。 App Service > 開発ツール > コンソール からも確認。 D:\home\site\wwwroot>magick --version Version: ImageMagick 7.0.7-11 Q16 x86 2017-11-23 http://www.imagemagick.org Copyright: Copyright (C) 1999-2015 ImageMagick Studio LLC License: http://www.imagemagick.org/script/license.php Visual C++: 191125547 Features: Cipher DPC HDRI Modules OpenMP Delegates (built-in): bzlib cairo flif freetype jng jp2 jpeg lcms lqr openexr pangocairo png ps raw rsvg tiff webp xml zlib 動作確認 imagick.php <?php $im = new Imagick(); $im->newPseudoImage(50, 50, "gradient:red-black"); $draw = new ImagickDraw(); $draw->pushPattern('gradient', 0, 0, 50, 50); $draw->composite(Imagick::COMPOSITE_OVER, 0, 0, 50, 50, $im); $draw->popPattern(); $draw->setFillPatternURL('#gradient'); $draw->setFontSize(52); $draw->annotation(20, 50, "Hello World!"); $canvas = new Imagick(); $canvas->newImage(350, 70, "white"); $canvas->drawImage($draw); $canvas->borderImage('black', 1, 1); $canvas->setImageFormat('png'); header("Content-Type: image/png"); echo $canvas; Ghostscript のインストール ImageMagick で PDF を扱う場合はこちらも必要。 任意のバージョンの Windows インストーラー形式をダウンロードする。 今回は最新バージョンであった 9.55.0 を使用。 gs9550w32.exe まず、自身のローカル端末へ Ghostscript をインストールする。 インストールされたローカルの C:\Program Files (x86)\gs\gs9.55.0 を D:\home\site\gs9.55.0 へアップロードする。 以下のように applicationHost.xdt を変更して bin と lib へパスを通す。 ※変更後、要サーバー再起動 home\site\applicationHost.xdt <?xml version="1.0"?> <configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform"> <system.webServer> <runtime xdt:Transform="InsertIfMissing"> <environmentVariables xdt:Transform="InsertIfMissing"> - <add name="PATH" value="%PATH%;d:\home\site\ImageMagick" xdt:Locator="Match(name)" xdt:Transform="InsertIfMissing" /> + <add name="PATH" value="%PATH%;d:\home\site\ImageMagick;d:\home\site\gs9.55.0\bin;d:\home\site\gs9.55.0\lib" xdt:Locator="Match(name)" xdt:Transform="InsertIfMissing" /> </environmentVariables> </runtime> </system.webServer> </configuration> 以上で Ghostscript インストールは完了。 App Service > 開発ツール > コンソール から Ghostscript がインストールされていることを確認。 D:\home\site\wwwroot>gs -v GPL Ghostscript 9.55.0 (2021-09-27) Copyright (C) 2021 Artifex Software, Inc. All rights reserved. しかし、このまま Imagick の readImage() メソッドで PDF ファイルを読み込もうとすると以下のようなエラーが発生する。 PHP Fatal error: Uncaught ImagickException: PDFDelegateFailed `The system cannot find the file specified. ' @ error/pdf.c/ReadPDFImage/794 in D:\home\site\wwwroot\convert.php:3 Stack trace: #0 D:\home\site\wwwroot\convert.php(3): Imagick->readImage('test.pdf') #1 {main} thrown in D:\home\site\wwwroot\convert.php on line 3 その場合、D:\home\site\gs9.55.0\bin\gswin32c.exe を D:\home\site\gs9.55.0\bin\gs.exe へリネームしてやると解決する。 下記コードと同一階層に適当な PDF ファイルを配置し、コードを実行すると PDF から画像が生成される。 convert.php <?php $imagick = new Imagick(); $imagick->readImage('test.pdf'); $imagick->writeImage('test.jpg'); おわりに 最終の記事推敲を行っていたところ https://windows.php.net/downloads/pecl/deps/ から ImageMagick-7.0.7-11-vc15-x86.zip がダウンロード出来なくなっており 唯一ダウンロードできる x86 版は ImageMagick-7.1.0-13-1-vs15-x86.zip になっていた。2 試しに本記事の手順で導入したところ ImageMagick + Imagick 以下の Warning を吐くので Imagick インストール時に CORE_RL_* ファイルの上書きを推奨 PHP Warning: Version warning: Imagick was compiled against ImageMagick version 1799 but version 1808 is loaded. Imagick will run but may behave surprisingly in Unknown on line 0 Ghostscript gswin32c.exe のリネーム工程が不要となる というように必要な作業工程に差異があるものの、一応動いてはいた。 やはり ImageMagick 関係を導入する場合は Linux 推奨か。だらしない記事ですまない。 参考 https://azureossd.github.io/2015/12/07/php-imagemagick-on-azure-web-apps/ https://github.com/snobu/php-imagick-webapps https://yotazo.hateblo.jp/entry/2016/12/21/%E3%83%AD%E3%83%BC%E3%82%AB%E3%83%AB%E7%92%B0%E5%A2%83_Windows%EF%BC%8BIIS%EF%BC%8BPHP%E3%81%A7ImageMagick%E3%82%92%E4%BD%BF%E3%81%86 今回、32 ビット環境だった為か Azure 拡張機能として用意されている PHP7_64BIT_IMAGICK_6937 は上手く動かなかった。 ↩ 2021/12/02 時点 ↩
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHP 8.1 がリリース!だけど思わぬ落とし穴も

PHP 8.1 がリリース! 2021年11月25日に PHP 8.1 がリリースされました enum の追加や readonly プロパティが設定できるなど、嬉しい機能が盛りだくさんですね。 詳しい内容は既に記事がありますので、ぜひそちらをご覧ください(感謝! )。 Laravel の対応は? 普段は Laravel をメインで使っていますので、 PHP 8.1 の恩恵を受けるためには Laravel のバージョンアップをしなければなりません。 Github を見ると、2021/10/22 リリースの v8.67.0 からサポートされているようです。 ただ、依存パッケージが対応しているかなどは調査が必要ですし、リリース済のコードだと尚更気軽にバージョンアップはできないです。 つまりプロジェクトによってはあまり関係ない...そんなふうに考えていた時期が僕にもありました。 一方その頃... それはいつも通りチームメンバーが PHP 8.0 × Laravel v8.33.1 環境で CD/CI を回したときでした。 急に CD/CI が泡を吹いて倒れたのです。 エラーログ Fatal error: During inheritance of ArrayAccess: Uncaught ErrorException: Return type of Illuminate\Support\Collection::offsetExists($key) should either be compatible with ArrayAccess::offsetExists(mixed $offset): bool, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in vendor/laravel/framework/src/Illuminate/Collections/Collection.php:1411 みたことないエラーだったので、パッと見は原因がわかりませんでした。 ちょっと前までは元気に動いていたのに一体どうして... ただ、幸いにも原因はすぐに目星がつきました。 原因は composer の docker image このエラーはバックエンドのビルド時に起きたもので、そこでは Docker で composer:2 というイメージを使用していました。 このイメージの中で使用されている PHP のバージョンが 8.1 に上げられたことが原因でした。 イメージを composer:2.1.11 に指定したところ、無事にビルドが通るようになりました。 composer:2.1.12 以降は PHP 8.1 を使うようになっているようなので、しばらくは注意が必要です。 教訓 Docker のイメージはちゃんとマイナーバージョンまで指定した方がいいですね。 あと今回は PHP 8.1 がリリースされたことを知っていたことが大きく、エラーになったタイミングとリリースされた日が近かったので目星がつき、早期の解決につながりました。 自分が使っている技術については最新情報をキャッチアップすることが大事だと改めて認識しました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

細かすぎて伝わらないLaravel選手権8(laravel8)

Laraevl選手権8と言っていますが8回もやってません。versionに合わせました。 laravelのchangelogを眺めて、新しく追加された細かい機能を見ていこうぜ!っていうコーナーです 全部を網羅した訳ではなく、Addedされた中でも自分が気になるものをピックアップしました。 paginationにlinksプロパティーが追加 v8.0.3 src/Illuminate/Pagination/LengthAwarePaginator.php こんなふうにpaginateをjsonで返した場合にlinksプロパティーが追加されました。linksプロパティーの詳細は「laravel pagination カスタマイズ」で検索! Route::get('/users', function () { return User::paginate(); }); { "total": 50, "per_page": 15, "current_page": 1, "last_page": 4, "first_page_url": "http://laravel.app?page=1", "last_page_url": "http://laravel.app?page=4", "next_page_url": "http://laravel.app?page=2", "prev_page_url": null, "path": "http://laravel.app", "from": 1, "to": 15, "data":[ { // レコード… }, { // レコード… } ], // ↓追加された "links": [ { "url": null, "label": "&laquo; Previous", "active": false }, { "url": "http://laravel.app?page=1", "label": "1", "active": true }, { "url": null, "label": "Next &raquo;", "active": false } ], } Testクラス内でjson型のカラムと比較できるcastAsJson()メソッドが追加 v8.3.0 src/Illuminate/Foundation/Testing/Concerns/InteractsWithDatabase.php $this->assertDatabaseHas('users', [ 'name' => 'Peter Parker', 'email' => 'spidey@yahoo.com', 'skills' => $this->castAsJson(json_encode(['web slinging', 'spidey-sense', 'sticky feet'])), ]); キュー処理のジョブバッチで使用するbatchメソッドにクロージャ(無名関数)を渡せるように v8.3.0 src/Illuminate/Bus/Batch.php Bus::batch([ new ProcessPodcast, function () { // ... }, new ReleasePodcast ])->dispatch(); ジョブをクリアできるphp artisan queue:clearコマンドが追加 v8.4.0 src/Illuminate/Queue/Console/ClearCommand.php ジョブ(jobsテーブル)はクリアできるけど、ジョブバッチ(job_batchesテーブル)には残るので注意です! v8.21.0からは別コマンドでジョブバッチもクリアできます php artisan queue:clear クエリービルダにcrossJoinSub()メソッド追加 v8.5.0 src/Illuminate/Database/Query/Builder.php joinSub、leftJoinSub、rightJoinSubに加えてcrossJoinSubが追加 LazyCollectionにタイムアウトを設定できるように v8.6.0 src/Illuminate/Collections/LazyCollection.php LazyCollectionにタイムアウトを設定できるようになりました。 $lazyCollection ->takeUntilTimeout(now()->add(2, 'minutes')) ->each(fn ($item) => doSomethingThatMayTakeSomeTime($item)); // ^^ This will only process items for up to 2 minutes ^^ Httpクライアントでのレスポンスエラー発生時にonError()でコールバック処理ができるように v8.7.0 src/Illuminate/Http/Client/Response.php throw()との違いは例外を投げるかどうかです。 Before $response = $client->withHeaders($headers)->post($url, $payload); if ($response->failed()) { Log::error('Twitter API failed posting Tweet', [ 'url' => $url, 'payload' => $payload, 'headers' => $headers, 'response' => $response->body(), ]); $response->throw(); } return $response->json(); After return $client->withHeaders($headers) ->post($url, $payload) ->onError(fn ($response) => Log::error('Twitter API failed posting Tweet', [ 'url' => $url, 'payload' => $payload, 'headers' => $headers, 'response' => $response->body(), ]) )->throw()->json(); CollectionにpipeInto()メソッドが追加されました v8.8.0 src/Illuminate/Collections/Traits/EnumeratesValues.php APIリソースを返す時に使えそうです pipeInto class ResourceCollection { /** * コレクションインスタンス */ public $collection; /** * 新しいResourceCollectionインスタンスの生成 * * @param Collection $collection * @return void */ public function __construct(Collection $collection) { $this->collection = $collection; } } $collection = collect([1, 2, 3]); $resource = $collection->pipeInto(ResourceCollection::class); $resource->collection->all(); // [1, 2, 3] HttpクライアントでuserAgentをセットするwithUserAgent()メソッドが追加 v8.8.0 src/Illuminate/Collections/Traits/EnumeratesValues.php 地味だけど便利 Before Http::withHeaders(['User-Agent' => $userAgent])->get($url); After Http::withUserAgent($userAgent)->get($url); スケジュールをローカルで実行できるphp artisan schedule:workが追加 今まではスケジュールを動かすのにschedule:runコマンドをcronに登録しておく必要がありましたが、ローカルからフォアグラウンドで動かしておけるschedule:workが追加されました。 これは結構便利ですね paginatorのアイテムを変換できるthrough()メソッドが追加 v8.9.0 src/Illuminate/Pagination/AbstractPaginator.php 今までは$paginator->getCollection()->transform()なんてしていましたが、その必要も無くなりました! return Inertia::render('Contacts/Index', [ 'contacts' => Contact::paginate()->through(function ($contact) { return [ 'id' => $contact->id, 'name' => $contact->name, 'phone' => $contact->phone, 'city' => $contact->city, 'organization' => optional($contact->organization)->only('name') ]; }), ]); Eloquentやクエリービルダでupsertが使えるように v8.10.0 src/Illuminate/Database/Query/Builder.php MySQLの場合on duplicate key updateを使っており、uniqueかprimary keyが必要なので注意! User::upsert([ ['id' => 1, 'email' => 'taylor@example.com'], ['id' => 2, 'email' => 'dayle@example.com'], ], 'email'); バリデーションで倍数のチェックができるように v8.10.0 src/Illuminate/Validation/Concerns/ValidatesAttributes.php <input type="number" step="0.5" name="foo"> public function rules(): array { return [ 'foo' => [ 'multiple_of:0.5', ] ]; } マイグレーションで外部キー制約のカラム削除が簡単に v8.10.0 src/Illuminate/Database/Schema/Blueprint.php これも地味に便利 Before class AddCategoryIdToPostsTable extends Migration { public function down() { Schema::table('posts', function (Blueprint $table) { $table->dropForeign(['category_id']); $table->dropColumn('category_id'); }); } } After class AddCategoryIdToPostsTable extends Migration { public function down() { Schema::table('posts', function (Blueprint $table) { $table->dropConstrainedForeignId('category_id'); }); } } Eloquentのcastsにencryptedが使えるように v8.12.0 src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php 以前はアクセサやカスタムキャストを作る必要がありましたが、直接指定できるように public $casts = [ 'access_token' => 'encrypted', ]; リレーションのクエリにwithMax()|withMin()|withSum()|withAvg()メソッドが追加 v8.12.0 src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php これは結構便利ですね。ただ発行されるSQLはゴリゴリのサブクエリなので注意 Post::withCount('comments'); Post::withMax('comments', 'created_at'); Post::withMin('comments', 'created_at'); Post::withSum('comments', 'foo'); Post::withAvg('comments', 'foo'); Eloquentやクエリービルダでexplainが使えるように v8.12.0 src/Illuminate/Database/Concerns/ExplainsQueries.php 便利! >>> DB::table('users')->where('name', 'Illia Sakovich')->explain() => Illuminate\Support\Collection {#5348 all: [ {#5342 +"id": 1, +"select_type": "SIMPLE", +"table": "users", +"partitions": null, +"type": "ALL", +"possible_keys": null, +"key": null, +"key_len": null, +"ref": null, +"rows": 9, +"filtered": 11.11111164093, +"Extra": "Using where", }, ], } ルーティングに正規表現メソッドが追加 v8.12.0 src/Illuminate/Routing/RouteRegexConstraintTrait.php whereNumber|whereAlpha|whereAlphaNumeric|whereUuidなどが使えるようです Before Route::get('authors/{author}/{book}')->where(['author' => '[0-9]+', 'book' => '[a-zA-Z]+']); After Route::get('authors/{author}/{book}')->whereNumber('author')->whereString('book'); EloquentのCollectionにloadMax()|loadMin()|loadSum()|loadAvg()メソッド追加。EloquentにloadMax()|loadMin()|loadSum()|loadAvg()|loadMorphMax()|loadMorphMin()|loadMorphSum()|loadMorphAvg()メソッド追加 v8.13.0 src/Illuminate/Database/Eloquent/Collection.php src/Illuminate/Database/Eloquent/Model.php これも便利ですね。ただゴリゴリのサブクエリ(略 //Eloquent/Collection public function loadAggregate($relations, $column, $function = null) {...} public function loadCount($relations) {...} //Just modified. public function loadMax($relations, $column) {...} public function loadMin($relations, $column) {...} public function loadSum($relations, $column) {...} public function loadAvg($relations, $column) {...} //Eloquent/Model public function loadAggregate($relations, $column, $function = null) {...} public function loadCount($relations) {...} //Just modified. public function loadMax($relations, $column) {...} public function loadMin($relations, $column) {...} public function loadSum($relations, $column) {...} public function loadAvg($relations, $column) {...} public function loadMorphAggregate($relation, $relations, $column, $function = null) {...} public function loadMorphCount($relation, $relations) {...} //Just modified. public function loadMorphMax($relation, $relations, $column) {...} public function loadMorphMin($relation, $relations, $column) {...} public function loadMorphSum($relation, $relations, $column) {...} public function loadMorphAvg($relation, $relations, $column) {...} こんな感じで使える $user = User::find(1); $user->loadCount('posts'); $user->loadMax('posts', 'created_at'); Eloquentのcastsencryptedで使用する暗号鍵を設定できるModel::encryptUsing()が追加 v8.14.0 src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php use Illuminate\Database\Eloquent\Model; use Illuminate\Encryption\Encrypter; $databaseEncryptionKey = config('database.encryption_key'); $encrypter = new Encrypter($databaseEncryptionKey); Model::encryptUsing($encrypter); HttpのResponseを返す際に指定のクッキーを無くすwithoutCookie()メソッドが追加 v8.15.0 src/Illuminate/Http/ResponseTrait.php 単純に有効期限切れにする感じですね /** * Expire a cookie when sending the response. * * @param \Symfony\Component\HttpFoundation\Cookie|mixed $cookie * @param string|null $path * @param string|null $domain * @return $this */ public function withoutCookie($cookie, $path = null, $domain = null) { if (is_string($cookie) && function_exists('cookie')) { $cookie = cookie($cookie, null, -2628000, $path, $domain); } $this->headers->setCookie($cookie); return $this; } アップロードファイルのテスト時にexistsと合わせてcontentもチェックできるように v8.15.0 src/Illuminate/Filesystem/FilesystemAdapter.php これも便利 Before Storage::disk('reports')->assertExists('foo.csv'); $this->assertSame('my;csv;content', Storage::disk('reports')->read('foo.csv')); After Storage::disk('reports')->assertExists('foo.csv', 'my;csv;content'); Collectionで複数ソートができるように v8.16.0 src/Illuminate/Collections/Collection.php これはナイス $collection = collect([ ['name' => 'Taylor Otwell', 'age' => 34], ['name' => 'Abigail Otwell', 'age' => 30], ['name' => 'Taylor Otwell', 'age' => 36], ['name' => 'Abigail Otwell', 'age' => 32], ]); $sorted = $collection->sortBy([ ['name', 'asc'], ['age', 'desc'], ]); $sorted->values()->all(); /* [ ['name' => 'Abigail Otwell', 'age' => 32], ['name' => 'Abigail Otwell', 'age' => 30], ['name' => 'Taylor Otwell', 'age' => 36], ['name' => 'Taylor Otwell', 'age' => 34], ] */ DBに接続できるphp artisan dbが追加 v8.16.0 src/Illuminate/Database/Console/DbCommand.php べんり!でもmysql-clientがインストールされている必要があるので注意! factoryで、存在する親モデルのインスタンスを紐付けするサポートが追加 v8.18.0 src/Illuminate/Database/Eloquent/Factories/Factory.php factoryを作成する際にforでは親のfactoryを渡す方法だけでしたが use App\Models\Post; use App\Models\User; $posts = Post::factory() ->count(3) ->for(User::factory()->state([ 'name' => 'Jessica Archer', ])) ->create(); 親のモデルインスタンスを渡して紐付けできるように $user = User::factory()->create(); $posts = Post::factory() ->count(3) ->for($user) ->create(); メール本文の内容をテストするためのメソッドがいくつか追加 v8.18.0 src/Illuminate/Mail/Mailable.php これは積極的に使っていきたい use App\Mail\InvoicePaid; use App\Models\User; public function test_mailable_content() { $user = User::factory()->create(); $mailable = new InvoicePaid($user); $mailable->assertSeeInHtml($user->email); $mailable->assertSeeInHtml('Invoice Paid'); $mailable->assertSeeInText($user->email); $mailable->assertSeeInText('Invoice Paid'); } スケジュール一覧を表示するphp artisan schedule:listが追加 v8.19.0 src/Illuminate/Console/Scheduling/ScheduleListCommand.php ジョブデータを暗号化できるように v8.19.0 src/Illuminate/Contracts/Queue/ShouldBeEncrypted.php jobsテーブルのpayloadを見ると大体の内容がわかってしまうが、ShouldBeEncryptedを実装してあげれば暗号可能に use Illuminate\Contracts\Queue\ShouldBeEncrypted; class VerifyUser implements ShouldQueue, ShouldBeEncrypted { private $user; private $socialSecurityNumber; public function __construct($user, $socialSecurityNumber) { $this->user = $user; $this->socialSecurityNumber = $socialSecurityNumber; } } ジョブバッチをクリアできるphp artisan queue:prune-batchesコマンドが追加 v8.21.0 src/Illuminate/Queue/Console/PruneBatchesCommand.php >>> php artisan queue:prune-batches 199 entries deleted. $schedule->command('queue:prune-batches --hours=48')->daily(); クエリービルダに、レコードが1つだけ存在かつそれを取得するsole()メソッドが追加 v8.23.0 src/Illuminate/Database/Concerns/BuildsQueries.php レコードが存在しない、複数レコードが存在する場合は例外を投げます。 Djangoではget、Railsではsoleやfind_sole_byで存在するそうです $user = User::where('name', 'test1')->sole(); throw_if / throw_unless がデフォルトでRuntimeExceptionを投げるように v8.23.0 src/Illuminate/Support/helpers.php これは、、、 並列テストが可能に v8.25.0 src/Illuminate/Testing/ParallelRunner.php これはありがたい!詳しい使い方はドキュメントをチェック! Listeners, Mailables, NotificationsでShouldBeEncryptedが使えるように v8.25.0 src/Illuminate/Events/CallQueuedListener.php src/Illuminate/Mail/SendQueuedMailable.php src/Illuminate/Notifications/SendQueuedNotifications.php v8.19.0でジョブの内容を暗号化するようにリスナー、メール、通知でも使えるみたいですね ルーティングにmissing()メソッドが追加 v8.26.0 src/Illuminate/Routing/Middleware/SubstituteBindings.php ルートモデルバインディングを使用した際に、該当のデータがない場合にModelNotFoundExceptionが投げられますが、それを任意で処理できるように Route::get('/locations/{location:slug}', [LocationsController::class, 'show']) ->name('locations.view') ->missing(fn($request) => Redirect::route('locations.index', null, 301)); after columnで複数追加できるように v8.27.0 src/Illuminate/Database/Schema/Blueprint.php Schema::table('users', function (Blueprint $table) { $table->after('remember_token', function ($table){ $table->string('card_brand')->nullable(); $table->string('card_last_four', 4)->nullable(); }); }); EloquentのcastにAsArrayObject、AsCollectionが使えるように v8.28.0 src/Illuminate/Database/Eloquent/Casts/AsArrayObject.php src/Illuminate/Database/Eloquent/Casts/AsCollection.php castsにarrayを指定すると、jsonやtext型のフィールドに対して配列形式で保存することが可能ですが、AsArrayObjectやAsCollectionを指定することでObjectやCollectionで扱えるようになりました。詳しくはドキュメントをチェック! <?php namespace App\Models; use Illuminate\Database\Eloquent\Model; class User extends Model { /** * キャストする必要のある属性 * * @var array */ protected $casts = [ 'options' => AsArrayObject::class, ]; } バリデーションで最初にエラーが見つかったら即終了するstopOnFirstFailure()メソッドが追加 v8.30.0 src/Illuminate/Foundation/Http/FormRequest.php if ($validator->stopOnFirstFailure()->fails()) { // ... } FluentなjsonのAssertが追加 v8.32.0 src/Illuminate/Testing/Fluent/Assert.php Fluent(メソッドチェーンライク)にjsonのassertができるようになりました。 use Illuminate\Testing\Fluent\Assert; class PodcastsControllerTest extends TestCase { public function test_can_view_podcast() { $this->get('/podcasts/41') ->assertJson(fn (Assert $json) => $json ->has('podcast', fn (Assert $json) => $json ->where('id', $podcast->id) ->where('subject', 'The Laravel Podcast') ->where('description', 'The Laravel Podcast brings you Laravel & PHP development news.') ->has('seasons', 4) ->has('seasons.4.episodes', 21) ->has('host', fn (Assert $json) => $json ->where('id', 1) ->where('name', 'Matt Stauffer') ) ->has('subscribers', 7, fn (Assert $json) => $json ->where('id', 2) ->where('name', 'Claudio Dekker') ->where('platform', 'Apple Podcasts') ->etc() ->missing('email') ->missing('password') ) ) ); } } Eloquentにlazy()とlazyById()メソッドが追加 v8.34.0 src/Illuminate/Database/Concerns/BuildsQueries.php lazyを使うことで、LazyCollectionが返ってくるのでメモリ消費を抑えつつ、Collectionのように扱えるようになった!という感じですね $lazyCollection = User::lazy(); MySQLのdatetime型へのuseCurrentOnUpdateをサポート v8.36.0 src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php 無名クラスでマイグレーションが可能に v8.37.0 src/Illuminate/Database/Migrations/Migrator.php クラス名の衝突がなくなりますね <?php use Illuminate\Database\Migrations\Migration; return new class extends Migration { // }; Httpクライアントで同時非同期リクエスト処理ができるように v8.37.0 src/Illuminate/Http/Client/PendingRequest.php src/Illuminate/Http/Client/Pool.php これもかなり便利! use Illuminate\Http\Client\Pool; use Illuminate\Support\Facades\Http; $responses = Http::pool(fn (Pool $pool) => [ $pool->get('http://localhost/first'), $pool->get('http://localhost/second'), $pool->get('http://localhost/third'), ]); return $responses[0]->ok() && $responses[1]->ok() && $responses[2]->ok(); Stringable::whenNotEmpty()が追加 v8.39.0 src/Illuminate/Support/Stringable.php これは汎用性高そう! Str::of(env('SCOUT_PREFIX', '')) ->whenNotEmpty(fn (Stringable $prefix) => $prefix->finish('_')); バリデーションにPasswordルールが追加 v8.39.0 src/Illuminate/Validation/Rules/Password.php こんな感じで使えて $request->validate([ // Makes the password require at least one uppercase and one lowercase letter. 'password' => ['required', 'confirmed', Password::min(8)->mixedCase()], // Makes the password require at least one letter. 'password' => ['required', 'confirmed', Password::min(8)->letters()], // Makes the password require at least one number. 'password' => ['required', 'confirmed', Password::min(8)->numbers()], // Makes the password require at least one symbol. 'password' => ['required', 'confirmed', Password::min(8)->symbols()], // Ensures the password has not been compromised in data leaks. 'password' => ['required', 'confirmed', Password::min(8)->uncompromised()], ]); 全部を組み合わせることも可能 public function store(Request $request) { $request->validate([ 'name' => 'required|string|max:255', 'email' => 'required|string|email|max:255|unique:users', 'password' => ['required', 'confirmed', Password::min(8) ->mixedCase() ->letters() ->numbers() ->symbols() ->uncompromised(), ], ]); Modelのイベントを発生せずに更新するメソッドが追加 v8.41.0 src/Illuminate/Database/Eloquent/Model.php update([])と同じように使えます Model::updateQuietly([]) ちなみにsaveメソッドにはすでにあります $user = User::findOrFail(1); $user->name = 'Victoria Faith'; $user->saveQuietly(); カーソルページネーションが追加 v8.41.0 src/Illuminate/Contracts/Pagination/CursorPaginator.php 無限スクロールや、ビッグデータを扱う際のページング時に使うといいらしい 詳しくはドキュメントをチェック! $users = DB::table('users')->orderBy('id')->cursorPaginate(15); おわりに 全部は見切れませんでした!すみません! changelogは議論も含め結構おもしろいので、Laravelっ子はぜひ見てみてください collect()->containsOneItem()のくだりはさすがに笑った 認識間違っている場合はコメントくださると! 次は 「LaravelのOSS動向について書きます」by @hiro5963 さん
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Viewがない場合のLaravelのデバッグ(ロギング)

API化したコントローラーなどViewのない場合は, var_dump() や dd() といった関数が使えない この場合のデバッグの方法はいくつかあるが,手っ取り早くロギング(ログに書き込む)しても大丈夫 LogはLaravelフレームワークの一機能であるため使用する場合は下記を追加し呼び出せるようにする use Illuminate\Support\Facades\Log; あとは下記の通り調べたい変数などを記載 Log::debug($変数); ログの場所 /storage/logs/laravel.log (デフォルト設定の場合) (出力先の変更はconfig/logging.phpを書き換え) ターミナルにログを自動出力できるようにする $ tail -f storage/logs/laravel.log ちろんPostmanとか使い勝手の良いテストツールはあるけどね
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHPMD の ExcessiveClassComplexity (過剰なクラスの複雑さ) を解消しよう

これは ランサーズ Advent Calendar 2021 2 日目の記事です。 DBRE の まみー です。 DB 相談やサポートをチーム横断、事業横断でやっているのですが、その一環として PHPMD や PHPStan のサポートをしています。 中でも最近 PHPMD にて ExcessiveClassComplexity (過剰なクラスの複雑さ) を指摘される事例が増えてきました。 結論としては 指摘は必ず完全対応を前提に考える で、例外はありません。 とはいえ理由もよくわからないまま対応を考えるのはしんどいので、実例をエントリーにしてみます。 起きていたこと 自分の修正がトリガーとなって PHPMD に ExcessiveClassComplexity (過剰なクラスの複雑さ) を指摘されました。 そもそも複雑さがそこそこ高い既存クラス メソッド追加、分岐追加 閾値を超える 上記のように、既存クラスに何かしらの複雑さを追加した場合が多いです。 この場合の感情は概ね以下の感じなんじゃないかなと思います。 メソッド増やしただけなのに 分岐を追加しただけなのに 既存のコードは触っていないのに つまり、 修正したら突然怒られた となりがちなんですよね。 そもそも「ExcessiveClassComplexity (過剰なクラスの複雑さ)」とはなんなのか。 知っておく必要があります。 PHPMD の指摘内容 これは実際に社内の管理画面開発時に起こった事例です。 The class SearchComponent has an overall complexity of 50 which is very high. The configured complexity threshold is 50. 直訳すると以下です。 SearchComponent クラスの全体的な複雑さは 50 で、非常に高くなっています。設定されている複雑さのしきい値は50です。 複雑さの計算方法はこのあとで解説します。 その前に大事なことを書きます。 安易にやってはいけないこと 今回の原因は ExcessiveClassComplexity (過剰なクラスの複雑さ) が閾値を超えたためです。 ここで @SuppressWarnings(PHPMD.ExcessiveClassComplexity) をクラスの PHP Doc に書くと、PHPMD の分析範囲外にできます。 コードは CakePHP 4 前提です。 /** * Search Component * * ... * * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) */ class SearchComponent extends Component { ... が、安易にコメントでスルーせず、クラスの複雑さに向き合ういい機会を得たと受け止め、本質的な修正をしましょう。 サービスはこれからも成長を続けるので、抜本的な設計の見直しを行わない限りは 今現在が最も複雑さが低い状態 です。今後放っていても自然に低くなることはありません。 今やらないと後でもっと大変になるだけ。放置はデメリットでしかないです。 学習の文脈でも「学び始めるのは今が一番若い」と言われます。 今が一番シンプル である、と受け止め、品質と向き合いましょう。 また、閾値の緩和は設定ファイル (xml) で可能ですが、安易に行わないようにしましょう。 妥協から品質低下が始まります。 何を指摘されているのか ひとことで言うと 1 つのクラス内でのメソッドごとの複雑さの合計が閾値を超えた 状態です。 以下の場合に指摘されます。 メソッドの循環的複雑度 (CyclomaticComplexity) を計算 クラス単位の合計が閾値を超えた 複雑さの閾値はデフォルト 50 では PHPMD 公式 https://phpmd.org/rules/codesize.html から、過剰なクラスの複雑さ (ExcessiveClassComplexity) を見ていきましょう。 過剰なクラスの複雑さ (ExcessiveClassComplexity) ExcessiveClassComplexity の章を翻訳します。 過剰なクラスの複雑さ Since: PHPMD 0.2.5 あるクラスの Weighted Method Count (WMC、重み付けされたメソッド数) は、そのクラスの変更や保守にどれだけの時間と労力が必要かを示す良い指標です。WMC の指標は、クラスで宣言されたすべてのメソッドの複雑さの合計として定義されます。メソッドの数が多いということは、そのクラスが派生クラスに大きな影響を与える可能性があるということでもあります。 メソッドに重みを付ける基準が「循環的複雑度 (CyclomaticComplexity) 」で、重みの計算方法が公式にも掲載されています。 この、メソッドごとの循環的複雑度 (CyclomaticComplexity) の合計が、過剰なクラスの複雑さ (ExcessiveClassComplexity) の閾値である 50 を超えた際に指摘されます。 では次に、循環的複雑度 (CyclomaticComplexity) を見てみましょう。 循環的複雑度 (CyclomaticComplexity) CyclomaticComplexity を翻訳します。 循環的複雑度(CyclomaticComplexity) Since: PHPMD 0.1 複雑さは、メソッド内の決定点の数にメソッドのエントリの数を加えたもので決まります。決定点とは、'if', 'while', 'for', および 'case label' のことです。一般的に、1-4 は低い複雑さ、5-7 は中程度の複雑さ、8-10 は高い複雑さ、11+ は非常に高い複雑さを示します。 公式からコード例を引用します。 // Cyclomatic Complexity = 11 class Foo { 1 public function example() { 2 if ($a == $b) { 3 if ($a1 == $b1) { fiddle(); 4 } elseif ($a2 == $b2) { fiddle(); } else { fiddle(); } 5 } elseif ($c == $d) { 6 while ($c == $d) { fiddle(); } 7 } elseif ($e == $f) { 8 for ($n = 0; $n < $h; $n++) { fiddle(); } } else { switch ($z) { 9 case 1: fiddle(); break; 10 case 2: fiddle(); break; 11 case 3: fiddle(); break; default: fiddle(); break; } } } } 判断基準は以下のようです。 分岐 2 に関しては、実際に書いて試した結果としてカウントされていることを確認したものです。 メソッドそのものが 1 分岐 分岐 1 if elseif switch-case catch 分岐 2 && || ? ループ メソッド単位で循環的複雑度に問題がある場合は、CyclomaticComplexity も指摘されます。 今回の事例では、メソッド単位では問題なかったものの、合計してみるとクラス単位で問題になっていました。 実際に循環的複雑度を数えてみる 閾値は以下の合計数 50 です。 (カウント対象に抜け漏れあればご指摘ください) メソッド数 使う場合、使わない場合、で分岐と換算する 分岐数 三項演算子も分岐 if の中に複数条件あればそれぞれカウント if をカウントしているので else はカウントしない case をカウントしているので default はカウントしない コード 実際に、過剰なクラスの複雑さ (ExcessiveClassComplexity) の指摘を受けたコードを見ていきます。 一部省略したり改変していますが、カウント対象はそのままです。 <?php declare(strict_types=1); namespace App\Controller\Component; use Cake\Core\Configure; ... class SearchComponent extends Component { use ModelAwareTrait; 1 public function initialize(array $_config): void { parent::initialize(); } 2 public function existsIndex(): bool { return ...; } 3 public function deleteIndex(): bool { $response = ...; return !empty($response['...']); } 4 private function getCharFilter(): array { return [ ..., ]; } 5 private function getTokenizer(): array { return [ ..., ]; } 6 private function getAnalyzer(): array { return [ ..., ]; } 7 private function getTypeText(): array { return [ ..., ]; } 8 public function createIndex(): bool { $params = [ ..., ]; $response = ...; return !empty($response['...']); } 9 public function getProjectsQuery(): \Cake\ORM\Query { return $this->projects->find()->contain([ ..., ]); } 10 public function getProjectsQueryByStartIdAndEndId(int $startId = 1, int $endId = -1): \Cake\ORM\Query { $query = $this->getProjectsQuery(); 11 if ($endId < 1) { $query->where(['Projects.id >=' => $startId]); } else { $query->where(function (QueryExpression $exp) use ($startId, $endId) { return $exp->between('Projects.id', $startId, $endId); }); } return $query; } 12 public function getProjectsQueryByIds(array $ids): \Cake\ORM\Query { $query = $this->getProjectsQuery(); $query->where(function (QueryExpression $exp) use ($ids) { return $exp->in('Projects.id', $ids); }); return $query; } 13 public function getProjectsOfLimit(\Cake\ORM\Query $query, int $i = 0): array { return $query ->limit(100) ->offset(100 * $i) ->toList(); } 14 public function executeBulk(\Cake\ORM\Query $query): void { $total = 0; 15 for ($i = 0;; $i++) { $projects = $this->getProjectsOfLimit($query, $i); 16 if (count($projects) == 0) { break; } $loopIndexCount = $i + 1; $indexProjects = []; $deleteProjects = []; 17 foreach ($projects as $project) { 18 $isIndex = !$project->is_disabled && !$project->project_status->is_archived; 19 if ($isIndex) { $indexProjects[] = $project; } else { $deleteProjects[] = $project; } } 20 if (count($indexProjects)) { $this->elasticSearchComponent->bulkDocument( $this->makeIndexBulkParams($indexProjects) ); } 21 if (count($deleteProjects)) { $this->elasticSearchComponent->bulkDocument( $this->makeDeleteBulkParams($deleteProjects) ); } $total += count($projects); } } 22 public function message(string $message, array $params = [], ?string $className = null, ?string $functionName = null): string { 23 if (!$className) { $className = self::class; } 24 if (!$functionName) { $functionName = ...; } $outParams = ...; return vsprintf('[%s] %s ' . $message, $outParams); } 25 private function makeIndexBulkParams(array $projects): array { $params = ['body' => []]; 26 foreach ($projects as $project) { $params['body'][] = [ ..., ]; $params['body'][] = $this->getBody($Project); } return $params; } 27 public function makeDeleteBulkParams(array $projects): array { $params = ['body' => []]; 28 foreach ($projects as $project) { $params['body'][] = [ ..., ]; } return $params; } 29 private function getBody(Project $project): array { return [ ..., ]; } 30 private function makeRootAndPrimary(Project $project): array { $list = array_map(function ($p) { ...; }, ...); return [$list[0], $list[1]]; } 31 private function makeSecondary(Project $project): array { return array_map(function ($c) { return [ ..., ]; }, $Project->project_categories); } 32 private function makeUser(Project $project): array { return [ ..., ]; } 33 private function makeService(Project $project): array { return array_map(function ($i) { ...; }, ...); } 34 private function makePrice(Project $project): array { return array_map(function ($m) { ...; }, ...); } 35 private function makeDeliveryTime(Project $project): array { return array_map(function ($m) { ...; }, ...); } 36 private function makeTag(Project $project): array { return array_map(function ($t) { ...; }, ...); } 37 private function makeCountAll(Project $project): int { return $project->project_summary->count_all; } 38 private function makeAvgAll(Project $project): float { return $project->project_summary->avg_all; } 39 private function makeSumAll(Project $project): int { return $project->project_summary->sum_all; } 40 private function makeImpressionCount7d(Project $project): int { $fromDate = Carbon::now()->subDays(8)->format('Y-m-d'); $toDate = Carbon::now()->subDays(1)->format('Y-m-d'); return $this->getPointSummary($project, 'ImpressionProject', $fromDate, $toDate); } 41 private function makeFootprintCount7d(Project $project): int { $fromDate = Carbon::now()->subDays(8)->format('Y-m-d'); $toDate = Carbon::now()->subDays(1)->format('Y-m-d'); return $this->getPointSummary($project, 'FootprintProject', $fromDate, $toDate); } 42 private function makeQuoteCount7d(Project $project): int { $fromDate = Carbon::now()->subDays(8)->format('Y-m-d'); $toDate = Carbon::now()->subDays(1)->format('Y-m-d'); return $this->getPointSummary($project, 'QuoteProject', $fromDate, $toDate); } 43 private function makeLikeCount7d(Project $project): int { $fromDate = Carbon::now()->subDays(8)->format('Y-m-d'); $toDate = Carbon::now()->subDays(1)->format('Y-m-d'); return $this->getPointSummary($project, 'LikeProject', $fromDate, $toDate); } 44 private function makeOrderCount7d(Project $project): int { $fromDate = Carbon::now()->subDays(8)->format('Y-m-d'); $toDate = Carbon::now()->subDays(1)->format('Y-m-d'); return $this->getPointSummary($project, 'OrderProject', $fromDate, $toDate); } 45 private function makeCreated(Project $project): string { return Carbon::parse($project->created) ->setTimezone('UTC') ->format('Y-m-d\TH:i:s\Z'); } 46 private function makeModified(Project $project): string { return Carbon::parse($project->modified) ->setTimezone('UTC') ->format('Y-m-d\TH:i:s\Z'); } 47 private function getPointSummary(Project $project, string $metricName, ?string $fromDate = null, ?string $toDate = null): int { $metric = $this->Metrics ->findByName($metricName) ->cache($metricName) ->first(); 48 if (!$metric) { return 0; } $query = $this->... ->find('all') ->where([ 'metric_id' => $metric->id, 'project_id' => $Project->id, ]); if ( 49 !empty($fromDate) 50 && !empty($toDate) ) { $query = $query->where(function ($exp) use ($fromDate, $toDate) { return $exp->between('date', $fromDate, $toDate); }); } $pointSummary = $query ->select(['value' => $query->func()->sum('...')]) ->first(); return intval($pointSummary->value); } } 解説・改善 ちょうど 50 に達していました。 では、主な部分を上から順に簡単に解説します。 No. 4 ~ 7 ElasticSearch へインデックスを更新する際に必要な array を返すメソッドです。 似たようなメソッドが並びますが、単なる定数ではなくどれも違う処理で、具体的には以下の内容で個別メソッド化が適切でしたので、問題なしと判断しました。 返す array の構造がそれぞれで違う 用途に合わせた別のメソッド実行結果などから array を作る No. 9 ~ 12 クエリビルダの各条件をメソッド化しました。 今回はメソッドチェーンで書くには長く可読性に欠けると判断し、問題なしとしました。 (個人的には SQL は 1 箇所で見通しが良いのが好みではあります。) No. 14 ~ 21 : 改善の余地あり ElasticSearch へ実際にデータを投入するメソッドです。 limit をかけて DB から一定のレコードを取得 取得レコード全件ループ ElasticSearch のインデックスに対する振り分け 挿入対象を振り分け 削除対象を振り分け 挿入対象があればインデックスへ挿入 削除対象があればインデックスから削除 分岐数とループのネストに改善の余地があります。 が、以下の理由で改善対象外としました。 読めばわかる 十分にテストが書かれている 今後の拡張性が低い(修正される可能性が低い) 処理対象レコードがなくなるまで実行、の場合のループ処理を for で書くか while で書くかなどは、意見の分かれるところだとは思います。 No. 22 ~ 24 このコード実はバッチ処理なのですが、ログメッセージの編集を実行しています。 のちの実行経過観察、不具合時の追跡などに必要な処理でした。 No. 25 ~ 36 : 改善の余地あり No. 4 ~ 7 で ElasticSearch へインデックスを更新する際に必要な array を作っていましたが、その中で call されているメソッド群です。 これらは必要と判断できました。 ただしここでは array_map() を 6 箇所で使っています。 array_map() は foreach と等価なループを別の構文に書き換えたものと解釈できますので、循環的複雑度 (CyclomaticComplexity) が実質 6 箇所不当に隠蔽されているとも言えます。 引き続き複雑度の課題として、今後も改善を続けていく必要があります。 No. 37 ~ 39 : 要改善 getter メソッドです。 今回の用途は No. 4 ~ 7 の array に値をセットするための private メソッドで、わざわざ切り出す必要がなかったので廃止し、array に直接値をセットするよう修正しました。 メソッドが消えたことにより、 循環的複雑度 (CyclomaticComplexity) が 3 つ解消 されました。 が、 $project->project_summary->count_all のように、 デメテルの法則 に反しているコードは依然残ったままです。 詳細は後続の「今後の課題」で触れますが、循環的複雑度 (CyclomaticComplexity) が解消したとはいえ、引き続き課題と認識し改善を続けていく必要があります。 No. 40 ~ 44 : 要改善 さまざまな count を取得するメソッドです。 複数の問題点がありました。 期間が 7 days から 14 days などに変更された場合、5 つ全て修正する必要がある どの count を取りたいかが各メソッド内の固定文字列で書かれており、メソッド名と意味が重複する 取りたい count の種類が増えた場合、メソッドも増える 5 つのメソッドは統合できると判断し、以下の修正を行いました。 以下を引数で渡す 期間 取りたい count の文字列 結果 1 つのメソッドにまとまり、 循環的複雑度 (CyclomaticComplexity) が 4 つ解消 されました。 No. 45 ~ 46 : 要改善 DB の Datetime 型から、ElasticSearch の Timestamp フォーマットへ変換する処理です。 処理としては必要なのですが、2 つのメソッドの処理自体は同じで以下の問題がありました。 今後フォーマットが変わると 2 箇所修正が必要 ElasticSearch にインデックスしたい Datetime が増えると似たようなメソッドが増える よって、No. 40 ~ 44 と同じようにメソッドを統合することで、 循環的複雑度 (CyclomaticComplexity) が 1 つ解消 されました。 注意点 改修ポイントとは別に、カウントする際につい見落としがちな部分を解説します。 No. 18 論理積演算子 && です。 分岐は if や case が認識しやすいですが、以下も分岐です。 && || ? catch 17 foreach ($projects as $project) { 18 $isIndex = !$project->is_disabled && !$project->project_status->is_archived; 19 if ($isIndex) { $indexProjects[] = $project; } else { $deleteProjects[] = $project; } } 今回、これを見落としたために「あれ、1 つ足りない?」となって何度かコードを往復しました。 No. 49 ~ 50 && も分岐ですと書きましたが、 if 文の中でも同じです。 if と、その中の && は別物なのでそれぞれカウントする必要があります。 PSR-12 https://www.php-fig.org/psr/psr-12/ (5.1 if, elseif, else を参照) に従って改行していると気づきやすいですね。 if ( 49 !empty($fromDate) 50 && !empty($toDate) ) { if をカウントしていても、その中の分岐は忘れがちでした。 まとめ 重複するメソッドを統合することで、循環的複雑度 (CyclomaticComplexity) が 8 つ改善され、結果として ExcessiveClassComplexity (過剰なクラスの複雑さ) の閾値を下回りました。 確実に問題があったことがわかりました。 今回の PHPMD の指摘を追いかけてみましたが、以下のことが明白になるなと実感しました。 複雑さには必ず理由がある 理由を知る メソッド数、分岐数と種類 改善する クラスを分割 似たようなメソッドを 1 つにまとめる 無駄な分岐を減らす 改善の余地が残っていますし、今後は引き続き品質に向き合い、妥協せず改善していきます。 今後の課題 PHPMD が指摘してくれている循環的複雑度 (CyclomaticComplexity) は解消できましたが、以下の課題が残っています。 array_map() 使用による、実質的な foreach ループの隠蔽 array_map() を 6 箇所で使用していますが、 6 箇所ある= 循環的複雑度 (CyclomaticComplexity) が 6 箇所残っています。 8 箇所対応できて、閾値の 50 から 42 に改善できたと思っていても、実質的には 48 にしかなっていません。 メソッド、分岐、ループの追加で簡単に閾値を超えてしまう可能性が高いです。 その際は責務に応じてクラスを分割するなど、今後の課題として向き合い改善を続けていきます。 デメテルの法則違反 $project->project_summary->count_all のように、 デメテルの法則 に反しているコードがあります。 デメテルの法則から引用すると以下の部分が該当します。 基本的な考え方は、任意のオブジェクトが自分以外(サブコンポーネント含む)の構造やプロパティに対して持っている仮定を最小限にすべきであるという点にある。 あるオブジェクトAは別のオブジェクトBのサービスを要求してもよい(メソッドを呼び出してもよい)が、オブジェクトAがオブジェクトBを「経由して」さらに別のオブジェクトCのサービスを要求してはならない。これが望ましくないのは、オブジェクトAがオブジェクトBに対して、オブジェクトB自身の内部構造以上の知識を要求してしまうためである。 このような場合には、クラスBを変更し、クラスAがクラスBに対して行った要求を適切なBのサブコンポーネントに伝播させるようにすればよい。または、AがCへのリファレンスを持つようにして、AがCを直接呼ぶようにしてもよい。この法則に従えば、オブジェクトBが知っているのは自分自身の内部構造だけになる。 $project->project_summary->count_all は、アロー演算子で「自分 (ここでは $project) 以外の構造を参照」していることが問題なのですが、実は以下の処理結果でもあります。 projects テーブルと project_summaries テーブルは 1 対 1 の関係 CakePHP 4 の Model でアソシエーションされている ORM の contain で関連するテーブルを指定 具体的には $this->Projects->find()->contain([ 'project_summaries']); 結果、 $project オブジェクトの子として $project_summary を参照 project_summaries.count_all カラムを参照が目的 これが本当にデメテルの法則に反しているのか考える必要がありますし、CakePHP 4 と ORM の使い方としての正しさと合わせて、より深く調べる必要があるかと考えています。 おわりに ExcessiveClassComplexity (過剰なクラスの複雑さ) がちょうど 50 の実例をもとに書いてみました。 ちょうど 50 というと「それ作ったでしょ?」と思うかもですが、 実際に現場で起こっている事実 です。 同じ問題で悩んでいる人の記事で、 SuppressWarnings で対象外にしているのもありました。 現場によって事情が異なるのでそれはそれなのですが、 妥協は品質低下の始まりであることは常に意識したい なと考えています。 PHPMD を導入してみて、少しずつですが構造的な問題点が可視化されてきました。 特に今回は「今後の課題」でも触れたように、 PHPMD の指摘があったからこそより本質的な問題が表面化 しました。 問題の表層を取り除き本質を可視化することは PHPMD 導入の醍醐味の 1 つだと思いますし、非常にポジティブです。 自動化できるところは任せ、人間はより本質的なことへ注力するためにも、便利なツールを吟味の上で導入していきたいです。 そして、 妥協せず改善を続けていくことこそ成長 だと信じ、今後も続けていきます。 余談ですが、ランサーズの CakePHP バージョンアッププロジェクトでは PHPStan も導入しました。 これがまたすごく良くて最高です (語彙力)。 品質は上がるし勉強になるしで良いことしかありません。 みなさんもぜひ、品質に向き合ってみてはいかがでしょうか。 アドベントカレンダー、明日は QA チームのいさな(@isanasan_) さんの「CI を待たずに phpcs の検証を自動実行するためのエディタ中立な手法を模索する」です。コードの品質を向上するツールの話、楽しみです!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[PHP] [Laravel] ログ出力

Controller.php use Illuminate\Support\Facades\Log; class Controller { public function index(Request $request) { $message = ''; $context = []; try { // System is unusable. Log::emergency($message, $context); // Action must be taken immediately. Log::alert($message, $context); // Critical conditions. Log::critical($message, $context); // Exceptional occurrences that are not errors. Log::warning($message, $context); // Normal but significant events. Log::notice($message, $context); // Interesting events. Log::info($message, $context); // Detailed debug information. Log::debug($message, $context); // Logs with an arbitrary level. Log::log($message, $context); } catch (\Exception $e) { $context = [ 'error_message' => $e->getMessage(), 'error_detail' => $e, ]; Log::error($message, $context); } } } 参考URL https://readouble.com/laravel/8.x/ja/facades.html https://laravel.com/api/8.x/Illuminate/Log/LogManager.html
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【PHP】Facebook広告APIをcurlで操作したい!

はじめに 今まで広告関係では、 ・Google広告のAPI ・Yahoo検索広告 ・Yahooディスプレイ広告 について、 ライブラリを使わず直で叩く!をモットーに あれこれ調べ試行錯誤して導入してきました。 YahooはAPIを統一してくれ。。。 APIで使ってきた機能は 広告費やクリック数、インプレッション数などの レポート情報を取得することと、 入札単価を更新すること。 さて、そんな中、 今回新たにFacebook広告もAPI化したいということで、 運用の方から調査依頼がきましたよ!わくわく!! 調べてみたところ、FacebookのAPIは通称グラフAPIというらしい。 2021/12現在の最新バージョンはv12.0。 ドキュメントはこちら 一方、Facebook広告で使いたい機能を検索して 出てくるのはマーケティングAPI。 ドキュメントはこちら 資料もたくさん揃えてくれててなんて親切なんでしょう。 (ほとんど日本語版はないけど) マーケティングAPIもグラフAPIも、 「API」って付くからそれぞれ独立したAPIなのかと思っていたわけですが、 どうやらマーケティングAPIはグラフAPIの拡張版みたいなやつ?らしく。 それってつまりただのグラフAPI。。。 機能的に必要なのはマーケティングAPIなのですが、 エンドポイントはgraphなのです。 ライブラリの1つってかんじかな。 まぎらわしい。。 なので、考えるべきは 「グラフAPIをどう導入するか」 ということになります。 APIを使うための準備 どの媒体を利用するにしても、 APIを使えるようにするためにまずはあれこれ準備作業が必要です。 私は毎回この作業で躓いて心バッキバキに折られてきたわけで。 Googleほんとめんどくさかったな。 ドキュメント英語だし 資料ありすぎて何見ていいかわからんし。 Yahooは、取り掛かったのがGoogleのあとだったってのと ドキュメント日本語だし、機能も絞り込まれてて (絞り込まれてるゆえに、逆にめんどくさいことも多い) 比較的親切でわかりやすかったな。 Facebookはどうか。 結論: じみにわかりにくい。 でも導入に関してはググると、結構情報が出てきたので助かりました。 参考にしたサイトは 以下の手順説明の際に紹介させていただきます。 手順1:Facebookビジネスアカウントを作る 私はすでにアカウントがある状態から始めているので 作り方については詳しくないです。 がんばって作ってください・・・ 参考はこちらなど。。 手順2:Facebook for developersにビジネスIDを登録する for developersにビジネスIDでログインします。 (ログイン?紐付け?どっちか忘れたけど) APIを使うためには、「アプリ」というものを登録する必要があります。 ということで右上の「アプリ作成」から アプリタイプをえらび、次へ。 名前やメアドなど必要な項目を入力し、アプリを作成。 たぶんパスワード入力求められる 「アプリを製品に追加」の画面が表示されるので、 ここで「マーケティングAPI」を選びましょう。 あとはドキュメント見ろとかSDK入れるとか紹介されるけど、 基本あとでも確認できるので無視で大丈夫です。 手順3:アクセストークンを取得する APIを利用するためには、鍵であるアクセストークンが必要です。 FacebookAPIのアクセストークンは、GoogleやYahooのAPIとは違い、 手順2で設定したアプリの画面から生成できます。 ただし2時間の期限付き。 アクセストークンデバッガーというツールを使って 最大2ヶ月(3ヶ月?)までは延長できます。 システムに組み込む場合は、 期限ごとにアクセストークンを変更する必要があります。 うーん・・・めんどくさ(ry なんとかならんもんかと調べたところ、 期限なしアクセストークンというのが作れるとのこと。 セキュリティ的には定期的に入れ替えるのがベストだとは思うけど、 とりあえず期限は気にせず使えるやつがほしい!!! という手順を書いていただいたありがたい説明がこちら あとこちらも参考になりましたありがとうございました。 注意として、 「アクセス許可」にpages_show_listを付けるのを忘れないようにしましょう。 これしないと、アクセストークンデバッガーで「me/accounts」しても無反応です。 私できなくてこれで1日悩みました。。 設定したアクセストークンは、パスワード級に保管。 つぎに、アプリ左メニューの「設定」->「ベーシック」を開くと app secretがパスワード表示されています。 「表示」をクリックしてパスワード入力すると正しいものが表示されます。 これも実質パスワード扱いなので、厳重保管で。 ここまできたら、準備完了です。 あとは鍵を渡してデータ取得! 広告アカウントに紐づくキャンペーンを取ってみる GoogleでもYahooでも、PHP+curlでデータ取ってるので (意地でもライブラリは使わない) 今回もcurlでやります。直取りで!! API認証には、アクセストークンとともに、 アクセストークンとapp secretを暗号化したappsecret_proofという鍵を使います。 ドキュメントはこちら FacebookAPIは非常に簡単で、エンドポイントにGETでパラメータを渡します。 googleやyahooみたくheaderが不要です。 ドキュメントにちゃんとcURLでの取り方も書いてくれてて親切。 ノード(階層)によってパラメータが多少変わるので注意。 //エンドポイント $endpoint = "https://graph.facebook.com"; //バージョン:2021/12最新 $version = "v12.0"; //広告アカウントID $_ad_account_id = "AD_ACCOUNT_ID"; //アクセストークン $access_token = "ACCESS_TOKEN"; //app secret $app_secret = "APP_SECRET"; //appsecret_proofを生成 $appsecret_proof= hash_hmac('sha256', $access_token, $app_secret); //広告アカウントに紐付いたキャンペーン一覧を取る //デフォルトで上限25件なので、複数取りたい場合はlimit=1000とかを追加する $_url = "$endpoint/$version/act_$_ad_account_id/campaigns?"; $_url .= "access_token=$access_token&appsecret_proof=$appsecret_proof"; $_url .= "&fields=id,name,account_id,bid_strategy,status"; //curl START $_curl = curl_init(); //OPTIONをセット curl_setopt_array($_curl, [ CURLOPT_URL => $_url, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 120, ]); //curl EXEC(文字列で取得) $_resp = curl_exec($_curl); //エラーハンドリング用 $_errno = curl_errno($_curl); //curl END curl_close($_curl); //エラーハンドリング if ($_errno !== CURLE_OK) { } GETパラメータにつけてるfields項目で SELECTする項目を指定します。 ノードの構造について 詳細はリファレンス参照。 URL指定の際に、{ノード}/{エッジ}という指定をします。 ノードに、広告グループIDやキャンペーンなどで エッジが操作にあたります。 マーケティングAPIの構造としては 広告アカウント > キャンペーン > 広告セット(広告グループ) > 広告 となっていて、 各情報の詳細はひとつ下の階層まで取れるようです。 たとえばキャンペーンを指定して取るとき、 //キャンペーンID $_campaign_id = "CAMPAIGN_ID"; //A:広告アカウントのノード $_url = "$endpoint/$version/act_$_ad_account_id/campaigns?"; $_url .= "access_token=$access_token&appsecret_proof=$appsecret_proof"; $_url .= "&campaign_id=$_campaign_id"; //B:キャンペーンのノード $_url = "$endpoint/$version/$campaign_id/?"; $_url .= "access_token=$access_token&appsecret_proof=$appsecret_proof"; fieldsで指定できる項目は多少異なるかもしれませんが、 AとBで取れるものは基本的に同じです。 Aに広告セットIDを指定しても(&adset_id=XXXXというかんじ) 絞り込んでくれませんでした。エラーにはならなかったけど。 同様に、広告セットを指定して取るときは、 //広告セットID $_adset_id = "ADSET_ID"; //A:キャンペーンのノード $_url = "$endpoint/$version/$campaign_id/adsets?"; $_url .= "access_token=$access_token&appsecret_proof=$appsecret_proof"; $_url .= "&adset_id=$_adset_id "; //B:広告セットのノード $_url = "$endpoint/$version/$_adset_id /?"; $_url .= "access_token=$access_token&appsecret_proof=$appsecret_proof"; となります。 AでもBでも情報が取れるはず。 広告レポートを取ってみる キャンペーンや広告セット情報が取れたので、 次に肝心のレポートを取ってみます。 Facebook広告のレポートは「インサイト」と呼ばれるものらしいので ドキュメントでもinsightsってのを探しましょう。 呼び出し方としては、前述と変わらず、 認証情報とほしい項目と条件を全部GETで渡すだけです。 たとえば、 2021/11/24~25の広告セットのレポートを取る場合。 $_url = "$endpoint/$version/$campid/insights?"; $_url .= "access_token=$access_token&appsecret_proof=$appsecret_proof"; $_url .= "&level=adset";//levelに取りたいノードを指定 $_url .= "&fields=campaign_id,campaign_name,adset_id,adset_name,impressions,clicks,spend"; $_url .= "&time_range={'since':'2021-11-24','until':'2021-11-25'}"; $_url .= "&limit=10000";//デフォルト25件+ページング情報で返って来るので、たくさんある場合はlimit指定必須 上記の指定にすると、数値はすべて24~25の合計値が戻ってきます。 たとえばこれを1日ごとに欲しい場合は、条件をさらに追加して $_url .= "&time_increment=1"; さらにこれを1時間単位で取りたい場合は、条件に $_url .= "&breakdowns=['hourly_stats_aggregated_by_advertiser_time_zone']"; のように指定します。 ドキュメントは基本英語なので分かりにくい部分もありつつ、 いろんな条件を用意してくれているので 細かい情報も取ってこれるようになっています。 予算を更新してみる これまではAPIから情報を参照していただけですが、 登録処理はどうやるのか。 今回は「1日の予算」という項目をアップデートしてみます。 ドキュメントはこちら データ取得はずっとGETリクエストだったわけですが、 更新処理はPOSTリクエストになるようです。 なんでできないの~!?と1日悩んでしまったわけですが なんてことないPOSTを見逃してただけです。 ちゃんと読めと。。 //UPDATEはPOSTリクエスト $_url = "$endpoint/$_version/$_adset_id"; $_param = [ "access_token" => $access_token, "appsecret_proof" => $appsecret_proof, "daily_budget" => "3000", ]; //curl START $_curl = curl_init(); //OPTIONをセット curl_setopt_array($_curl, [ CURLOPT_URL => $_url, CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 120, CURLOPT_POSTFIELDS => $_param, ]); //curl EXEC(文字列で取得) $_resp = curl_exec($_curl); //エラーハンドリング用 $_errno = curl_errno($_curl); //curl END curl_close($_curl); //エラーハンドリング if ($_errno !== CURLE_OK) { //エラーハンドリング } 新規登録も基本的に同じではないかと思います。 (試してないけど) おわりに Facebook広告APIで検索してみても、 広告の設定方法とか説明はたくさん出てきても ほしい技術的な情報があんまり出てこなかったので、 自分なりに試行錯誤した結果をメモしてみました。 バージョン変わったりすると仕様も変わるし、 APIによって作りもさまざまなので 都度アップデートしていかないといけないなあと思っています。 おわり。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

LarastanでLaravelプロジェクトを静的解析しよう!

はじめに Laravel Advent Calendar 2021 2日目の投稿です。 PHPのソースコードの品質管理はどのように実施していますでしょうか。 チームで開発している場合はコーディング規約に従ってコードレビューを実施している場合もあるかと思います。 しかし、コードレビューを人の手で実施している場合は以下のような問題があるかと思います。 人力によるコードレビューのつらみ レビューに時間がかかる 修正コード量が多いと修正差分を見ただけで「うっ・・・」っとなります ルール違反していても見逃してしまう場合がある ルールが増えれば増えるほどチェック漏れのリスクが増加します コードレビューしてくれる人を探さなければならない コードレビューできる人が限られている場合、修正したコードがマージされるまでにかなり時間差が生まれることも コードレビューできない人をできるように育てないといけない 特定のスーパープログラマがレビュアーになっている場合は新しいメンバーに後任を務めてもらえるようにするのは簡単ではありません 差し戻し時にコードレビューする人の主観が入ってしまう場合がある 意見が半々に分かれるような指摘は受け入れられない場合があります そもそものやりたいことって? コードレビュー時は 型がかけているか とか PHPDocがちゃんと書けているか などを1つ1つ血眼でチェックしたいわけではないと思います。 基本的なルールは機械的にチェックしてしまい、コードレビューでは可読性や保守性を高めるにはどうしたらよいかといったコードと向き合う時間をできるだけ多く確保したいですね。 静的解析 静的解析ではプログラムを実行せずともソースコードを解析することで構文のチェックができます。つまり、プログラムを実行する前に、エラーとなりうる実装箇所や非推奨となっている実装箇所を発見することができます。 PHP はスクリプト言語であるため、プログラムを実行するまでそのソースコードが文法上問題ないかがわかりません。場合によっては特殊な条件でしか処理されないような分岐があったとして、その分岐内の処理がテストされないままリリースされてしまった結果、リリース後に不具合が発覚するなどもありえます。そのため、静的解析ツールでソースコード全体を機械的に構文チェックすることが有効です。 PHPStan PHPStan はPHPで有名な静的解析ツールの1つです。 Composerでインストール可能であったり公式のDockerコンテナが存在するなど、手軽に導入できる点が嬉しいです。 また、解析の厳密さを示すレベルを9段階で指定できることから、いきなりすべてのエラーに対応するのが大変な巨大なプロジェクトにも比較的導入しやすいかと思います。 各レベル別の解析内容は公式のドキュメントを参照していただけますと幸いです。 Larastan 前置きが長くなりましたが本題のLarastanについて記述していきます。 Larastan は PHPStanの拡張であり、Laravel プロジェクトの静的解析が可能です。 Larastan では Laravel でアプリケーションを作成する上でパフォーマンスを劣化させる可能性がある実装箇所や実行時エラーになりえる箇所をチェックすることができます。 導入方法 公式のREADMEにもしっかりと記載されていますが改めて解説します。 composer 経由での取得 Laravelプロジェクトのルートに移動して以下コマンドを実行 composer require nunomaduro/larastan --dev 設定ファイルの作成 Laravelプロジェクトのルートにて phpstan.neon または phpstan.neon.dist というファイル名のファイルを作成して以下の内容を記載します。 includes: - ./vendor/nunomaduro/larastan/extension.neon parameters: paths: - app # The level 9 is the highest level level: 5 ignoreErrors: - '#Unsafe usage of new static#' excludePaths: - ./*/*/FileToBeExcluded.php checkMissingIterableValueType: false 各パラメータの詳しい内容は以下をご参照ください。 Larastan による静的解析の実行 以下のコマンドを実行することで Larastan による静的解析が実行できます。 ./vendor/bin/phpstan analyse 解析結果はPHPStanによる実行結果に加えてLarastanで独自拡張された解析ルールで解析された結果が表示されます。 Larastanの解析ルール Larastanによって解析できる特別な解析ルールをいくつかご紹介いたします。 解析ルールの詳細はGitHub上のドキュメントより参照させていただきます。 NoModelMake Modelクラスの静的メソッドmake()を利用している箇所を検知してくれます。 User::make(); ソースコード上、上記ような書き方をしてもエラーにはなりませんが、インスタンスが無駄に2つ生成されてしまいます。 モデルクラスを利用する場合は単純に new でインスタンスを生成した方が効率的という理由から解析対象となっております。 内部実装を知っていなければそれがパフォーマンスを悪化させる原因になっていることに気づけないのでとてもありがたいですね。 NoUnnecessaryCollectionCall Illuminate\Support\Collection とそのサブクラスの組み合わせにおいて、無駄に重いSQLが生成される可能性がある実装箇所を検知してくれます。 // 改善前 User::all()->count(); $user->roles()->pluck('name')->contains('a role name'); // 改善後 User::count(); $user->roles()->where('name', 'a role name')->exists(); こういうことも知っていないとうっかりやってしまいそうですね。 RelationExistenceRule Eloquent 経由でDBアクセスする際、存在しないカラムに対してデータ取得しようとしている場合に検知することができます。 // Userにというカラムが存在しない場合にエラーを検知 \App\User::query()->has('foo'); // 複数カラムに対するデータ取得の場合でもすべてのカラムが存在しているかが検知可能 \App\Post::query()->has('users.transactions.foo'); マイグレーションを行い、DB構造が変わった場合などに特定処理が壊れてしまっていないかなどを簡単に検知できそうです。 CheckDispatchArgumentTypesCompatibleWithClassConstructorRule ジョブのディスパッチ引数の型がジョブクラスのコンストラクターと互換性があるかをチェックします。 class ExampleJob implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; /** @var int */ protected $foo; /** @var string */ protected $bar; public function __construct(int $foo, string $bar) { $this->foo = $foo; $this->bar = $bar; } // Rest of the job class } // 引数が足りないパターン ExampleJob::dispatch(1); // 引数の型が一致していないパターン ExampleJob::dispatch('bar', 1); これも嬉しいですね。 バッチ処理は通常の画面操作テストでは確認しづらい部分かと思いますので、単純ミスなどが早期に見つかる仕組みがあるのは心強いです。 おわりに Larastan は比較的簡単に導入できますが、きっちり有益なエラーを検知してくれます。実行時にようやく見つかるような不具合をより早い段階で気づけるようにしておくことで実装時の手戻りも減らせると思います。また、どういうコードが不適切であるかは知らなければ気付けません。コード品質を高めるための前提知識を静的解析経由で知ることができるのも導入のメリットかも知れません。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravelについて

Laravelとは PHPのフレームワークで、webアプリケーションを制作に用いられる。 1 MVCモデルを活用している。 MVCとはビジネスロジックをModelが担当し、表示などをViewが処理に、全体の制御を Controllerで行う形態いなる。 2パッケージ管理はComposerを行なってくれている。 依存関係などもまとめてインストールしてくれる。 3 Eloquent(エロクアント) ORM  データベスの操作を行なってくれている。 Eloquent Object Relational Mappingの略で、データベースとモデルにリレーションを持たせて、データを柔軟に扱えるようにしたもの。 Laravelのディレクトリ構成 ─── Laravel ├── app ├── bootstrap ├── config ├── database ├── public ├── resources ├── routes ├── storage ├── tests └── vendor App アプリケーションのコアコードを配置されている。ほとんどの全部のクラスは、このディレクトリに設定されている。 bootstrap フレームワークを初期起動処理するapp.phpファイルを設置している。。このディレクトリには、ルートやサービスのキャッシュファイルなど、パフォーマンスを最適化するためのフレームワークで生成されたファイルを含むcacheディレクトリも含む。通常、このディレクトリ内のファイルを変更する必要はない。 config アプリケーションの全設定ファイルを設置している。 database マイグレーションとモデルファクトリ、初期値設定(シーディング)を配置している。 このディレクトリをSQLiteデータベースの設置場所としても利用できます。 public アプリケーションへの全リクエストの入り口となり、オートローディングを設定するindex.phpファイルがある。また、このディレクトリにはアセット(画像、JavaScript、CSSなど)を配置する。 resources Viewと、CSSやJavaScriptなどの未加工のコンパイルされていないアセットを含む。すべての言語ファイルも格納してる。 routes web.php api.php console.php channels.php アプリケーションのすべてのルート定義を配置している。デフォルトでルートファイルをいくつかLaravelは用意されいる。 web.phpファイルは、RouteServiceProviderがwebミドルウェアグループへ配置するルートを記述する。これにより、セッション状態、CSRF保護、およびクッキー暗号化が提供される。アプリケーションがステートレスのRESTful APIを提供しない場合は、すべてのルートがweb.phpファイルで定義される。 api.phpファイルは、RouteServiceProviderがapiミドルウェアグループへ配置するルートを記述しる。これらのルートはステートレスであることが意図されているため、これらのルートを介してアプリケーションに入るリクエストは、トークンを介してで認証されることを意図しており、セッション状態にアクセスできない。 console.phpファイルは、クロージャベースのコンソールコマンドをすべて定義する場所。各クロージャはコマンドインスタンスと結合されるため、各コマンドのIOメソッドを操作する簡単なアプローチが可能。このファイルはHTTPルートを定義しませんが、アプリケーションへのコンソールベースのエントリポイント(ルート)を定義している。 channels.phpファイルは、アプリケーションがサポートするすべてのイベントブロードキャストチャンネルを登録できる場所。 storage app framework logs ディレクトリに分離される。 appディレクトリは、アプリケーションが作成したファイルを保存するために使用できる。 frameworkディレクトリは、フレームワークが作成したファイルとキャッシュを保存するために使用する。 最後に、logsディレクトリにはアプリケーションのログファイルを保存している。 tests 自動テストを設置する。 vendor Composerによる依存パッケージが配置されます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む