20201013のPHPに関する記事は23件です。

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で続きを読む

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で続きを読む

開発環境でのメール確認用にMailHogを利用する(Docker利用)

開発環境でメールの確認方法が面倒だったり、配信先に気を使う場合がありますが、MailHogというツールを使うとローカル内に閉じたメールの確認ができる環境を簡単に立ち上げることができます。

MailHogはGo言語でできているらしいです。

以前はMailCatcherという別の類似ツールを利用していましたが、MailHogの方が構築が容易だったので乗り換えました。

docker-composeで以下の構成で立ち上げます。

ファイル構成
.
├── docker-compose.yml
└── php
    ├── docker
    │   ├── Dockerfile
    │   └── php.ini
    └── mail_test.php
docker-compose.yml

8025番ポートでwebインターフェースを利用するのでポートを設定します

version: '3'
services:
 php:
  build:
   context: ./
   dockerfile: php/docker/Dockerfile
  volumes:
   - ./php/:/var/www/html/
 mailhog:
  image: mailhog/mailhog
  ports:
   - "8025:8025" #webインターフェース用ポート
Dockerfile

phpからのメール送信にmhsendmailが必要なので、Dockerfileでインストールします。

FROM php:5.6-apache
WORKDIR /var/www/html
RUN curl -sSL https://github.com/mailhog/mhsendmail/releases/download/v0.2.0/mhsendmail_linux_amd64 -o mhsendmail \
    && chmod +x mhsendmail \
    && mv mhsendmail /usr/local/bin/mhsendmail
COPY ./php/docker/php.ini /usr/local/etc/php/
php.ini

[mail function]を書き換えておきます。

[mail function]
; For Unix only.  You may supply arguments as well (default: "sendmail -t -i").
; http://php.net/sendmail-path
; sendmail_path = /usr/sbin/sendmail -t -i
; 以下に書き換え
sendmail_path = "/usr/local/bin/mhsendmail --smtp-addr=mailhog:1025"
mail_test.php
<?php

$to      = "hoge@localhost.local";
$subject = "TEST";
$message = "メールテスト";
$headers = "From: from@example.com";

mb_send_mail($to, $subject, $message, $headers);
docker-compose で up
$ docker-compose up -d
Building php
Step 1/4 : FROM php:5.6-apache
 ---> 24c791995c1e
Step 2/4 : WORKDIR /var/www/html
 ---> Using cache
 ---> 1294d05c5c03
Step 3/4 : RUN curl -sSL https://github.com/mailhog/mhsendmail/releases/download/v0.2.0/mhsendmail_linux_amd64 -o mhsendmail     && chmod +x mhsendmail     && mv mhsendmail /usr/local/bin/mhsendmail
 ---> Using cache
 ---> db0719944c4e
Step 4/4 : COPY ./php/docker/php.ini /usr/local/etc/php/
 ---> 264eb166413a
Successfully built 264eb166413a
Successfully tagged mailhog_php:latest
WARNING: Image for service php was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`.
Pulling mailhog (mailhog/mailhog:)...
latest: Pulling from mailhog/mailhog
df20fa9351a1: Already exists
ed8968b2872e: Pull complete
a92cc7c5fd73: Pull complete
f17c8f1adafb: Pull complete
03954754c53a: Pull complete
60493946972a: Pull complete
368ee3bc1dbb: Pull complete
Digest: sha256:8d76a3d4ffa32a3661311944007a415332c4bb855657f4f6c57996405c009bea
Status: Downloaded newer image for mailhog/mailhog:latest
Creating mailhog_php_1     ... done
Creating mailhog_mailhog_1 ... done
mailhogのWEBインターフェース

localhost:8025で表示

image.png

phpのコンテナ内からメール送信スクリプトを実行
$ docker exec -it mailhog_php_1 /bin/bash
root@9c4b6a5df613:/var/www/html# php mail_test.php

windows上にメールの到着を通知が表示され

image.png
メールが到着していることが確認できます

image.png
image.png

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

【PHP】配列のキーを維持したまま array_column を使いたい

array_column について

PHPのarray_columnは,多次元配列から単一のカラムの値を返してくれる,とても便利な関数です。

php
// このような多次元配列から…
$array = [
  2 => [ 'id' => 111, 'name' => 'suzuki', 'age' => 21 ],
  3 => [ 'id' => 222, 'name' => 'sato',   'age' => 34 ],
  5 => [ 'id' => 333, 'name' => 'tanaka', 'age' => 55 ],
];

// 単一のカラム値の配列を作れる
var_export( array_column( $array, 'name' ) );
// array (
//   0 => 'suzuki',
//   1 => 'sato',
//   2 => 'tanaka',
// )

しかしながら,返り値の配列のキーは

  • 第3引数を指定しない場合:単純に0から振り直される
  • 第3引数を指定した場合 :指定カラムの値がキーとして使われる

のどちらかで,元の配列のキー(上の例だと「2」「3」「5」)は維持されません。

元の配列のキーを維持したいとき

次の3通りの方法を紹介します。

  • 方法① array_columnと他の関数を組み合わせる
  • 方法② array_mapを使う
  • 方法③ foreachを使う

※2020/10/14 当記事の初投稿時には方法①のみを紹介していたのですが,コメント欄で@tadsanさんに教えていただいた方法②に加えて,foreachを使う場合の方法③も追加しました。また,まとめの内容を変更し,array_columnについての蛇足も追記しました。


方法①array_columnと他の関数を組み合わせる

array_keysで元の配列のキーを取得し,第3引数を指定しないarray_columnの返り値と合体させましょう。対応する配列の生成にはarray_combineを使います。

php
$array = [
  2 => [ 'id' => 111, 'name' => 'suzuki', 'age' => 21 ],
  3 => [ 'id' => 222, 'name' => 'sato',   'age' => 34 ],
  5 => [ 'id' => 333, 'name' => 'tanaka', 'age' => 55 ],
];

// キーを配列として取得
$keys = array_keys( $array ); // => [ 2, 3, 5 ]
// 指定要素を配列として取得
$values = array_column( $array, 'name' ); // => [ 'suzuki', 'sato', 'tanaka' ]
// それぞれの配列を合体
$combined = array_combine( $keys, $values );
// 出力
var_export( $combined );
// array (
//   2 => 'suzuki',
//   3 => 'sato',
//   5 => 'tanaka',
// )

上記の操作を1行で書くと以下のようになります。

php
array_combine( array_keys( $array ), array_column( $array, 'name' ) );
  • メリット :関数の使い方として何をやっているかは分かりやすい
  • デメリット:関数の呼び出しが多い


方法②array_mapを使う

array_columnを使いたい」という記事なのにタイトル詐欺もいいとこですが,こちらの方が簡潔に記述できます(コメント欄で@tadsanさんに教えていただきました)。

(コメント欄からコピペ)
$array = [
  2 => [ 'id' => 111, 'name' => 'suzuki', 'age' => 21 ],
  3 => [ 'id' => 222, 'name' => 'sato',   'age' => 34 ],
  5 => [ 'id' => 333, 'name' => 'tanaka', 'age' => 55 ],
];

var_dump(array_map(fn($a) => $a['name'], $array));

上記のコードの配列操作部分を抜き出すと以下のようになります。

(コメント欄からコピペ)
array_map(fn($a) => $a['name'], $array);

array_mapの第1引数のfn($a) => $a['name']の部分がPHP7.4で導入された「アロー関数」なので,7.4より前の環境では「無名関数」を使って置き換えることになります。

php
array_map( function ($a) { return $a['name']; }, $array );
  • メリット :アロー関数が使える環境では簡潔に記述できる
  • デメリット:ワンライナーで無名関数を使う場合には少し可読性が下がる


方法③foreachを使う

foreachを使わずに済むようにarray_columnがあるのでは?」と言われればそれまでなのですが,方法②でarray_columnを使わなかったので一応。

php
$array = [
  2 => [ 'id' => 111, 'name' => 'suzuki', 'age' => 21 ],
  3 => [ 'id' => 222, 'name' => 'sato',   'age' => 34 ],
  5 => [ 'id' => 333, 'name' => 'tanaka', 'age' => 55 ],
];

$result = [];
foreach ( $array as $key => $values ) {
  $result[ $key ] = $values['name'];
}
var_export( $result );
// array (
//   2 => 'suzuki',
//   3 => 'sato',
//   5 => 'tanaka',
// )
  • メリット :一見して何をやっているか分かりやすい
  • デメリット:一時変数が必要,コードも複数行にわたる

まとめ

方法②のarray_mapとアロー関数を使ったワンライナーが一番タイプ数も少なくおすすめです。思想信条上の理由でクロージャを使いたくない方は,

  • 1行にまとめたければ方法①
  • だれが見ても分かりやすいコードにしたければ方法③

が良いと思います。記事タイトルにそぐわない結論になってしまいましたが。

蛇足

array_columnはPHP5.5から導入された関数ですが,よくよく調べてみると「第3引数が未指定の場合に元配列のキーを維持する案」がリリース前に出ており,実際にプルリクエストも出されていたみたいですが,議論の末に見送られた経緯があるようです。こんな記事を書いといてアレですが,元配列のキーを維持したくない場合の方が多いはずなので,これは見送られて良かったと思われます。

それとは別に,「第3引数に true が指定された場合に元配列のキーを維持する案」もあったようで,この案についてはリリース後もたまにリクエストがあがっているみたいです(PHPで実装された array_column リポジトリの issue とか)。

ただ,PHP公式サイト上のこのページ(←何故かつながりづらい)では,「第3引数に true を指定する案」や「第4引数を追加する案」などのいかなる変更も既存の予想される動作を破壊し得るとして,array_columnの実装者である ramsey さんがリクエストを close しており,キーを維持するための機能追加が行われることは今後まず無さそうです。

参考

array_column - PHP.net1


  1. array_columnの公式ページに「第3引数に -1 を渡すと元の配列のキーを維持する関数」が掲載されています。この関数をコピペして使えば,array_column_ext( $array,'name', -1 )のように簡潔な記述ができます(どんな場合でも使えるかどうかは検証してないので分かりません)。 

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

【PHP】キーを維持したままarray_columnを使いたい

array_column関数

PHPのarray_columnは,多次元配列から単一のカラムの値を返してくれる,とても便利な関数です。

// このような多次元配列から…
$array = [
  2 => [ 'id' => 100, 'name' => 'suzuki', 'age' => 21 ],
  3 => [ 'id' => 101, 'name' => 'sato',   'age' => 34 ],
  5 => [ 'id' => 102, 'name' => 'tanaka', 'age' => 55 ],
];

// 単一のカラム値の配列を作れる
var_export( array_column( $array, 'name' ) );
// array (
//   0 => 'suzuki',
//   1 => 'sato',
//   2 => 'tanaka',
// )

しかしながら,返り値の配列のキーは

  • 第3引数を指定しない場合:単純に0から振り直される
  • 第3引数を指定した場合 :指定カラムの値がキーとして使われる

のどちらかで,元の配列のキー(上の例では「2」「3」「5」)は維持されません。

元の配列のキーを維持したいとき

array_keysで元の配列のキーを取得し,第3引数を指定しないarray_columnの返り値と合体させましょう。対応する配列の生成にはarray_combineを使います。

$array = [
  2 => [ 'id' => 100, 'name' => 'suzuki', 'age' => 21 ],
  3 => [ 'id' => 101, 'name' => 'sato',   'age' => 34 ],
  5 => [ 'id' => 102, 'name' => 'tanaka', 'age' => 55 ],
];

// キーを配列として取得
$keys = array_keys( $array ); // => [ 2, 3, 5 ]
// 指定要素を配列として取得
$values = array_column( $array, 'name' ); // => ['suzuki', 'sato', 'tanaka']
// それぞれの配列を合体
$combined = array_combine( $keys, $values );
// 出力
var_export( $combined );
// array (
//   2 => 'suzuki',
//   3 => 'sato',
//   5 => 'tanaka',
// )

まとめ

元の配列のキーを維持してarray_columnを使うことができました。上記の操作を1行で書くと以下のようになります。

$combined = array_combine( array_keys( $array ), array_column( $array, 'name' ) );

参考

PHP array_column - how to keep the keys? - Stack Overflow1


  1. 値が''nullfalseなどのときに配列に取り込みたくない場合は,上記のワンライナーをarray_filterで囲むといいそうです。 

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

【PHP】キーを維持したまま array_column を使いたい

array_column関数

PHPのarray_columnは,多次元配列から単一のカラムの値を返してくれる,とても便利な関数です。

php
// このような多次元配列から…
$array = [
  2 => [ 'id' => 100, 'name' => 'suzuki', 'age' => 21 ],
  3 => [ 'id' => 101, 'name' => 'sato',   'age' => 34 ],
  5 => [ 'id' => 102, 'name' => 'tanaka', 'age' => 55 ],
];

// 単一のカラム値の配列を作れる
var_export( array_column( $array, 'name' ) );
// array (
//   0 => 'suzuki',
//   1 => 'sato',
//   2 => 'tanaka',
// )

しかしながら,返り値の配列のキーは

  • 第3引数を指定しない場合:単純に0から振り直される
  • 第3引数を指定した場合 :指定カラムの値がキーとして使われる

のどちらかで,元の配列のキー(上の例では「2」「3」「5」)は維持されません。

元の配列のキーを維持したいとき

array_keysで元の配列のキーを取得し,第3引数を指定しないarray_columnの返り値と合体させましょう。対応する配列の生成にはarray_combineを使います。

php
$array = [
  2 => [ 'id' => 100, 'name' => 'suzuki', 'age' => 21 ],
  3 => [ 'id' => 101, 'name' => 'sato',   'age' => 34 ],
  5 => [ 'id' => 102, 'name' => 'tanaka', 'age' => 55 ],
];

// キーを配列として取得
$keys = array_keys( $array ); // => [ 2, 3, 5 ]
// 指定要素を配列として取得
$values = array_column( $array, 'name' ); // => ['suzuki', 'sato', 'tanaka']
// それぞれの配列を合体
$combined = array_combine( $keys, $values );
// 出力
var_export( $combined );
// array (
//   2 => 'suzuki',
//   3 => 'sato',
//   5 => 'tanaka',
// )

まとめ

元の配列のキーを維持してarray_columnを使うことができました。上記の操作を1行で書くと以下のようになります。

php
$combined = array_combine( array_keys( $array ), array_column( $array, 'name' ) );

参考

PHP array_column - how to keep the keys? - Stack Overflow1
array_column - PHP.net2


  1. 値が''nullfalseなどのときに配列に取り込みたくない場合は,上記のワンライナーをarray_filterで囲むといいそうです。 

  2. array_columnの公式ページに「第3引数に -1 を渡すと元の配列のキーを維持する関数」が掲載されています。 

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

【PHP】元の配列のキーを維持して array_column を使いたい

array_column関数

PHPのarray_columnは,多次元配列から単一のカラムの値を返してくれる,とても便利な関数です。

php
// このような多次元配列から…
$array = [
  2 => [ 'id' => 100, 'name' => 'suzuki', 'age' => 21 ],
  3 => [ 'id' => 101, 'name' => 'sato',   'age' => 34 ],
  5 => [ 'id' => 102, 'name' => 'tanaka', 'age' => 55 ],
];

// 単一のカラム値の配列を作れる
var_export( array_column( $array, 'name' ) );
// array (
//   0 => 'suzuki',
//   1 => 'sato',
//   2 => 'tanaka',
// )

しかしながら,返り値の配列のキーは

  • 第3引数を指定しない場合:単純に0から振り直される
  • 第3引数を指定した場合 :指定カラムの値がキーとして使われる

のどちらかで,元の配列のキー(上の例では「2」「3」「5」)は維持されません。

元の配列のキーを維持したいとき

array_keysで元の配列のキーを取得し,第3引数を指定しないarray_columnの返り値と合体させましょう。対応する配列の生成にはarray_combineを使います。

php
$array = [
  2 => [ 'id' => 100, 'name' => 'suzuki', 'age' => 21 ],
  3 => [ 'id' => 101, 'name' => 'sato',   'age' => 34 ],
  5 => [ 'id' => 102, 'name' => 'tanaka', 'age' => 55 ],
];

// キーを配列として取得
$keys = array_keys( $array ); // => [ 2, 3, 5 ]
// 指定要素を配列として取得
$values = array_column( $array, 'name' ); // => ['suzuki', 'sato', 'tanaka']
// それぞれの配列を合体
$combined = array_combine( $keys, $values );
// 出力
var_export( $combined );
// array (
//   2 => 'suzuki',
//   3 => 'sato',
//   5 => 'tanaka',
// )

まとめ

元の配列のキーを維持してarray_columnを使うことができました。上記の操作を1行で書くと以下のようになります。

php
$combined = array_combine( array_keys( $array ), array_column( $array, 'name' ) );

参考

PHP array_column - how to keep the keys? - Stack Overflow1
array_column - PHP.net2


  1. 値が''nullfalseなどのときに配列に取り込みたくない場合は,上記のワンライナーをarray_filterで囲むといいそうです。 

  2. array_columnの公式ページに「第3引数に -1 を渡すと元の配列のキーを維持する関数」が掲載されています。この関数をコピペして使えば,array_column_ext( $array,'name', -1 )のようにさらに簡潔な記述ができそうです。 

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

Google Analytics Reporting API v4を使ってPV数とUU数を取得する

はじめに

はじめてのアナリティクス Reporting API v4: サービス アカウント向け PHP クイックスタートを参考にして、指定した期間のPV数とUU数を、日毎に取得するバッチを作成できたので、クイックスタートのHelloAnalytics.phpのgetReportメソッド にあたる部分の備忘録。
cronでバッチ実行して日毎のPV数、UU数を取得するバッチを実行したい人の役に立てたらと思います。

前提

  • PHP7.4
  • Google API ConsoleでJSON形式の秘密鍵を作成済み
  • クライアントライブラリインストール済み
  • 例外系の処理も書いていますが省略。

PHPプログラム

HelloAnalytics.php
<?php

function getReport($analytics) {

    // Create the DateRange object.
    $dateRange = new Google_Service_AnalyticsReporting_DateRange();
    $dateRange->setStartDate("7daysAgo");
    $dateRange->setEndDate("today");

    // Create the Metrics object.
    // PV
    $pageviews = new Google_Service_AnalyticsReporting_Metric();
    $pageviews->setExpression('ga:pageviews');
    // UU
    $uniqueUsers = new Google_Service_AnalyticsReporting_Metric();
    $uniqueUsers->setExpression('ga:users');

    // Create the Dimension object.
    // Daily
    $byDay = new Google_Service_AnalyticsReporting_Dimension();
    $byDay->setName("ga:date");

    // Create the ReportRequest object.
    $request = new Google_Service_AnalyticsReporting_ReportRequest();
    $request->setViewId($viewId);
    $request->setDateRanges($dateRange);
    $request->setMetrics([$pageviews, $uniqueUsers]);
    $request->setDimensions([$byDay]);

    $body = new Google_Service_AnalyticsReporting_GetReportsRequest();
    $body->setReportRequests([$request]);
    $result = $analytics->reports->batchGet($body);

    //今回は日毎のPV数とUU数が欲しかったので、日付をキーとした連想配列にpvとuuを入れてみました。
    $data = [];
    foreach ($result['reports'][0]['data']['rows'] as $row) {
        $data[$row['dimensions'][0]] = [
            "pv" => $row['metrics'][0]['values'][0],
            "uu" => $row['metrics'][0]['values'][1],
        ];
    }

実際の中身の形式は以下のようになります。

    var_dump($data);
    //以下結果例
    [20201013] =>
     array(2) {
     'pv' =>
         string(5) "12345"
     'uu' =>
         string(4) "8585"
     }...

最後に

ソートの設定しなくても古い順から結果を得ることができるんですね。
クイックスタートの参考例ではPV数だけが返ってきており、かつ指定した合計数が返ってきてしまったのですが、

$uniqueUsers->setExpression('ga:users');
$byDay->setName("ga:date");

こちらの設定をすることで、PV数だけでなくUU数と、指定した期間の日毎の結果を得ることができたので嬉しかったです。

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

Moodle 3.9 マニュアル - アクセスを制限する

原文

アクセス制限機能を使用すると、教師は、日付、取得した成績、グループまたはアクティビティの完了などの特定の条件に従って、アクティビティまたはコースセクションの可用性を制限できます。

RestrictAccessExample.png

注:コースのアクティビティとセクションを設定するときに[アクセスを制限する]オプションが表示されない場合は、[サイト管理]> [高度な機能]で有効になっていることを管理者に確認してください。

アクセス設定を制限する
アクセス制限の使用
アクセス制限に関するFAQ

カテゴリコース 完了 アクセスを制限する

  • このエントリーをはてなブックマークに追加
  • 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で続きを読む

Moodle 3.9 マニュアル - コースの形式

コース形式とは、コースのレイアウトを指します。コース形式は、[管理]> [コース管理]> [設定の編集]で選択できます。

管理者は、[管理]> [サイト管理]> [プラグイン]> [コース形式]> [コース形式の管理]で、コースのコース形式を有効、無効、または削除できます。また、コース設定で利用可能なコース形式が教師に表示される順序を変更することもできます。

300px-DefaultCourseFormatTopics.png
管理者向けのコース形式の管理画面

内容

1 標準コース形式
1.1 ウィークリーフォーマット
1.2 トピック形式
1.2.1 セクションを1つだけ表示する
1.3 ソーシャルフォーマット
1.4 単一アクティビティ形式
2 寄稿されたコース形式
2.1 ボタンコース形式
2.2 折りたたまれたトピック
2.2.1 概要
2.3 デイリーフォーマット
2.4 グリッドフォーマット
2.5 メニュートピック形式
2.6 掲示板フォーマット
2.7 ワントピックフォーマット
2.8 タイル形式
2.9 トピック形式(色)
2.10 柔軟なセクション形式
2.11 キックスタート形式(コースウィザード)
3 関連項目

1 標準コース形式

1.1 ウィークリーフォーマット

ウィークリーフォーマットの例
コースは週ごとに編成されており、各セクションには日付の見出しがあります。 Moodleはあなたのコースの毎週のセクションを作成します。毎週のセクションにコンテンツ、フォーラム、クイズなどを追加できます。今週が強調表示されます。

300px-Weeklyoutline.png

ヒント:すべての生徒に同時に同じ資料を作成してもらいたい場合は、これを選択することをお勧めします。

注:コースの開始日が正しいことを確認してください。そうでない場合は、週の日付が間違っています。これは、新しいセクションの学生で使用するコースを復元する場合に特に重要です。
学生が異なる時間にコースを開始する場合は、コース相対日付機能を使用して、コース内の学生の開始日を基準にした各コースセクションの日付を表示できます。

1.2 トピック形式

トピック形式の例
このコースは、教師がタイトルを付けることができるトピックセクションに編成されています。各トピックセクションは、アクティビティ、リソース、およびラベルで構成されています。 Moodleの新規インストールでは、これがデフォルトのフォーマットです。

300px-Topicoutline.png

ヒント:これは、コースが目的に基づいており、各目的が完了するまでに異なる時間がかかる場合に使用するのに最適です。この例は、学生が以前のトピックからの知識に基づいて構築している足場です。

1.2.1 セクションを1つだけ表示する

ウィークリー、トピック、および機能を実装する提供された形式を使用すると、[管理]> [コース管理]> [設定の編集]から「ページごとに1つのセクションを表示」することができます。これは8つのトピックからなるコースで、3番目のトピックが現在表示されています。左右のリンクはトピック2と4につながります。

Single_section_page.png
単一セクションページ

ノート:
コースのホームページには、セクション名とセクションの説明のテキストだけが、アクティビティ番号とリソース番号とともに表示され、名前はクリック可能です。これは、コースに初めてアクセスしたときに表示されるものです。名前をクリックしてトピックを選択すると、一度に1つのセクションが表示されます。
編集がオンの場合、メインコースページにはすべてのセクションのすべてのコンテンツが表示されます。
各セクションページの下部に「Jumpto ...」メニューがあります

1.3 ソーシャルフォーマット

この形式は、メインページにリストされているソーシャルフォーラムという1つのメインフォーラムを対象としています。これは、より自由な形式の状況で役立ちます。コースではないかもしれません。 Moodleラウンジはソーシャルフォーマットコースの一例です。

500px-SocialFormatN.png
ソーシャルフォーマットの例-クリックして拡大

ソーシャルフォーマットを選択するときに、コースページに表示するディスカッションの数を指定できます。

socialformatdiscussions.png
socialformatdiscussions.png

ソーシャルフォーラムは、ソーシャルフォーラムページの[このフォーラムを更新]ボタンをクリックして編集できます。フォーラムの紹介は、コースページの上部に表示されます。ソーシャルアクティビティブロックを使用して、アクティビティとリソースをサイドに追加できます。

1.4 単一のアクティビティ形式

単一のアクティビティ形式には1つのセクションしかなく、教師はコースに1つのアクティビティのみを追加できます。単一のアクティビティ形式を選択すると、教師が使用するアクティビティを選択するためのドロップダウンメニューが表示されます。シングルアクティビティコース形式のスクリーンキャストと実際の例については、以下の「関連項目」セクションを参照してください。

200px-26singleactivity.png
単一アクティビティ形式の選択

注:このコース形式は、以前のバージョンのMoodleのSCORM形式に置き換わるものです。ここでは、単一のアクティビティとしてSCORMパッケージを選択できるためです。

活動と社会的形式が非常に似ているので、フォーラムを伴う単一の活動形式。唯一の違いは、コースに他のアクティビティが含まれている場合です。単一のアクティビティ形式では、それらは「孤立した」ものとして、教師のみに表示されます。ソーシャルフォーマットはそれらをアクティビティブロックに表示し、学生が利用できるようにします。

2 寄稿されたコース形式

Moodleコミュニティは、プラグインディレクトリで利用可能な多くのコースフォーマットを開発しました。新しいものを作成する場合は、開発者向けドキュメントを参照してください。

MDL-27646およびMDL-28555を参照してください。おそらく、ディスカッションhttp://moodle.org/mod/forum/discuss.php?d=175758#p770737を参照してください。

2.1 ボタンコース形式

ボタンコース形式では、セクションに1つずつアクセスするためのJavaScriptのボタンを含むメニューが作成されます。セクションのグループ(例:モジュール、期間)を作成し、ボタンの色を変更する機能があります。詳細については、ボタンコース形式をご覧ください。

600px-Buttons_course_format_9_buttons_section_2.png
ボタンコースフォーマット9ボタンセクション2.png

2.2 折りたたまれたトピック

これは、基本的に標準のトピックおよびウィークリー形式と同じ形式ですが、「0」を除く各セクションに「トグル」があります。トグルの目的は、ユーザーに提示される初期情報の量を減らして、多くのコンテンツでコースを悩ます可能性のある「死のスクロール」を減らすことです。トグルの「状態」は、コースごと、ユーザーごとに記憶されます。詳細については、Collapsed_Topics_course_formatにアクセスしてください。

2.2.1 概要概要

「折りたたまれたトピック」の概要については、次のビデオを参照してください。

video

2.3 デイリーフォーマット

日次形式は、週ごとではなく日ごとにセクションを表示する週次形式の変更です。

2.4 グリッドの形式

グリッドコース形式は、モジュール式の視覚的なコース形式です。すべてのトピックを非表示にし、短いタイトルのアイコンのグリッド(トピックごとに1つ)を作成します。アイコンをクリックすると、対応するトピックのコンテンツが「ライトボックス」スタイルの表示で表示されます。

2.5 メニュートピック形式

menutopic形式では、メニューにトピック/セクションを表示できます。

2.6 掲示板フォーマット

掲示板形式は、コース上部のニュースフォーラムに最新の投稿を表示します。

2.7 ワントピックフォーマット

onetopic形式では、各トピックがタブに表示され、リソースへの呼び出しの間に現在のタブが保持されます。これにより、ブログまたは用語集としてモジュールから戻ると、開始した場所からタブに戻ります。このフォーマットは、Moodleの標準フォーマットである「トピック」に基づいています。

2.8 タイル形式

タイル形式では、コーストピックがリストではなく「タイル」として表示されます。クリックすると、タイルのコンテンツがアニメーション化されたトランジションとともにタイルの下に表示されます。レイアウトは、さまざまな画面サイズと向きに適応します。各タイル内で、アクティビティを「サブタイル」として表示するように設定することもできます。各タイルのアイコンは、事前定義されたセットから選択できます(つまり、教師はそれらをアップロードする必要はありません)。

2.9 トピック形式(色)

色付きのトピック形式は「トピック」標準形式に基づいており、教師は各コースセクションの前景色と背景色を指定できます。

2.10 柔軟なセクション形式

Moodle 2.4+の柔軟なセクションフォーマットでは、ネストされたセクションを使用でき、各セクションは展開(親セクションページのすべてのコンテンツを含む)または折りたたみ(別のページへのリンクとして)で表示できます。

2.11 キックスタート形式(コースウィザード)

キックスタートコース形式は、教師がコースを作成するのに役立つ特別なコース形式です。このコース形式では、教師が通常見つける空のスケルトン(場合によっては学生)の代わりに、最大3つのコーステンプレートが提供され、コースの作成がはるかに簡単になります。ドキュメントはここにあります

その他の寄稿されたコース形式は、モジュールおよびプラグインデータベースから入手できます。

3 関連項目

スクリーンキャスト:シングルアクティビティコース形式
学校のデモサイトでの単一のアクティビティコース形式の例:Moodle履歴クイズ(ユーザー名:親とパスワード:moodleでログイン)
Moodleコースフォーマットフォーラムの使用

Moodleフォーラムディスカッションの使用:

ソーシャルフォーマット:紹介を置く場所
ソーシャルフォーマットコースのフォーラムのRSSフィード

カテゴリコース Courseの形式

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

文字列の基本について(シングルクオート、ダブルクオート、ヒアドキュメント)

文字列の扱い方

phpにおいて文字列を囲む「シングルクオート("")」「ダブルクオート(””)」「ヒアドキュメント(<<<EOF)」の使い分けを明確に理解できていますでしょうか。
それぞれを以下にまとめていきます。

シングルクオート('')

シングルクオートは最も簡単な文字列の指定方法です。
特徴は指定された範囲内でのエスケープ処理、変数の展開を行わないという点です。

特殊文字等はそのまま表示されます。
echo 'これは \$1,000 です。\n'; 

これは \$1,000 です。\n

変数は展開されません
$sample = 'サンプル';
echo 'これは$sampleです。'; 

これは$sampleです。

ダブルクオーテーション("")

ダブルクオーテーションはシングルクオーテーションとは異なり、
指定された範囲内でのエスケープ処理、変数の展開を行うという特徴を持ちます。

特殊文字等は変換されます。
echo "これは \$1,000 です。\n"; 

これは \$1,000 です。[改行]

変数は展開されます
$sample = 'サンプル';
echo "これは$sampleです。"; 

これはサンプルです。

ヒアドキュメント(<<<EOF)

基本的にはダブルクオーテーションと同じで、指定された範囲内でのエスケープ処理、変数の展開を行うという特徴を持ちます。
さらにスペースや改行などもそのまま出力されるという特徴を持ちます。

$result = <<<EOF
a
i
u
EOF;

echo $result;

a
i
u

おわり

基本的な部分ではありますが意外ときっちり理解できていなかったので面白かったです。

  • このエントリーをはてなブックマークに追加
  • 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で続きを読む

オブジェクト指向ベストプラクティス(個人学習用)

初めに

ズブズブの素人の私ががクラスをを設計できる様に学習を進めていきます。
オブジェクト思考を理解しながら、独立性の高いクラスを構築を目指す。
個人学習目的なのであまり参考にはならない可能性があります。

目指すクラス設計とは

  • 直感的に理解しやすいもの
  • メンテナンスが少ない設計また、変更点があっても他のクラスに影響がない設計
  • 凝集度は高く
  • 結合度は低く

このことから依存関係が少ないクラス設計が望ましいと考えられます。1つのクラスをメンテナンスをすると他のクラスに影響が及ぼしていてはメンテナンスの困ってしまう訳です。
スパゲッティコードを防げ!!→キーワードは「カプセル化」と「多様性」

classの中身を隠蔽化する

例えばお寿司屋さんとお客様がいるとします。
ここではお寿司屋さんの対象がclassとし、お客さんがメインルーティンとします。
お客さんは寿司ネタのオーダーをすると、寿司屋の大将がお寿司を握ってくれる(method)お寿司を提供してくれます。
お客さんはオーダーさえすれば、後は握り終えてお寿司の提供を待つだけです。
この時、お客さんはこの「ネタは握り方をこうしてくれ」、「酢飯の酢の割合はこの程度にしてくれ」とオーダーされても大将は受け付けてくれません。それは大将の独自の経験や技術があるからです。その為、お客さんのオーダーは聞きますが、作成方法などは要求は聞きません。

ここまでで振り返ると
お寿司をオーダーする→publicメソッド
 握り方      →privateメソッド
 酢の割合     →privateメソッド
という風に情報や振る舞いを隠蔽かしているのです。

Untitled Diagram-ページ2 (3).png

ここではアクセスして欲しくないものを隠蔽していますが、これらを一つのclassのまとめたものがカプセル化です。

カプセル化がうまいとコードの見通しがよくなる
実際の例をみてみると

見通しが良い例
$sushiya = new Sushiya();

$sushiya = make_sushi('Tuna');
見通しが悪い例
$sushiya = new Sushiya();

$sushiya = createSumeshi();
$sushiya = cutFish();
$sushiya = holdSushi();

この様に目的に着目しておらず、手段に重点が置かれている。これではコードの肥大化をうむ。また今回は単純な内容ですが、これでは何を目的にしているのかがわかりにくいです。その点良い例では一目で目的がわかります。これが後者が隠蔽化ができていないことが原因となります。これをアクセス修飾子を調整することで理想的なカプセル化ができるはずです。

classの凝縮度

classがclassに与えられた役割にどれだけ集中しているかを示す尺度を凝集度という。
凝集度が高ければclass間の依存関係は低くなります。
class名と関連するmethodのmissmachをなくそうということです。
例えば、MailSendクラスがあり、そこにsemd(),checkMailAddress(),resaizeImage()があったとします。
この場合send()はメール送信、checkMailAddress()はバリデーション機能,resaizeImage()は画像をリサイズする機能となっておりMailSenderクラスに本質的にはcheckMailAddress(),resaizeImage()は関連しているとはいえません。
それならばそれに関連するImageCoverクラスやValidaterクラスを作成し、そこに上記のメソッドを格納して上げる方が、役割分担が明確でクラスでのメソッドやプロパティの関連性も明確になります。

Untitled Diagram-ページ2 (4).png

凝集度が高いとメリットがたくさんる
例えばNetRadioクラスがありそこにプロパティとして現在時刻、再生中のチャンネル、文字色、背景色 メソッドとして再生する、時刻表示するがあったとするとここでクラスに関連し本質は「ラジオ放送を聞くということ」なので上記と一緒でクラスの切り分けがを行うことが良いでしょう。
Untitled Diagram-ページ2 (5).png

切り分けを行い凝縮度を高めたいきます。そうすると今度はMyNetRaidoAppと行ったクラスを作成しカスタマイズができます。

Untitled Diagram-ページ2 (6).png
こうすることで以下のメリットがあります

  • MyNetRaidoAppクラスの機能更新がしやすくなる。また時計機能がを削除しても他の機能やクラスに影響がない
  • MyNetRaidoApp以外のクラスにも機能を移植できる。
  • NetRaido ,Clock, DesignRTmplateクラスは独立しているので1つのクラスに対する変更があっても影響がない。

この様にクラスの凝縮度を高めると保守性が高まりますが、過度にクラスを細分化しすぎるとプログラムファイルを探すにの時間がかかってしまったりするので、ある程度の匙加減は必要です。

classの結合度

クラス間の結合度は低い方が良いです。結合度が高ければ、1つのクラスに何か修正を行った場合関連するクラスに影響を及ぼすからです。

結合度が高くクラス間の依存関係が強い場合
クラスの関連性が循環しているパターン
Untitled Diagram-ページ2.png

結合度が低いクラスの関係
関係性が一方方向である。
Untitled Diagram-ページ2 (1).png

終わりに

上記のことを意識してクラス設計を行っていきます。
また今後もアップデートしていきます。

  • このエントリーをはてなブックマークに追加
  • 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で続きを読む

Moodle 3.9 マニュアル - コース設定

原文

教師またはコース設定の更新ケイパビリティを持つ他のユーザーは、[管理]> [コース管理]> [設定の編集]でコース設定を変更できます。

内容
1 一般
1.1 コースのフルネーム
1.2 略称
1.3 コースカテゴリ
1.4 コース開始日
1.5 コース終了日
1.6 セクション数から終了日を計算する
1.7 コースの可視性
1.8 コースID番号
2 説明
2.1 コースの概要
2.2 コース画像
3 コース形式
3.1 フォーマット
3.2 非表示セクション
3.3 コースレイアウト
4 外観
4.1 テーマの強制
4.2 強制言語
4.3 発表数
4.4 成績表を生徒に表示する
4.5 アクティビティレポートを表示する
5 ファイルとアップロード
5.1 アップロードサイズ
6 完了追跡
7 グループ
7.1 グループモード
7.2 強制
7.3 デフォルトのグループ化
8 役割の名前変更
9 タグ
10 コースのカスタムフィールド
11 サイト管理設定
12 教師がコース設定を編集できないようにする

1 一般

1.1 コースのフルネーム

これがコースの名前です。フロントページダッシュボード、レポートのコースリストにリンクとして表示されます。コースを表示するときに、ブラウザのタイトルバーでも使用されます。

ケイパビリティ moodle/course:changefullname は、ユーザーがコースのフルネームを編集できるかどうかを制御します。

1.2 shortname

多くの教育機関では、BP102やCOMMSなどのコースを簡単に参照できます。コースにそのような名前がまだない場合でも、ここで作成してください。ナビゲーションブロックなど、長い名前が適切でないいくつかの場所で使用されます。

ケイパビリティ moodle/course:changeshortname は、ユーザーがショートネームフィールドを編集できるかどうかを制御します。

デフォルトでは、コースのフルネームのみがコースのリストに表示されます。ただし、管理者は、必要に応じて、[管理]> [サイト管理]> [外観]> [コース]のチェックボックスをオンにすることで、短い名前を表示することもできます。

1.3 コースカテゴリー

サイト管理者は、教師と生徒がコースを簡単に見つけられるようにコースカテゴリを作成している場合があります。コースカテゴリは、ナビゲーションブロックに反映される場合があります。

ケイパビリティ moodle/course:changecategory は、ユーザーがコースカテゴリを編集できるかどうかを制御します。

1.4 コース開始日

この設定は、ログの表示と毎週の形式のトピックの日付に影響します。

「毎週」のコース形式を使用する場合、開始日はコースの最初のセクションに表示されます。たとえば、7月27日を選択すると、最初のセクションに「7月27日-8月2日」と表示されます(そのセクションでデフォルトの表示が選択されている場合)。

この設定は、ログの表示に影響します。これは、ログアクティビティが表示される最も早い日付になります。

この設定は、「ソーシャル」または「トピック」形式を使用するコースには影響しません。

ヒント:教育機関が毎週のスケジュールで運営されている場合は、月曜日などの週の最初の日にコースの開始日を設定することを検討してください。 「週の最初の日」は、言語langconfigファイルによって設定され、英語の言語パックのデフォルトの月曜日とは異なる場合があることに注意してください。
ヒント:通常、コースに実際の開始日がない場合は、日付を昨日に設定し、空き状況の設定を使用してコースを学生に公開します。
ヒント:学生が特定の日時より前にコースに参加できないようにするには、自己登録コースの設定を参照してください。

1.5 コース終了日

コース終了日は、コースをユーザーのコースリストに含めるかどうかを決定するために使用されます。終了日が過ぎると、コースはナビゲーションブロック/ドロワーに表示されなくなり、学生のダッシュボードのコース概要に過去として表示されます。

コース終了日は、データが削除されるまでの保存期間の計算にも使用されます。また、カスタムレポートで使用することもできます。コース期間中のアクティビティの報告。

ユーザーは終了日後もコースに参加できます。言い換えれば、日付はアクセスを制限しません。

1.6 セクション数から終了日を計算する

週単位のコースの場合のみ、コースの終了日は、コースの開始日とセクション数に基づいて自動的に計算される場合があります。コースセクション(週)が追加または削除されると、コースの日付が自動的に変更されます。 「セクション数から終了日を計算する」にチェックマークが付いている場合、コース終了日を手動で設定することはできません。

1.7 コースの可視性

ここで、コースを完全に「非表示」にすることができます。管理者、コース作成者、教師、および非表示のコースを表示するケイパビリティを持つその他のユーザーを除いて、コースのリストには表示されません。学生がコースのURLに直接アクセスしようとしても、入力することはできません。

コースの非表示/表示ケイパビリティは、ユーザーがコースを非表示にできるかどうかを制御します。

1.8 コースID番号

ID番号は英数字フィールドです。いくつかの潜在的な用途があります。通常、学生には表示されません。ただし、このコースをコースカタログIDとして外部システムのIDと照合するために使用したり、証明書モジュールで印刷フィールドとして使用したりすることができます。

ケイパビリティ moodle/course:changeidnumber は、ユーザーがID番号を編集できるかどうかを制御します。

2 説明

coursesummary.png
説明設定はデフォルトで拡張されています

2.1 コースの概要

概要はコースリストページに表示されます。このフィールドは、コースを検索するときに検索され、コース/サイトの説明ブロックにも表示されます。

ケイパビリティ moodle/course:changesummary は、ユーザーがコース概要を編集できるかどうかを制御します。

2.2 コース画像

画像(および管理者によって許可されている場合は他のファイルタイプ)がコースの概要に添付される場合があります。コース名や概要と同じように、コース外から誰でもアクセスできます。 ケイパビリティ moodle/course:changesummary を持つユーザーのみがコースサマリーファイルをアップロード/変更できます。管理者がこれを許可していない場合、コース概要ファイルをアップロードするためのボックスは表示されないことに注意してください。

デフォルトでは、jpg、gif、およびpngファイルタイプのみがコースサマリーファイルとして許可されます。管理者は、[管理]> [サイト管理]> [外観]> [コース]で許可されるファイルの種類を変更できます。

3 コース形式

3.1 フォーマット

コース形式を見る

3.2 非表示セクション

このオプションを使用すると、コースの非表示セクションを学生にどのように表示するかを決定できます。デフォルトでは、非表示のセクションがどこにあるかを示す小さな領域(折りたたまれた形式、通常は灰色)が表示されますが、非表示のアクティビティやテキストは実際には表示されません。これは、週単位の形式で特に役立ちます。これにより、クラス以外の週が明確になります。または、クイズがある場合は、生徒に見せたくありません。

ヒント:必要に応じて、これらの利用できないアイテムを完全に非表示にすることができるため、学生はコース内のセクションやアクティビティが非表示になっていることすらわかりません。

3.3 コースレイアウト

コースレイアウト設定は、コース全体を1ページに表示するか、複数ページに分割して表示するかを決定します。この設定は現在、トピックと毎週のコアコース形式に適用され、折りたたまれたトピックのコース形式にのみ適用されます。

教師は、ドロップダウンから、使い慣れたスクロール形式で「すべてのセクションを1ページに表示する」か、「ページごとに1つのセクションを表示する」かを選択します。

ページごとに1つのセクションが選択されている場合、コースページは個々のセクションへのリンクのリストに省略されます。個々のセクションが表示されている場合は、セクションの上下のリンクから次のセクションと前のセクションにアクセスできます。

450px-one_section_per_page_course_format.png
ページごとに1つのセクションを使用するコースのコースページコースレイアウト

450px-Single_section_page.png
ページごとに1つのセクションを使用するコースの個々のセクションコースレイアウト

学校のデモサイトコーススポーツの種類にアクセスして、「ページごとに1つのセクションを表示」コースのレイアウトを確認してください。

4 外観

(これらの設定はデフォルトで折りたたまれています。)

appearancecourse252.png
外観設定が拡張されました

4.1 テーマの強制

サイト管理者が教師にコーステーマの設定を許可している場合、このプルダウンメニューが表示され、サイトのテーマのリストが表示されます。教師はこれを使用して、Moodleサイトの他の部分とは異なるコースの外観を選択できます。

4.2 言語を強制する

コースで言語を強制すると、学生が自分の個人プロファイルで別の優先言語を選択した場合でも、このコースのMoodleのインターフェースはこの特定の言語になります。

デフォルトでは、編集教師とマネージャーはケイパビリティ moodle/course:forcelanguage を備えており、生徒の言語を選択して強制することができます。管理者は、コースの教師/マネージャーにこれを行わせたくない場合は、この機能を削除できます。

4.3 発表数

アナウンスフォーラムからの最近のアナウンスの数を最新のアナウンスブロックに表示する必要があります。

コースでアナウンスフォーラムが必要ない場合は、この設定をゼロに設定する必要があります。

4.4 成績表を生徒に表示する

ここで、学生が管理ブロックの成績へのリンクを表示できるようにするかどうかを決定できます。コースで段階的なアクティビティを使用しない場合は、これを無効にすることをお勧めします。成績が使用され、このリンクが無効になっている場合でも、生徒は課題などの実際のアクティビティ自体から自分の成績を確認できます。

4.5 アクティビティレポートを表示する

ここで、生徒にアクティビティレポートを表示するかどうかを決定できます。ただし、そうするとサーバーに負荷がかかるため、デフォルトで設定が無効になっています。

5 ファイルとアップロード

(これらの設定はデフォルトで折りたたまれています。)

5.1 最大アップロードサイズ

ここで、学生がコースにアップロードできるファイルの最大サイズを決定できます。サイト管理者は、教師が選択できるサイズを決定できます。

編集教師または他のユーザーは、ケイパビリティ moodle/course:ignorefilesizelimitsを与えることにより、最大サイズよりも大きいファイルをアップロードできる場合があります

6 完了追跡

(これらの設定はデフォルトで折りたたまれています。)

アクティビティを完了するには、完了追跡を有効にする必要があります。コース完了基準は、アクティビティの設定にあるアクティビティ完了値に基づく場合もあります。

7 グループ

(これらの設定はデフォルトで折りたたまれています。)

coursegroups25.png
グループ設定が拡張されました

7.1 グループモード

ここでは、プルダウンメニューを使用してコースレベルでグループモードを定義できます。 グループなし個別のグループ表示可能なグループが選択肢です。選択した設定は、そのコース内で定義されたすべてのアクティビティのデフォルトのグループモードになります。グループ設定は、ユーザーが参加者リストに表示する内容と、アクティビティで対話できるユーザーに影響を与える可能性があります。

7.2 強制

グループモードがコースレベルで「強制」されている場合、この特定のグループモードはそのコースのすべてのアクティビティに適用されます。これにより、特別なグループ設定を持つ可能性のあるすべてのアクティビティが上書きされます。

7.3 デフォルトのグループ化

グループ化(https://docs.moodle.org/39/en/Groupings)が有効になっている場合、コースアクティビティとリソースのデフォルトのグループ化が設定される場合があります。

8 役割の名前変更

(これらの設定はデフォルトで折りたたまれています。)

rolerenaming.png
役割の名前変更設定が拡張されました

コースで使用されている役割の名前を変更できます。たとえば、教師の役割の名前を「ファシリテーター」、「チューター」、または「ガイド」に変更したい場合があります。これらの新しい役割名はコース内に表示されます。たとえば、参加者とオーバーライド許可ページ。

サイト管理者が名前を変更したか、新しい役割を追加した可能性があることに注意してください。これらの名前が表示され、教師は名前を変更できます。

「教師」の単語をサイトのすべてのコースで異なるものにする場合、サイト管理者は、[管理]> [サイト管理]> [ユーザー]> [権限]> [役割の定義]で教師と非編集教師の役割を編集し、カスタムフルを変更できます。そこに名前を付けます。

9 タグ

教師はここに、新しいタグまたは公式タグのいずれかのコースタグを追加できます。詳細については、タグの使用を参照してください。

10 コースのカスタムフィールド

管理者がサイト管理/コース/コースカスタムフィールドで有効にした場合、教師が構成できるフィールドがここで利用可能になる場合があります。

video
コースカスタムフィールド

11 サイト管理設定

管理者は、[管理]> [サイト管理]> [コース]> [コースのデフォルト設定]でコースのデフォルト設定を設定できます。

サイト上のすべてのコースに最大週数/トピックを設定できます。デフォルト値は52です。

コースカスタムフィールドは、サイト管理/コース/コースカスタムフィールドから追加して、教師がコース設定で構成することができます。

12 教師がコース設定を編集できないようにする

次のフィールドのいずれか/すべて(コースのフルネーム、ショートネーム、ID番号とカテゴリ、概要)は、教師が編集できないようにロックされている場合があります。そうするために:

  1. [サイト管理]> [ユーザー]> [権限]> [役割の定義]にアクセスします。
  2. 教師の役割の反対側にある編集アイコンをクリックします。
  3. moodle/course:changefullname、moodle/course:changeshortname、moodle/course:changeidnumber、moodle/course:changecategory、moodle/course:changesummaryのいずれか/すべてのケイパビリティを許可から未設定に変更します。
  4. ページ下部の[変更を保存]ボタンをクリックします。

カテゴリコース

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

[Codewars] Find The Parity Outlier

概要

Codewarsの問題 Find The Parity Outlier の回答の復習とベストプラクティスをまとめる個人メモです。

問題

You are given an array (which will have a length of at least 3, but could be very large) containing integers. The array is either entirely comprised of odd integers or entirely comprised of even integers except for a single integer N. Write a method that takes the array as an argument and returns this "outlier" N.

examples

[2, 4, 0, 100, 4, 11, 2602, 36]
Should return: 11 (the only odd number)

[160, 3, 1719, 19, 11, 13, -21]
Should return: 160 (the only even number)

回答

配列をそれぞれ用意し、奇数と偶数のチェック後リストの大きさが1の方を返す処理です。

function find($integers) {

  $odd_list = [];
  $even_list = [];
  for ($i=0;$i<count($integers);$i++){
    if ($integers[$i] % 2 != 0){
      array_push($odd_list, $integers[$i]);
    }else{
      array_push($even_list, $integers[$i]);
    }
  }
  return count($odd_list) === 1 ? $odd_list[0] : $even_list[0];
}

ベストプラクティス

処理自体はほぼ同じです。
foreachを使用した方が簡潔に書けるので次から意識していきたいです。

function find($a) {
  $odds = [];
  $evens = [];
  foreach ($a as $n) {
    if ($n % 2) array_push($odds, $n);
    else array_push($evens, $n);
  }
  return count($evens) === 1 ? $evens[0] : $odds[0];
}
function find($i) {
   foreach ($i as $x)
      $x % 2 == 0 ? $even[] = $x : $odd[] = $x;
   return $odd < $even ? $odd[0] : $even[0];
}

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