がるの健忘録

エンジニアでゲーマーで講師で占い師なおいちゃんのブログです。

PHPで「型宣言された引数の型の名前」を知る方法 続編

PHPで「型宣言された引数の型の名前」を知る方法 - がるの健忘録 の続編。

いやPHP8になって「union typesとか増えたしなぁ」とか思って検証コード書いてみたので、勿体ないんで、忘れる前に(絶対忘れるだろうしw)、記録しておく。

<?php
namespace Foo;

//
class Bar {
}

// 調査用クラス
class Hoge {
    public function t(\Exception $e, Bar $ar, $s, int|float $i, mixed $m) {
    }
}

/*
//
$ref = new \ReflectionMethod('Hoge', 't');
//
$params = $ref->getParameters();
//
foreach($params as $p) {
    echo $p->getType() , "\n";
}
*/

// 一行にまとめてみるとこんなん
foreach(( new \ReflectionMethod('Foo\\Hoge', 't'))->getParameters() as $p) {
    echo $p->getType() , "\n";
}

結果

Exception
Foo\Bar

int|float
mixed

ほむ、概ね「すげぇ素直」にかえってくるんだ。
クラスは流石に完全修飾クラス名だけど。

「useを使ってクラスが使えるようになる」までの道程

ちょいとお仕事で、要約すると
・useすればクラス名の解決が可能になるのではないか?
 → 参照したいクラスがあるファイルのnamespace\クラス名をuseで指定すれば使用はできるはず
という趣旨の質問をいただきまして。

これはとても興味深い内容だなぁ、と思ったので「Blogで解説してよい?」と聞いたら快諾いただいたので、早速、ネタに(笑

今回書くのに近しいお話が
namespaceとuse https://note.com/gallu/n/nd4efd1c17e25
autoloadについて https://note.com/gallu/n/n2d0bb7718ceb
requireとinclude https://note.com/gallu/n/n658fbf6fd7ce
に乗っておりますので、よかったら。


結論から書くと
・ use は「別名(エイリアス)の作成をするだけでそれ以外は何もしてくれない」ので「なにもなくてuseだけだとクラスは(多分)使えない」
・クラスの解決には「クラスのオートローディング」という機能を使う
 → 自力で実装してもいいけど、まぁ「既存の実装」があるので、ルールに則りつつそれを使うと楽

となります。
ちょいとこの辺を、昔々のあたりから紐解いてみませう。

以下のクラスがあるとします。

class Hoge
{
}

で、以下のPHPがあった場合、これはHogeクラスを解決できます。

<?php

class Hoge
{
}

//
$obj = new Hoge();

まぁねぇ。
ただこんな風に「1ファイルに全部書く」とか色々と以下略なので、別ファイルに切り出します。

Libs/Hoge.php

<?php

class Hoge
{
}

t.php

<?php
//
$obj = new Hoge();

これだと解決できません。なんでかってぇと「t.phpを実行しても、Hoge.phpを読み込むような記述がどこにもないから」。
古(いにしえ)の術者は、こんな風に解決をしていました。

t.php

<?php
require_once('./Libs/Hoge.php');

//
$obj = new Hoge();

require_onceでもrequireでもinclude_onceでもincludeでもいいんですが(詳しい解説は上述の notes 参照)*1
こんな風に「ファイルを読み込んでやる」と、使えるようになります。
……えぇまぁなので昔は「先頭に100行とか200行とかrequire_onceが書いてある」なんてブツも拝見した記憶がございます。

まぁ「この辺は出来るだけ簡略化したい」と考えるのが優秀なプログラマというものでございます。
上述のnotesの「autoloadについて」を読んでいただくと手っ取り早いのですが。

https://www.php.net/manual/ja/language.oop5.autoload.php

オブジェクト指向アプリケーションを作成する開発者の多くは、 クラス定義毎に一つのPHPソースファイルを作成します。 最大の問題は、各スクリプトの先頭に、必要な読み込みを行う長いリストを 記述する必要があることです(各クラスについて一つ)。
spl_autoload_register() 関数を使うと、 任意の数のオートローダーを登録でき、 クラスやインターフェイスが定義されていなくても自動的に読み込めるようになります。 オートローダーを登録すれば、PHPがエラーで止まる前にクラスをロードする最後の チャンスが与えられます。

こんな手法がございます。

つまり、オートローディングの設定がないと
Hogeを使おうとする
・見つからない → エラー

な所が、オートローディングを使うと
Hogeを使おうとする
・見つからない → オートローディングをしてみる → それでも見つからないならエラー

となって、1クッション増えるわけなんですね。
クッションが増えるだけなので、「本当にどこにもそのクラスが存在しない」なら最終的にエラーにはなるのですが。

となると次は「オートローディングを、自力じゃなくて他人様が実装しておいてくれないもんかしらん?」となるのですが。
昨今のPHPにおいて、比較的高確率で「composer」を使っているのではないか、と思います。
で、もし「composerを使っている」とすると、composerにはオートローディングの実装があるので、こいつを借用する事ができます。

「public/index.php の最初の三行(vendor/autoload.php の挙動) https://note.com/gallu/n/ne1bcb25c412a 」を見ていただくと色々書いてあるのですが。
雑に書くと
・とりあえず、composerでインストールしたライブラリは、オートローディングが通用するようになる
感じです。

……だけだと「じゃぁ自作のクラスはどうすんだよ?」って話になるのですが。
実はcomposerのautoload.phpは、composer.jsonの['autoload']['psr-4']に「名前空間名 => ディレクトリ名」を書いておくと*2、こちらも自動で解決してくれるようになります。

なので、例えば上述であれば。
仮に名前空間を「\Gallu 」と仮定して

Libs/Hoge.php

<?php
namespace Gallu;

class Hoge
{
}

composer.json

{
(前略)
    "autoload": {
        "psr-4": {
            "Gallu\\" : "Libs/"
        }
    },
(後略)


t.php

<?php
require_once('./vendor/autoload.php');

//
$obj = new \Gallu\Hoge();

という風に書くと
・\Gallu\Hoge は、ない
・autoload.phpで登録されたオートローディングが動き出す
 → Galluって名前空間だからLibsを探してみる
  → HogeクラスだからHoge.phpってファイルを探してみる
  → あるから require する
・使えるようになった!!

という流れで、使えるようになります。
これがオートローディングの世界です。

まぁ「特殊なニーズがある」のであれば「自力実装を追加する」とかってのもあり得るんでしょうが、普通の用途ならまずは「あるモノを使ってみる」からでよいのではないかなぁ、と思います。
学習用途であれば一度は「自力実装」してみていただきたい所ですが。


じゃぁ改めて「useってなによ?」って話なのですが。
序盤にも書いた通り「別名の作成」となります。

かみ砕いて。

$obj = new \Gallu\Hoge();

ここ。
このまんま書くのであれば、useいらんです。
なお、\Gallu\Hogeを「完全修飾名」って言います。

これくらいならまぁまだ、とも思わんでもないのですが。
これが例えば

\Foo\Bar\Baz\Piyo\Fuga\Hoge

とかいうクラス名で、これが何カ所にも出てくる時に毎回毎回

$obj = new \Foo\Bar\Baz\Piyo\Fuga\Hoge() ;

とか書いていくのも、ちょっと「ぞっとしない」お話でございます。

こんな時に便利なのが別名。カタカナ表記だとエイリアス。これがuse。
こんな風に定義ができます。

use \Foo\Bar\Baz\Piyo\Fuga\Hoge as Hogera;

$obj = new Hogera() ;

こんな風に書くと「Hogera、って出てきたら \Foo\Bar\Baz\Piyo\Fuga\Hoge ってことで一つよろノ」って感じになります。
まぁエイリアスの名前通り、まんまの機能ですね。

さて。
わざわざ「クラス名と違う名前を毎回考えるのも面倒」だと思うので、割とこうなります。

use \Foo\Bar\Baz\Piyo\Fuga\Hoge as Hoge;

$obj = new Hoge() ;

これで目出度しとしてもよいのですが、「Hoge as Hoge」がちょいとウザい……で、実はこれが省略可能。

use \Foo\Bar\Baz\Piyo\Fuga\Hoge;

$obj = new Hoge() ;

スッキルする、と共に、多分見慣れた構文。
こうやってuseは「別名」を付ける機能を持ちます、が、一方で「クラス存在の解決」にはなんにも関わっておりませんので、従って「useだけ、だと、クラスは解決できない(かもしれない)」となります。


なので。
もし「ちゃんとuseしているのにクラスが解決できなくてPHPがエラーを吐く」のであれば、基本的には「所定の場所にクラスがかかれたファイルがない」か「そもそもそのクラスが存在していない」ので。
楽ちん順のやり方としては
・とりあえず find コマンドでファイルを探してみる(普通、 クラス名.php ってファイル名のはずなので)
・ちぃと時間がかかるかもですが、grepで「class クラス名」で全ファイルをあさってみる
で、対象のファイルが
・あるんだけど位置が違う
・ない
のあたりをつけておくと、次のアクションに繋がりやすいかなぁ、と思うです。

残りのお話は個別の内容になってしまうので、commonな内容としてはこの辺まで。
質問とかあったら、コメントにお願いいたします ノ

*1:まぁ普通、プログラムならrequire_onceですな

*2:正確には、書いた後 php composer.phar dump-autoload を実行すると

0部 まずは初めてみよう

0部 まずは初めてみよう

本書では、最終的にはPHPを「概ねある程度のレベルまでを一通り」文法を中心に学習していけるように作成する予定です。
ここで諸々の蘊蓄をたれてもよいのですが、きっとこういったものを読むからにはなにがしか「プログラムを組む学習をしたい!!」というモチベーションがあるかと思うので。
その勢いを大切に、速やかにプログラムの学習に移っていきましょう。

PHP初心者本、書いてみます!!

今年の目標「アウトプットを増やす」の一環として、PHP初心者本を書いていこうかなぁ、と思っています。

GItHub
github.com
で書いていきますが、同時にここにも内容を記載していこうかなぁ、と……修正までは反映しないと思うので、修正版まで見たいようであればgithubをみていただくと確実かと思われます。

目次を
github.com
に書いていく予定なので、ここみておいてもらうと手っ取り早いのかもしれません。

……というわけで主に「自分を追い込むよう」に、ここに宣言をします(笑

2a問題が解決した!!!!

素晴らしき自動的な世界~或いは「型のない」世界~ https://gallu.hatenadiary.jp/entry/20061108/p1 で書いた、こんなお話、記憶にございますでしょうか?

PHP驚愕の事実

if ('2a' == 2) {
 ここ通る
}

ここから幾星霜、涙が大河となるくらいまであちこちで色々な問題を引き起こしてきました2a問題(大げさ)。
2a問題再びw https://gallu.hatenadiary.jp/entry/20070516/p1 でも書いた通り、switch でも遺憾なくその威力を発揮してまいりました。

が!!!
https://www.php.net/manual/ja/migration80.incompatible.php

下位互換性のない変更点
PHP コア
文字列と数値の比較

(厳密でないやり方で)数値と非数値文字列を比較する場合、 数値を文字列にキャストし、文字列と比較するようになりました。

との事でさりげなく書いてあるこの内容が! 全てを! 改善しました!!!!

ってなわけで、まずソースコード

<?php
declare(strict_types=1);

var_dump( PHP_VERSION );

var_dump( 2 == '2a' );

$i = '2a';
switch ($i){
    case 2:
        echo "2\n";
        break;
    case '2a':
        echo "2a\n";
        break;
}

環境その1

string(6) "7.4.11"
bool(true)
2

まぁ毎度お困りの状態*1

環境その2

string(5) "8.0.0"
bool(false)
2a

ききました奥様?
falseですって!
2aですって!!

これですよコレ!!!

というわけで。
まぁもう少し色々と掘ってみないと、な所はありますが、とはいえ一端「2a問題が(多分大体)落ち着いてきた!!」ってことで、これは快挙なんじゃないかなぁ? と思うわけでございます。

ってなわけで記念Blog。

&余談。
PHP8.0.0ですが、PHP7.4.xのconfigureオプションで(おいちゃんの場合)問題なくコンパイルできました。
インストール、とりあえずは楽なんじゃないですかしらん???

*1:なんでメンテナンスが11か、ってぇと、12と13が「This is a bug fix release.」だから、ちょいと止めてる状態

Slim4で「404のログ」だけ消したい(&任意のエラー画面出したい)

Slim4です。
エラー周りの基本処理で。

 * @param bool $displayErrorDetails -> Should be set to false in production
 * @param bool $logErrors -> Parameter is passed to the default ErrorHandler
 * @param bool $logErrorDetails -> Display error details in error log
 * which can be replaced by a callable of your choice.
 * @param \Psr\Log\LoggerInterface $logger -> Optional PSR-3 logger to receive errors

ってなコメントもございますので、おいちゃんは大概

    $errorMiddleware = $app->addErrorMiddleware($container->get('settings')['displayErrorDetails'], true, true, $container->get('logger'));

って書いておくでございます。
ただ、これだけだと「404もエラーログに書かれるので結構ウザい」でございます。

いや「Slimのエラーを一通りログに出さない」んなら

    $errorMiddleware = $app->addErrorMiddleware($container->get('settings')['displayErrorDetails'], true, true);

でよいのですが、「404以外のエラーは念のために補足しておきたい」んですよねぇ、という乙女心*1

まぁ色々調べてみるわけなのですが。

まず「エラー時に任意の画面を出したい」場合、
https://www.slimframework.com/docs/v4/middleware/error-handling.html

// Get the default error handler and register my custom error renderer.
$errorHandler = $errorMiddleware->getDefaultErrorHandler();
$errorHandler->registerErrorRenderer('text/html', MyCustomErrorRenderer::class);

ってな記述があるのでございます。
なお上述クラスは

<?php
use Slim\Interfaces\ErrorRendererInterface;

class MyCustomErrorRenderer implements ErrorRendererInterface
{
    public function __invoke(Throwable $exception, bool $displayErrorDetails): string
    {
        return 'My awesome format';
    }
}

こんな風に実装する感じでございます。

さて。
とりあえずgetDefaultErrorHandler()てのがあるので、軽くgrepります*2
"vendor/slim/slim/Slim/Middleware/ErrorMiddleware.php"

    public function getDefaultErrorHandler()
    {
        if ($this->defaultErrorHandler === null) {
            $this->defaultErrorHandler = new ErrorHandler(
                $this->callableResolver,
                $this->responseFactory,
                $this->logger
            );
        }

        return $this->callableResolver->resolve($this->defaultErrorHandler);
    }

getがあるんならsetもあるんじゃなかろうか。

    public function setDefaultErrorHandler($handler): self
    {
        $this->defaultErrorHandler = $handler;
        return $this;
    }

DefaultがあるんならDefault無しもあるんじゃなかろうか。

    public function setErrorHandler($typeOrTypes, $handler, bool $handleSubclasses = false): self
    {
        if (is_array($typeOrTypes)) {
            foreach ($typeOrTypes as $type) {
                $this->addErrorHandler($type, $handler, $handleSubclasses);
            }
        } else {
            $this->addErrorHandler($typeOrTypes, $handler, $handleSubclasses);
        }

        return $this;
    }

ここからちゃんとコードを確認してもよいのですが……面倒なんでvar_dumpで調査します。
つまり
・setErrorHandler()の先頭に var_dump($typeOrTypes); exit; を仕込んで
・404をわざと発生させます
まぁ予想通り Slim\Exception\HttpNotFoundException でございます(Slim4、この辺は例外のクラスで判別するので)。

ってことは

    public function getErrorHandler(string $type)
    {
        if (isset($this->handlers[$type])) {
            return $this->callableResolver->resolve($this->handlers[$type]);
        } elseif (isset($this->subClassHandlers[$type])) {
            return $this->callableResolver->resolve($this->subClassHandlers[$type]);
        } else {
            foreach ($this->subClassHandlers as $class => $handler) {
                if (is_subclass_of($type, $class)) {
                    return $this->callableResolver->resolve($handler);
                }
            }
        }

        return $this->getDefaultErrorHandler();
    }

これも大体予想通り。

なので、ざっくりと実装してみます。

    $errorMiddleware->setErrorHandler(\Slim\Exception\HttpNotFoundException::class, function($request, $e) use($app){
        $response = $app->getResponseFactory()->createResponse(404);
        $response->getBody()->write('めっからないよん??');
        return $response;
    });

確認……うん、OK。

というわけで、備忘録を兼ねて、メモ。

*1:なにゆえに乙女?

*2:ぐれぷります、とかお読みいただければ幸いです

あえての「WAF(FW)使わない」PHPサイトの開発(副題: でなきゃ定期的にバージョン上げろ)

微妙暴言のお時間でございます*1
いやまぁ昨今、サイト作るのにWAF/FW(Web Application Framework で Framework の略な)を使わない、って選択肢も稀かなぁと思っております。
……いやまぁ「うちのFWはSmartyです」とか言われた頃が(うっすらした殺意と共に)懐かしいのですが、昨今はそーゆー事もなく、Laravelとかチョイスされる事は多いんじゃないかと思います*2

でまぁ、別にFWがLaravelでもCakePHPでもSlimでもSymfonyでもYiiでもPhalconでもZend FrameworkでもFuelPHPでもCodeIgniterでもBEAR.Sundayでも ちいたん でもKonahaでもEthnaでもMojaviでもPiece Frameworkでもいいのですが*3 *4
割と気になるのが「そのFWのサポート寿命」と「メジャーバージョンアップにおけるハードルの高さ」。

勿論理想としては「FWのバージョンアップに合わせてこまめに調整とリファクタをしてちゃんとついていく」事で、それが出来るんならどのFW使ってもよいかと思うんですよ……次点は「FWのサポートが終了する時」で、その時に「どうするの?」ってあたりがちゃんとしているんなら。

ただ、割と見かけるのが
・FWのバージョンアップはしない(マイナーとメンテナンスは上げるけどメジャーは上げない、とか、メンテナンスは上げるんだけどマイナーメジャーは上げない、とかも含む)
・FWが「新しいバージョンのPHP」に対応していないからPHPのバージョンを上げない
ってスタンス。

いや別に「3ヶ月で終了するサービス」ならよいんですよ?
ただ、それが「年単位でビジネス上の計画も立っている」ような、それなりに末永いサイト(になるといいなぁ)である場合、ちょっとそれは「どうなんだろう???」と。

ちなみに最近「ローンチのタイミングですでに"Security fix期限が切れている"バージョンのFW*5をお使いになりやがってらっしゃる」ケースとかを以下検閲削除。
……じゃぁそれが「珍しい話か?」と問われると「割と見かける」んですよねぇ……… 勘弁してくれ orz

そーゆーのを数見てしまうと。
ふと「FW使わないで作ったほうが、PHPのバージョンはコンスタントに上げる事が条件だけど、より安全に回せるんじゃなかろうか?」とか思う事があったりするのですよ。
……いやまぁ「PHPのバージョンをコンスタントに上げる」事自体のハードルが高いんだろうけど……多分……高いかなぁ?……おいちゃんのやり方だとあんまり高くないっていうかマジでコンスタントに上げてるけど困った事ないし……そのうちその辺のノウハウ書こうかしらん? おいといて。
下手にFWに「振り回されて」「FW都合で"バージョン上げられません」って言うくらいなら「plainなPHPで開発してみたら?」とか思う事はまぁ、あったりするわけでございます。
まぁ「FWがやってくれていること」を全部自前、なので、相応のハイスペックスキルが求められますけどねぇ……*6 *7

とまぁ色々と考えたりするので。
ナニが言いたいかっていうと「運用までちゃんと想定して開発しようず、選択しようず」って話なんだけど、その辺、割と浸透してないような気がするんですよねぇ……という、「気付いて欲しい」ネタ。

ちな、Laravelは「LTS単位で乗り換えるとよいのかしらん?」って思ってたら、Laravelに熟達している複数人から「やめとけ。コンスタントにこまめに上げるのが一番楽」って言われたので、そーゆーもんらしいす。
おいちゃんは最近、Slimがお気に入り(自前のFW、どうしましょうかねぇ???)。

Slim、3から4へは「まぁまぁ書き換えがいる」んだけど「coreロジックは多分ほとんどいらない」ので……ニーズがあるんなら「Slim 3から4への移植方法」とか、どこかで書いてもよいのかも、って感じでございます。

……って話が、もうちょっと活発に行われるとよいんですけどねぇ。
PHPカンファレンスとかでしゃべれば?」ごもっともでございます orz *8

*1:いつものこと、って気がせんでもないんだが、まぁ

*2:個人的には嫌いですが、まぁ

*3:順不同

*4:色々微妙だったり懐かしかったりするんだが気にしちゃいけない

*5:えぇLaravelでございましたともさ

*6:古参だと割といけそうな気もするが

*7:……思い返すに「絶対無理だろう」と思われるメンツも相応にいらっしゃるので以下検閲削除

*8:なんとか参加は出来そうですが、ちょい前まで、参加できるかも微妙だったのですよ orz