20210302のPHPに関する記事は10件です。

【MAMP環境】新アプリ作成コマンド・初期設定

◆ MAMP「新アプリ作成・初期設定」記事作成の目的

完全に私のメモです。忘れないように記述しました!
MAMP環境でアプリを作ったことがあるけど、毎回コマンド忘れるんだよね〜
という方には便利に使っていただけるかも?

◆ アプリケーションを作成する

1)composerインストール済みの場合

$ cd /Applications/MAMP/htdocs/
$ composer create-project laravel/laravel --prefer-dist test1 
  **アプリできる**
$ cd test1
$ chmod -R 777 storage
$ chmod -R 777 bootstrap/cache
※ -R  ディレクトリ内の複数のファイルも変更対象とする

2) composerのインストール未の場合

$ cd /Applications/MAMP/htdocs/ 
$ curl -sS https://getcomposer.org/installer​ | php 
$ sudo mv composer.phar /usr/local/bin/composer 
$ composer -V 
$ composer create-project laravel/laravel --prefer-dist blog 
$ cd blog/ 
$ chmod -R 777 storage 
$ chmod -R 777 bootstrap/cache

◆ 初期設定(アプリ作成後)

1)config/app.php

'timezone' => ‘UTC’           'timezone' => ‘Asia/Tokyo’
'locale' => ‘en’                   ‘locale' => ‘ja’
'faker_locale' => 'en_US’,    'faker_locale' => ‘ja_JP’

2).env

APP_NAME=Laravel          APP_NAME=Name
※MAMPの設定(公式サイトにあり)
DB_PORT=3306               DB_PORT=8889
DB_DATABASE=laravel     DB_DATABASE=test1
DB_PASSWORD=             DB_PASSWORD=root
※追記
DB_SOCKET=/Applications/MAMP/tmp/mysql/mysql.sock

3)app/Providers/

※mysqlの文字コードが対応していない時に必要
※2箇所追記
use Illuminate\Support\Facades\Schema;
public function boot()
{
Schema::defaultStringLength(191);
}

以上!

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

PHPの多次元配列を特定の値でソート(array_multisort関数)

単一の配列のソートはPHPマニュアルの配列のソートを参考にすればできますが、多次元配列の場合のソートは複雑…
というわけで、いつでも確認できるように記録しておきます。

多次元配列の例

次のような2次元配列のデータがあるとします。

$records = [
  [
    'ID' => "A00004",
    'PAYMENTNO' => "11111",
    'PRICE' => "7500",
    'ITEMNAME' => "スニーカー"
  ],
  [
    'ID' => "A00002",
    'PAYMENTNO' => "22222",
    'PRICE' => "4500",
    'ITEMNAME' => "カットソー"
  ],
  [
    'ID' => "A00001",
    'PAYMENTNO' => "05555",
    'PRICE' => "5000",
    'ITEMNAME' => "ニット"
  ],
  [
    'ID' => "A00003",
    'PAYMENTNO' => "03333",
    'PRICE' => "7000",
    'ITEMNAME' => "ジーンズ"
  ],
  [
    'ID' => "A00003",
    'PAYMENTNO' => "03331",
    'PRICE' => "9000",
    'ITEMNAME' => "ジャケット"
  ]
];

この配列に対して、「ID」の昇順で並べ、「ID」の値が同じ場合は「PAYMENTNO」の昇順で並べたいとします。
この場合、array_multisort関数array_column関数を組み合わせて使えばスッキリ実装できます。

ソート実行

// array_multisort(ソートの軸となる配列(1), ソートの軸となる配列(2), ソートしたい配列)
array_multisort(array_column($records, 'ID'), array_column($records, 'PAYMENTNO'), $records);

ここではソートの軸を2つにしていますが、もちろん1つでも良いですし、引数を追加すれば3つでも4つでも増やせます。

省略せずに書くなら
array_multisort(ソートの軸となる配列(1), ソート順(1), ソート方法(1), ソートの軸となる配列(2), ソート順(2), ソート方法(2), ソートしたい配列)
となりますが、以下2つはデフォルトの設定を使うのであれば省略可能です。

  • ソート順:デフォルトがSORT_ASC
  • ソート方法:デフォルトがSORT_REGULAR

※設定の詳細は公式リファレンス参照

この「ソートの軸となる配列」は、$recordsをforeachで回してIDを取り出して、配列を作成して…としても良いのですが、array_columnを使えば配列から特定のキーに対する要素を1発で取り出せるので、この方が楽でコードもスッキリします。
array_column関数

ソート後

上でソートした配列$recordsをvar_dumpしてみると、以下のようになります。

array(5) {
  [0]=>
  array(4) {
    ["ID"]=>
    string(6) "A00001"
    ["PAYMENTNO"]=>
    string(5) "05555"
    ["PRICE"]=>
    string(4) "5000"
    ["ITEMNAME"]=>
    string(9) "ニット"
  }
  [1]=>
  array(4) {
    ["ID"]=>
    string(6) "A00002"
    ["PAYMENTNO"]=>
    string(5) "22222"
    ["PRICE"]=>
    string(4) "4500"
    ["ITEMNAME"]=>
    string(15) "カットソー"
  }
  [2]=>
  array(4) {
    ["ID"]=>
    string(6) "A00003"
    ["PAYMENTNO"]=>
    string(5) "03331"
    ["PRICE"]=>
    string(4) "9000"
    ["ITEMNAME"]=>
    string(15) "ジャケット"
  }
  [3]=>
  array(4) {
    ["ID"]=>
    string(6) "A00003"
    ["PAYMENTNO"]=>
    string(5) "03333"
    ["PRICE"]=>
    string(4) "7000"
    ["ITEMNAME"]=>
    string(12) "ジーンズ"
  }
  [4]=>
  array(4) {
    ["ID"]=>
    string(6) "A00004"
    ["PAYMENTNO"]=>
    string(5) "11111"
    ["PRICE"]=>
    string(4) "7500"
    ["ITEMNAME"]=>
    string(15) "スニーカー"
  }
}

ちゃんと「ID」の昇順かつ、「ID」が同じ場合は「PAYMENTNO」の昇順になっていますね!

参考にした記事

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

line-bot-sdkをインストールしたらsocketsがないって言われた

環境・前提

筆者は素人です。
この記事は記録が目的です。
ただ、流れくらいは参考になって欲しい。

windows10homeにdocker-composeでLEMP環境を作り、Laravelのプロジェクトを作成。
LaravelでLINE Botを作りたいのでline-bot-sdkを入れたかった。

やったこと

dockerのコンテナで
$ composer require linecorp/line-bot-sdk 4.2.*
を実行。

エラーの内容

Problem 1
    - Installation request for linecorp/line-bot-sdk 4.2.* -> satisfiable by linecorp/line-bot-sdk[4.2.0].
    - linecorp/line-bot-sdk 4.2.0 requires ext-sockets * -> the requested PHP extension sockets is missing from your system.

要はext-socketsというやつがないからエラーになるっぽい。

解決策を調べた

ググったら2つのやり方が出てきた。

1つはphp.iniファイルにある
;extension=sockets
のコメントアウトを外す、というもの。
でも自分のphp.iniにはそもそも;extension=sockets自体がなかった。

2つ目はDockerfileの

RUN
  docker-php-ext-install

socketsを加えるというもの。

で、php -mコマンドを実行してインストールされているモジュールを確認したところ、
socketのモジュールがなかったので2つ目のやり方でいけると予想。

解決方法

2つ目のやり方でDockerfileに変更を加え、コンテナから出て
$docker-compose down
$docker-compose up -d --build
を実行。
これでDockerfileの再読み込みを行う。

結果

無事いけました!

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

PHPで簡単なAPIを作った話

今回作るもの

こんな感じのGETパレメーターを指定すれば足し算して返してくれるコードです

https://example.com/api.php?front=1&back=2

レスポンス

{"status":"true","result":3,"formula":"1+2"}

API化するメリットはマジでなさそうですが、簡単に作れるので今回はこれで解説していきます

コード

一旦すべてのコードを置いておきます(コピペならここをお使いください)
一応 githubにも同じものをアップしています

api.php
<?php
$front=$_GET['front'];
$back=$_GET['back'];
$result=$front+$back;
$response['result']=$result;
$response['status']="true";
$response['formula']=$front.'+'.$back;
print json_encode($response,  JSON_UNESCAPED_UNICODE);

とっても簡単なコードです。
GETパラメーターから数字を抜き出し

足し算して

表示する
ただこれだけ。APIと聞くと難しそうですが基礎的な仕組み自体は簡単です

解説

一つずつ解説していきます(といっても7行しかありませんが)

$front=$_GET['front'];
$back=$_GET['back'];

GETパラメーターから指定した値を取得するコードです。今回はfrontというキーの値を$frontという変数に、backというキーの値を$backという変数に代入しています。

$result=$front+$back;

ただの足し算です。$front$backに入っている数字を足して$resultという変数に代入します。

$response['result']=$result;

このコードは$resultに入っている計算結果を$responseという配列に代入します。
今回はキーにresultを指定しました。

$response['status']="true";

APIを使う側がレスポンスが返ってきたか確認しやすいようにするために記述しておいた方がいいでしょう。
無くても当然動きますが。

$response['formula']=$front.'+'.$back;

計算式を作り、配列である$responseのformulaキーに代入します。
javascriptなどの言語とは違って文字列をつなげるときは.を使わないといけないのは注意ポイントですね。
たまーにやらかしてエラーが出ちゃいます(笑)

print json_encode($response,  JSON_UNESCAPED_UNICODE);

最後に出力します。echoを使ってもprintを使ってもどちらでもいいと思います。
'''json_encode'''でjson化します。その際日本語が含まれていると勝手にエスケープしてしまうのでJSON_UNESCAPED_UNICODEを指定しましょう。
今回はなくても問題ないですが、、、

最小構成

「別に私は足し算するAPIなんか作りたいわけじゃないよ!!」
という人のためにできるだけ最小構成にしたものを最後に載せておきます。
APIを作る際にコピペかなんかで使ってください

api.php
<?php
$response['status']="true";
print json_encode($response);

では楽しいphpライフ(?)を~

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

jQueryで上からslideToggleでメッセージを出す

はじめに

slideToggleを使ってニュルっとメッセージを出す方法の記録です。

jQueryのコード

以下のコードでメッセージの出す、戻すを行う。
表示するメッセージを受け取った場合は空白を全て取り除く。
そしてslideToggleで表示し、5秒後にslideToggleで戻す。

let $slideMsg = $('#js-slide-msg');
      let msg = $slideMsg.text();
      if(msg.replace(/\s+/g, "")){
        $jsShowMsg.slideToggle().show();
        setTimeout(function(){
          $jsShowMsg.slideToggle().show();
        }, 5000);
      }

表示について

$msgに表示するメッセージが入っていれば表示させる。

<div id="js-slide-msg">
    <?php if(!empty($msg) echo $msg; ?>
</div>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHP5.4からS3への画像アップロード

概要

  • PHP環境からS3への画像アップロードできるようにするため、 aws-sdk-phpを使って試してみた。
  • aws-sdk-phpバージョン3がPHP5.5以降のみ対応のため、SDKのバージョンを2に変更して再度実施。
  • S3バケットの作成とIAMからユーザー作成を実施。
  • vagrantの開発環境からS3へのアップするサンプル内容を記載。

環境

Mac Big Sur 11.2.1
vagrant 2.2.7
cake php 1系
php 5.4.45

1回目のaws-sdk-phpインストール(失敗)

vagrant sshでアクセスして実行。失敗する。

$ composer require aws/aws-sdk-php
zsh: correct composer to _composer [nyae]?
zsh: command not found: composer

zshの問題っぽいので、こちらのサイトを参考にzshrcファイルに情報をセットする。

$ vim ~/.zshrc

.zshrcファイル内に以下の文字を設定して保存。

export PATH="$HOME/.composer/vendor/bin:$PATH"

設定の反映をする。

$ source ~/.zshrc

2回目のaws-sdk-phpインストール(失敗)

$ composer require aws/aws-sdk-php

今度は以下のエラー
Could not open input file: /usr/local/bin/composer.phar

composerが入っていないと思わるので、インストールする。

Vagrant環境内でcomposerをインストール

[vagrant@dev bin]$ pwd
/usr/local/bin

# インストールコマンド
curl -sS https://getcomposer.org/installer | php
curl: (35) error:1407742E:SSL routines:SSL23_GET_SERVER_HELLO:tlsv1 alert protocol version

→TLS1.2問題っぽい

phpコマンドを使ってインストール

$ php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
$ ls -la
:
-rw-r--r--   1 xxxxx  1451310402     58460 Mar  1 15:33 composer-setup.php
:
:

$ php composer-setup.php
All settings correct for using Composer
Downloading...

Composer (version 2.0.11) successfully installed to: /Users/.../composer.phar
Use it: php composer.phar

$ php -r "unlink('composer-setup.php');" 
$ ls -la
:
:
-rwxr-xr-x   1 xxxx  1451310402   2210024 Mar  1 15:34 composer.phar
:

移動

$ mv composer.phar /usr/local/bin/composer  
$ composer 
   ______
  / ____/___  ____ ___  ____  ____  ________  _____
 / /   / __ \/ __ `__ \/ __ \/ __ \/ ___/ _ \/ ___/
/ /___/ /_/ / / / / / / /_/ / /_/ (__  )  __/ /
\____/\____/_/ /_/ /_/ .___/\____/____/\___/_/
                    /_/
Composer version 2.0.11 2021-02-24 14:57:23

Usage:
:
:
:

参考情報

3回目のaws-sdk-phpインストール(成功)

$ composer require aws/aws-sdk-php
Using version ^3.173 for aws/aws-sdk-php
./composer.json has been created
Running composer update aws/aws-sdk-php
Loading composer repositories with package information
Updating dependencies
Lock file operations: 9 installs, 0 updates, 0 removals
  - Locking aws/aws-sdk-php (3.173.18)
  - Locking guzzlehttp/guzzle (7.2.0)
  - Locking guzzlehttp/promises (1.4.0)
  - Locking guzzlehttp/psr7 (1.7.0)
  - Locking mtdowling/jmespath.php (2.6.0)
  - Locking psr/http-client (1.0.1)
  - Locking psr/http-message (1.0.1)
  - Locking ralouphie/getallheaders (3.0.3)
  - Locking symfony/polyfill-mbstring (v1.22.1)
Writing lock file
Installing dependencies from lock file (including require-dev)
Package operations: 9 installs, 0 updates, 0 removals
  - Downloading symfony/polyfill-mbstring (v1.22.1)
  - Downloading mtdowling/jmespath.php (2.6.0)
  - Downloading ralouphie/getallheaders (3.0.3)
  - Downloading psr/http-message (1.0.1)
  - Downloading guzzlehttp/psr7 (1.7.0)
  - Downloading guzzlehttp/promises (1.4.0)
  - Downloading psr/http-client (1.0.1)
  - Downloading guzzlehttp/guzzle (7.2.0)
  - Downloading aws/aws-sdk-php (3.173.18)
  - Installing symfony/polyfill-mbstring (v1.22.1): Extracting archive
  - Installing mtdowling/jmespath.php (2.6.0): Extracting archive
  - Installing ralouphie/getallheaders (3.0.3): Extracting archive
  - Installing psr/http-message (1.0.1): Extracting archive
  - Installing guzzlehttp/psr7 (1.7.0): Extracting archive
  - Installing guzzlehttp/promises (1.4.0): Extracting archive
  - Installing psr/http-client (1.0.1): Extracting archive
  - Installing guzzlehttp/guzzle (7.2.0): Extracting archive
  - Installing aws/aws-sdk-php (3.173.18): Extracting archive
5 package suggestions were added by new dependencies, use `composer suggest` to see details.
Generating autoload files
2 packages you are using are looking for funding.
Use the `composer fund` command to find out more!

ここでaws-sdk-phpバージョン3の対応PHPバージョンが5.5以降だと気づく。

SDKのバージョンを2にしてPHPバージョンを指定してみる

まずは、PHPバージョンを指定する。

$ composer config platform.php 5.4.45

composer.jsonの中身を変更する。aws-sdk-phpを2にする。

{
    "require": {
        "aws/aws-sdk-php": "^2.8"
    },
    "config": {
        "platform": {
            "php": "5.4.45"
        }
    }
}

再度composerセットアップする。

$ php composer-setup.php

これで、PHPを側の環境は整ったはず。

ディレクトリ構成

project_root
  |_app
  |_cake
  :
  :
  |_vendor
  |_composer.json
  |_composer.lock

AWS S3作成とS3利用するIAMユーザー作成

S3バケット作成

  • ログイン画面→サービス→S3と検索
  • S3のダッシュボードが表示されるので、「バケットを作成」を選択
    • バケット名を設定
    • リージョンはデフォルト(ここではap-northeast-1)
    • パブリックアクセスをすべて ブロックのチェックを外す
      • これをすると全公開となるので注意が必要
  • 内容を確認して 「バケットを作成」ボタンを押す

S3アクセス用のIAMを作成

  • ログイン画面→サービス→IAMと検索
  • ユーザーを追加を選択
    スクリーンショット 2021-03-02 12.47.45.png

  • ユーザー情報を追加して、「次のステップ:アクセス権限」を選択

    • ユーザー名:任意
    • AWSアクセスの種類を選択:プログラムによるアクセスをチェック スクリーンショット 2021-03-02 12.48.30.png
  • アクセス許可の設定で、既存のポリシーを直接アタッチを選択

  • ポリシー名:AmazonS3FullAccessにチェックをして「次のステップ:タグ」を選択
    スクリーンショット 2021-03-02 12.50.49.png

  • タグの追加(ここは一旦何も設定しないで「次のステップ:確認」を選択)

  • 内容確認後、ユーザー作成を選択
    スクリーンショット 2021-03-02 12.55.55.png

  • ユーザーの追加完了後、アクセスキーとシークレットアクセスキーが発行

    • CSVファイルのダウンロードはしておいたほうがいい(後で確認ができなくなるため) スクリーンショット 2021-03-02 15.19.27.png

AWS S3への画像アップロード

PHPで以下の記述をして実行した。
(下記ソースは開発中のサービスにあるConstroller内で記載して実行)

<?php
require 'vendor/autoload.php';

use Aws\S3\S3Client;
use Aws\Common\Enum\Region;
use Aws\S3\Exception\S3Exception;

class xxxx {


    public function 'xxxxメソッド名'() 
        $bucket = 'awsで設定したバケット名';
        $aws_account = array(
            'key'    => 'ユーザー作成時のアクセスキー',
            'secret' => 'ユーザー作成時のシークレットアクセスキー');
        $this->s3client = S3Client::factory($aws_account);
        $this->s3client->registerStreamWrapper();

        $sourcefile = 'アップ元のファイルフルパス';
        $filename = 'xxxx.jpg';

        try {
            $this->s3client->putObject(array(
                'Bucket' => $bucket,
                'Key' => 'アップ先のディレクトリ名'.$filename,
                'ACL' => 'public-read',
                'SourceFile'   => $sourcefile,
            ));
        } catch (S3Exception $exc) {
            echo "Upload NG";
            echo $exc->getMessage();
            var_dump($exc);
            return false;
        }
        var_dump("DONE");
        return true;
    }
}
?>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Composer】オートロードの仕組みを追う

初めに

Laravelを使っている方ならこんな疑問を抱いたことがあるんじゃないでしょうか。

なんで\App\Userとか\Illuminate\Hogehogeって書くだけでApp/User.phpのUserクラスとかvendor/laravel/framework/src/Illuminate/Hogehoge.phpのHogeHogeクラスが使えるようになってんの?

通常、外部のクラスを使うときにはrequireなどを使って該当のファイルをインポートしなければならないはずです。

今回はそんな疑問と密接に関わるComposerのオートロードについて調べていき、提示した疑問を一緒に解決していきましょう。

実行環境

  • PHP 7.3.4
  • Composer 2.0.11
  • Laravel 6.20.16

そもそもオートロードとは

オートロードの仕組みについて追っていく前に、そもそもオートロードって何をしているのでしょうか。

結論を言いますと、単純にrequireincludeを使ってソースコードを1つ1つインポートしているだけです。

ただし、

require "/path/to/vendor/laravel/src/framework/Illuminate/Foo/Hogehoge1.php";
require "/path/to/vendor/laravel/src/framework/Illuminate/Foo/Hogehoge2.php";
// ...
require "/path/to/vendor/laravel/src/framework/Illuminate/Foo/Hogehoge10.php";

のように、べた書きで1つ1つソースコードをインポートしているわけではありません。

ちゃんとPHPの仕組みを利用して自動でソースコードを読み込む機能を構築しています。

PHPでは通常、外部のファイルに記述されているクラスなどを利用するときは、

require "path/to/Foo.php"
require "path/to/Bar.php"

というように、読み込みたいファイルを絶対パスもしくは自ファイルからの相対パスでrequireもしくはincludeしなければなりません。

もしもこれを行わないまま外部ファイルに書かれているクラスを呼びだそうとしても、

Uncaught Error: Call to undefined class ...

というようにエラーが出力され、クラスを呼び出すことができません。

このようにオートロードをせずとも、自身でrequireすることで外部ファイルを読み込むことはできます。

しかし、この方法では扱うファイル数が増えてくるにつれて以下のようなデメリットが生じてきます。

  • requireするためには呼び出したいクラスの定義されているソースコードへのパスをあらかじめ知っていなければならない
  • 必要なクラスが5個や6個となってくるとさすがに記述するのが煩わしいし、typoも発生しやすくなる。
  • パッケージの更新などによりディレクトリ構造が変更された日には、改めてrequireするソースコードのパスを正しいものに書き直さなければならない

想像しただけで萎えてきます。

人の手でソースコードの所在を管理することが如何に面倒で非効率的であるか分かるでしょうか。

こういった面倒なインポート作業を、Composerのオートロード機能が代わりに担当しています。

そのおかげで、私たちはクラスの所在などを一切気にせずビジネスロジックの開発に注力することができるのですね。

オートロード処理の起点:vendor/autoload.php

オートロードがなにをしているかが分かったところで、ではどのようにオートロードが行われているをソースコードを追いながら見てみましょう。

ここではLaravelを使っているものだと仮定します。

まずはLaravelアプリのエントリポイントとなるpublic/index.phpを見てみます。

public/index.php
<?php

define('LARAVEL_START', microtime(true));

require __DIR__.'/../vendor/autoload.php';

$app = require_once __DIR__.'/../bootstrap/app.php';

$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

$response = $kernel->handle(
    $request = Illuminate\Http\Request::capture()
);

$response->send();

$kernel->terminate($request, $response);

コメントなどを省くと、Laravelによる一連の処理はここに書かれていることがすべてです。Laravelの豊富な機能がこのかなり短いコードを起点に提供されているんですね。

この中の2番目の処理を見てください。

public/index.php
require __DIR__.'/../vendor/autoload.php';

名前から見ていかにもオートロードしてそうなファイルをrequireしてますね。このファイルを見てみましょう。

vendor/autoload.php
<?php

require_once __DIR__ . '/composer/autoload_real.php';

return ComposerAutoloaderInitcde23787628405c61112bd11321f024e::getLoader();

ComposerAutoloaderInitcde23787628405c61112bd11321f024eの部分は人によって異なっているのだと思います。

public/index.phpに続きかなり短いコード量です。

ここではComposerAutoloaderInitcde23787628405c61112bd11321f024eという謎のクラスで定義されているgetLoader()とやらクラスメソッドが実行されているだけです。

vendor/autoload.phpの行っている処理がこれだけなので、おそらく::getLoader()メソッドの中にどうやってオートロード機能を提供しているかの答えがありそうです。

このクラスの実装は一つ手前の処理で読み込まれている、vendor/composer/autoload_real.phpにありそうです。

では、autoload_real.phpを見てみましょう。

オートロード機能の中核:vendor/composer/autoload_real.php

vendor/autoload.phprequire_onceされていたvendor/composer/autoload_real.phpを見てみると、
確かに先ほどの謎のクラスComposerAutoloaderInitcde23787628405c61112bd11321f024eが実装されていました。

vendor/composer/autoload_real.php
// ①
class ComposerAutoloaderInitcde23787628405c61112bd11321f024e
{
    private static $loader;

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

    public static function getLoader()
    {
        if (null !== self::$loader) {
            return self::$loader;
        }

        require __DIR__ . '/platform_check.php';

        spl_autoload_register(array('ComposerAutoloaderInitcde23787628405c61112bd11321f024e', 'loadClassLoader'), true, true);
        self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__)));
        spl_autoload_unregister(array('ComposerAutoloaderInitcde23787628405c61112bd11321f024e', 'loadClassLoader'));

        $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
        if ($useStaticLoader) {
            require __DIR__ . '/autoload_static.php';

            call_user_func(\Composer\Autoload\ComposerStaticInitcde23787628405c61112bd11321f024e::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\ComposerStaticInitcde23787628405c61112bd11321f024e::$files;
        } else {
            $includeFiles = require __DIR__ . '/autoload_files.php';
        }
        foreach ($includeFiles as $fileIdentifier => $file) {
            composerRequirecde23787628405c61112bd11321f024e($fileIdentifier, $file);
        }

        return $loader;
    }
}

// ②
function composerRequirecde23787628405c61112bd11321f024e($fileIdentifier, $file)
{
    if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
        require $file;

        $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
    }
}

このファイルには①で示したComposerAutoloaderInitcde23787628405c61112bd11321f024eクラスと、②で示したcomposerRequirecde23787628405c61112bd11321f024e()関数が定義されているようです。

クラスのほうにはさらに、静的プロパティである$loaderと静的メソッドであるloadClassLoader()getLoader()が実装されています。

vendor/autoload.phpで呼び出されているのは::getLoader()の方なのでそちらに注目してみましょう。

オートロード機能の実態を生成:getLoader()

以下、getLoader()部分のみの抜粋です。

vendor/composer/autoload_real.php
public static function getLoader()
{
    if (null !== self::$loader) {  // ①
        return self::$loader;
    }

    require __DIR__ . '/platform_check.php';  // ②

    // ③
    spl_autoload_register(array('ComposerAutoloaderInitcde23787628405c61112bd11321f024e', 'loadClassLoader'), true, true);
    self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__)));
    spl_autoload_unregister(array('ComposerAutoloaderInitcde23787628405c61112bd11321f024e', 'loadClassLoader'));

    // ④
    $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
    if ($useStaticLoader) {
        // ⑤
        require __DIR__ . '/autoload_static.php';

        call_user_func(\Composer\Autoload\ComposerStaticInitcde23787628405c61112bd11321f024e::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\ComposerStaticInitcde23787628405c61112bd11321f024e::$files;
    } else {
        $includeFiles = require __DIR__ . '/autoload_files.php';
    }
    foreach ($includeFiles as $fileIdentifier => $file) {
        composerRequirecde23787628405c61112bd11321f024e($fileIdentifier, $file);
    }

    return $loader;
}

ここから一つ一つ処理を追っているのでとても長くなります。耐えつつ読み進めていただければと思います。

①の処理:$loaderの有無を確認

①を見てください。

vendor/composer/autoload_real.php
if (null !== self::$loader) {  // ①
    return self::$loader;
}

クラスプロパティ、self::$loadernullでなければ$loaderを返すという分岐を行っています。

vendor/autoload.phpgetLoader()が実行されたときにはまだ$loaderには何も代入されていないので、この条件分岐ではなにもしません。

②の処理:動作環境のチェック

②を見てみましょう。

vendor/composer/autoload_real.php
require __DIR__ . '/platform_check.php';  // ②

名前から鑑みに、実行環境のチェックを行いそうなファイルがrequireされていますね。

このファイルの処理は本筋からそれるので、コードの記載とコメントによる簡単な説明にとどめておきます。

vendor/composer/platform_check.php.php
<?php

$issues = array(); // 問題があった時にその情報を保存する配列を保存する配列

if (!(PHP_VERSION_ID >= 70205)) { // PHPのバージョンが7.2.5より下だった場合
    $issues[] = 'Your Composer dependencies require a PHP version ">= 7.2.5". You are running ' . PHP_VERSION . '.';
}

// 上記の問題が保存されていた場合
if ($issues) {
    if (!headers_sent()) { // まだヘッダが送信されていない場合
        header('HTTP/1.1 500 Internal Server Error');  // ステータスコード500のヘッダを作成
    }
    if (!ini_get('display_errors')) { // php.iniにdisplay_errorsの項目がないか、またはあってもOffであるか
        if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { // 使用しているPHPのインターフェースがcliもしくはphpdbgか
            fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);  //STDERRに問題の情報を出力する
        } elseif (!headers_sent()) { // まだヘッダが送信されていない場合
            echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
            fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);  // 自身の使っているPHPバージョンの情報を省いて出力する
        }
    }

    // エラーを引き起こして、エラーメッセージを通知する。
    trigger_error(
        'Composer detected issues in your platform: ' . implode(' ', $issues),
        E_USER_ERROR
    );
}

③:spl_autoload_register()で未定義クラス呼び出し時の処理を登録

③の処理は、Composerがオートロード機能を構築する上で重要な関数を使っている部分なので、重点的に解説していきます。

まずはコードの抜粋です

vendor/composer/autoload_real.php
spl_autoload_register(array('ComposerAutoloaderInitcde23787628405c61112bd11321f024e', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__)));
spl_autoload_unregister(array('ComposerAutoloaderInitcde23787628405c61112bd11321f024e', 'loadClassLoader'));

1行目

vendor/composer/autoload_real.php
spl_autoload_register(array('ComposerAutoloaderInitcde23787628405c61112bd11321f024e', 'loadClassLoader'), true, true);

このspl_autoload_register()という関数は何をしているのでしょうか。

以前私が投稿した記事でも解説したのですが、この関数は

未定義のクラスやインターフェースが読み込まれた際に、実行してほしい処理を登録しておくことができる関数

と解釈すればおおむね問題ないと思います。

通常、

new HogeHoge();

のように呼び出さしたクラスが自ファイルやrequireしたファイルに定義されていないものだった場合、

PHP Error:  Class 'Hogehoge' not found in ...

とエラーが出力されてその場で処理が終了するのが普通です。

ここで、

spl_autoload_register(function($class) {
    echo "Want to load $class.\n";
    require __DIR__ . "/${class}.php";
})

new HogeHoge();

のような例で、spl_autoload_register()の第1引数に、未定義クラスが呼び出された時の処理を登録してみましょう。

このソースコードを実行すると、new HogeHoge()の部分でエラーが起きる前に、

Want to load <呼び出したクラス名>.\n

と出力されます。即座にエラーが出力されなかったので、確かに未定義クラス呼び出し時の処理が登録できているようです。

そして、echoの内容が出力された後、同一ディレクトリ内の$class.php$classには未定義クラスのクラス名が自動で代入されます)があればそれをrequireしようと試みます。

この場合ですと、同一ディレクトリ内にHogeHoge.phpがあればrequireします。

もしHogeHoge.phpHogeHogeクラスが存在すればそれを呼び出して、new HogeHoge()でインスタンスを生成します。

試しに同一ディレクトリ上にHogeHoge.phpという名前のファイルを作成し、その中でHogeHogeクラスを定義してください。

その後でnew HogeHoge()の処理があるソースコードを実行してみてください。

先ほどまではnew HogeHoge()でエラーが出力されたのに対して、spl_autoload_register()を追加した後ではエラーも何も起こらなくなったはずです。

これはつまり、HogeHoge()が正しくインスタンス化されたことを意味します。

sql_autoload_register()の動きが分かったところで改めてソースコードの1行目を見てみましょう。

vendor/composer/autoload_real.php
spl_autoload_register(array('ComposerAutoloaderInitcde23787628405c61112bd11321f024e', 'loadClassLoader'), true, true);

spl_autoload_register()の第1引数には、登録するオートロード関数を配列として指定することもできます。

今回の場合ですと、

ComposerAutoloaderInitcde23787628405c61112bd11321f024eクラス(つまり自身のクラスですね)のloadClassLoaderメソッドをオートロード関数として登録するという処理になります。

このloadClassLoader()メソッドは自クラスに実装されていた2つのクラスメソッドのうちのもう1つですね。(1つはgetLoader()

第2、第3引数の意味はそれぞれ

第2引数
第1引数のオートロード関数が登録できなかった場合に例外をthrowするか
第3引数
すでに別のオートロード関数が登録されている場合、それらより優先して第1引数のオートロード関数を実行させるか

となっています。

以上のことから、1行目の処理を説明しますと、

ComposerAutoloaderInitcde23787628405c61112bd11321f024e::loadClassLoaderをオートロード関数として最優先に登録する。もし登録できなかったら例外をthrowする。

といった処理になります。

ではこのloadClassLoader()メソッドについて見る、その前に次の行の説明をしておいたほうがloadClassLoader()メソッドについても理解しやすいと思います。

2行目

vendor/composer/autoload_real.php
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__)));

self::$loaderとローカルスコープの$loader\Composer\Autoload\ClassLoaderクラスのインスタンスを代入しています。

ですが\Composer\Autoload\ClassLoaderはこの時点では未定義です。

なので、通常ではここでエラーが出力され処理が止まるのですが、先ほどspl_autoload_register()でオートロード関数としてloadClassLoader()を登録しておきましたね。

なので、エラーが出力される前にそのメソッドが実行されます。

では改めて、loadClassLoader()メソッドを見てみましょう。

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

中身を見てわかるように、やはり\Composer\Autoload\ClassLoaderの実装ファイルであろうvendor/composer/ClassLoader.phprequireしてましたね。

詳しく処理を見てみると、呼び出された未定義クラスが\Composer\Autoload\ClassLoaderであれば、同一ディレクトリ上のClassLoader.phprequireする。という処理になっています。

vendor/composer/ClassLoader.phpについて、現在必要な部分のみを見てみましょう。

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

class ClassLoader
{
    private $vendorDir;

    // ~~中略~~

    public function __construct($vendorDir = null)
    {
        $this->vendorDir = $vendorDir;
    }

    // ~~後略~~
}

やはり、\Composer\Autoload\ClassLoaderクラスが実装されていましたね。

__construct()でパラメータに受け取ったパスをprivate $vendorDirに代入しているようです。

\Composer\Autoload\ClassLoaderクラスについては今はこの程度の理解で十分です。

改めて2行目の処理を見てみましょう。

(以下、該当部分を再掲)

vendor/composer/autoload_real.php
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__)));

\dirname(\dirname(__FILE__))は内側から

__FILE__ /path/to/vendor/composer/autoload_real.php
\dirname(__FILE__) /path/to/vendor/composer
\dirname(\dirname(__FILE__)) /path/to/vendor

と、解決されます。

ですので、new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__)));によって、
$vendorDirプロパティに'/path/to/vendor'が代入されたClassLoaderインスタンスをself::$loader$loaderに代入する。

というのが2行目で行っている処理になります。

ここで代入された\Composer\Autoload\ClassLoaderインスタンスが、Composerのオートロードを実現する上での核となるインスタンスになります。

詳細については後ほど(⑥あたりで)説明します。

3行目

vendor/composer/autoload_real.php
spl_autoload_unregister(array('ComposerAutoloaderInitcde23787628405c61112bd11321f024e', 'loadClassLoader'));

これは関数名からなんとなく察せるんじゃないでしょうか。

ここでは、spl_autoload_register()の反対、オートロード関数の登録解除を行っています。

具体的には、先ほどのloadClassLoader()メソッドをオートロード関数から削除しています。

③のまとめ

つまるところ③では、

  1. spl_autoload_register()を使い、loadClassLoader()をオートロード関数として登録。
  2. \Composer\Autoload\ClassLoaderクラスがloadClassLoader()により読み込まれ、vendorディレクトリへの絶対パスをパラメータとして\Composer\Autoload\ClassLoaderクラスのインスタンスを作成、self::$loader$loaderに代入。
  3. spl_autoload_unregister()を使い、loadClassLoader()をオートロード関数から削除

という処理が行われていることが分かりました。

④の処理:マッピング情報の読み込み方法を決定

vendor/composer/autoload_real.php
$useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());

④では論理演算式によって、どのような読み込み方法を取るのかを決定しています。

この値によって、この後の⑤が⑤´に分岐するが決まります。

ここも②と同様本筋からそれますので軽い説明程度にとどめておきます。

条件式 説明
PHP_VERSION_ID >= 50600 PHPのバージョンが5.6.0以上であればtrue
!defined('HHVM_VERSION') HHVMというvirtual machineを使用していなければtrue
(!function_exists('zend_loader_file_encoded') zend guard loaderというランタイムを使っていなければtrue
!zend_loader_file_encoded() zend guard loaderを使っていてもzend guardによりエンコードされていなければtrue

HHVM_VERSIONzend_loader_file_encodedについてはかなりあいまいな説明です。有識者の方がいらっしゃっいましたらコメントしていただけると助かります)

正直ほとんどの方が、PHP_VERSION_ID >= 50600くらいにしか結果を左右されないと思います。

ですので、PHPバージョンが5.6.0以上であれば$useStaticLoaderにはtrueが入ると思って差し支えないでしょう。(差し支えありましたらすいません)

⑤の処理:マッピング情報を読み込み

ここから先、$useStaticLoaderの値によって⑤と⑤´に処理が分かれますが、行っていることとしてはどちらとも、クラスとソースコードのマッピング情報を$loaderにセットしている処理になりますので、そのことを念頭に置いておくと良いかもしれません。

$useStaticLoadertrueの場合はこちらになります。

vendor/composer/autoload_real.php
require __DIR__ . '/autoload_static.php';

call_user_func(\Composer\Autoload\ComposerStaticInitcde23787628405c61112bd11321f024e::getInitializer($loader));

ここではまず、vendor/composer/autoload_static.phprequireしています。

その後に、\Composer\Autoload\ComposerStaticInitcde23787628405c61112bd11321f024eというまた長ったらしい名前をしているクラスのクラスメソッド、getInitializer()を実行しているようです。

最後にgetInitializer()から返されるクロージャ(でしょうか?)をcall_user_func()を使って実行しています。

おそらく\Composer\Autoload\ComposerStaticInitcde23787628405c61112bd11321f024eクラスの定義もvendor/composer/autoload_static.phpで行われているでしょうから、そちらを見てみましょう。

このファイルを開くと、人によっては数千行にも及ぶソースコードが表示されると思います。

ですが記述のほとんどが、[クラス名 => 定義されているソースコードへのパス]という連想配列で構成されているはずです。

なので\Composer\Autoload\ComposerStaticInitcde23787628405c61112bd11321f024e
の実態は、以下のようにpublicなクラスプロパティが5つとpublicなクラスメソッドが1つだけのクラスです。

<?php

namespace Composer\Autoload;

class ComposerStaticInitcde23787628405c61112bd11321f024e
{
    public static $files = array (
        // ...
    );

    public static $prefixLengthsPsr4 = array (
        // ...
    );

    public static $prefixDirsPsr4 = array (
        // ...
    );

    public static $prefixesPsr0 = array (
        // ...
    );

    public static $classMap = array (
        // ...
    );

    public static function getInitializer(ClassLoader $loader)
    {
        return \Closure::bind(function () use ($loader) {
            $loader->prefixLengthsPsr4 = ComposerStaticInitcde23787628405c61112bd11321f024e::$prefixLengthsPsr4;
            $loader->prefixDirsPsr4 = ComposerStaticInitcde23787628405c61112bd11321f024e::$prefixDirsPsr4;
            $loader->prefixesPsr0 = ComposerStaticInitcde23787628405c61112bd11321f024e::$prefixesPsr0;
            $loader->classMap = ComposerStaticInitcde23787628405c61112bd11321f024e::$classMap;

        }, null, ClassLoader::class);
    }
}

今回vendor/composer/autoload_real.phpで呼び出されたのは::getInitializer()ですので、そちらを詳しく見てみましょう。

以下、getInitializer()の抜粋です。

public static function getInitializer(ClassLoader $loader)
{
    return \Closure::bind(function () use ($loader) {
        $loader->prefixLengthsPsr4 = ComposerStaticInitcde23787628405c61112bd11321f024e::$prefixLengthsPsr4;
        $loader->prefixDirsPsr4 = ComposerStaticInitcde23787628405c61112bd11321f024e::$prefixDirsPsr4;
        $loader->prefixesPsr0 = ComposerStaticInitcde23787628405c61112bd11321f024e::$prefixesPsr0;
        $loader->classMap = ComposerStaticInitcde23787628405c61112bd11321f024e::$classMap;

    }, null, ClassLoader::class);
}

やはり、\Closure::bind()によってクロージャが返されていました。

ところで、この\Closure::bind()は一体何をしているのでしょうか。

(ここでは\Closure::bind()について簡単な説明のみにとどめておきます。

\Closure::bind()の詳しい説明は公式マニュアルに記載されていますのでご参照ください)

\Closure::bind()について

かいつまんで説明すると、\Closure::bind()

第1引数に指定した関数に対して、第2引数に指定したオブジェクトと第3引数に指定したクラスのスコープを関連付けてクロージャを複製する

メソッドです。

もう少しわかりやすい例でいうと

第3引数にクラス名を指定することで、複製されたクロージャはまるでそのクラスに実装されたメソッドであるかのように、そのクラスに定義されているプロパティに対して(privateであっても)アクセスが可能となります

また今回の例では関係ないのですが、

第2引数にインスタンスを渡すことで、クロージャ内で$thisと記述した部分にそのインスタンスが割り当てられます

\Closure::bind()について具体的な説明

以下のようなFooクラスがあります。

class Foo
{
    private static $sFoo = "private static foo\n";
    private $foo = "private foo\n"; 
}

このクラスはプロパティを2つ持っていますが両方ともprivateなプロパティなので

echo Foo::$sFoo;  // PHP Fatal error:  Uncaught Error: Cannot access private property Foo::$sFoo in ...
echo (new Foo())->foo;  // PHP Fatal error:  Uncaught Error: Cannot access private property Foo::$foo in ...

のように外部からアクセスしようとしても当然エラーが起きます。

そこで、\Closure::bind()を使いprivateなプロパティへアクセスできるクロージャを作成してみましょう。

class Foo
{
    private static $sFoo = 'private static foo\n';
    private $foo = 'private foo\n';
}

$cls = \Closure::bind(function (): void {
    echo Foo::$sFoo;
    (new Foo())->foo;
}, null, Foo::class);

$cls();

すると、

private static foo
private foo

privateなプロパティを出力することができました。

このクロージャはさながら、

class Foo
{
    private static $sFoo = 'private static foo\n';

    private $foo = 'private foo\n';

    public function cls()
    {
        echo Foo::$sFoo;
        (new Foo())->foo;   
    }
}

のようなクラスを定義した時の、cls()メソッドと同等の振る舞いをしているものだとみなせます。

ただし違いが1つあり、それはメソッドなら再利用可能だが、\Closure::bind()によって複製されたクロージャは他の変数に保存しないと再利用不可になるという点です。

これは、初期化処理のように最初の1回だけしかその処理が必要じゃない、しかしprivateprotectedされたプロパティに対して処理を施したい、という場面でこそ有効な特徴になると思います。

では本筋に戻ります。

以上の説明を踏まえて、\Closure::bind()が何をやっているのかを見てみましょう。

以下、::getInitializer()の再掲です。

public static function getInitializer(ClassLoader $loader)
{
    return \Closure::bind(function () use ($loader) {
        $loader->prefixLengthsPsr4 = ComposerStaticInitcde23787628405c61112bd11321f024e::$prefixLengthsPsr4;
        $loader->prefixDirsPsr4 = ComposerStaticInitcde23787628405c61112bd11321f024e::$prefixDirsPsr4;
        $loader->prefixesPsr0 = ComposerStaticInitcde23787628405c61112bd11321f024e::$prefixesPsr0;
        $loader->classMap = ComposerStaticInitcde23787628405c61112bd11321f024e::$classMap;

    }, null, ClassLoader::class);
}

\Closure::bind()の第1引数に複製したい無名関数を渡すことは先ほど説明しました。

第2引数はnull、つまりどのインスタンスともバインドしない静的メソッドとして複製することを示しています。

第3引数では、classキーワードを使ってClassLoaderクラスの完全修飾名を渡しています。

つまりここで複製されるクロージャは、まるで\Composer\Autoload\ClassLoaderクラスに実装されたメソッドであるかのように\Composer\Autoload\ClassLoaderクラスのプロパティに対してアクセスが可能となっているクロージャです。

そのことを確認する方法として、試しに第3引数をClassLoader::classからnullに変更した後

php vendor/autoload.php

をコンソール上で実行してみてください。

PHP Fatal error:  Uncaught Error: Cannot access private property Composer\Autoload\ClassLoader::$prefixLengthsPsr4

というようなエラーが出力されるはずです。どうやら、

$loader->prefixLengthsPsr4 = ComposerStaticInitcde23787628405c61112bd11321f024e::$prefixLengthsPsr4;

の部分でComposer\Autoload\ClassLoaderprivateなプロパティにアクセスしてしまっているようですね。

Composer\Autoload\ClassLoaderクラスのプロパティを確認してみましょう。

<?php

namespace Composer\Autoload;

class ClassLoader
{
    private $vendorDir;

    // PSR-4
    private $prefixLengthsPsr4 = array();
    private $prefixDirsPsr4 = array();
    private $fallbackDirsPsr4 = array();

    // PSR-0
    private $prefixesPsr0 = array();
    private $fallbackDirsPsr0 = array();

    private $useIncludePath = false;
    private $classMap = array();
    private $classMapAuthoritative = false;
    private $missingClasses = array();
    private $apcuPrefix;

    private static $registeredLoaders = array();

    public function __construct($vendorDir = null)
    {
        $this->vendorDir = $vendorDir;
    }

    // 後略
}

このようにすべてprivateで定義されています。

ですので先ほどのクロージャからではクラス外からのアクセスになってしまうので、Cannot access private propertyと怒られてしまったんですね。

以上で、\Closure::bind()自体の説明は終わりです。

vendor/composer/autoload_static.phpの変更した部分をClassLoader::classに戻して、

php vendor/autoload.php

がエラーを出力することなく完了したことを確認したら次に進みましょう。

\Closure:bind()に渡した無名関数

では、\Closure:bind()が複製する対象の無名関数を見てみましょう。

以下、無名関数の部分のみの抜粋です。

function () use ($loader) {
    $loader->prefixLengthsPsr4 = ComposerStaticInitcde23787628405c61112bd11321f024e::$prefixLengthsPsr4;
    $loader->prefixDirsPsr4 = ComposerStaticInitcde23787628405c61112bd11321f024e::$prefixDirsPsr4;
    $loader->prefixesPsr0 = ComposerStaticInitcde23787628405c61112bd11321f024e::$prefixesPsr0;
    $loader->classMap = ComposerStaticInitcde23787628405c61112bd11321f024e::$classMap;
}

無名関数の中で外部の変数を使う場合は、予めuseを使って内部で使う変数を渡してあげる必要があります。

今回では、$loaderが外部の変数ですのでuseに含めておきます。

先ほど示した、\Composer\Autoload\ClassLoaderクラスのプロパティにこの4つのプロパティもあります。

これらのプロパティに対してそれぞれ、ComposerStaticInitcde23787628405c61112bd11321f024eクラス(つまり自クラスですね)で定義されている同名のクラスプロパティを代入しています。

つまりこの無名関数の中で、オートローディングに必要なマッピング情報を$loaderに渡しているのです。

以上でこの無名関数に関する説明は終了です。

まとめ

さて、⑤の処理の流れを総括すると

  1. \Composer\Autoload\ComposerStaticInitcde23787628405c61112bd11321f024eが呼び出せるように、requirevendor/composer/autoload_static.phpを読み込み

  2. ComposerStaticInitcde23787628405c61112bd11321f024e::getInitializer($loader)の戻り値をcall_user_func()の引数に指定

  3. ComposerStaticInitcde23787628405c61112bd11321f024e::getInitializer()では、渡ってきた$loaderを無名関数の処理に組み込み、適切なクラススコープを付与したクロージャを返す。

  4. call_user_func()は返ってきた無名関数を実行して、$loaderのプライベートなプロパティにマッピング情報を渡して読み込ませる。

といったことが行われていることが分かりました。

⑤´の処理:⑤と異なる方法でマッピング情報を読み込み

こちらは$useStaticLoaderfalseの場合の処理になります。

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

大きく分けて3つの処理に分けることができますが、どれも流れは同じです。

まずはvendor/composer/autoload_namespaces.phpを見てみましょう。

vendor/composer/autoload_namespaces.php
<?php

$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);

return array(
    'Mockery' => array($vendorDir . '/mockery/mockery/library'),
    'Highlight\\' => array($vendorDir . '/scrivo/highlight.php'),
    'HighlightUtilities\\' => array($vendorDir . '/scrivo/highlight.php'),
);

このように、[名前空間 => 対応させるファイルパスの入った配列]という連想配列を$mapに返しています。

これを、foreachを使って$namespace$pathに切り分けながら1つずつ$loader->set()にかけているようです。

名前から想像できると思いますが一応、中身を見てみましょう。

vendor/composer/ClassLoader.php
public function set($prefix, $paths)
{
    if (!$prefix) {
        $this->fallbackDirsPsr0 = (array) $paths;
    } else {
        $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
    }
}

このメソッドでは、

  • $prefixつまり$mapのキーが空文字だったらfallbackDirsPsr0プロパティにパスを追加

  • $prefixがあれば、prefixesPsr0プロパティ内の$prefix[0]つまり頭文字に対応する配列>prefixesPsr0[$prefix[0]][$prefix => array($path)]の形で追加

している処理を行っていると思われます。

同様にvendor/comoser/autoload_psr4.phpを見てみると、

vendor/comoser/autoload_psr4.php
<?php

$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);

return array(
    'phpDocumentor\\Reflection\\' => array($vendorDir . '/phpdocumentor/reflection-common/src', $vendorDir . '/phpdocumentor/reflection-docblock/src', $vendorDir . '/phpdocumentor/type-resolver/src'),
    // ...
);

と、autoload_namespaces.phpと同様の構造でマッピング情報を$mapに返します。

そしてやはり先ほどと同様、foreachを使って$namespace$pathに切り分けながら1つずつ$loader->setPsr4()にかけているようです。

こちらも実装を見てみましょう。

vendor/composer/ClassLoader.php
public function setPsr4($prefix, $paths)
{
    if (!$prefix) {
        $this->fallbackDirsPsr4 = (array) $paths;
    } else {
        $length = strlen($prefix);
        if ('\\' !== $prefix[$length - 1]) {
            throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
        }
        $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
        $this->prefixDirsPsr4[$prefix] = (array) $paths;
    }
}

流れを追うと、

  • $prefixつまり$mapが空文字だったらfallbackDirsPsr0プロパティにパスを追加

  • $prefixの最後の文字が'\\'でなければ例外を投げる。

  • そのどちらでもなければ

    • 先ほどと同様の指定方法でprefixLengthsPsr4[$prefix[0]][$prefix => $length]を保存
    • prefixDirsPsr4プロパティに[$prefix => array($paths)]の形で保存

という処理が行われているようです。

これまた同様に、vendor/composer/autoload_classMap.phpを見てみましょう。

vendor/composer/autoload_classMap.php
<?php

$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);

return array(
    'App\\Console\\Kernel' => $baseDir . '/app/Console/Kernel.php',
    'App\\Exceptions\\Handler' => $baseDir . '/app/Exceptions/Handler.php',
    // ...
);

先ほど2つと同様に連想配列の形式です。ただし、値には配列ではなくて文字列を指定しているようです。

このマップ情報を$classMapに返し、$classMapが空ではなければ$loader->addClassMap()に渡しているようです。

以下、addClassMap()の実装です。

public function addClassMap(array $classMap)
{
    if ($this->classMap) {
        $this->classMap = array_merge($this->classMap, $classMap);
    } else {
        $this->classMap = $classMap;
    }
}

これは見たまんまの処理ですね。

  • 既にclassMapプロパティに何かあれば、渡された$classMapclassMapプロパティに統合

  • classMapプロパティが空なら、$classMapをそのままclassMapプロパティに代入

となります。

以上3つの読み込み処理を経て、⑤´でもマッピング情報の読み込みが行われています。

⑥の処理:オートローダーの登録

⑥の処理を見てみましょう。

vendor/composer/autoload_real.php
$loader->register(true);

おそらく、メソッド名から鑑みてオートロードを行う上で重要な処理を担っているものだと思われます。

それではregister()の定義を見てみましょう。

vendor/composer/ClassLoader.phpを探してみますと確かにClassLoaderクラスのメソッドとして定義されていました。

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

    if (null === $this->vendorDir) {
        //no-op
    } elseif ($prepend) {
        self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
    } else {
        unset(self::$registeredLoaders[$this->vendorDir]);
        self::$registeredLoaders[$this->vendorDir] = $this;
    }
}

処理を追ってみます。

復習になりますが、spl_autoload_register()は未定義のクラス、インターフェースが呼び出されたときに実行されるオートロード関数を登録しておけるPHPの関数でしたね。

ここでは、オートロード関数として自インスタンスのloadClassメソッドをオートロード関数として実行順を最優先で登録しています。

登録したオートロード関数が何をするのかは後で詳しく見るとして、続けてif文の処理を見てみましょう。

記述されている条件分岐は以下のようになります。

  • $this->vendorDirnull、つまりインスタンス生成時にパスを渡さしていなかった場合何もしない

  • $this->vendorDirが設定されていて、かつregister()の引数$prependtrueを渡していた場合、[$this->vendorDir => $this]という配列を順番的に最初にしてself::$registeredLoadersに追加。

  • $prependfalseなら、self::$registeredLoadersから$this->vendorDirをキーとする要素を削除した後、順番的に一番後ろに[$this->vendorDir => $this]という配列を追加。

どうやら、if文ではself::$registeredLoadersというクラスプロパティに、

「このvendorDirのパスの時は、このローダを使う」という情報を[$this->vendorDir => $this]という形式の配列で蓄積しているようです。

register()メソッドの行っていることはこれで以上です。

では先ほどオートロード関数として登録したloadClassメソッドの実装を見てみましょう。

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

        return true;
    }
}

オートロード関数には実行時に、引数へ呼び出したパラメータが渡されることは既に説明致しました。

処理を予測してみると、

$this->findFile($class)によってクラスが定義されているファイルの所在を探し出し、見つかればincludeFile($file)でそれをincludeする。

といったところでしょうか。

実際に、includeFile()関数はvendor/composer/ClassLoader.phpの一番下で定義されています。

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

純粋に渡されたファイルパスをincludeするだけの関数のようです。

ですのでloadClass()メソッドの肝はどうやら$this->findFile($class)にありそうです。

ではその実装を見てみましょう。

findFile():マッピング情報の検索

vendor/composer/ClassLoader.php
 public function findFile($class)
{
    // class map lookup
    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;
}

少々長いです。前提として、$classには先ほどのloadClassメソッドと同じものが渡されています。

では割り振った番号ごとに処理を追ってみます。

vendor/composer/ClassLoader.php
if (isset($this->classMap[$class])) {  // ①
    return $this->classMap[$class];
}

$this->classMap$classのキーが見つかればその値を返します。

vendor/composer/ClassLoader.php
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {  // ②
    return false;
}

$this->classMapAuthoritativetrueもしくは$this->missingClasses$classのキーが見つかれば、見つからなかったという意味のfalseを返す。

$this->classMapAuthoritativeprefixfallbackのディレクトリ検索を禁止するかのプロパティでしょうか、デフォルトではfalseなので禁止しません)

vendor/composer/ClassLoader.php
if (null !== $this->apcuPrefix) { // ③
    $file = apcu_fetch($this->apcuPrefix.$class, $hit);
    if ($hit) {
        return $file;
    }
}

$this->apcuPrefixnullでなければ、APCuというPHPの拡張機能(?)が提供するapcu_fetch()という関数を使ってキャッシュに$this->apcuPrefix.$classというキーがないか検索、ヒットしたらその検索結果をリターンします。

デフォルトではnullなのでここの部分はスキップされます。

(この部分に関してはかなりあいまいな説明となっています、見識のある方がおられましたらご指摘していただけると助かります)

参考:PHP: apcu_fetch - Manual

vendor/composer/ClassLoader.php
$file = $this->findFileWithExtension($class, '.php'); // ④

$this->findFileWithExtension()というメソッドを使ってファイルを探しているようです。

かいつまんで説明すると、findFileWithExtensiton()メソッドは$this->prefixDirsPsr4$this->prefixesPsr0から、$classに対応するファイルパスを検索し、見つかればそれを返すメソッドになります。

このメソッドの処理はコード量が長いので折りたたんで解説します。

findFileWithExtension()の解説

このメソッドでは、渡された文字列を地道に解析して$this->prefixDirsPsr4$this->prefixesPsr0からファイルパスを取り出しています。

1つずつ解説するととても量が多くなるのでコメントに付記する形で説明を行います。

vendor/composer/ClassLoader.php
private function findFileWithExtension($class, $ext)  
{
    // 以下$class = Illuminate\\Support\\Facades\\Auth として実行

    $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
    // $logicalPathPsr4 = Illuminate/Support/Facades/Auth.php

    $first = $class[0];  // $first = I
    if (isset($this->prefixLengthsPsr4[$first])) {
        $subPath = $class;  // $subPath = Illuminate\\Support\\Facades\\Auth


        // $subPathから"\\"のうち最後のものの位置を$lastPosに代入。無ければfalseを返す
        while (false !== $lastPos = strrpos($subPath, '\\')) {
            /*
            $subPathがIlluminate\\Support\\Facades\\Authなら
            Illuminate\\Support\\Facadesに置き換え
            */
            $subPath = substr($subPath, 0, $lastPos);
            $search = $subPath . '\\';

            /*
            $searchが$this->prefixDirsPsr4にあればif文以下を実行
            この場合であれば$subPath = Illuminate, $search = Illuminate\\, $lastPos = 10 の時にtrueとなる
            */
            if (isset($this->prefixDirsPsr4[$search])) {  

                /*
                substr()の結果がSupport/Facades/Authなので
                $pathEnd = /Support/Facades/Auth.php
                */
                $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);

                /*
                $searchに対応するファイルパスを$dirに取得。
                この場合であれば $dir = /path/to/vendor/composer/../laravel/framework/src/Illuminate
                */
                foreach ($this->prefixDirsPsr4[$search] as $dir) {

                    // $file =  /path/to/vendor/composer/../laravel/framework/src/Illuminate/Support/Facades/Auth.php
                    if (file_exists($file = $dir . $pathEnd)) { // $fileの位置にファイルがあればtrue
                        return $file; // $fileのパスを返す
                    }
                }
            }
        }
    }

    // $this->fallbackDirsPsr4があればループを実行
    foreach ($this->fallbackDirsPsr4 as $dir) {
        if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
            return $file;
        }
    }

    // 以下、$class = Mockery, $logicalPathPsr4 = Mockery.php, $first = M として実行 

    // $classに"\\"があれば$posに最後のそれの位置を代入。無ければfalseを返す
    if (false !== $pos = strrpos($class, '\\')) { 

        // ここでは$class =  Hightlight\\Hogehoge, $$logicalPathPsr4 = Hightlight/Hogehoge.php  として実行

        // $logicalPathPsr0 = Hightlight/Hogehoge.php
        $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
            . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
    } else {
        // $logicalPathPsr0 = Mockery.php
        $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
    }

    if (isset($this->prefixesPsr0[$first])) { // $this->prefixesPsr0["M"] があればif文以下を実行

        // $this->prefixesPsr0["M"]を$prefixと$dirsに切り分けてループ
        foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {

            // ここでは$prefix = Mockery として実行

            // $classに"Mockery"があり、その最初の位置が0番目ならif文以下を実行
            if (0 === strpos($class, $prefix)) {

                // $dirsを1つずつループ
                foreach ($dirs as $dir) {

                    // ここでは$dir = /path/to/vendor/composer/../mockery/mockery/library として実行

                    /*
                    $file = /path/to/vendor/composer/../mockery/mockery/library/Mockery.php
                    $fileがあれば$fileを返す
                    */
                    if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
                        return $file;
                    }
                }
            }
        }
    }

    // $this->fallbackDirsPsr0があればループを実行
    foreach ($this->fallbackDirsPsr0 as $dir) {
        if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
            return $file;
        }
    }

    // $this->useIncludePathがtrueで、$logicalPathPsr0のファイルパスが存在すれば$fileを返す
    if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
        return $file;
    }

    // ここまで来たら何も見つからなかったということでfalseを返す
    return false;
}

vendor/composer/ClassLoader.php
// Search for Hack files if we are running on HHVM
if (false === $file && defined('HHVM_VERSION')) { // ⑤
    $file = $this->findFileWithExtension($class, '.hh');
}

コメントアウトされた文章にもあるようにHHVMが起動している場合に限り、$filefindFileWithExtension()によって見つからなかったときに、拡張子を.hhに変えて再びfindFileWithExtension()を実行し、ファイルパスを取得しようと試みています。

この時点でファイルパスの検索結果は$fileに保存されています。
ですので後の処理は次回以降の処理を省くためのものがほとんどです。

vendor/composer/ClassLoader.php
if (null !== $this->apcuPrefix) { // ⑥
    apcu_add($this->apcuPrefix.$class, $file);
}

$this->apcuPrefixnullでなければ、APCuというPHPの拡張機能(?)が提供するapcu_add()という関数を使ってキャッシュに、$this->apcuPrefix.$classというキーで検索結果をキャッシュに追加しています。

デフォルトではnullなのでここの部分はスキップされます。

(この部分に関してもかなりあいまいな説明となっています、見識のある方がおられましたらご指摘していただけると助かります)

vendor/composer/ClassLoader.php
if (false === $file) { // ⑦
    // Remember that this class does not exist.
    $this->missingClasses[$class] = true;
}

return $file;

検索の結果何も見つからなかった場合、次回以降の検索処理を省略するために$this->missinglasses[$class => true]という形式の配列を追加します。

これによって、次回以降同じクラス名を呼び出した際に①から⑥まで処理を経なくとも②の時点で該当ファイルがないという結果を返すことができます。

以上の処理を経てfindFile()メソッドは呼び出しクラスに対するファイルパス情報を検索しているのですね。

⑦の処理:クラスファイル以外のソースコードを読み込む

いよいよ最後の処理です。ここでは、クラスファイル以外のもの、例えば起動処理をまとめただけのファイルでしたり、ヘルパ関数をまとめたファイルなどをハッシュ値と紐づけて読み込ませる処理を行っています。

vendor/composer/autoload_real.php
if ($useStaticLoader) {
    $includeFiles = Composer\Autoload\ComposerStaticInitcde23787628405c61112bd11321f024e::$files;
} else {
    $includeFiles = require __DIR__ . '/autoload_files.php';
}
foreach ($includeFiles as $fileIdentifier => $file) {
    composerRequirecde23787628405c61112bd11321f024e($fileIdentifier, $file);
}

return $loader;

では処理を追ってみましょう。

まず、$useStaticLoaderの値によって処理が分岐していますがどちらとも$includeFiles[ハッシュ値 => ファイルパス]の構造をした配列を代入しています。

Composer\Autoload\ComposerStaticInitcde23787628405c61112bd11321f024e::$filesプロパティにも、vendor/composer/autoload_files.phpの戻り値にも同様の配列が記載されていると思います。

この配列の要素をハッシュ値$fileIdentifierとファイルパス$fileに切り分けて1つずつ、composerRequirecde23787628405c61112bd11321f024e()という関数にかけているようです。

このcomposerRequirecde23787628405c61112bd11321f024e()関数はvendor/composer/autoload_real.phpの一番下、クラスの外に定義されています。

実装を見てみましょう。

vendor/composer/autoload_real.php
function composerRequirecde23787628405c61112bd11321f024e($fileIdentifier, $file)
{
    if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
        require $file;

        $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
    }
}

この関数によって、クラスファイル以外のソースコードが読み込まれているようです。

処理を追うと、

  1. $GLOBALS['__composer_autoload_files'][$fileIdentifier]が空でないか確認

  2. 空であればそのハッシュ値に対応するファイルがまだ読み込まれていないのでrequireする

  3. $fileIdentifierに対応するファイルを読み込んだことを記憶しておくために、
    $GLOBALS['__composer_autoload_files'][$fileIdentifier]trueを代入。

といった流れになっています。

この処理が終わると、getLoader()$loaderをリターンして処理を終了します。

getLoader()の処理まとめ

とても長くなってしまいましたが以上でgetLoader()の説明は終わりになります。

最後に各番号の処理を簡単に説明します。

  1. self::loaderがすでにあればそれを返す。

  2. 実行環境を確認。

  3. コンストラクタの引数に/path/to/vendorを指定して\Composer\Autoload\ClassLoaderクラスをインスタンス化、それをself::$loader$loaderに代入。

  4. useStaticLoaderをするか決定。

  5. マッピング情報を$loaderに読み込ませる。

  6. 実際にオートロード処理を登録。これ以降の未定義クラスの呼び出しはマッピング情報をもとに解決される。

  7. クラスファイル以外のソースコードをハッシュ値と対応させて読み込む。その後$loaderをリターンする

このような処理の流れで、オートロード機能の構築が行われているということがわかりましたね。

この::getLoader()によって生成された$loaderは呼び出し元であるvendor/autoload.phpに返ってきます。

return ComposerAutoloaderInitcde23787628405c61112bd11321f024e::getLoader();

返ってきた$loadervendor/autoload.phpは更にリターンしています。なのでもし外部でローダーを使う処理を実装したい場合は

$loader = require "/path/to/vendor/autoload.php";

とすれば::getLoader()によって生成されたローダーを使うことができます。

今回起点としたLaravelのindex.phpでは

public/index.php
<?php
// ...

require __DIR__.'/../vendor/autoload.php';

// ...

のようにローダーの保存は行わず、::getLoader()を介してオートロード関数の登録だけを行っています。

保存を行わなかった場合でもクラスプロパティとしてローダーは保存されているので、未定義のクラスが呼び出された場合でもそちらに保存されているローダーのloadClassメソッドを通してオートロード処理が行われている、といったところでしょうか。

処理の流れまとめ

では、最後に今まで説明してきたことを順序だてて簡潔に追ってみましょう。

  1. public/index.phpvendor/autoload.phprequireされる。

  2. vendor/autoload.phpvendor/composer/autoload_real.phprequireされ、そこに定義されているComposerAutoloaderInitcde23787628405c61112bd11321f024e::getLoader()を実行。

  3. ::getLoader()の処理の中でオートロード機能の実体となる\Composer\Autoload\ClassLoaderインスタンスを生成し、自クラスの静的プロパティとして保存。

  4. そのインスタンスに対してマッピング情報の読み込み、オートロード処理の登録、そしてクラス以外のソースコードに関しても読み込み、といった処理を行う。

このような流れでComposerによるファイルのオートロード処理が行われているのだということが分かりました。

終わりに

非常に長い説明になってしまったことをお詫びしたいと思います。

ですが、このように処理を1つ1つ丁寧に追っていったことでComposerのオートロード機能についてはほとんどぬけのない理解が出来たのではないでしょうか。

以前私のほうでも投稿しました記事でも述べたことがあるのですが、普段何気なく使っている機能について一度は深くまで追ってみるという経験をするのは、非常に力がつきます。

現に、この機能を調べるまで知りもしなかった情報や仕様について理解する機会を得られました。

月並みな締めになってしまいますが、皆さんも一度はこのように外部パッケージが提供している仕組みについて、1から深堀りして追ってみる経験をしてみてはいかがでしょうか。

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

SQLiteを使ったテストで初学者がはまったエラー ~Laravel~

前提

テスト用データベースはSQLiteインメモリを使用しています。

環境

PHP Laravel PHPUnit SQLite
7.4.5 7.28.4 8.5 3.28.0

エラー内容 「リレーションが辿れない」

前提

SQLiteは以下のデータ型の値しか格納しない。

内容
INTEGER 符号付整数。1, 2, 3, 4, 6, or 8 バイトで格納
NUMERIC 負号付きの整数 (1、2、3、4、6、8バイト)
REAL 浮動小数点数。8バイトで格納
TEXT テキスト。UTF-8, UTF-16BE or UTF-16-LEのいずれかで格納
NONE カラムにデータを指定しなかった場合この型になる

以前Laravelで書いていたマイグレーションファイルの一部はこんな感じ。

profilesテーブル

/**
 * Run the migrations.
 *
 * @return void
 */
 public function up()
 {
     Schema::create('profiles', function (Blueprint $table) {
         $table->increments('id');
         $table->unsignedInteger('user_id');
         $table->string('name');
         $table->text('introduction');
         $table->timestamps();

         $table->foreign('user_id')->references('id')->on('users');
     });
  }

profilesテーブルのuser_idカラムとusersテーブルidカラムで紐付かせている。

エラー

テストする際にこのリレーションの箇所でサーバーエラーが連発した。ここの紐づきだけでなく、他の全てのリレーションを辿るアクションでサーバーエラーが発生した。
ここでかなり詰まってしまった。原因が分からない。。。

原因

いろいろ調べた結果、SQLiteがサポートしている型にエラーの原因が存在することが判明した。

SQLiteのサポートしている型一覧

サポートしている型 実際にSQLiteが格納する型
INT
INTEGER
TINYINT
SMALLINT
MEDIUMINT
BIGINT
UNSIGNED BIG INT
INT2
INT8
INTENGER
CHARACTER(20)
VARCHAR(255)
VARYING CHARACTER(255)
NCHAR(55)
NATIVE
CHARACTER(70)
NVARCHAR(100)
TEXT
CLOB
TEXT
NUMERIC
DECIMAL(10,5)
BOOLEAN
DATE
DATETIME
NUMERIC
REAL
DOUBLE
DOUBLE PRECISION
FLOAT
REAL
BLOB NONE

どういうことかというと、テーブル作成の際にカラムの型を指定をするが、そのカラムの型がSQLiteでは使用できない型である場合、SQLite自身がSQliteでも扱えるデータ型に強制的に変換し、データを格納する。
表の左側が変換対象のカラムの型、表の右側は変換後の型。

これを踏まえて上のマイグレーションファイルをもう一度確認してみると、外部キーに設定しているuser_idカラムの型は「unsignedInttenger」。
SQLiteを見てみると、「UNSIGED BUG INT」は存在するが、「UNSIGNED INT」は存在しない。

つまり外部キーに設定したカラムの型は、SQLiteではサポートされていなかったということ。
したがってprofilesテーブルのuser_idカラムの値はINTENGERと扱われず、その結果リレーションメソッドを用いても紐づきを辿ることができず、サーバーエラーになったっぽい。

解決案

外部キーカラムを「UNSIGNED INT」型にしてしまえばよい。

僕はLaravel7で新しく導入された外部キーのフォーマットにしたがってマイグレーションファイルを書き換えた。

profilesテーブル(変更後)

/**
 * Run the migrations.
 */
 public function up(): void
 {
     Schema::create('profiles', function (Blueprint $table) :void {
         $table->id();
         $table->foreignId('user_id')->constrained();
         $table->string('name');
         $table->text('introduction');
         $table->timestamps();
     });
  }

変更前の外部キー設定

$table->unsignedInteger('user_id');
$table->foreign('user_id')->references('id')->on('users');

変更後の外部キー設定

$table->foreignId('user_id')->constrained();

この変更された外部キー設定の特徴は、
・1行で外部キー設定が完了する(テーブル名とカラム名が規約通りではない場合、constrainedの引数にテーブル名を指定する)
この外部キーのカラムの型は自動的に「unsignedBigInteger」となる

したがって、このフォーマットにしたがって外部キー設定をすることで外部キーカラムの型は「UNSIGNED BIG INT」型になり、SQLiteのサポート範囲内となり、きちんとINTENGER型としてデータを格納してくれる。

結果、リレーションを辿ることができた。

補足
ミドルウェアにて以下のポリシーを用いてアクセス制限をしていた。

/**
 * プロフィール編集の権限があるかチェック
 *
 * @param User $user
 * @return mixed
 */
 public function view(User $user)
 {
      $profile = $user->profiles()->first();
      return $user->id === $profile->user_id;
 }

SQLiteを使用したテストのときだけこのポリシーを通過できなかった。
「===」を使って厳密な型比較せず、「==」に変更した場合はクリアした。
外部キーをunsignedBigIntengerにして、SQLiteにIntengerとして扱ってもらったが、厳密な型比較をすると、なぜか差異がでるそう。。。ここらへんはまだ理解していません。。。

最後に

このように対処することで解決致しましたが、何か間違いがあるかもしれません。
何かお気づきの方いらっしゃいましたら、気軽にコメントのほどよろしくお願い致します。

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

初学者がPHPを学び始める4

フォームを作成する

フォームを作りたい時は、HTMLのformタグを用います。
action属性にはデータを渡す先のURLを指定します。
method属性は値の送信の方法で、「get」と「post」のどちらかを指定します。getの場合は送信される値がURLに表示され、postの場合はURLに表示されません。
以下の場合はフォームに入力した値をsent.phpというページに送信するという意味になります。

   <form action = "sent.php" method = "post"></form>

テキストボックスを作成する

テキストボックスをつくるにはHTMLのinput type="text"を使います。name属性は入力された値を取得するときに使う名前です。後にフォームのデータを受け取るときに使用します。またinputタグは閉じタグが必要ありません。(textareaタグの場合は必要)

以下のように記述します。
inputは一行分のテキストが入力できます。
textareaの場合は複数行のテキストが入力できます。

       <input type = "text" name = "email">

       <textarea name = "body"></textarea>

送信ボタンを作る

送信ボタンをつくるにはinput type="submit"を用います。
value属性に指定された値がボタン上に表示されます。
以下の記述で送信ボタンを作ることができます。(cssは別に用意する必要があります。)

   <input type = "submit" value = "送信">

フォームで送信した値を受け取るには「$_POST」を使用します。
「$_POST」は連想配列になっています。[ ]の中に、inputとtextareaのname属性に指定した値を入れることで、それぞれの送信した値を受け取ることが出来ます。
以下の記述で送られてきた値を(email)を受け取ることができます。

      <?php echo $_POST['email']; ?>            

セレクトボックスの作り方

セレクトボックスをつくるには図のようにselectタグの中にoptionタグを並べます。
optionタグの中身が選択肢として表示されます。
selectタグには「$_POST」で値を受け取るためのname属性を指定します。
optionタグのvalue属性が送信される値です。

以下の記述でセレクトボックスが表示できます。

        <select name = "age">
          <option value ="未選択">選択してください</option>
          <option value ="20代">20代</option>
          <option value ="30代">30代</option>
        </select>        

以下の記述でageを受け取り出力することができます。

      <?php echo $_POST["age"]; ?>

forを使ったセレクトボックス

下記の場合だとセレクトボックスに1から100までの数字を選択することができる。

      <?php 
        for($i = 1; $i <= 100; $i++){
          echo "<option value='{$i}'>{$i}</option>";            
       }
      ?>

     この二つは同じ意味↑↓

     echo "<option value ="1">1</option>"
     echo "<option value ="2">2</option>"
     echo "<option value ="3">3</option>"

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

(備忘録)laravelでCURDとデータ操作

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