- 投稿日:2020-10-13T23:53:12+09:00
PHP 、Laravel 学習 ルーティングの設定
Laravelのダウンロード
これはあまり苦労せず終わったので、簡単に書いていきます。
とりあえず、LaravelをダウンロードするためにComposerと言うパッケージのダウンロードが必要になりますがそちらもあまり苦労ないと思うので順番さえ間違えなければすんなりいくと思います。「Composerってなんですか?」等、詳しく知りたい方はぐぐるとたくさん情報が出てきますのでググってみてください
ルーティングの設定
Laravelがインストールし終わったら早速最初の工程としてルーティングを設定していきます。
「リクエストが送られてきたら任意のレスポンスをする」という物です
自分だけ理解している記事は書きたくないので、噛み砕いて説明したいと思います。
普段ホームページを見ている時、何気なく操作しているところの一部です。
例えば、あるサイトを回遊している時「Aのページを見たいからAのページへ飛ぶためボタンをクリックして、Bのページへ飛ぶ」という操作をしたとします。何気なくやっているいつもの操作だと思いますが、この時の
●「Aのページへ飛びたい」というユーザーの要望をリクエストと言い●「Bのページを表示する」というサイト側の設定をレスポンスと言う
そしてこの「このリクエストが来たら、このレスポンスをすると言うのを決める設定」のことをルーティングと言います。
はじめにテストとして、「/hello」と言うリクエストが送られてきたら
「Hello World!」と言う言葉をブラウザに表示すると言うレスポンスを返してみます。ダウンロードしたLaravelファイルの中に「routes」と言うフォルダーがあるのでその中の「web.php」ファイルの一番下にRoute::get('/hello', 'HomeController@home');と言うコードを書いてみました。
このコードを詳しく解説すると「/helloと言うリクエストが来たらHomeControllerのhomeアクションが呼ばれ、然るべきレスポンスをする」
というコードです。
コントローラーとアクションも出てきましたが、説明が長くなるので次回以降に詳しく解説します。
しかし、ここで問題発生
ブラウザをのURLをhttp://127.0.0.1:8000/helloにして接続してみるとつながらず・・・「ホームコントローラーは存在しませんよ」と言うエラーが出てしまいました。
このエラーを解決するのにかなり時間がかかってしまったのですが、原因は①Laravelのバージョンアップでルーティングの表記の仕方が変わってしまった
②ルートの表記が間違えていた。の2点でした。
①の原因の解説としては、参考にした記事がLaravel7と言うバージョンだったのに対し、私がインストールしたのはLaravel8でした。
②の原因、ルートの表記が間違えていたと言うところですが「web.php」と言うファイル(ルーティングを実際に書いて設定するファイル)の中にどのような階層を踏んでルーティングするかを書くところがあるのですがこちらが間違えていました。
正しいソースコードを以下に載せておきます。①↓
Route::get('/hello', [HomeController::class, 'home']);②↓
use App\Http\Controllers\HomeController;次回以降の予定
①Controllerの詳しい説明と設定
②viewの詳しい説明と設定
③viewを簡単にスタイリングできるBootstrapの説明
こちらを念頭に置いて更新していきたいと思います。
- 投稿日:2020-10-13T23:29:27+09:00
PHP Laravel アプリ作成アウトプット
method_exists()関数
methood_exists(オブジェクト, メソッド)
第一引数で指定したクラスオブジェクトのクラス内に第二引数で指定したメソッドが含まれていればtrueを返し、そうでない場合falseを返します。property_exists
PHPマニュアル(https://www.php.net/manual/ja/function.property-exists.php)
機能 オブジェクトもしくはクラスにプロパティが存在するかどうかを調べる。
説明 property_exists ( mixed $class , string $property ) : bool
パラメータ class:確認するクラス、もしくはクラスのオブジェクトを指定します。 property:プロパティ名を指定します。env関数
env関数は、第一引数に指定した環境変数の値を返し、もしその環境変数が無ければ第二引数の値をデフォルト値として返します。
- 投稿日:2020-10-13T23:10:09+09:00
yps並走備忘録 Task5 Webアプリの作成
今回の主な課題
・MySQLのVIEWテーブルの理解
・Bootstrap UIの導入
・Laravel Mixの使い方事前準備
Node.jsとnpmのアップデート
sudo yum remove node npm -y
curl -sL https://rpm.nodesource.com/setup_12.x | sudo bash -
sudo yum install nodejs -y
インストールで来たら下記コマンドでアップデートされていることを確認
$ node -v
例:v12.18.3
$ npm -v
例:6.14.6一度Laravelのディレクトリに戻り、Laravel Mix(開発用)でビルドします
cd /var/www/html/yps
rm -rf ./node_modules
npm install && npm run dev
LaravelのuiにBootstrapを指定
composer require laravel/ui
php artisan ui bootstrap
Bootstrapをビルド
npm install && npm run dev
今回使用するデータをMySQLへ
1. VS Code内でターミナルを起動(ctrl+shift+@)
2.pwd
⇒Laravelのプロジェクトフォルダ(/var/www/html/yps)にいることを確認
3.mkdir resources/sql
⇒SQLファイル格納用のディレクトリを作成
4.mysqldump -u root -p -d worldcup2014db > resources/sql/worldcup2014db.sql
⇒テーブルの定義ファイルを取得(データは入手出来ていません)
5.grep -i 'create table' resources/sql/worldcup2014db.sql
⇒テーブルを確認[^1]
6. 各テーブル(countries, goals, pairings, players)にフィールド(expired_at, deleted_at, updated_at, created_at)を追加5の結果
CREATE TABLE `countries` ( CREATE TABLE `goals` ( CREATE TABLE `goals_tmp` ( CREATE TABLE `pairings` ( CREATE TABLE `pairings_tmp` ( CREATE TABLE `players` ( CREATE TABLE `players_tmp` (6の参考例
CREATE TABLE `countries` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(50) DEFAULT NULL, `ranking` int(11) DEFAULT NULL, `group_name` varchar(1) DEFAULT NULL, `expired_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `deleted_at` timestamp NULL DEFAULT NULL, `updated_at` timestamp NULL DEFAULT NULL, `created_at` timestamp NULL DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;MySQLでデータベースを削除⇒作り直し
1.mysql -u root -p
2.drop database worldcup2014db;
3.create database worldcup2014db;
4.use worldcup2014db;
5.source resources/sql/worldcup2014db.sql;
⇒ 作ったSQLファイルを元にテーブルを作成
6.show tables;
⇒ テーブルが作成されているか確認
7. OKならexit;
でMySQL CLiから出るTask 3 同様にデータを取得~流し込みの準備
1.cd /tmp
2.sudo yum install wget unzip -y
3.wget http://tech.pjin.jp/wp-content/uploads/2016/04/worldcup2014.zip
4.unzip http://worldcup2014.zip
5.ls -la worldcup2014.sql
6. 各テーブル(countries, goals, goals_tmp, pairings, pairings_tmp, players, players_tmp)のCREATE TABLE ~ DEFAULT CHAESET=utf8;までを削除(↓の部分)CREATE TABLE `countries` ( `id` int(11) NOT NULL, `name` varchar(50) DEFAULT NULL, `ranking` int(11) DEFAULT NULL, `group_name` varchar(1) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8;**データの流し込み
7. mysql -u root -p -D worldcup2014db
8. source /tmp/worldcup2014.sql; //warningが出ますが特に問題はないそうです
9. 各テーブルのデータ件数をチェックして下記になっていればOKmysql> select count(*) from countries; +----------+ | count(*) | +----------+ | 32 | +----------+ mysql> select count(*) from goals; +----------+ | count(*) | +----------+ | 188 | +----------+ mysql> select count(*) from pairings; +----------+ | count(*) | +----------+ | 144 | +----------+ mysql> select count(*) from players; +----------+ | count(*) | +----------+ | 736 | +----------+問題なければ
exit;
でMySQL Cliから出る簡易アプリケーションの作成
使うデータは用意できたので、今度はデータを使ってアプリケーションを作成してみます。
モデルクラスの作成
Task3で作成したモデルクラスを削除
cd /var/www/html/yps
rm app/Models/Player.php
以下のコマンドを打ってデータベースと連動したモデルクラスを作成します
php artisan make:model Models/Country -m
php artisan make:model Models/Goal -m
php artisan make:model Models/Pairing -m
php artisan make:model Models/Player -m
上記で作成した各モデルクラスに明示的にテーブルを指定し、ついでに$dateも指定します。
※以下はPlayerモデルの例ですPlayer.php<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; class Player extends Model { protected $table = "players"; protected $dates = ["expired_at", "deleted_at", "updated_at", "created_at"]; }コントローラークラスの作成
php artisan make:controller CountryController --resource --model=Models/Country
php artisan make:controller GoalController --resource --model=Models/Goal
php artisan make:controller PairingController --resource --model=Models/Pairing
php artisan make:controller PlayerController --resource --model= Models/Player
php artisan make:controller WelcomeController --resource
Viewの作成
resources/views/welcome.blade.phpに以下をコピペ<!DOCTYPE html> <html lang="{{ str_replace('_', '-', app()->getLocale()) }}"> <head> <title>yotaro prg</title> <!-- Fonts --> <link href="https://fonts.googleapis.com/css?family=Nunito:200,600" rel="stylesheet"> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="description" content=""> <meta name="author" content="Mark Otto, Jacob Thornton, and Bootstrap contributors"> <meta name="generator" content="Jekyll v4.1.1"> <!-- Bootstrap core CSS --> <link href="{{ asset('css/app.css') }}" rel="stylesheet"> <!-- Favicons --> <link rel="apple-touch-icon" href="/yotaro.jpg" sizes="180x180"> <link rel="icon" href="/yotaro.jpg" sizes="32x32" type="image/jpg"> <link rel="icon" href="/yotaro.jpg" sizes="16x16" type="image/jpg"> <?php /* <link rel="manifest" href="/docs/4.5/assets/img/favicons/manifest.json"> <link rel="mask-icon" href="/docs/4.5/assets/img/favicons/safari-pinned-tab.svg" color="#563d7c"> */ ?> <link rel="icon" href="/yotaro.jpg"> <meta name="theme-color" content="#563d7c"> <style> body { padding-top: 5rem; } .starter-template { padding: 3rem 1.5rem; text-align: center; } .bd-placeholder-img { font-size: 1.125rem; text-anchor: middle; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } @media (min-width: 768px) { .bd-placeholder-img-lg { font-size: 3.5rem; } } </style> </head> <body> <nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top"> <a class="navbar-brand" href="#">Navbar</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarsExampleDefault" aria-controls="navbarsExampleDefault" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarsExampleDefault"> <ul class="navbar-nav mr-auto"> <li class="nav-item active"> <a class="nav-link" href="#">Home <span class="sr-only">(current)</span> </a> </li> <li class="nav-item"> <a class="nav-link" href="#">Link</a> </li> <li class="nav-item"> <a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Disabled</a> </li> <li class="nav-item dropdown"> <a class="nav-link dropdown-toggle" href="#" id="dropdown01" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Dropdown</a> <div class="dropdown-menu" aria-labelledby="dropdown01"> <a class="dropdown-item" href="#">Action</a> <a class="dropdown-item" href="#">Another action</a> <a class="dropdown-item" href="#">Something else here</a> </div> </li> </ul> <form class="form-inline my-2 my-lg-0"> <input class="form-control mr-sm-2" type="text" placeholder="Search" aria-label="Search"> <button class="btn btn-secondary my-2 my-sm-0" type="submit">Search</button> </form> </div> </nav> <main role="main" class="container"> <h1>WorldCup 2014 選手一覧</h1> </br> <table class="table table-striped table-hover table-sm table-responsive-sm"> <thead> <tr> <th scope="col">#</th> <th scope="col">country</th> <th scope="col">uniform_num</th> <th scope="col">position</th> <th scope="col">name</th> <th scope="col">club</th> <th scope="col">birth</th> <th scope="col">height</th> <th scope="col">weight</th> <th scope="col">total_goals</th> </tr> </thead> <tbody> @foreach ($players as $player) <tr> <td>{{ $player->id }}</td> <td>{{ $player->c_name }}</td> <td>{{ $player->uniform_num }}</td> <td>{{ $player->position }}</td> <td>{{ $player->name }}</td> <td>{{ $player->club }}</td> <td>{{ $player->birth }}</td> <td>{{ $player->height }}</td> <td>{{ $player->weight }}</td> <td>{{ $player->t_goals }}</td> </tr> @endforeach </tbody> </table> <div class="row justify-content-end"> {{ $players->links() }} </div> </main> <!-- /.container --> <script src="{{ asset('js/app.js') }}"></script> </body> </html>ルーター作成
routes/web.phpに下記を記述web.phpRoute::get('/', 'WelcomeController@index'); Route::resource('players', 'PlayerController'); Route::resource('countries', 'CountryController'); Route::resource('goals', 'GoalController'); Route::resource('pairings', 'PairingController');課題はここから…
- テーブルビューを1つ(あるいは2つ)追加
- functionを1つ追加
- controllerからviewに変数渡し してこのツイートと同じ見た目になるようにします
答えはコチラ
Task 5はいくつか追加の課題もあります
1. レコード追加
2. 論理削除
3. phpMyAdminのインストールイージーモードになるyps委員長のブログはこちら
miyupaca log ⇒ yps学習記録その5以上でTask5は終了です。
(ここでかなりの脱落者が出ましたが、今は答えもGitHubに載っているのでコピペでもいけてしまうかと思います…)
- 投稿日:2020-10-13T23:10:09+09:00
yps並走備忘録 Task5 簡易Webアプリの作成(SQLとモデルを理解する編)
今回の主な課題
・MySQLのVIEWテーブルの理解
・Bootstrap UIの導入
・Laravel Mixの使い方事前準備
Node.jsとnpmのアップデート
sudo yum remove node npm -y
curl -sL https://rpm.nodesource.com/setup_12.x | sudo bash -
sudo yum install nodejs -y
インストールで来たら下記コマンドでアップデートされていることを確認
$ node -v
例:v12.18.3
$ npm -v
例:6.14.6一度Laravelのディレクトリに戻り、Laravel Mix(開発用)でビルドします
cd /var/www/html/yps
rm -rf ./node_modules
npm install && npm run dev
LaravelのuiにBootstrapを指定
composer require laravel/ui
php artisan ui bootstrap
Bootstrapをビルド
npm install && npm run dev
今回使用するデータをMySQLへ
1. VS Code内でターミナルを起動(ctrl+shift+@)
2.pwd
⇒Laravelのプロジェクトフォルダ(/var/www/html/yps)にいることを確認
3.mkdir resources/sql
⇒SQLファイル格納用のディレクトリを作成
4.mysqldump -u root -p -d worldcup2014db > resources/sql/worldcup2014db.sql
⇒テーブルの定義ファイルを取得(データは入手出来ていません)
5.grep -i 'create table' resources/sql/worldcup2014db.sql
⇒テーブルを確認[^1]
6. 各テーブル(countries, goals, pairings, players)にフィールド(expired_at, deleted_at, updated_at, created_at)を追加5の結果
CREATE TABLE `countries` ( CREATE TABLE `goals` ( CREATE TABLE `goals_tmp` ( CREATE TABLE `pairings` ( CREATE TABLE `pairings_tmp` ( CREATE TABLE `players` ( CREATE TABLE `players_tmp` (6の参考例
CREATE TABLE `countries` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(50) DEFAULT NULL, `ranking` int(11) DEFAULT NULL, `group_name` varchar(1) DEFAULT NULL, `expired_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `deleted_at` timestamp NULL DEFAULT NULL, `updated_at` timestamp NULL DEFAULT NULL, `created_at` timestamp NULL DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;MySQLでデータベースを削除⇒作り直し
1.mysql -u root -p
2.drop database worldcup2014db;
3.create database worldcup2014db;
4.use worldcup2014db;
5.source resources/sql/worldcup2014db.sql;
⇒ 作ったSQLファイルを元にテーブルを作成
6.show tables;
⇒ テーブルが作成されているか確認
7. OKならexit;
でMySQL CLiから出るTask 3 同様にデータを取得~流し込みの準備
1.cd /tmp
2.sudo yum install wget unzip -y
3.wget http://tech.pjin.jp/wp-content/uploads/2016/04/worldcup2014.zip
4.unzip http://worldcup2014.zip
5.ls -la worldcup2014.sql
6. 各テーブル(countries, goals, goals_tmp, pairings, pairings_tmp, players, players_tmp)のCREATE TABLE ~ DEFAULT CHAESET=utf8;までを削除(↓の部分)CREATE TABLE `countries` ( `id` int(11) NOT NULL, `name` varchar(50) DEFAULT NULL, `ranking` int(11) DEFAULT NULL, `group_name` varchar(1) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8;**データの流し込み
7. mysql -u root -p -D worldcup2014db
8. source /tmp/worldcup2014.sql; //warningが出ますが特に問題はないそうです
9. 各テーブルのデータ件数をチェックして下記になっていればOKmysql> select count(*) from countries; +----------+ | count(*) | +----------+ | 32 | +----------+ mysql> select count(*) from goals; +----------+ | count(*) | +----------+ | 188 | +----------+ mysql> select count(*) from pairings; +----------+ | count(*) | +----------+ | 144 | +----------+ mysql> select count(*) from players; +----------+ | count(*) | +----------+ | 736 | +----------+問題なければ
exit;
でMySQL Cliから出る簡易アプリケーションの作成
使うデータは用意できたので、今度はデータを使ってアプリケーションを作成してみます。
モデルクラスの作成
Task3で作成したモデルクラスを削除
cd /var/www/html/yps
rm app/Models/Player.php
以下のコマンドを打ってデータベースと連動したモデルクラスを作成します
php artisan make:model Models/Country -m
php artisan make:model Models/Goal -m
php artisan make:model Models/Pairing -m
php artisan make:model Models/Player -m
上記で作成した各モデルクラスに明示的にテーブルを指定し、ついでに$dateも指定します。
※以下はPlayerモデルの例ですPlayer.php<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; class Player extends Model { protected $table = "players"; protected $dates = ["expired_at", "deleted_at", "updated_at", "created_at"]; }コントローラークラスの作成
php artisan make:controller CountryController --resource --model=Models/Country
php artisan make:controller GoalController --resource --model=Models/Goal
php artisan make:controller PairingController --resource --model=Models/Pairing
php artisan make:controller PlayerController --resource --model= Models/Player
php artisan make:controller WelcomeController --resource
Viewの作成
resources/views/welcome.blade.phpに以下をコピペ<!DOCTYPE html> <html lang="{{ str_replace('_', '-', app()->getLocale()) }}"> <head> <title>yotaro prg</title> <!-- Fonts --> <link href="https://fonts.googleapis.com/css?family=Nunito:200,600" rel="stylesheet"> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="description" content=""> <meta name="author" content="Mark Otto, Jacob Thornton, and Bootstrap contributors"> <meta name="generator" content="Jekyll v4.1.1"> <!-- Bootstrap core CSS --> <link href="{{ asset('css/app.css') }}" rel="stylesheet"> <!-- Favicons --> <link rel="apple-touch-icon" href="/yotaro.jpg" sizes="180x180"> <link rel="icon" href="/yotaro.jpg" sizes="32x32" type="image/jpg"> <link rel="icon" href="/yotaro.jpg" sizes="16x16" type="image/jpg"> <?php /* <link rel="manifest" href="/docs/4.5/assets/img/favicons/manifest.json"> <link rel="mask-icon" href="/docs/4.5/assets/img/favicons/safari-pinned-tab.svg" color="#563d7c"> */ ?> <link rel="icon" href="/yotaro.jpg"> <meta name="theme-color" content="#563d7c"> <style> body { padding-top: 5rem; } .starter-template { padding: 3rem 1.5rem; text-align: center; } .bd-placeholder-img { font-size: 1.125rem; text-anchor: middle; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } @media (min-width: 768px) { .bd-placeholder-img-lg { font-size: 3.5rem; } } </style> </head> <body> <nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top"> <a class="navbar-brand" href="#">Navbar</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarsExampleDefault" aria-controls="navbarsExampleDefault" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarsExampleDefault"> <ul class="navbar-nav mr-auto"> <li class="nav-item active"> <a class="nav-link" href="#">Home <span class="sr-only">(current)</span> </a> </li> <li class="nav-item"> <a class="nav-link" href="#">Link</a> </li> <li class="nav-item"> <a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Disabled</a> </li> <li class="nav-item dropdown"> <a class="nav-link dropdown-toggle" href="#" id="dropdown01" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Dropdown</a> <div class="dropdown-menu" aria-labelledby="dropdown01"> <a class="dropdown-item" href="#">Action</a> <a class="dropdown-item" href="#">Another action</a> <a class="dropdown-item" href="#">Something else here</a> </div> </li> </ul> <form class="form-inline my-2 my-lg-0"> <input class="form-control mr-sm-2" type="text" placeholder="Search" aria-label="Search"> <button class="btn btn-secondary my-2 my-sm-0" type="submit">Search</button> </form> </div> </nav> <main role="main" class="container"> <h1>WorldCup 2014 選手一覧</h1> </br> <table class="table table-striped table-hover table-sm table-responsive-sm"> <thead> <tr> <th scope="col">#</th> <th scope="col">country</th> <th scope="col">uniform_num</th> <th scope="col">position</th> <th scope="col">name</th> <th scope="col">club</th> <th scope="col">birth</th> <th scope="col">height</th> <th scope="col">weight</th> <th scope="col">total_goals</th> </tr> </thead> <tbody> @foreach ($players as $player) <tr> <td>{{ $player->id }}</td> <td>{{ $player->c_name }}</td> <td>{{ $player->uniform_num }}</td> <td>{{ $player->position }}</td> <td>{{ $player->name }}</td> <td>{{ $player->club }}</td> <td>{{ $player->birth }}</td> <td>{{ $player->height }}</td> <td>{{ $player->weight }}</td> <td>{{ $player->t_goals }}</td> </tr> @endforeach </tbody> </table> <div class="row justify-content-end"> {{ $players->links() }} </div> </main> <!-- /.container --> <script src="{{ asset('js/app.js') }}"></script> </body> </html>ルーター作成
routes/web.phpに下記を記述web.phpRoute::get('/', 'WelcomeController@index'); Route::resource('players', 'PlayerController'); Route::resource('countries', 'CountryController'); Route::resource('goals', 'GoalController'); Route::resource('pairings', 'PairingController');課題はここから…
- テーブルビューを1つ(あるいは2つ)追加
- functionを1つ追加
- controllerからviewに変数渡し してこのツイートと同じ見た目になるようにします
答えはコチラ
Task 5はいくつか追加の課題もあります
1. レコード追加
2. 論理削除
3. phpMyAdminのインストールイージーモードになるyps委員長のブログはこちら
miyupaca log ⇒ yps学習記録その5以上でTask5は終了です。
(ここでかなりの脱落者が出ましたが、今は答えもGitHubに載っているのでコピペでもいけてしまうかと思います…)
- 投稿日:2020-10-13T22:39:40+09:00
【laravel 8.x】Logging の設定とログフォーマットの実装方法【基本編】
laravel
のログにはPHPでよく使われているらしいMonolog
が実装されている。
こいつをごにょごにょしていい感じにしたいのが本記事。
(簡潔に書きたかったがソースも書くと長くなるねぇ…)Laravel は
ver8.9.0
。(v6くらいから同じだと思う)
目的はログのフォーマットの変更とカラーリング。
- ソース:ログ 8.x Laravel
- monolog:Seldaek/monolog: Sends your logs to files, sockets, inboxes, databases and various web services
- カラーリング: bramus/monolog-colored-line-formatter: Colored/ANSI Line Formatter for Monolog
- 大いに参考:Laravelでログのフォーマットを変えたい - Qiita
1. ログの出力方法
Laravel のログは RFC5424 ってので定義されている8つのレベルが扱える。
emergency
>alert
>critical
>error
>warning
>notice
>info
>debug
ログを出力する方法は facade を使う方法と helper を使う方法の2種類(たぶん)。
デフォルトではstorage/logs/laravel.log
に出力される。Log Facade でログを出力する。
use Illuminate\Support\Facades\Log; $name = 'sample kunn'; Log::info('Showing user: '.$name); // => [2020-10-13 19:27:04] local.INFO Showing user: sample kunn Log:notice('User failed to login.', ['id' => 3]); // => [2020-10-13 19:32:38] local.INFO User failed to login. {"id":3}第2引数(context) に渡したデータはシリアライズされて出力される(デフォルト)。
Logger Helper でログを出力する
logger()->info('sample text'); // => [2020-10-13 19:35:43] local.INFO sample textどちらとも
LogManager
を内部で呼び出しているので使い方は同じっぽい。
RFC5424 の 8 つのレベルの関数が定義されているので使い分けるべし。
Facade と Helper、決めた方に統一して使うのが賢そう。
参考:Illuminate\Log\LogManager | Laravel API
local.xxx
は.env
のAPP_ENV
の値かな?
production
とかlocal
といった実行環境の値となる。他のロガーでいうカテゴリやタイプといった塊は存在しない。(ほんとぉ?)
必要ならラッパーを作るなりする必要がありそう。
- Laravel ConsoleとHttpのログファイルを分ける - Qiita
- この辺りを参考に
processor
にフィールドを追加させるのが最適解だと思う。Exception ログ
use Exception; logger()->error(new Exception('Reigai'));Exception 系列を渡せば stackTrace も出力される。
2. ログの設定ファイル
基本的には
app/config/logging.php
に設定を書き込むだけで使える。app/config/logging.php'default' => env('LOG_CHANNEL', 'stack'), 'channels' => [ 'stack' => [ 'driver' => 'stack', 'channels' => ['single'], 'ignore_exceptions' => false, ], 'single' => [ 'driver' => 'single', 'path' => storage_path('logs/laravel.log'), 'level' => 'debug', ], 'daily' => [ 'driver' => 'daily', 'path' => storage_path('logs/laravel.log'), 'level' => 'debug', 'days' => 14, ], 'stderr' => [ 'driver' => 'monolog', 'handler' => StreamHandler::class, 'formatter' => env('LOG_STDERR_FORMATTER'), 'with' => [ 'stream' => 'php://stderr', ], ], ... ]
channel
というのはログの出力方法を指したもので、これを使ってログを出力する。
TVのチャンネルみたいに電波を分ける役割(でいいのかな?)。
デフォルトではstack
というものが指定されている。
stack
だけは特殊で、プロパティのchannels
で指定した複数のチャンネルに処理を渡すことができる。
TVでいう多チャンネル同時視聴。
デフォルトでは['single']
が指定されているので、single
というチャンネル1つにログの出力を渡している。
single
はlogs/laravel.log
に書き込むチャンネルなので、logファイルに追記されるわけだ。初めから定義されていたチャンネルを以下にまとめる。
channel名 説明 stack 複数のチャンネルへログを渡す single 指定したファイルにログを書き出す daily 指定したファイルにログを書き出す(所謂ローテート)
一日ごとにファイルを区切り、一定の日数が経過したら削除する(任意)slack Slack に出力する papertrail Papertrail に出力する stderr 標準エラー出力に出力する syslog Linux 等の syslog に出力する errorlog PHPの error_log() に出力する null 何もしない emergency (emergency ログだけ出力?) 他にも SNS に送ったり DB に送ったりと、大抵のやりたいことは誰かがライブラリを実装してくれてる。
3. stdout channel の追加
log を使ってて思ったのが、cli を操作している時は標準出力にも出力して欲しい点。
stderr
チャンネルは存在するので、それをベースにstdout
チャンネルを作成してみる。app/config/logging.php'stdout' => [ 'driver' => 'monolog', 'handler' => StreamHandler::class, 'with' => [ 'stream' => 'php://stdout', ], 'level' => 'debug', // all log ],
StreamHandler
のstream
パラメータにPHPの標準出力(stdout)を指定した。
level
によって出力するログレベルを制限できるが、最低値のdebug
にすることにより、全てのログを対象としている。
試しにchannels.stack.channels
にstdout
を追加すると標準出力にも出力されるようになるはず。
Handler
は monolog 側のシステムなので今回は省く。
詳しくはこのあたり。[オプション] CLI実行時(php artisan)のみ標準出力にも出力する
このまま
stack
に追記でもいいのだが、CLIを操作していない時に標準出力に出力されるのはあまり好みではない。
(schedule 処理、http のアクセス時等にも出力されてしまう(握りつぶされるが))
なので CLI で実行したときに限って出力するようにしてみる。App\Providers\AppServideProvider.phpclass AppServiceProvider extends ServiceProvider { public function boot() { ... // logger cli mode if(strpos(strtolower(php_sapi_name()), 'cli') !== false) { $path = 'logging.channels.stack.channels'; $stacks = collect(config($path, [])) ->push('stdout') ->unique(); config([$path => $stacks]); } } }php_sapi_name() という関数を実行すると、
apache
やcli
、cgi
といった文字列が帰ってくる。
これを利用して、名前にCLIが含まれていたら stack にstdout
を追加する仕組み。
collection の unique() を使うことで重複実装を防いでいる。とりあえず
AppServiceProvider
に実装したが、LoggerServideProvider
とかを作って実装したほうが賢い。参考:php - Detect if running from the command line in Laravel 5 - Stack Overflow
4. ログ文字列のフォーマット
さて本題。
デフォルトの出力文字列は以下のような形式だった。これは monolog のデフォルトの
Formatter\LineFormatter
で以下が適用されているためである。
[%datetime%] %channel%.%level_name%: %message% %context% %extra%\n
参照:monolog/LineFormatter.php at master · Seldaek/monologこいつを変更したいので
LineFormatter
を拡張した class を作成する。App\Logging\Formatters\CustomLineFormatter.php<?php namespace App\Logging\Formatters; use Monolog\Formatter\LineFormatter; class CustomLineFormatter extends LineFormatter { public function __construct() { // 2020-10-13 17:12:15.375 [local.INFO] log message という形式で出力 $lineFormat = "%datetime% [%channel%.%level_name%] %message%" . PHP_EOL; $dateFormat = "Y-m-d H:i:s.v"; // PHP: DateTime::format parent::__construct($lineFormat, $dateFormat, true, true); } public function format(array $record): string { // var_dump($record); $output = parent::format($record); // var_dump($output); return $output; } }
LineFormatter
のコンストラクタは以下を引数に取る。
引数 説明 $format フォーマット文字列 $dateFormat 日付(%datetime%)のフォーマット文字列
参考:PHP: DateTime::format - Manual$allowInlineLineBreaks ログの改行を許すか $ignoreEmptyContextAndExtra context と extra の値が空のときに [] を削除するかどうか 参照:monolog/LineFormatter.php at master · Seldaek/monolog
format()
は今回は親のものを呼び出しているだけだが、ここでフォーマットのやり方を定義することができる。
引数のrecord
配列は以下の通り。$record = [ 'message' => $message, 'context' => $context, 'level' => $level, 'level_name' => $levelName, 'channel' => $this->name, 'datetime' => new DateTimeImmutable($this->microsecondTimestamps, $this->timezone), 'extra' => [], ];参照:monolog/Logger.php at master · Seldaek/monolog
ここで
level
やmessage
を加工してparent::format($record)
を呼べば、かなり自由度の高いことが可能だ。
context
はログ関数実行時の第2引数(配列?オブジェクト?)。
今のままだと context は握りつぶされているので、
$record['message] .= json_encode($record['context']);
みたいな実装が要るかも。ここで DB に保存をかけたりしても面白そう。
参考にした文献だと context をvsprintf()
に突っ込むことでprintf()
like な実装を示してた。
Levelに応じた絵文字とかタイマーとか実装できたら楽しそう。
extra
は実行された行番号やIPアドレスなどを取得できるが、今回は扱わない。
processor
という仕組みを使うのだが、この辺りが参考になるかも。Laravel にフォーマットを適用する
laravel に適用するには
tap
という配列パラメータに__invoke()
が実装されているクラスを指定してあげる必要がある。参考:ログ 8.x/ Laravel
の下の方の「チャンネル用Monologカスタマイズ」App\Logging\LineFormatterApply.php<?php namespace App\Logging; use App\Logging\Formatters\CustomLineFormatter; class LineFormatterApply { public function __invoke($logging) { $customLineFormatter = new CustomLineFormatter(); foreach($logging->getHandlers() as $handler) { $handler->setFormatter($customLineFormatter); } } }おそらく
CustomLineFormatter
を指定があった handler 全てに渡すのだと思う。
(extend とか implements とかが無い暗黙実装は嫌いだぁ…)これを
config/logging
の使わせたいチャンネルに指定してあげる。
今はstdout
とsingle
に。
(stack
に渡すと全チャンネルに適用できるが、子で指定していた場合上書きされた)
(formatter は一つしか追加できない?)app/config/logging.phpuse App\Logging\LineFormatterApply; 'stdout' => [ 'driver' => 'monolog', 'handler' => StreamHandler::class, 'with' => [ 'stream' => 'php://stdout', ], 'level' => 'debug', 'tap' => [ColorFormatterApply::class], // <-- これ ],こんな感じの出力に変えられた。
5. ログをカラフルに出力する
標準出力のログはやっぱり色が欲しい。
視認性も上がるし、なによりログの把握速度が圧倒的である。調べると monolog 側に良さそうなライブラリがあった。
bramus/monolog-colored-line-formatter: Colored/ANSI Line Formatter for Monologconsolecomposer require bramus/monolog-colored-line-formatter ~3.0
これをそのまま
formatter
扱いで定義すると色が付くのだが、それでは先程作成した文字列のフォーマットが適用されない。
なので、CustomLineFormatter
を拡張して色を付けることにする。App\Logging\Formatters\ColorLineFormatter.php<?php namespace App\Logging\Formatters; use Bramus\Monolog\Formatter\ColorSchemes\DefaultScheme; // quote by: // https://github.com/bramus/monolog-colored-line-formatter/blob/master/src/Formatter/ColoredLineFormatter.php class ColorLineFormatter extends CustomLineFormatter { private $colorScheme = null; public function getColorScheme() { if (!$this->colorScheme) { $this->colorScheme = new DefaultScheme(); } return $this->colorScheme; } public function format(array $record) : string { $output = parent::format($record); $colorScheme = $this->getColorScheme(); return $colorScheme->getColorizeString($record['level']).trim($output).$colorScheme->getResetString()."\n"; } }
parent::format($record)
した後に、その文字列に level に応じた色を付ける。
ライブラリのフォーマット部分を参考に必要な処理を移植した形だ。
参考:monolog-colored-line-formatter/ColoredLineFormatter.php at master · bramus/monolog-colored-line-formatterApp\Logging\ColorFormatterApply.php<?php namespace App\Logging; use App\Logging\Formatters\ColorLineFormatter; class ColorFormatterApply { public function __invoke($logging) { $coloredLineFormattetr = new ColorLineFormatter(); foreach($logging->getHandlers() as $handler) { $handler->setFormatter($coloredLineFormattetr); } } }で、これを
stdout
のチャンネルのtap
に指定すればOK。
※制御文字を使用しているので、ファイル出力には使わないように。とてもいい感じになった。
ログのメッセージ部は白のままにしたい時は、正規表現でその部分だけ抜き取って色を付けると良いかも。6. サンプル
自分が使用したいログの仕様をまとめて、それを実装したサンプルを置いておく。
- NOTICE 以上のログが1日毎に別のファイルに保存される
daily
チャンネル
- 保存期間(
days
)は7日- ERROR 以上のログが1つのファイルに保存される
singleError
チャンネル- CLIで実行した際、全てのログが標準出力に色付きで出力される
stdout
チャンネル
- これは
AppServiceProvide
で追加するのでstack
では触らないApp\Config\logging.phpuse Monolog\Handler\StreamHandler; use App\Logging\LineFormatterApply; use App\Logging\ColorFormatterApply; ... 'channels' => [ 'stack' => [ 'driver' => 'stack', 'channels' => ['dailyNotice', 'singleError'], 'ignore_exceptions' => false, ], 'singleError' => [ 'driver' => 'single', 'path' => storage_path('logs/laravel_error.log'), 'level' => 'error', // upper error log 'tap' => [LineFormatterApply::class], ], 'dailyNotice' => [ 'driver' => 'daily', 'path' => storage_path('logs/laravel.log'), 'level' => 'notice', // upper notice log 'days' => 7, 'tap' => [LineFormatterApply::class], ], 'stdout' => [ 'driver' => 'monolog', 'handler' => StreamHandler::class, 'with' => [ 'stream' => 'php://stdout', ], 'level' => 'debug', // all log 'tap' => [ColorFormatterApply::class], ], ],出力は以下の感じ。
log ファイルのカラーリングは VSCode の Log File Highlighter を使用している。おわりに
monolog の
handler
とprocessor
周りをまだ理解しきれてないので、とりあえず基本をまとめた。
あと laravel の Exception 周りも知らないといけないかも。これ
node-log4js
より楽かもしれぬ。
個人的にタイマーは実装してみたい。
綺麗なログを吐けると開発が捗る捗る…。続きは、今のログに手を入れるとき(書くとは言っていない)。
- 投稿日:2020-10-13T21:52:58+09:00
blade構文
【概要】
1.値
2.条件分岐ディレクティブ
3.繰り返しディレクティブ
補足(i)
補足(ii)
1.値
{{***}}{{}}の中身は値だけでなく、変数・関数なども使用できます。HTML文でエスケープ処理されたくない時は、下記のように使用します。
{{!! !!}}
2.条件分岐ディレクティブ
@if (条件式) //内容 @elseif (条件式) //内容 @else //内容 @endifまた@ifの逆の@unless-@endunlessもあります。
3.繰り返しディレクティブ
for
@for(初期値 ; 条件 ; 処理) #例:@for($i=0 ; $i<=10 ; $i++) //内容 #例:echo $i; @endforまた@breakは繰り返し処理の中断、@continueはcontinue以降は表示せず@forの繰り返し処理を引き続き行います。
foreach
@foreach ($配列 as $変数) //内容 @empty #--❶ //変数が空の時の内容 @endforeach❶については記載しなくてもOKです。@ifでいう@elseにあたります。
while
@while (条件) //内容 @endwhile
補足(i)
3.の繰り返しディレクティブには、$loopというループ変数があり、繰り返し処理のプロパティとして使用することが可能です。
@foreach($category as $item) @if ($loop->first) #---❶ <h1>アイテム一覧</h1> <ul> @endif <li>No.{{$loop->iteration}}.{{$item}}</li> #---❷ @if ($loop->last) #---❸ </ul> @endif @endforeach❶:loop->firstは、最初の繰り返しならtrueを返します。
❷:loop->iterationは、1から順番に繰り返し、その回数を返します。
❸:loop->lastは、最後の繰り返しならtrueを返します。
補足(ii)
index.phpファイルとindex.blade.phpファイルが同じviewフォルダの中の指定のフォルダに入っていたとします。その時に、controllerにある”return view('@@@@.index' , $@@@@)'はLaravelでは'index'としてした場合はindex.blade.phpファイルが優先して読み込まれます。
- 投稿日:2020-10-13T21:16:34+09:00
docker を用いてlaravel6+nuxt.js+mysql+nginx構築
laravel6+nuxt.js+mysqlの構築
laravel,mysql,nginxの構築は@simotarooさんの記事が優秀なのでこちらを参考にしてください
https://qiita.com/shimotaroo/items/29f7878b01ee4b99b951本題に関係ないんですけどngixってエンジンエックスって読むんですね,,,(笑
nuxt.jsの構築
@simotarooさんの記事で作成したdocker-compose.ymlに PORT:3000, HOST: 0.0.0.0を追加
理由としてコンテナで設定されているポート番号とnode.jsのポート番号が違うままだと繋がらないためですdocker-compose.yml#docker-compose.ymlのバージョン version: '3.8' #docker volumeの設定 volumes: docker-volume: #services以下に各コンテナの設定を書く services: #Webサーバーのコンテナ web: image: nginx:1.18 ports: - '8000:80' depends_on: - app volumes: - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf - .:/var/www/html #アプリケーションのコンテナ app: build: ./docker/php volumes: - .:/var/www/html environment: PORT: 3000 #追加 HOST: 0.0.0.0 #追加 ports: - 3000:3000 #データベースのコンテナ db: image: mysql:5.7 ports: - '3306:3306' environment: MYSQL_DATABASE: ###### MYSQL_USER: ##### MYSQL_PASSWORD: ####### MYSQL_ROOT_PASSWORD: ########## TZ: 'Asia/Tokyo' volumes: - docker-volume:/var/lib/mysqldockerfileに RUN npm install -g create-nuxt-app を追加
dockerfileFROM php:7.2-fpm #composerのインストール COPY --from=composer:1.10 /usr/bin/composer /usr/bin/composer #npmのインストール COPY --from=node:10.22 /usr/local/bin /usr/local/bin COPY --from=node:10.22 /usr/local/lib /usr/local/lib #パッケージ管理ツールapt-getの更新と必要パッケージのインストール RUN apt-get update \ && apt-get install -y \ git \ zip \ unzip \ && docker-php-ext-install pdo_mysql bcmath RUN npm install -g create-nuxt-app #追加 #コンテナ内に入った時のディレクトリを指定 WORKDIR /var/www/html追加後はdocker-compose buildし変更を反映
反映できたらdocker内で
tarminalroot@########:/var/www/html npx create-nuxt-app ディレクトリ名で実行
tarminal? Successfully created project ディレクトリ名 To get started: cd ディレクトリ名 npm run dev To build & start for production: cd ディレクトリ名 npm run build npm run start成功したらこんな画面になります。
作成したディレクトリ内に入り,npm run devを実行後動作確認で以下のURLに入ってnode.jsの画面が出れば成功です
http://localhost:3000/
お疲れ様でした?
- 投稿日:2020-10-13T20:49:55+09:00
Laravelのキャッシュクリアの備忘録
上5つしかしてなかったので備忘録
php artisan cache:clear && php artisan config:clear && php artisan config:cache && php artisan route:clear && php artisan view:clear && php artisan clear-compiled && php artisan optimize && composer dump-autoload && rm -f bootstrap/cache/config.php
- 投稿日:2020-10-13T15:48:45+09:00
Laravelでログ出力と併せてメールで通知する
はじめに
バグを完全に防ぐことは難しいですが、問題が起こった場合には迅速に対処できるようにしておきたいものです。
CloudWatch LogsやZabbixなどでログ監視できていると安心ですが、実際には導入が困難な場合もあるでしょう。そこで、Laravelにてログ出力と併せてメールで通知する機能を設け、簡易的なログ監視を行いたいと思います。
この場合サーバー自体がダウンしているとどうにもならないですが、プログラムのバグであればすぐに気づくことができるでしょう。なお、LaravelはデフォルトでSlack通知に対応しているので、Slackを利用されている方はそちらの方が便利かもしれません。
Laravelのバージョンは5.7にて確認しています。
Handler.phpでメール通知
Laravelのドキュメントには下記のようにあります。
Laravelの例外はすべて、App\Exceptions\Handlerクラスで処理されます。
このクラスはreportとrender二つのメソッドで構成されています。両メソッドの詳細を見ていきましょう。reportメソッドは例外をログするか、BugSnagやSentryのような外部サービスに送信するために使います。つまり、下記のreportメソッド内にメール送付するロジックを記述すれば実現できるということです。
さっそく記述してみました。App\Exceptions\Handler.phppublic function report(Exception $exception) { $error['message'] = $exception->getMessage(); $error['code'] = $exception->getCode(); $error['file'] = $exception->getFile(); $error['line'] = $exception->getLine(); $error['url'] = url()->current(); Mail::send(['text' => 'emails.exception'], ["e" => $error], function (Message $message) { $message ->to(config('mail.to.address')) ->from(config('mail.from.address')) ->subject('【'.config('app.name').'】['.ENV('APP_ENV').'] サーバーエラー発生の連絡'); }); parent::report($exception); }resources\views\emails\exception.blade.php<?php $action = (\Route::getCurrentRoute()) ? \Route::getCurrentRoute()->getActionName() : "n/a"; ?> ---------------------------------------------------------------------- このメールは{{ config('app.name') }}から自動で配信しています。 ---------------------------------------------------------------------- {{ config('app.name') }}サーバーでエラーが発生しました。 ====================================================================== [Message] {{ $e['message'] }} ====================================================================== [Action] {{ $action }} [URL] {{ $e['url'] ?? '' }} [File] {{ $e['file'] }} [Line] {{ $e['line'] }} [Code] {{ $e['code'] }} ======================================================================うまく通知できましたが、しかしこれではすべての例外を通知してしまいます。
404などの400系エラーやバリエーションエラー、未認証でのログイン画面リダイレクトまで通知されてしまうと、肝心のサーバーエラーが起こった際に通知が埋もれてしまう危険性があります。Handler.phpでメール通知(改)
というわけで通知したいエラーをある程度限定したいと思います。
再びLaravelのドキュメントを見返すと、$dontReportプロパティを利用する事で特定の例外をログ対象から外すことができるとあります。例外ハンドラの$dontReportプロパティは、ログしない例外のタイプの配列で構成します。たとえば、404エラー例外と同様に、他のタイプの例外もログしたくない場合です。
今回は認証関連の例外とバリデーションチェック関連の例外を除外してみました。
また上記に加え、400系エラーの除外と、本番環境とステージング環境のみ通知を行う判定を加えたのが下記です。App\Exceptions\Handler.phpprotected $dontReport = [ \Illuminate\Auth\AuthenticationException::class, \Illuminate\Auth\Access\AuthorizationException::class, \Illuminate\Validation\ValidationException::class, ]; public function report(Exception $exception) { $status = $this->isHttpException($exception) ? $exception->getStatusCode() : 500; if ($exception instanceof \Exception && $this->shouldReport($exception)) { if (\App::environment(['staging', 'production'])) { $status = $this->isHttpException($exception) ? $exception->getStatusCode() : 500; if ($status >= 500) { $error['message'] = $exception->getMessage(); $error['status'] = $status; $error['code'] = $exception->getCode(); $error['file'] = $exception->getFile(); $error['line'] = $exception->getLine(); $error['url'] = url()->current(); Mail::send(['text' => 'emails.exception'], ["e" => $error], function (Message $message) { $message ->to(config('mail.to.address')) ->from(config('mail.from.address')) ->subject('【'.config('app.name').'】['.ENV('APP_ENV').'] サーバーエラー発生の連絡'); }); } } } parent::report($exception); }resources\views\emails\exception.blade.php<?php $action = (\Route::getCurrentRoute()) ? \Route::getCurrentRoute()->getActionName() : "n/a"; ?> ---------------------------------------------------------------------- このメールは{{ config('app.name') }}から自動で配信しています。 ---------------------------------------------------------------------- {{ config('app.name') }}サーバーでエラーが発生しました。 ====================================================================== [Message] {{ $e['message'] }} ====================================================================== [Action] {{ $action }} [URL] {{ $e['url'] ?? '' }} [File] {{ $e['file'] }} [Line] {{ $e['line'] }} [Code] {{ $e['code'] }} [Status] {{ $e['status'] }} ======================================================================おわりに
アプリケーションの安定稼働に少しでも寄与できれば幸いです。
- 投稿日:2020-10-13T13:44:03+09:00
初心者がLaravelのindex.phpを読み解いてみた(オートロード編)
動機
パーフェクトPHPの7章、8章を読んでみて、じゃあLaravelはどうなってるんだろうかと気になったので調べてみました。
環境
Laravel 8.x
PHP 7.3結論
require __DIR__.'/../vendor/autoload.php';
の1行で、クラスのオートロード設定、組み込み関数の宣言などを実行している。index.php
全体としては以下の通りです。非常に簡潔ですが、かなり内容は重いです。今回は
require __DIR__.'/../vendor/autoload.php';
までみていきたいと思います。public/index.php<?php use Illuminate\Contracts\Http\Kernel; use Illuminate\Http\Request; define('LARAVEL_START', microtime(true)); if (file_exists(__DIR__.'/../storage/framework/maintenance.php')) { require __DIR__.'/../storage/framework/maintenance.php'; } require __DIR__.'/../vendor/autoload.php'; $app = require_once __DIR__.'/../bootstrap/app.php'; $kernel = $app->make(Kernel::class); $response = tap($kernel->handle( $request = Request::capture() ))->send(); $kernel->terminate($request, $response);名前空間の利用
こちらは問題ないかと思います。詳細は、名前空間の利用に関するドキュメントをご参照ください。
public/index.phpuse Illuminate\Contracts\Http\Kernel; use Illuminate\Http\Request;メンテナンスモード
メンテナンスモードの場合は、
storage/framework/maintenance.php
が生成され、エラーレスポンス503を返すようになります。
通常時はファイルが存在しないのでfalse
になります。メンテナンスモードの詳細はLaravelのドキュメントをご参照ください。
file_exists
:引数のファイルが存在するかどうかを判定する関数。
__DIR__
:ファイルのディレクトリを返す。public/index.phpif (file_exists(__DIR__.'/../storage/framework/maintenance.php')) { require __DIR__.'/../storage/framework/maintenance.php'; }オートロード
ここから本題になります。オートロードに関しては、galluさんのnoteが非常に参考になりました。
それでは、以下の一行の詳細をみていきたいと思います。public/index.phprequire __DIR__.'/../vendor/autoload.php';まず、読み込んでいるファイルをみにいくと、以下のようになっています。
また、違うファイルを読み込んだ後に、クラスメソッドのgetLoader()
を実行しています。vendor/autoload.php<?php require_once __DIR__ . '/composer/autoload_real.php'; return ComposerAutoloaderInitbee6542d79f53acb601ba3c7d134558a::getLoader();
getLoader()
は、以下のようになっています。長いのでいくつかに分けてみていきます。vendor/composer/autoload_real.php<?php // autoload_real.php @generated by Composer class ComposerAutoloaderInitbee6542d79f53acb601ba3c7d134558a { private static $loader; /** * @return \Composer\Autoload\ClassLoader */ public static function getLoader() { if (null !== self::$loader) { return self::$loader; } spl_autoload_register(array('ComposerAutoloaderInitbee6542d79f53acb601ba3c7d134558a', 'loadClassLoader'), true, true); self::$loader = $loader = new \Composer\Autoload\ClassLoader(); spl_autoload_unregister(array('ComposerAutoloaderInitbee6542d79f53acb601ba3c7d134558a', 'loadClassLoader')); $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); if ($useStaticLoader) { require_once __DIR__ . '/autoload_static.php'; call_user_func(\Composer\Autoload\ComposerStaticInitbee6542d79f53acb601ba3c7d134558a::getInitializer($loader)); } else { $map = require __DIR__ . '/autoload_namespaces.php'; foreach ($map as $namespace => $path) { $loader->set($namespace, $path); } $map = require __DIR__ . '/autoload_psr4.php'; foreach ($map as $namespace => $path) { $loader->setPsr4($namespace, $path); } $classMap = require __DIR__ . '/autoload_classmap.php'; if ($classMap) { $loader->addClassMap($classMap); } } $loader->register(true); if ($useStaticLoader) { $includeFiles = Composer\Autoload\ComposerStaticInitbee6542d79f53acb601ba3c7d134558a::$files; } else { $includeFiles = require __DIR__ . '/autoload_files.php'; } foreach ($includeFiles as $fileIdentifier => $file) { composerRequirebee6542d79f53acb601ba3c7d134558a($fileIdentifier, $file); } return $loader; } }classLoaderのインスタンス化
まず、
self::loader
はprivate static $loader;
と宣言しただけなので、null
となります。そのため、はじめのif
文はスルーします。
その後、spl_autoload_register()
でオートロードの設定をしています。
spl_autoload_register()
は、配列を与えると、クラスメソッドを登録できます。
ここでは、loadClassLoader()
をオートロードしています。すなわち、今後使おうとしたクラスが宣言されてなかったら、loadClassLoader()
が実行されます。オートロードの設定後、新しいインスタンスを生成して、オートロードの設定を解除しています。
vendor/composer/autoload_real.phpif (null !== self::$loader) { return self::$loader; } spl_autoload_register(array('ComposerAutoloaderInitbee6542d79f53acb601ba3c7d134558a', 'loadClassLoader'), true, true); self::$loader = $loader = new \Composer\Autoload\ClassLoader(); spl_autoload_unregister(array('ComposerAutoloaderInitbee6542d79f53acb601ba3c7d134558a', 'loadClassLoader'));
loadClassLoader
をみてみると、以下のようになっています。
引数には、クラスの読み込みでエラーがあったときのクラス名が入っています。
読み込めなかったのがComposer\Autoload\ClassLoader
だったら、ファイルを読み込んでください、となります。vendor/composer/autoload_real.phppublic static function loadClassLoader($class) { if ('Composer\Autoload\ClassLoader' === $class) { require __DIR__ . '/ClassLoader.php'; } }
ClassLoader
をみてみると、クラスが宣言されています。ここではrequire
しているだけなので、今すぐ何かが実行されるわけではないので、クラスを読み込んでるんだな、というくらいの理解に留めます。vendor/composer/ClassLoader.phpclass ClassLoader { // PSR-4 private $prefixLengthsPsr4 = array(); private $prefixDirsPsr4 = array(); private $fallbackDirsPsr4 = array(); ...
$loader
の初期化それでは、
autoload_real.php
の続きをみていきます。vendor/composer/autoload_real.php$useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); if ($useStaticLoader) { require_once __DIR__ . '/autoload_static.php'; call_user_func(\Composer\Autoload\ComposerStaticInitbee6542d79f53acb601ba3c7d134558a::getInitializer($loader)); } else { $map = require __DIR__ . '/autoload_namespaces.php'; foreach ($map as $namespace => $path) { $loader->set($namespace, $path); } $map = require __DIR__ . '/autoload_psr4.php'; foreach ($map as $namespace => $path) { $loader->setPsr4($namespace, $path); } $classMap = require __DIR__ . '/autoload_classmap.php'; if ($classMap) { $loader->addClassMap($classMap); } }まずは1行目をみていきます。
PHP_VERSION_ID
は、PHPのバージョンが定数になっており、50600はPHP5.6を意味します。今回はPHP7.3のためtrue
となります。
defined
は引数の定数が宣言済みかどうかを検証します。
HHVM_VERSION
は実行環境がHHVMの場合に定義される定数で、通常はHHVMを使用していないので、定義されていません。
zend_loader_file_encoded()
はZend Guardを使用している場合に、定義される関数のようです。vendor/composer/autoload_real.php$useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());まとめると、以下のようになっているので、
$useStaticLoader
はtrue
が入ります。true && true && (true || false)となると、実行されるのは以下の部分だけとなります。ファイルを読み込んで、クラスメソッドを実行しています。
call_user_func()
は引数で指定したコールバックを実行します。
今回の場合、普通に実行する場合と何が違うんだと思って、call_user_func()
を使わないで実行してみると、エラーになりました。このあたりは理解できていません。。。vendor/composer/autoload_real.phpif ($useStaticLoader) { require_once __DIR__ . '/autoload_static.php'; call_user_func(\Composer\Autoload\ComposerStaticInitbee6542d79f53acb601ba3c7d134558a::getInitializer($loader)); }読み込んでいるファイルは以下の通りです。クラスの宣言になります。中身は非常に長いですが、クラスプロパティに配列を代入しています。
vendor/composer/autoload_static.phpnamespace Composer\Autoload; class ComposerStaticInitbee6542d79f53acb601ba3c7d134558a { public static $files = array ( 'ec07570ca5a812141189b1fa81503674' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Assert/Functions.php', ...では、クラスメソッドを見ていきたいと思います。
\Closure:bind()
の中の無名関数の中で、プロパティが代入されています。
右辺の値は長くて分かりにくいですが、上で示したような配列が代入されたクラスプロパティとなります。/vendor/composer/autoload_static.phppublic static function getInitializer(ClassLoader $loader) { return \Closure::bind(function () use ($loader) { $loader->prefixLengthsPsr4 = ComposerStaticInitbee6542d79f53acb601ba3c7d134558a::$prefixLengthsPsr4; $loader->prefixDirsPsr4 = ComposerStaticInitbee6542d79f53acb601ba3c7d134558a::$prefixDirsPsr4; $loader->prefixesPsr0 = ComposerStaticInitbee6542d79f53acb601ba3c7d134558a::$prefixesPsr0; $loader->classMap = ComposerStaticInitbee6542d79f53acb601ba3c7d134558a::$classMap; }, null, ClassLoader::class); }問題となるのが左辺で、
$loader
にはClassLoader
のインスタンスが入っています。vendor/composer/autoload_real.phpself::$loader = $loader = new \Composer\Autoload\ClassLoader();
ClassLoader
のプロパティをみてみると、以下のように代入しているプロパティは全てprivate
となっています。なぜComposerStaticInitbee65...
クラスの中で、ClassLoader
のprivate
プロパティに代入できるんだろうか、となると思うのですが、それを解決するのが\Clousure::bind()
となります。vendor/composer/ClassLoader.php// PSR-4 private $prefixLengthsPsr4 = array(); private $prefixDirsPsr4 = array(); // PSR-0 private $prefixesPsr0 = array(); private $classMap = array();
\Closure::bind()
は簡単にまとめると、関数を好きなスコープ内で実行することができます。これに関しては、公式ドキュメントだけだと分かりにくいと思いますので、galluさんのnoteも参照すると非常にいいと思います。
今回の場合は、bind
の引数の無名関数を、第三引数のClassLoader::class
のスコープにあるとして、実行しています。そのためprivate
なプロパティも代入できています。オートロードの設定
1行だけですが、詳しくみていきます。
vendor/composer/autoload_real.php$loader->register(true);
register
メソッドは以下のようになっています。
$this
はClassLoader
のインスタンスが入っています。ここではClassLoader
のloadClass
メソッドを、オートロードの設定の対象としています。vendor/composer/ClassLoader.phppublic function register($prepend = false) { spl_autoload_register(array($this, 'loadClass'), true, $prepend); }
loadClass()
メソッドをみていきます。if
文の条件式でfindFile()
メソッドが呼ばれています。もしファイルが見つかれば、includeFile()
を実行してファイルを読み込んでいます。これが先ほどの一時的なものとは異なり、通常のオートロードになります。
今後はクラスが使用されると、まずこのメソッドが実行されることになります。vendor/composer/ClassLoader.phppublic function loadClass($class) { if ($file = $this->findFile($class)) { includeFile($file); return true; } }findFile()
findFile()
メソッドは以下の通りです。ファイルを探して、そのファイルを返しています。
引数の$class
にはクラスの名前(名前空間+クラス名)が入ります。vendor/composer/ClassLoader.phppublic function findFile($class) { if (isset($this->classMap[$class])) { return $this->classMap[$class]; } if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { return false; } if (null !== $this->apcuPrefix) { $file = apcu_fetch($this->apcuPrefix.$class, $hit); if ($hit) { return $file; } } $file = $this->findFileWithExtension($class, '.php'); // Search for Hack files if we are running on HHVM if (false === $file && defined('HHVM_VERSION')) { $file = $this->findFileWithExtension($class, '.hh'); } if (null !== $this->apcuPrefix) { apcu_add($this->apcuPrefix.$class, $file); } if (false === $file) { // Remember that this class does not exist. $this->missingClasses[$class] = true; } return $file; }findFile 前半部分
まず、はじめの
if
文をみていきます。$this->classMap
には、vendor/composer/autoload_static.php
で代入した値が入っています。もしあらかじめ登録しておいたクラスであれば、指定したファイルを返します。vendor/composer/ClassLoader.phpif (isset($this->classMap[$class])) { return $this->classMap[$class]; }
$this->classMap
の例をみていきます。いくつかあるのですが、馴染みのありそうなのものをピックアップしました。Illuminate
が何故vendor/laravel/framework/src/Illuminate
を指すのか今まで分かっていませんでしたが、ここでようやく分かりました。vendor/composer/autoload_static.phppublic static $classMap = array ( 'Illuminate\\Http\\Request' => __DIR__ . '/..' . '/laravel/framework/src/Illuminate/Http/Request.php', 'Illuminate\\Support\\Facades\\Auth' => __DIR__ . '/..' . '/laravel/framework/src/Illuminate/Support/Facades/Auth.php', 'Illuminate\\Support\\Str' => __DIR__ . '/..' . '/laravel/framework/src/Illuminate/Support/Str.php',
findFile()
の、次のif
文をみていきます。
$this->classMapAuthoritative
は、基本的にはfalse
となります。
$this->missingClasses[$class]
は、以前にもクラスが見つからなかった場合のみ、true
が代入されています。
ここは、$this->classMap
に登録されていない場合の検索が無効であったり、既に見つかってないことが分かっている場合にfalse
を返します。vendor/composer/ClassLoader.phpif ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { return false; }念のため、
classMapAuthoritative
をみてみると、まず初期値はfalse
となっています。
setClassMapAuthoritative()
を使用してtrue
に設定すると、上述のif
文の条件がtrue
になり、findFile()
がfalse
を返すようになります。すなわち、$this->classMap
に登録されていないクラスは、検索が無効になります。
setClassMapAuthoritative()
は、検索してみても見つからなかったので、$this->classMapAuthoritative
は基本的にはfalse
と考えていいと思います。vendor/composer/ClassLoader.phpprivate $classMapAuthoritative = false; ... /** * Turns off searching the prefix and fallback directories for classes * that have not been registered with the class map. * * @param bool $classMapAuthoritative */ public function setClassMapAuthoritative($classMapAuthoritative) { $this->classMapAuthoritative = $classMapAuthoritative; }
findFile()
の、次のif
文をみていきます。$this->apcuPrefix
は調べてみても理解できませんでした。キャッシュドライバにAPCを利用している場合に値がsetされている、と思っていますが定かではありません。今回は、null
が入っていたので、この文はスキップします。vendor/composer/ClassLoader.phpif (null !== $this->apcuPrefix) { $file = apcu_fetch($this->apcuPrefix.$class, $hit); if ($hit) { return $file; } }findFileWithExtension()
findFile()
の次の1行になります。インスタンスメソッドがでてきたので見ていきます。vendor/composer/ClassLoader.php$file = $this->findFileWithExtension($class, '.php');引数の
$class
にはクラス名、$ext
には拡張子の.php
という文字列が入っています。非常に長いので、ここも少しずつみていきます。vendor/composer/ClassLoader.phpprivate function findFileWithExtension($class, $ext) { // PSR-4 lookup $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; $first = $class[0]; if (isset($this->prefixLengthsPsr4[$first])) { $subPath = $class; while (false !== $lastPos = strrpos($subPath, '\\')) { $subPath = substr($subPath, 0, $lastPos); $search = $subPath . '\\'; if (isset($this->prefixDirsPsr4[$search])) { $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); foreach ($this->prefixDirsPsr4[$search] as $dir) { if (file_exists($file = $dir . $pathEnd)) { return $file; } } } } } // PSR-4 fallback dirs foreach ($this->fallbackDirsPsr4 as $dir) { if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { return $file; } } // PSR-0 lookup if (false !== $pos = strrpos($class, '\\')) { // namespaced class name $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); } else { // PEAR-like class name $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; } if (isset($this->prefixesPsr0[$first])) { foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { if (0 === strpos($class, $prefix)) { foreach ($dirs as $dir) { if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { return $file; } } } } } // PSR-0 fallback dirs foreach ($this->fallbackDirsPsr0 as $dir) { if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { return $file; } } // PSR-0 include paths. if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { return $file; } return false; }PSR-4 lookup
はじめの1行をみていきます。
strtr()
は、文字列を置換する組み込み関数になります。
クラス名は、各階層がバックスラッシュ\
で区切られているので、それをDIRECTORY_SEPARATOR
(自分の環境ではスラッシュ/
)に置換しています。その後、拡張子と結合することでファイルのパスを作成しています。vendor/composer/ClassLoader.php// PSR-4 lookup $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;次の
if
文ですが、まず$first
にはクラス名の1文字目を代入しています。
その文字が$this->prefixLengthsPsr4
に代入されていた場合に処理が走ります。
ちなみに、var_dump
で確認したところ、Symfony\Component\Translation\TranslatorInterface
が呼ばれているようですので、具体例として採用して見ていきたいと思います。vendor/composer/ClassLoader.php$first = $class[0]; if (isset($this->prefixLengthsPsr4[$first])) { ... }まず
$this->prefixLengthsPsr4
はvendor/composer/autoload_static.php
で既に代入されており、以下のような配列になっています。
Symfony\Component\Translation\TranslatorInterface
の1文字目はS
なので、S
がキーとなる値はあるかどうか確認してみます。以下の通り、S
がキーとなる配列が存在しているので、上述のif
文はtrue
となります。vendor/composer/autoload_static.phppublic static $prefixLengthsPsr4 = array ( 'v' => array ( 'voku\\' => 5, ), ... 'S' => array ( 'Symfony\\Polyfill\\Php80\\' => 23, 'Symfony\\Polyfill\\Php73\\' => 23, 'Symfony\\Polyfill\\Php70\\' => 23, ... ),続きをみていきます。まず
$subPath
に値を代入しています。
strrpos
は、文字列中に、指定した文字が最後に現れる場所を返します。
今回の場合、Symfony\Component\Translation\TranslatorInterface
で、\
が最後に現れるのは、29文字目となるので、29を返します。
したがって条件式はfalse !== 29
となり、while
文は実行されます。vendor/composer/ClassLoader.php$subPath = $class; while (false !== $lastPos = strrpos($subPath, '\\')) { ... }
while
文の中身をみていきます。
substr
は、第一引数の文字列の、第二引数から第三引数までの部分を返します。今回はSymfony\Component\Translation\TranslatorInterface
からSymfony\Component\Translation
を取り出します。
$search
では末尾に\
を追加して、$this->prefixDirsPsr4
にあるかどうか確認しています。vendor/composer/ClassLoader.phpwhile (false !== $lastPos = strrpos($subPath, '\\')) { $subPath = substr($subPath, 0, $lastPos); $search = $subPath . '\\'; if (isset($this->prefixDirsPsr4[$search])) { $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); foreach ($this->prefixDirsPsr4[$search] as $dir) { if (file_exists($file = $dir . $pathEnd)) { return $file; } } } }ディレクトリは登録されているので、
true
を返すことが分かります。vendor/composer/autoload_static.phppublic static $prefixDirsPsr4 = array ( ... 'Symfony\\Component\\Translation\\' => array ( 0 => __DIR__ . '/..' . '/symfony/translation', ), ... );
if
文の中身をみていきます。
$logicalPathPsr4
の中身はSymfony/Component/Translation/TranslatorInterface.php
となっており、
substr($logicalPathPsr4, $lastPos + 1)
は$lastPos + 1
の場所から後ろ全部の文字列を取り出しますので、TranslatorInterface.php
を返します。
したがって、$pathEnd
には/TranslatorInterface.php
が入ります。
$this->prefixDirsPsr4[$search]
は複数のディレクトリが入っている場合もあるので、foreach
で各ディレクトリごとにファイルがあるかどうかを確認しています。
ファイルが見つかれば、そのファイルを返します。
ちなみに、vendor/composer/../symfony/translation/TranslatorInterface.php
は見つからないので、次に進みます。vendor/composer/ClassLoader.phpif (isset($this->prefixDirsPsr4[$search])) { $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); foreach ($this->prefixDirsPsr4[$search] as $dir) { if (file_exists($file = $dir . $pathEnd)) { return $file; } } }PSR-4 fallback dirs
findFileWithExtension()
の次の文になりますが、$this->fallbackDirsPsr4
は設定されていないので、処理は行われません。vendor/composer/ClassLoader.php// PSR-4 fallback dirs foreach ($this->fallbackDirsPsr4 as $dir) { if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { return $file; } }PSR-0 lookup
$pos
は先ほどみたとおり29
となるので、条件式はtrue
となります。
ここでのstrtr()
は文字列中に_
が含まれる場合は/
に変換しています。substr($logicalPathPsr4, $pos + 1)
にはTranslatorInterface.php
が入っているので、特に変換することなく文字列を結合しています。
結果、$logcalPathPsr0
にはSymfony/Component/Translation/TranslatorInterface.php
が代入されます。vendor/composer/ClassLoader.php// PSR-0 lookup if (false !== $pos = strrpos($class, '\\')) { // namespaced class name $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); } else { ... }その後、
$this->prefixesPsr0[$first]
に登録されているかどうか確認しますが、登録されていないので処理は行われません。vendor/composer/ClassLoader.phpif (isset($this->prefixesPsr0[$first])) { foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { if (0 === strpos($class, $prefix)) { foreach ($dirs as $dir) { if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { return $file; } } } } }ちなみに、中身は以下の通りです。
vendor/composer/autoload_static.phppublic static $prefixesPsr0 = array ( 'M' => array ( 'Mockery' => array ( 0 => __DIR__ . '/..' . '/mockery/mockery/library', ), ), 'H' => array ( 'Highlight\\' => array ( 0 => __DIR__ . '/..' . '/scrivo/highlight.php', ), 'HighlightUtilities\\' => array ( 0 => __DIR__ . '/..' . '/scrivo/highlight.php', ), ), );PSR-0 fallback dirs
$this->fallbackDirsPsr0
は登録されていないので、処理は行われません。vendor/composer/ClassLoader.php// PSR-0 fallback dirs foreach ($this->fallbackDirsPsr0 as $dir) { if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { return $file; } }PSR-0 include paths
$this->useIncludePath
の初期値はfalse
となります。特に設定しなければfalse
のため、ここも処理は行われません。どうやら、Symfony\Component\Translation\TranslatorInterface
のファイルは見つからないので、false
を返すようです。vendor/composer/ClassLoader.php// PSR-0 include paths. if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { return $file; } return false; }findFile 後半部分
ようやく
findFile()
に戻ってこれました。
$file
には$loader->classMap
に登録されているものはファイルのパスが返され、Symfony\Component\Translation\TranslatorInterface
はfalse
が返されています。
はじめのif
文ですが、$file
にファイルのパスが返ってきていても、右辺がfalse
となるので、条件式はfalse
となります。
次の$this->apcuPrefix
もnull
となります。
次のif
文で、Symfony\Component\Translation\TranslatorInterface
はみつからなかったので、missingClasses
に代入して、次からは検索する前にfalse
を返すようにしています。
最後に、$file
を返しています。vendor/composer/ClassLoader.phppublic function findFile($class) { ... $file = $this->findFileWithExtension($class, '.php'); // Search for Hack files if we are running on HHVM if (false === $file && defined('HHVM_VERSION')) { $file = $this->findFileWithExtension($class, '.hh'); } if (null !== $this->apcuPrefix) { apcu_add($this->apcuPrefix.$class, $file); } if (false === $file) { // Remember that this class does not exist. $this->missingClasses[$class] = true; } return $file; }includeFile
ようやく
loadClass()
メソッドに戻ってきました。$this->findFile($class)
はファイルのパス or falseが格納されています。つまり、ファイルのパスが見つかった場合はincludeFile()
を実行し、見つからなかった場合はなにもしません。
オートロードでこのような関数になっているため、クラスが使用されると、このloadClassa()
が実行され、ファイルのパスが見つかれば読み込んで、見つからない場合はエラーとなります。vendor/composer/ClassLoader.phppublic function loadClass($class) { if ($file = $this->findFile($class)) { includeFile($file); return true; } }
includeFile()
は以下の通りです。ファイルを読み込んでいるだけです。vendor/composer/ClassLoader.phpfunction includeFile($file) { include $file; }組み込み関数の読み込み
長かった
$loader->register(true);
も終わり、autoload_real.php
に戻ってきました。
$useStaticLoader
はtrue
なので、$includeFiles
に値が代入されます。
その値をforeach
でループして、composerRequire...()
という関数を実行しています。
最後に$loader
を返して終わりです。vendor/composer/autoload_real.phpif ($useStaticLoader) { $includeFiles = Composer\Autoload\ComposerStaticInitbee6542d79f53acb601ba3c7d134558a::$files; } else { $includeFiles = require __DIR__ . '/autoload_files.php'; } foreach ($includeFiles as $fileIdentifier => $file) { composerRequirebee6542d79f53acb601ba3c7d134558a($fileIdentifier, $file); } return $loader; }
$files
プロパティは以下の通りです。数字の羅列のキーに対して、ファイルのパスが値となっています。vendor/composer/autoload_static.phpclass ComposerStaticInitbee6542d79f53acb601ba3c7d134558a { public static $files = array ( 'ec07570ca5a812141189b1fa81503674' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Assert/Functions.php', 'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php', '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php', 'e69f7f6ee287b969198c3c9d6777bd38' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/bootstrap.php', ...
composerRequire...()
という関数は以下のようになっています。
$GLOBALS['__composer_autoload_files']
は、上述の数字の羅列をキーとする配列となっており、まだファイルを読み込んでいない場合はファイルを読み込んでtrue
を代入しています。vendor/composer/autoload_real.phpfunction composerRequirebee6542d79f53acb601ba3c7d134558a($fileIdentifier, $file) { if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) { require $file; $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true; } }例として、以下のファイルをみてみたいと思います。
vendor/composer/autoload_static.php'f0906e6318348a765ffb6eb24e0d0938' => __DIR__ . '/..' . '/laravel/framework/src/Illuminate/Foundation/helpers.php',ファイルをみてみると、多くの関数が定義されています。他のファイルも同様の構造となっており、Laravelで使える関数が定義されています。
なかでも利用頻度の高そうなview()
をみてみたいと思います。ここで定義しているから、コントローラからreturn view();
とできることが分かります。
処理の内容はサービスコンテナの理解が必要で、とても難しいので、また別の機会に。。。vendor/laravel/framewordk/src/Illuminate/Foundation/helpers.php<?php use Illuminate\Contracts\View\Factory as ViewFactory; ... if (! function_exists('view')) { /** * Get the evaluated view contents for the given view. * * @param string|null $view * @param \Illuminate\Contracts\Support\Arrayable|array $data * @param array $mergeData * @return \Illuminate\View\View|\Illuminate\Contracts\View\Factory */ function view($view = null, $data = [], $mergeData = []) { $factory = app(ViewFactory::class); if (func_num_args() === 0) { return $factory; } return $factory->make($view, $data, $mergeData); } }
return $loader;
の後
getLoader()
が$loader
を返すところまでみてきたので、autoload.php
に戻ってきました。
ここでは、$loader
をそのままreturn
しています。vendor/autoload.php<?php require_once __DIR__ . '/composer/autoload_real.php'; return ComposerAutoloaderInitbee6542d79f53acb601ba3c7d134558a::getLoader();
$loader
を返された先のindex.php
ですが、特に$loader
の値を取得することはなく、autoload.php
のファイル読み込みで終わっています。非常に長かったですが、今回は
これで終わりです。public/index.phprequire __DIR__.'/../vendor/autoload.php';まとめ
require __DIR__.'/../vendor/autoload.php';
の1行で、クラスのオートロード設定、組み込み関数の宣言などを実行していることが分かりました。
また、フレームワークの中身を調べていくことで、自分が知らないPHPの組み込み関数や、いいコードの書き方など、非常に勉強になりました。
次の1行の$app = require_once __DIR__.'/../bootstrap/app.php';
も非常に難しいので、読み解いていきたいと思います。参考サイト
- 投稿日:2020-10-13T12:46:46+09:00
LambdaでLaravelを動作させようと思ったけど、少しコケたのでメモしとく
私はPHPerなのですが、最近、仕事関係でLambdaの話がちょくちょく出てきたので、ちょっと勉強がてら調査することに。
今更なのですが Lambda でPHPを動かせるようになっていたということに驚いた。
で、調べてみるとどうやらbrefというライブラリを使えば、簡単にLambda上でLaravelも動かせるらしい。
Bref - Serverless PHP made simpleわざわざ Lambda で PHPを使う必要があるのかと思うかもだけど、一緒にお仕事しているチームの構成を考えるとメンテナンスも含めてLaravelが使えるというのはとても助かるのです。
で、さくっと試してみようと思ったところデプロイで躓いちゃったので、メモ残しておきます。
環境
- Windows 10 Home
- Docker Toolbox
- PHP7.4
- Laravel 8
IAM ユーザーを作成する
Lambda を利用するにあたり必要な IAM ユーザーを作成。
Creating AWS access keys - Bref
とりあえず試したいだけだから、まんまガイドに従って作成。
簡単、簡単。serverless framwork のインストール
後述しますが、本当は docker で serverless が動作する環境を作ろうと思っていたのですが、なぜかメモリ不足というエラーのため動作せず。。。
なので、Windows に直接 serverless をインストール。
Serverless Getting Started Guide
docker で環境作りに悩んでいたのが馬鹿みたいに、サクッと環境ができた。
credential の設定もガイドに従って設定。私、この環境を作るまでChocolateyというソフトを存じ上げませんでした。
いや~、ダサいというイメージの Windows ですが、少しずつクールになってきていると思っているのは私だけ?wLaravel 環境を docker で作る
毎回 docker 自分で作るの面倒だなぁと思っていたら、多くのLGTMがついてた下記で環境作り。
最強のLaravel開発環境をDockerを使って構築する【新編集版】 - Qiita
いや~、ありがたい。
bref をインストール
公式ドキュメント通り。
Serverless Laravel applications - Bref
ガイドに従って、
.env
の設定も行いましたよ。.env#LOG_CHANNEL=stack LOG_CHANNEL=stderr #SESSION_DRIVER=file SESSION_DRIVER=array VIEW_COMPILED_PATH=/tmp/storage/framework/views楽ですね~
いざ deploy したら、動かなかった。。。
ここまで、ガイドに従うだけなので、超簡単に環境構築できていました。
serverless deploy
でデプロイもすんなり成功!が、、、エンドポイントにアクセスしたらエラー発生。。。
Exception The /var/task/bootstrap/cache directory must be present and writable. ---- /var/task/vendor/laravel/framework/src/Illuminate/Foundation/PackageManifest.php ---- return []; } return json_decode(file_get_contents( $this->basePath.'/composer.json' ), true)['extra']['laravel']['dont-discover'] ?? []; } /** * Write the given manifest array to disk. * * @param array $manifest * @return void * * @throws \Exception */ protected function write(array $manifest) { if (! is_writable($dirname = dirname($this->manifestPath))) { throw new Exception("The {$dirname} directory must be present and writable."); } $this->files->replace( $this->manifestPath, '<?php return '.var_export($manifest, true).';' ); } } ---- Arguments "The /var/task/bootstrap/cache directory must be present and writable."で結論からいうと、エラーの通り
/var/task/bootstrap/cache
に書き込もうとしてエラーが発生しているので、これを直す。
Lambda で書き込み権限あるのは、/tmp
配下だけらしいので、以下を.env
に追加。.envAPP_SERVICES_CACHE=/tmp/services.php APP_PACKAGES_CACHE=/tmp/packages.php APP_CONFIG_CACHE=/tmp/config.php APP_ROUTES_CACHE=/tmp/routes.php APP_EVENTS_CACHE=/tmp/events.phpで、再度デプロイして、アクセスすると200が返ってきましたよ~
上記の設定をする前にググってたら、$app->useStoragePath()
を設定すると良いよとか書いてあったので、試したりしたのですがうまくいかず、上記の記述を追加するまでにまぁまぁな時間がかかりました。。というわけで、どなたか同じ状況になったときの参考にしてもらえれば幸いです。
(番外)dockerにserverless環境を作ったけど、deploy できなかった。。
前述の docker 環境に以下のような serverless 用のコンポーネントを作った。
docker-compose.ymlserverless: build: context: . dockerfile: ./infra/docker/serverless/Dockerfile args: - AWS_ACCESS_KEY_ID=<key> - AWS_SECRET_ACCESS_KEY=<secret> tty: true stdin_open: true image: serverless working_dir: /app volumes: - ./backend:/app container_name: serverless./infra/docker/serverless/DockerfileFROM python:3.7-alpine ARG AWS_ACCESS_KEY_ID ARG AWS_SECRET_ACCESS_KEY ENV NODE_PATH /usr/lib/node_modules/ # install nodejs RUN apk update \ && apk add --no-cache nodejs npm # install aws-cli RUN pip install awscli # install serverless framework RUN npm install -g serverless serverless-plugin-existing-s3 # set aws key RUN sls config credentials --provider aws --key $AWS_ACCESS_KEY_ID --secret $AWS_SECRET_ACCESS_KEY # change work directory RUN mkdir -p /app WORKDIR /appほんでもって、docker内で
serverless deploy
を実行したところ、なぜか以下のエラー。consoleServerless: Setting up AWS... root@02e9b3d52dbe:/work/backend# serverless deploy Serverless: Packaging service... Serverless: Excluding development dependencies... Error -------------------------------------------------- Error: ENOMEM: not enough memory, open '/work/backend/vendor/mockery/mockery/library/Mockery/Matcher/NoArgs.php' For debugging logs, run again after setting the "SLS_DEBUG=*" environment variable. Get Support -------------------------------------------- Docs: docs.serverless.com Bugs: github.com/serverless/serverless/issues Issues: forum.serverless.com Your Environment Information --------------------------- Operating System: linux Node Version: 12.19.0 Framework Version: 2.5.0 Plugin Version: 4.0.4 SDK Version: 2.3.2 Components Version: 3.2.1メモリは十分にあると思うのに理由がわからない。
散々悩んだあげく、諦めたのさ~
- 投稿日:2020-10-13T10:26:10+09:00
Laravel8 1対1 DBのリレーションを定義しよう
目的
- LaravelにおけるDBの1対1のリレーション定義方法をまとめる
実施環境
- ハードウェア環境
項目 情報 OS macOS Catalina(10.15.5) ハードウェア MacBook Pro (13-inch, 2020, Four Thunderbolt 3 ports) プロセッサ 2 GHz クアッドコアIntel Core i5 メモリ 32 GB 3733 MHz LPDDR4 グラフィックス Intel Iris Plus Graphics 1536 MB
- ソフトウェア環境
項目 情報 備考 PHP バージョン 7.4.8 Homebrewを用いてこちらの方法で導入→Mac HomebrewでPHPをインストールする Laravel バージョン 8.6.0 commposerを用いてこちらの方法で導入→Mac Laravelの環境構築を行う MySQLバージョン 8.0.19 for osx10.13 on x86_64 Homwbrewを用いてこちらの方法で導入→Mac HomebrewでMySQLをインストールする 前提条件
- 実施環境に記載されている環境と近い環境が構築されていること。
前提情報
下記コマンドを実行してrelationアプリを作成した。
$ laravel new relation --authAuth認証で使用されるusersテーブルのIDとリンクするuser_idカラムを有するphone_numbersテーブルがあるとする。両テーブルのカラム情報を下記に記載する。
usersテーブル
Field Type Null Key Default Extra id bigint unsigned NO PRI NULL auto_increment name varchar(255) NO NULL 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 phonesテーブル
Field Type Null Key Default Extra id bigint unsigned NO PRI NULL auto_increment user_id bigint unsigned NO NULL number varchar(255) NO NULL created_at timestamp YES NULL updated_at timestamp YES NULL 両テーブルのカラムに注目するとわかるようにusersテーブルとphone_numbersテーブルは1対1の関係になっている。(usersテーブルのidとphone_numbersテーブルのuser_id)
前述した1対1のリレーションを定義する。
概要
- usersテーブルからphonesテーブルに対するモデルファイルへの定義記載
- phonesテーブルからusersテーブルに対するモデルファイルへの定義記載
詳細
usersテーブルからphonesテーブルに対するモデルファイルへの定義記載
アプリ名ディレクトリで下記コマンドを実行してUserモデルファイルを開く。
$ vi app/Models/User.php下記のようにリレーションを定義する。
アプリ名ディレクトリ/app/Models/User.php<?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; class User extends Authenticatable { use HasFactory, Notifiable; /** * The attributes that are mass assignable. * * @var array */ protected $fillable = [ 'name', 'email', 'password', ]; /** * The attributes that should be hidden for arrays. * * @var array */ protected $hidden = [ 'password', 'remember_token', ]; /** * The attributes that should be cast to native types. * * @var array */ protected $casts = [ 'email_verified_at' => 'datetime', ]; // 下記を追記 /** * ユーザの電話番号を取得 * * @return void */ public function phone() { return $this->hasOne('App\Models\Phone'); } // 上記までを追記 }アプリ名ディレクトリで下記コマンドを実行してtinkerを起動する。
$ php artisan tinkertinkerで下記を実行してnull以外が帰ってくることを確認する。(usersテーブルとphonesテーブルにそれぞれデータが格納されているものとする。スペルミスがないのにnullが帰ってきてしまうときはtinkerを再起動する。)
use App\Models\User; User::find(1)->phone;phonesテーブルからusersテーブルに対するモデルファイルへの定義記載
アプリ名ディレクトリで下記コマンドを実行してUserモデルファイルを開く。
$ vi app/Models/Phone.php下記のようにリレーションを定義する。
アプリ名ディレクトリ/app/Models/Phone.php<?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Phone extends Model { use HasFactory; // 下記を追記 public function user() { return $this->belongsTo('App\Models\user'); } // 上記までを追記 }アプリ名ディレクトリで下記コマンドを実行してtinkerを起動する。
$ php artisan tinkertinkerで下記を実行してnull以外が帰ってくることを確認する。(usersテーブルとphonesテーブルにそれぞれデータが格納されているものとする。スペルミスがないのにnullが帰ってきてしまうときはtinkerを再起動する。)
use App\Models\Phone; User::find(1)->user;超簡単なまとめ
- 1対1のリレーションはメインテーブル→サブテーブルのときはhasOne(サブテーブルに紐づくモデル名)を使用し、サブテーブル→メインテーブルのときはbelongsTo(メインテーブルに紐づくモデル名)と記載する。
参考文献