20201013のlaravelに関する記事は12件です。

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の説明
こちらを念頭に置いて更新していきたいと思います。

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

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-13 18.14.11.png

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

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. 各テーブルのデータ件数をチェックして下記になっていればOK

mysql> 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.php
Route::get('/', 'WelcomeController@index');
Route::resource('players', 'PlayerController');
Route::resource('countries', 'CountryController');
Route::resource('goals', 'GoalController');
Route::resource('pairings', 'PairingController');

課題はここから…

  1. テーブルビューを1つ(あるいは2つ)追加
  2. functionを1つ追加
  3. controllerからviewに変数渡し してこのツイートと同じ見た目になるようにします

答えはコチラ

Task 5はいくつか追加の課題もあります
1. レコード追加
2. 論理削除
3. phpMyAdminのインストール

イージーモードになるyps委員長のブログはこちら
miyupaca log ⇒ yps学習記録その5

以上でTask5は終了です。
(ここでかなりの脱落者が出ましたが、今は答えもGitHubに載っているのでコピペでもいけてしまうかと思います…)

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

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. 各テーブルのデータ件数をチェックして下記になっていればOK

mysql> 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.php
Route::get('/', 'WelcomeController@index');
Route::resource('players', 'PlayerController');
Route::resource('countries', 'CountryController');
Route::resource('goals', 'GoalController');
Route::resource('pairings', 'PairingController');

課題はここから…

  1. テーブルビューを1つ(あるいは2つ)追加
  2. functionを1つ追加
  3. controllerからviewに変数渡し してこのツイートと同じ見た目になるようにします

答えはコチラ

Task 5はいくつか追加の課題もあります
1. レコード追加
2. 論理削除
3. phpMyAdminのインストール

イージーモードになるyps委員長のブログはこちら
miyupaca log ⇒ yps学習記録その5

以上でTask5は終了です。
(ここでかなりの脱落者が出ましたが、今は答えもGitHubに載っているのでコピペでもいけてしまうかと思います…)

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

【laravel 8.x】Logging の設定とログフォーマットの実装方法【基本編】

laravel のログにはPHPでよく使われているらしい Monolog が実装されている。
こいつをごにょごにょしていい感じにしたいのが本記事。
(簡潔に書きたかったがソースも書くと長くなるねぇ…)

Laravel は ver8.9.0。(v6くらいから同じだと思う)
目的はログのフォーマットの変更とカラーリング。

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.envAPP_ENV の値かな?
production とか local といった実行環境の値となる。

他のロガーでいうカテゴリやタイプといった塊は存在しない。(ほんとぉ?)
必要ならラッパーを作るなりする必要がありそう。

Exception ログ

use Exception;

logger()->error(new Exception('Reigai'));

スクリーンショット 2020-10-13 210104.png

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つにログの出力を渡している。
singlelogs/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
],

StreamHandlerstream パラメータにPHPの標準出力(stdout)を指定した。
level によって出力するログレベルを制限できるが、最低値の debug にすることにより、全てのログを対象としている。
試しに channels.stack.channelsstdout を追加すると標準出力にも出力されるようになるはず。

Handler は monolog 側のシステムなので今回は省く。
詳しくはこのあたり。

[オプション] CLI実行時(php artisan)のみ標準出力にも出力する

このまま stack に追記でもいいのだが、CLIを操作していない時に標準出力に出力されるのはあまり好みではない。
(schedule 処理、http のアクセス時等にも出力されてしまう(握りつぶされるが))
なので CLI で実行したときに限って出力するようにしてみる。

App\Providers\AppServideProvider.php
class 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() という関数を実行すると、apacheclicgi といった文字列が帰ってくる。
これを利用して、名前にCLIが含まれていたら stack に stdout を追加する仕組み。
collection の unique() を使うことで重複実装を防いでいる。

とりあえず AppServiceProvider に実装したが、 LoggerServideProvider とかを作って実装したほうが賢い。

参考:php - Detect if running from the command line in Laravel 5 - Stack Overflow

4. ログ文字列のフォーマット

さて本題。
デフォルトの出力文字列は以下のような形式だった。

image.png

これは 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

ここで levelmessage を加工して 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 の使わせたいチャンネルに指定してあげる。
今は stdoutsingle に。
stack に渡すと全チャンネルに適用できるが、子で指定していた場合上書きされた)
(formatter は一つしか追加できない?)

app/config/logging.php
use App\Logging\LineFormatterApply;

'stdout' => [
    'driver' => 'monolog',
    'handler' => StreamHandler::class,
    'with' => [
        'stream' => 'php://stdout',
    ],
    'level' => 'debug',
    'tap' => [ColorFormatterApply::class], // <-- これ
],

こんな感じの出力に変えられた。

image.png

5. ログをカラフルに出力する

標準出力のログはやっぱり色が欲しい。
視認性も上がるし、なによりログの把握速度が圧倒的である。

調べると monolog 側に良さそうなライブラリがあった。
bramus/monolog-colored-line-formatter: Colored/ANSI Line Formatter for Monolog

console
composer 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-formatter

App\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。
※制御文字を使用しているので、ファイル出力には使わないように。

image.png

とてもいい感じになった。
ログのメッセージ部は白のままにしたい時は、正規表現でその部分だけ抜き取って色を付けると良いかも。

6. サンプル

自分が使用したいログの仕様をまとめて、それを実装したサンプルを置いておく。

  • NOTICE 以上のログが1日毎に別のファイルに保存される daily チャンネル
    • 保存期間(days)は7日
  • ERROR 以上のログが1つのファイルに保存される singleError チャンネル
  • CLIで実行した際、全てのログが標準出力に色付きで出力される stdout チャンネル
    • これは AppServiceProvide で追加するので stack では触らない
App\Config\logging.php
use 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 を使用している。

image.png
image.png
image.png

おわりに

monolog の handlerprocessor 周りをまだ理解しきれてないので、とりあえず基本をまとめた。
あと laravel の Exception 周りも知らないといけないかも。

これ node-log4js より楽かもしれぬ。
個人的にタイマーは実装してみたい。
綺麗なログを吐けると開発が捗る捗る…。

続きは、今のログに手を入れるとき(書くとは言っていない)。

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

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ファイルが優先して読み込まれます。

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

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/mysql

dockerfileに RUN npm install -g create-nuxt-app を追加

dockerfile
FROM 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内で

tarminal
root@########:/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/
お疲れ様でした?

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

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

参考
https://qiita.com/A-Kira/items/5fd039217ba992481267

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

Laravelでログ出力と併せてメールで通知する

はじめに

バグを完全に防ぐことは難しいですが、問題が起こった場合には迅速に対処できるようにしておきたいものです。
CloudWatch LogsやZabbixなどでログ監視できていると安心ですが、実際には導入が困難な場合もあるでしょう。

そこで、Laravelにてログ出力と併せてメールで通知する機能を設け、簡易的なログ監視を行いたいと思います。
この場合サーバー自体がダウンしているとどうにもならないですが、プログラムのバグであればすぐに気づくことができるでしょう。

なお、LaravelはデフォルトでSlack通知に対応しているので、Slackを利用されている方はそちらの方が便利かもしれません。

Laravelのバージョンは5.7にて確認しています。

Handler.phpでメール通知

Laravelのドキュメントには下記のようにあります。

Laravelの例外はすべて、App\Exceptions\Handlerクラスで処理されます。
このクラスはreportとrender二つのメソッドで構成されています。両メソッドの詳細を見ていきましょう。reportメソッドは例外をログするか、BugSnagやSentryのような外部サービスに送信するために使います。

Laravel ドキュメント

つまり、下記のreportメソッド内にメール送付するロジックを記述すれば実現できるということです。
さっそく記述してみました。

App\Exceptions\Handler.php
public 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.php
protected $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'] }}
======================================================================

おわりに

アプリケーションの安定稼働に少しでも寄与できれば幸いです。

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

初心者が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.php
use Illuminate\Contracts\Http\Kernel;
use Illuminate\Http\Request;

メンテナンスモード

メンテナンスモードの場合は、storage/framework/maintenance.phpが生成され、エラーレスポンス503を返すようになります。
通常時はファイルが存在しないのでfalseになります。メンテナンスモードの詳細はLaravelのドキュメントをご参照ください。

file_exists:引数のファイルが存在するかどうかを判定する関数。
__DIR__:ファイルのディレクトリを返す。

public/index.php
if (file_exists(__DIR__.'/../storage/framework/maintenance.php')) {
    require __DIR__.'/../storage/framework/maintenance.php';
}

オートロード

ここから本題になります。オートロードに関しては、galluさんのnoteが非常に参考になりました。
それでは、以下の一行の詳細をみていきたいと思います。

public/index.php
require __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::loaderprivate static $loader;と宣言しただけなので、nullとなります。そのため、はじめのif文はスルーします。
その後、spl_autoload_register()でオートロードの設定をしています。

spl_autoload_register()は、配列を与えると、クラスメソッドを登録できます。
ここでは、loadClassLoader()をオートロードしています。すなわち、今後使おうとしたクラスが宣言されてなかったら、loadClassLoader()が実行されます。

オートロードの設定後、新しいインスタンスを生成して、オートロードの設定を解除しています。

vendor/composer/autoload_real.php
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'));

loadClassLoaderをみてみると、以下のようになっています。
引数には、クラスの読み込みでエラーがあったときのクラス名が入っています。
読み込めなかったのがComposer\Autoload\ClassLoaderだったら、ファイルを読み込んでください、となります。

vendor/composer/autoload_real.php
public static function loadClassLoader($class)
{
    if ('Composer\Autoload\ClassLoader' === $class) {
        require __DIR__ . '/ClassLoader.php';
    }
}

ClassLoaderをみてみると、クラスが宣言されています。ここではrequireしているだけなので、今すぐ何かが実行されるわけではないので、クラスを読み込んでるんだな、というくらいの理解に留めます。

vendor/composer/ClassLoader.php
class 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());

まとめると、以下のようになっているので、$useStaticLoadertrueが入ります。

true && true && (true || false)

となると、実行されるのは以下の部分だけとなります。ファイルを読み込んで、クラスメソッドを実行しています。
call_user_func()は引数で指定したコールバックを実行します。
今回の場合、普通に実行する場合と何が違うんだと思って、call_user_func()を使わないで実行してみると、エラーになりました。このあたりは理解できていません。。。

vendor/composer/autoload_real.php
if ($useStaticLoader) {
    require_once __DIR__ . '/autoload_static.php';

    call_user_func(\Composer\Autoload\ComposerStaticInitbee6542d79f53acb601ba3c7d134558a::getInitializer($loader));
}

読み込んでいるファイルは以下の通りです。クラスの宣言になります。中身は非常に長いですが、クラスプロパティに配列を代入しています。

vendor/composer/autoload_static.php
namespace Composer\Autoload;

class ComposerStaticInitbee6542d79f53acb601ba3c7d134558a
{
    public static $files = array (
        'ec07570ca5a812141189b1fa81503674' =>
            __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Assert/Functions.php',

        ...

では、クラスメソッドを見ていきたいと思います。
\Closure:bind()の中の無名関数の中で、プロパティが代入されています。
右辺の値は長くて分かりにくいですが、上で示したような配列が代入されたクラスプロパティとなります。

/vendor/composer/autoload_static.php
public 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.php
self::$loader = $loader = new \Composer\Autoload\ClassLoader();

ClassLoaderのプロパティをみてみると、以下のように代入しているプロパティは全てprivateとなっています。なぜComposerStaticInitbee65...クラスの中で、ClassLoaderprivateプロパティに代入できるんだろうか、となると思うのですが、それを解決するのが\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メソッドは以下のようになっています。
$thisClassLoaderのインスタンスが入っています。ここではClassLoaderloadClassメソッドを、オートロードの設定の対象としています。

vendor/composer/ClassLoader.php
public function register($prepend = false)
{
    spl_autoload_register(array($this, 'loadClass'), true, $prepend);
}

loadClass()メソッドをみていきます。if文の条件式でfindFile()メソッドが呼ばれています。もしファイルが見つかれば、includeFile()を実行してファイルを読み込んでいます。これが先ほどの一時的なものとは異なり、通常のオートロードになります。
今後はクラスが使用されると、まずこのメソッドが実行されることになります。

vendor/composer/ClassLoader.php
public function loadClass($class)
{
    if ($file = $this->findFile($class)) {
        includeFile($file);

        return true;
    }
}

findFile()

findFile()メソッドは以下の通りです。ファイルを探して、そのファイルを返しています。
引数の$classにはクラスの名前(名前空間+クラス名)が入ります。

vendor/composer/ClassLoader.php
public 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.php
if (isset($this->classMap[$class])) {
    return $this->classMap[$class];
}

$this->classMapの例をみていきます。いくつかあるのですが、馴染みのありそうなのものをピックアップしました。Illuminateが何故vendor/laravel/framework/src/Illuminateを指すのか今まで分かっていませんでしたが、ここでようやく分かりました。

vendor/composer/autoload_static.php
 public 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.php
if ($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.php
private $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.php
if (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.php
private 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->prefixLengthsPsr4vendor/composer/autoload_static.phpで既に代入されており、以下のような配列になっています。
Symfony\Component\Translation\TranslatorInterfaceの1文字目はSなので、Sがキーとなる値はあるかどうか確認してみます。以下の通り、Sがキーとなる配列が存在しているので、上述のif文はtrueとなります。

vendor/composer/autoload_static.php
public 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.php
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;
            }
        }
    }
}

ディレクトリは登録されているので、trueを返すことが分かります。

vendor/composer/autoload_static.php
public 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.php
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

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.php
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;
                }
            }
        }
    }
}

ちなみに、中身は以下の通りです。

vendor/composer/autoload_static.php
public 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\TranslatorInterfacefalseが返されています。
はじめのif文ですが、$fileにファイルのパスが返ってきていても、右辺がfalseとなるので、条件式はfalseとなります。
次の$this->apcuPrefixnullとなります。
次のif文で、Symfony\Component\Translation\TranslatorInterfaceはみつからなかったので、missingClassesに代入して、次からは検索する前にfalseを返すようにしています。
最後に、$fileを返しています。

vendor/composer/ClassLoader.php
public 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.php
public function loadClass($class)
{
    if ($file = $this->findFile($class)) {
        includeFile($file);

        return true;
    }
}

includeFile()は以下の通りです。ファイルを読み込んでいるだけです。

vendor/composer/ClassLoader.php
function includeFile($file)
{
    include $file;
}

組み込み関数の読み込み

長かった$loader->register(true);も終わり、autoload_real.phpに戻ってきました。
$useStaticLoadertrueなので、$includeFilesに値が代入されます。
その値をforeachでループして、composerRequire...()という関数を実行しています。
最後に$loaderを返して終わりです。

vendor/composer/autoload_real.php
    if ($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.php
class 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.php
function 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.php
require __DIR__.'/../vendor/autoload.php';

まとめ

require __DIR__.'/../vendor/autoload.php';の1行で、クラスのオートロード設定、組み込み関数の宣言などを実行していることが分かりました。
また、フレームワークの中身を調べていくことで、自分が知らないPHPの組み込み関数や、いいコードの書き方など、非常に勉強になりました。
次の1行の$app = require_once __DIR__.'/../bootstrap/app.php';も非常に難しいので、読み解いていきたいと思います。

参考サイト

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

LambdaでLaravelを動作させようと思ったけど、少しコケたのでメモしとく

私はPHPerなのですが、最近、仕事関係でLambdaの話がちょくちょく出てきたので、ちょっと勉強がてら調査することに。

今更なのですが Lambda でPHPを動かせるようになっていたということに驚いた。

で、調べてみるとどうやらbrefというライブラリを使えば、簡単にLambda上でLaravelも動かせるらしい。
Bref - Serverless PHP made simple

わざわざ Lambda で PHPを使う必要があるのかと思うかもだけど、一緒にお仕事しているチームの構成を考えるとメンテナンスも含めてLaravelが使えるというのはとても助かるのです。

で、さくっと試してみようと思ったところデプロイで躓いちゃったので、メモ残しておきます。

環境

IAM ユーザーを作成する

Lambda を利用するにあたり必要な IAM ユーザーを作成。

Creating AWS access keys - Bref

とりあえず試したいだけだから、まんまガイドに従って作成。
簡単、簡単。

serverless framwork のインストール

後述しますが、本当は docker で serverless が動作する環境を作ろうと思っていたのですが、なぜかメモリ不足というエラーのため動作せず。。。

なので、Windows に直接 serverless をインストール。

Serverless Getting Started Guide

docker で環境作りに悩んでいたのが馬鹿みたいに、サクッと環境ができた。
credential の設定もガイドに従って設定。

私、この環境を作るまでChocolateyというソフトを存じ上げませんでした。
いや~、ダサいというイメージの Windows ですが、少しずつクールになってきていると思っているのは私だけ?w

Laravel 環境を 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 に追加。

.env
APP_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.yml
  serverless:
    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/Dockerfile
FROM 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 を実行したところ、なぜか以下のエラー。

console
Serverless: 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

メモリは十分にあると思うのに理由がわからない。
散々悩んだあげく、諦めたのさ~

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

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 --auth
    
  • Auth認証で使用される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
      email varchar(255) NO UNI NULL
      email_verified_at timestamp YES NULL
      password varchar(255) NO NULL
      remember_token varchar(100) YES NULL
      created_at timestamp YES NULL
      updated_at timestamp YES NULL
    • 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のリレーションを定義する。

概要

  1. usersテーブルからphonesテーブルに対するモデルファイルへの定義記載
  2. phonesテーブルからusersテーブルに対するモデルファイルへの定義記載

詳細

  1. usersテーブルからphonesテーブルに対するモデルファイルへの定義記載

    1. アプリ名ディレクトリで下記コマンドを実行してUserモデルファイルを開く。

      $ vi app/Models/User.php
      
    2. 下記のようにリレーションを定義する。

      アプリ名ディレクトリ/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');
          }
          // 上記までを追記
      }
      
      
    3. アプリ名ディレクトリで下記コマンドを実行してtinkerを起動する。

      $ php artisan tinker
      
    4. tinkerで下記を実行してnull以外が帰ってくることを確認する。(usersテーブルとphonesテーブルにそれぞれデータが格納されているものとする。スペルミスがないのにnullが帰ってきてしまうときはtinkerを再起動する。)

      use App\Models\User;
      User::find(1)->phone;
      
  2. phonesテーブルからusersテーブルに対するモデルファイルへの定義記載

    1. アプリ名ディレクトリで下記コマンドを実行してUserモデルファイルを開く。

      $ vi app/Models/Phone.php
      
    2. 下記のようにリレーションを定義する。

      アプリ名ディレクトリ/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');
          }
          // 上記までを追記
      }
      
    3. アプリ名ディレクトリで下記コマンドを実行してtinkerを起動する。

      $ php artisan tinker
      
    4. tinkerで下記を実行してnull以外が帰ってくることを確認する。(usersテーブルとphonesテーブルにそれぞれデータが格納されているものとする。スペルミスがないのにnullが帰ってきてしまうときはtinkerを再起動する。)

      use App\Models\Phone;
      User::find(1)->user;
      

超簡単なまとめ

  • 1対1のリレーションはメインテーブル→サブテーブルのときはhasOne(サブテーブルに紐づくモデル名)を使用し、サブテーブル→メインテーブルのときはbelongsTo(メインテーブルに紐づくモデル名)と記載する。

参考文献

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