ログ日記

作業ログと日記とメモ

EC-CUBEをフルスクラッチでリニューアルした話

もう数年前の話なんだけれど、メモを残していなかったのでここに書き留めておく。



今から数年前、当時はPHP5でEC-CUBE2を使っていた。
そして、OSの更新期限が迫ってきていて、OS更新と共にPHP7にバージョンアップする必要があった。

EC-CUBE 2 は PEAR を使っていて、クラス名などもnamespaceが無い時代の作り方で作られていた。


ゼロから作り直すか、別のオープンソースのシステムを使うか、新しいEC-CUBEを使うか、有志が EC-CUBE 2 の PHP7対応をやってくれるまで待つか、自分でPHP7対応をするか、どうするか…。

心情としては丸ごと作り変えたい。
でもどれだけかかるか分からないので、最初は動いているプログラムを少しずつ移動させる形でPHP7対応をやっていき、全てのコードをチェックが終わったら移行完了、だいたい半年ぐらいかなという気持ちだった。



しかし実際に取り掛かってみると、DBの構造ごと丸ごと変更しないとダメだという気持ちが大きくなってきた。
そもそも今取り掛かっているECサイトは物販では無いのだ。予約期間の日数指定みたいな形でかなりのカスタマイズが入っている。新しいEC-CUBEや他のOSSを利用するくらいならゼロから作った方が早い、ぐらいのカスタマイズが入っている。

それを無理やり物販用のDBとプログラムを使って強引につじつまを合わせていることがずっと気になっていたのだった。



早く移行を済ませるよりもじっくり設計していいという了解を得て、旧DBを完全に捨てて新しいDB設計から始めることにした。
時間があるなら独自システムにした方が絶対いいしね。
新しい画面を追加するためにDBにページを登録なんていうまどろっこしいことをしなくても良くなるし、CSVの項目を増やすために複雑なCSV項目定義DBをチェックする必要もなくなる。
汎用性のあるシステムは、その汎用性を維持するためにかなりの労力が使われているからね。やりたいこと特化でいいならだいぶシンプルな作りになる。

最新のOSSならプラグインやhookが充実しているだろうけど、EC-CUBEの新しいバージョンでは過去にできたことが出来なくなっていたりして、むしろ機能が制限されていそうだった。他のOSSWordPressほどじゃないだろうしね。その点WordPressはhookできない箇所を探す方が難しいぐらいに至るところにカスタマイズポイントが設定されていて、そういう意味ではかなりすごいツールだ。


新しいDBを作るため、まずはER図を書くツールでEC-CUBEのDBをインポートして、見やすく並べ替える。
自分が把握してないテーブルが結構あったし、使っていないテーブルも結構あった。

旧DBのことはひとまず考えずに、今から新システムのDBを設計するという気持ちでやっていった。データの移行はプログラムで頑張る。DBはMySQLからPostgreSQLに変更。
制約を厳しくしたら多くの箇所でエラーが出たけれども、そこもプログラムで修正しながらPostgreSQLにインサートするように書いていく。

移行プログラムを書いているときに、既存DBのデータバグもいくつか発見した。旧DBで直せるものは先に直しておく。




新しいプログラムは
PHPの開発環境とライブラリと振り返りと近況 - ログ日記
ここに書いたように特にフレームワークを使わずに作った。
今でも毎日開発しているのでPHP9万行、JavaScript 1万7千行、テスト1万6千行ぐらいになってる。



作っているときは、自分の把握していない機能が結構あって、ちゃんと見積もれていなかった。主に集計やCSV、PDF系の機能をあまり把握していなかった。機能追加することも修正することも無かった機能についてはあまり把握してなかった。機能があること自体は知っていた(知らないものも少しあった)けれど、コードの具体例が頭に入ってなかったというか。


結局手を付けてからどれくらいかかったのか、たぶん1年半ぐらいだと思うが…。長い期間かけてやっとフルスクラッチのリニューアルが完了した。



実はDB設計で妥協したところがあって、本当はもう少し納得のいくまでDB設計を考えたかったんだけれど、DBが決まらないとプログラムも画面も何も進まないのでね…。
今思えば、少しもやもやしてベストではないけれど、悪くはない、物販DBを流用するよりは格段に良くなった、と言えるだろうか。

仕様を決められずに諦めた新機能とか、古い設計を引きずらざるを得ない部分も少しあったりして。
決済プログラムなんかは決済会社のライブラリが無かったのでEC-CUBEプラグインを見ながら自分でリトライ処理を書いたりして、なかなか神経を使う作業だった。



当時は妥当に思えた選択でも、今考えたら別の方法にしたい大きなものに、テンプレートエンジンの利用がある。
htmlspecialcharsを手で書くのもミスが出るし、普通はテンプレートエンジンを使うだろうということで、BladeOneを使うことにしたのだった。
今ならhtmlspecialcharsをチェックする自作のPHPStanプラグインがあるので、テンプレートエンジンを使う方がバグが起こりやすくなっている。管理画面で新しく作るページはテンプレートエンジン無しの素のPHPに置き換えていっている。

あとは、ちょっとDDDを試した痕跡がコードにいくつか残っている。最初に設計を固めずに、中途半端に試してしまった…。なんちゃってDDDの箇所がある。まあこれは最初から上手い設計ができるわけもなく試してみないと分からなかった部分でもあるので、仕方ないといえば仕方ない。



リニューアル時の切り替えは、特にトラブルが無かった。少しの時間だけ停止して、その間にDB移行からIP切り替えでリニューアル後のサイトを表示。何の問題も無かったと思う。
それでもしばらくは仕様考慮漏れの修正をやっていたかな?あまり切羽詰まった記憶がないので緊急の案件は無かったんだろう。

そんな感じで、EC-CUBEをリニューアルした新しいシステムは今も開発し続けている。

PHPStanとPsalmを混ぜた機能が欲しい

PHPStanはテンプレート+callableのネストに対応していた。

<?php

/**
 * @template A
 * @template B
 * @template X
 * @param callable(A,B):X $f
 * @return callable(A):(callable(B):X)
 */
function f2(callable $f)
{
    return function($a) use ($f){
        return function ($b) use ($f, $a){
            return $f($a, $b);
        };
    };
}

/**
 * @template A
 * @template B
 * @template C
 * @template X
 * @param callable(A,B,C):X $f
 * @return callable(A):(callable(B):(callable(C):X))
 */
function f3(callable $f)
{
    return function($a) use ($f){
        return function($b) use ($f, $a){
            return function($c) use ($f, $a, $b){
                return $f($a, $b, $c);
            };
        };
    };
}

function str_repeat_split(string $s, int $time, string $separator):string
{
    $arr = [];
    for ($i = 0; $i < $time; $i++){
        $arr[] = $s;
    }
    return implode($separator, $arr);
}
function test(): string
{
    $f = f3('str_repeat_split');
    $g = $f('abc');
    $h = $g(1);
    // $h(1);
    $h("\n");

    $i = f2('str_repeat');
    //$j = $i('abc')('def');
    $j = $i('abc')(2);

    $k = f2('str_repeat')('abc');
    //$l = $k('def');
    $l = $k(2);

    // $m = f2('str_repeat')('abc')('foo');
    $n = f2('str_repeat')('abc')(2);
    return nl2br($n);
}

コメントを外すと正しいエラーが報告される。

ここで、括弧がちょっとめんどいので

<?php

$n = f2('str_repeat')('abc')(2);
$n = f2('str_repeat', 'abc', 2);

引数と関数呼び出しの区別を無くして、並べて呼び出せるようにしたい。

機能だけなら
github.com
昔作ったんだけど、型チェックが効かない。


PsalmのConditional return typesの機能を使ってfunc_num_argsで戻り値の型を条件分岐させたい。
でもPsalmは

 * @return callable(A):(callable(B):(callable(C):X))

みたいな複雑な型に対応してないっぽい。



github.com
PHPStanにもconditional return typesの機能を付ける話は出ているみたい。

That's what this open feature request asks for, it's on my roadmap to implement this.

issue登録者が間違ってクローズしたものを作者が再オープンしていたので、しばらく待てば実装されるんだろうか。
もうすぐ1年経ってしまうけれども。

TCPDFで円とベジェ曲線で図形を描く

TCPDFでちょっとした図形を入れたい場合、画像を使わずに直接PDFに出力してしまう方法がある。
TCPDFのソースとマニュアルを見ていたらベジエ曲線が簡単に描けるようなのでやってみた。
ベジェ曲線?どっち?昔聞いたときはベジエ曲線と発音されていたような気がする。)


まずは円弧を描く。

<?php
// (snip)
    $x = $pdf->GetX();
    $y = $pdf->GetY();
    $w = 20;
    $h = 20;

    $cx = $x + $w / 2;
    $cy = $y + $h / 2;
    $cw = $w / 2;

    $start = 0;
    $end = $start + 255;

    $pdf->Circle($cx, $cy, $cw, $start, $end, 'D');

角度は右が0度で半時計回りに指定するようだ。
これで右から255度分の円弧を描ける。

ベジエ曲線は、開始座標+3座標を指定する。

<?php
// (snip)
    $ax = $cx + $cw * cos(deg2rad($end));
    $ay = $cy - $cw * sin(deg2rad($end));
    $aw = $x + $w - $ax;

    $ax1 = $ax + $aw / 2 * $sign;
    $ax2 = $ax + $aw * $sign;
    $ax3 = $ax + $aw * $sign;
    $ay1 = $ay;
    $ay2 = $ay + $w / 3;
    $ay3 = $ay + $w / 2.5;
    $p1 = [$ax1, $ay1, $ax2, $ay2, $ax3, $ay3];

    $pdf->Polycurve($ax, $ay, [$p1], 'D');

円弧の端の座標をsinとcosで求める。
角度は$endをラジアンに変換する。deg2radっていう組み込み関数があったんだね。

そこからなめらかになるように、座標を配列で指定して Polycurve 関数で出力する。[$p1]となっているところは、複数繋げると複雑な曲線が描ける。

tcpdf-bezier

こんな感じに線を引けた。

中を塗る場合は、反転したベジエ曲線切らずに繋げて空白を埋めるように工夫する必要がある。

<?php

require_once 'vendor/autoload.php';


function drawCircle($pdf, $reverse)
{
    $x = $pdf->GetX();
    $y = $pdf->GetY();
    $w = 20;
    $h = 20;

    $cx = $x + $w / 2;
    $cy = $y + $h / 2;
    $cw = $w / 2;

    $start = 0;
    $end = $start + 255;

    $pdf->Circle($cx, $cy, $cw, $start, $end, 'D');
}

function drawBezier($pdf, $reverse)
{
    $x = $pdf->GetX();
    $y = $pdf->GetY();
    $w = 20;
    $h = 20;

    $cx = $x + $w / 2;
    $cy = $y + $h / 2;
    $cw = $w / 2;

    if (!$reverse){
        $start = 0;
        $end = $start + 255;
    }else{
        $start = 285;
        $end = 180 + 360;
    }

    if (!$reverse){
        $sign = 1;
        $point = $end;
    }else{
        $sign = -1;
        $point = $start;
    }

    $ax = $cx + $cw * cos(deg2rad($point));
    $ay = $cy - $cw * sin(deg2rad($point));


    if (!$reverse)
        $aw = $x + $w - $ax;
    else
        $aw = $ax - $x;
    $ax1 = $ax + $aw / 2 * $sign;
    $ax2 = $ax + $aw * $sign;
    $ax3 = $ax + $aw * $sign;
    $ay1 = $ay;
    $ay2 = $ay + $w / 3;
    $ay3 = $ay + $w / 2.5;
    $p1 = [$ax1, $ay1, $ax2, $ay2, $ax3, $ay3];

    $pdf->Polycurve($ax, $ay, [$p1], 'D');
}

$pdf = new TCPDF();
$pdf->AddPage();
$pdf->SetFont('genshingothic', '', 8);

$color = [255,100,100];

$pdf->SetFillColor(...$color);
$pdf->SetDrawColor(...$color);

$pdf->Cell(0, 0, '円弧を描く', 0, 1);
drawCircle($pdf, false);

$pdf->SetY($pdf->GetY() + 30);

$pdf->Cell(0, 0, '円弧を反転してもう一つ描く', 0, 1);
drawCircle($pdf, false);
$pdf->SetX($pdf->GetX() + 20);
drawCircle($pdf, true);

$pdf->SetY($pdf->GetY() + 30);
$pdf->Cell(0, 0, 'ベジエ曲線を描く', 0, 1);
drawCircle($pdf, false);
drawBezier($pdf, false);

$pdf->SetY($pdf->GetY() + 30);
$pdf->Cell(0, 0, '円弧とベジエ曲線を反転して描く', 0, 1);
drawCircle($pdf, false);
drawBezier($pdf, false);
$pdf->SetX($pdf->GetX() + 20);
drawCircle($pdf, true);
drawBezier($pdf, true);



$pdf->Output();

PHPでDDDのようなClean Architectureのような単純なLayeredとIoC

最近PHPコードをリファクタリングしている。

DDDやクリーンアーキテクチャの記事や
5年間 Laravel を使って辿り着いた,全然頑張らない「なんちゃってクリーンアーキテクチャ」という落としどころ
この辺の記事を読んだりして、自分なりの最適解を探している。


まだやり始めたところだけど、現状をメモしておく。


まず、PHPで今書いているコードは基本的にUIに関連する処理が最も多く、ドメインロジック的なものよりプレゼンテーションロジックが多いということをふまえて。

src/
└── Ddd
    ├── Application
    │   ├── UseCase
    │   │   └── Cart
    │   └── ViewUseCase
    │       ├── Cart
    │       └── Product
    ├── Domain
    │   ├── Cart
    │   └── Product
    └── Io
        └── Infrastructure
            ├── Cart
            ├── Order
            └── Product

アプリケーション層のユースケースと同じ階層に表示のためのユースケースを入れる。
これは、表示のための文字列を整形する処理が結構な分量あり、それをテストしたいため。CQRS(というかCQS)のクエリ結果を整形して、それが正しく整形されたかチェックしたい。なので複雑なクエリもアプリケーション層に入れる。
ViewUseCaseではデータの保存は無し。
「保存 => 結果を取得して表示」の場合はまずUseCaseで保存して ViewUseCase のクエリで表示用のデータを取得する。

また、リポジトリはアプリケーション層に所属する。ドメインリポジトリを扱わない。
他のDDDなどの解説で、ドメインロジックがfindやstoreを実行しているけど、それってユースケースでは?という単純な疑問がある。あとユースケースに沿ったSQLを書きたくて、ドメイン用の全部入りSQLは発行したくないので。
それにstrict layeredとの相性も悪そうだった。
ドメインロジックは、ロジックというか型変換に徹する。DomainではなくてType というネームスペースにしようか迷ったぐらい。基本的にはユースケースのコードが膨らむことになる。
そこは諦めてテストを書く。
そうすると結局、昔ながらのレイヤー構造+IoCリポジトリだけ依存関係を逆転させた普通の構造になったかもしれない。



テストはユースケースに対して行う。ドメイン(というかここでは細かい型用のクラス)はリファクタリングしたいのでテストを細かく書かない。
ユースケースにプレゼンテーションを含めたことにより、ブラウザが表示するUIと直結しているので、UIのテストも多少は兼ねることになっている。


PHPのファイルも込みで並べると

└── src
    ├── Controller
    │   ├── Cart
    │   │   └── IndexPage.php
    │   └── Product
    │       ├── DetailPage.php
    │       └── IndexPage.php
    └── Ddd
        ├── Application
        │   ├── UseCase
        │   │   └── Cart
        │   │       ├── AddCartUseCase.php
        │   │       ├── CartItemRow.php
        │   │       ├── CartRepository.php
        │   │       └── CartRow.php
        │   └── ViewUseCase
        │       ├── Cart
        │       │   ├── CartDto.php
        │       │   ├── CartItemDto.php
        │       │   ├── CartQuery.php
        │       │   ├── GetCartPageUseCase.php
        │       │   └── RowDtoConverter.php
        │       └── Product
        ├── Domain
        │   ├── Cart
        │   │   ├── Cart.php
        │   │   └── CartItem.php
        │   └── Product
        └── Io
            └── Infrastructure
                ├── Cart
                │   ├── CartQueryImpl.php
                │   └── CartRepositoryImpl.php
                ├── Order
                └── Product

こんな感じになっている。
UseCaseからViewUseCaseへの参照は無し。逆はあり。

Row はリポジトリからのマッピングクラス、Dto はUIへのマッピングクラスになっている。
本当はここもインターフェースと実装クラスを分ける方が綺麗だけど、大量にあるカラムのgetterを用意するのが面倒なので、オブジェクトのpublicプロパティにした。そうするとユースケースがDBのカラムを少し知っていることになるけれど、これは妥協で。インフラ層で詰め替えてもいいけど、インフラは基本的にコードを少なくしたくて、何の加工もせずにそのまま返したい。加工はアプリケーション層でやって、それはテスト対象になる。本気で切り離してもあまりメリットはなく、変更を追いやすく修正しやすければ良いので。

基本的にstrict layered、つまり階層を飛び越えて参照しない。
コントローラーからはドメインを参照しない。ここを甘くするとテンプレートでメソッドを呼び出したりしてしまうので。
テンプレートエンジンに渡すのは、ユースケースから返ってきたDtoをそのまま渡す。
コントローラーでフォームの処理とエラーチェックをしているけど、本当は Io\Form 等に入れたい。まだそこまで行ってない。

とりあえず今はこんな感じ。

PHPの静的解析 Phan/Psalm/PHPStan の違い

エラーチェックのためにPHPで静的解析ツールをする場合、Phan, Psalm, PHPStan を使えば良いということは検索ですぐ出てくるのだが、どれを使えばいいのか。
それぞれのツールで検知できるものが微妙に異なっているので、全部使うのが安全ではある。

それでも動作の違いや思想の違いを知っておきたいので調べる。

github.com
github.com
github.com

自分がエラーを検知してほしいまたは正しく型チェックしてほしいと思うコードを用意してそれぞれチェックしてみるのが手っ取り早い。
思いつくコードを調べてみた。
設定は基本的に初期のまま。PhanはREADMEにある設定、Psalmはレベル1、PHPStanはレベル8にした。(PsalmとPHPStanで検知レベルの向きが逆である)

<?php
/**
 * @param array<int,string> $arr
 */
function test1(array $arr, ?int $index): ?string
{
    $ret = null;
    if (isset($arr[$index]))
        $ret = $arr[$index];
    return $ret;
}
Phan Psalm PHPStan
PhanTypeMismatchDimFetchNullable エラーなし エラーなし

Phan は配列のインデックスに nullable な値を指定するとエラーになる。

<?php
/**
 * @param array<int,string> $arr
 */
function test1(array $arr): ?string
{
    if (isset($arr[null]))
        return $arr[null];
    return null;
}
Phan Psalm PHPStan
PhanTypeMismatchDimFetchNullable NullArrayOffset エラーなし

こちらはPsalmでもエラー。
PHPの文法的にはOKだが、null は自動的に空文字 '' に変換されるので、PHPStanも正しくはない。



<?php
/** @return mixed */
function test2()
{
    $a = $_GET['a'];
    return $a;
}
Phan Psalm PHPStan
エラーなし MixedAssignment エラーなし

Psalm は mixed の扱いに厳しい。

<?php
function test3(): int
{
    throw new Exception('err');
    return 0;
}
Phan Psalm PHPStan
PhanPluginUnreachableCode エラーなし Unreachable statement

Psalm は未到達コードをチェックしないようである。
https://github.com/vimeo/psalm/issues/403#issuecomment-354441656
オプションを付ければチェックされるようだが、デフォルトではreturnやexitの後のコードはチェックされないようだ。

<?php
/**
 * @template T of DateTimeInterface
 * @param T $d
 * @return T
 */
function test4($d)
{
    $d->format('d');
    return $d;
}
Phan Psalm PHPStan
PhanUndeclaredClassMethod エラーなし エラーなし

Phanはジェネリクスに of が使えないようだ。

<?php
function test4(): void
{
    var_dump(1);
}
Phan Psalm PHPStan
エラーなし ForbiddenCode エラーなし

Psalm は var_dump がエラーする。
エラーを見る限り、危険な関数の使用自体がチェックされるようである。

<?php
/**
 * @template T of DateTime
 * @template U of DateTime
 * @param iterable<T,iterable<string,U>> $dss
 * @param callable(string):int $f
 * @return ?int
 */
function test5($dss, $f)
{
    foreach ($dss as $k => $ds){
        foreach ($ds as $s => $d){
            $diff = $k->diff($d);
            if ($diff->format('%d') == 10)
                return $f($s);
        }
    }
    return null;
}
Phan Psalm PHPStan
PhanUndeclaredClassMethod エラーなし エラーなし

Phan は of が使えないのでエラー。PsalmもPHPStan も入れ子ジェネリクスとコールバック関数の型を理解できる。試しに戻り値の型を ?string にするとエラーになる。

<?php
function test6($a)
{
    echo 'a';
}
Phan Psalm PHPStan
エラーなし MissingParamType, MissingReturnType parameter $a with no typehint specified, no return typehint specified

PHPStanの方がエラーが詳しいような書き方になってしまったがそうではなく、Psalm のようなエラーの型がないだけである。Psalmも詳しいメッセージは出力されている。
Psalm と PHPStan は型が必須のようだ。

<?php
/**
 * @template T
 * @param class-string<T> $c
 * @return T
 */
function test7($c)
{
    return new $c;
}
Phan Psalm PHPStan
エラーなし MixedMethodCall エラーなし

Psalm は mixed に厳しい。コンストラクタの引数が分からないのでnew できるかは不明というのは正しい。

<?php
interface Foo {
    public function bar(string $bar, int $baz): int;
}
class FooImpl implements Foo {
    public function bar(string $_, int $i): int
    {
        return $i++;
    }
}
Phan Psalm PHPStan
PhanParamNameIndicatingUnused ParamNameMismatch エラーなし

PhanとPsalmはパラメーター名が異なればエラー。
今までは単なるコード整形だったけれども、PHP8 で named parameters が来ると意味が変わってくる。

<?php
abstract class P
{
    abstract protected function a(DateTimeImmutable $d): DateTimeInterface;
}
class C extends P
{
    protected function a(DateTimeInterface $d): DateTimeImmutable
    {
        return new DateTimeImmutable($d->format('y-m-d'));
    }
}
Phan Psalm PHPStan
PhanParamSignatureRealMismatchParamType, PhanParamSignatureRealMismatchReturnType MethodSignatureMismatch エラーなし

PHPStanだけPHPの文法に合うようになっている。 (※コメントも参照)
PhanとPsalmはわざとエラーにしている?でもエラーメッセージを見ると共変性と反変性をあえて禁止にしているようには見えない。

<?php
function test8(int $i): bool
{
    switch(true){
    case $i <= 10:
        return true;
        break;
    case $i > 10 && $i <= 20:
        return false;
        break;
    default:
        return true;
        break;
    }
    return true;
}
Phan Psalm PHPStan
PhanPluginUnreachableCode エラーなし Unreachable statement, Comparison operation ">" between int<11, max> and 10 is always true.

Psalm はデッドコードをチェックしない。
PHPStanは、条件10がかぶっていることを見つけてエラーにする。

<?php
function test9(string $s): string
{
    return sprintf('foo%s, %s', $s);
}
Phan Psalm PHPStan
PhanPluginPrintfNonexistentArgument エラーなし Call to sprintf contains 2 placeholders, 1 value given.

PhanとPHPStan はsprintfのフォーマットをチェックする。

<?php
function test9(string $s): string
{
    return sprintf('foo%s, %s', $s, 1);
}
Phan Psalm PHPStan
PhanPluginPrintfIncompatibleArgumentType エラーなし エラーなし

Phanは更に型までチェックする。
PHPStan も実は型をチェックしていて、1をnew DateTimeに変えるとエラーになる。bool|float|int|string|null を受け付けるようだ。



さて、大まかな違いが見えてきた。
Phanは全体的にバランス良くチェックしようとしているが、ジェネリクスの機能が弱い。関数のシグネチャに型がなくてもスルーする。レガシーコードに利用するのに向いていそうだ。
Psalmはmixedに厳しく、全ての型を指定しろという強い意志を感じる。新規プロジェクトに向いている。
PHPStanはPHPが許す文法には寛容に見えるし、mixedをスルーして共変性と反変性を正しく実装しているのは元々のPHPの動作を尊重しているからかもしれない。



※ この記事はQiitaの PHP その2 Advent Calendar 2020 の5日目の記事、今空いてたから登録した。


※ 12/7 追記
コメントをもらったので再チェック。
psalm の使い方が分かってないのかも…。

% ./vendor/bin/psalm --clear-cache
Cache directory deleted
% ./vendor/bin/psalm --version
Psalm 4.2.1@ea9cb72143b77e7520c52fa37290bd8d8bc88fd9
%
% ./vendor/bin/psalm
Scanning files...
Analyzing files...

E

ERROR: MethodSignatureMismatch - src/functions.php:9:5 - Method C::a with return type 'DateTimeImmutable' is different to return type 'DateTimeInterface' of inherited method P::a (see https://psalm.dev/042)
    public function a(DateTimeInterface $d): DateTimeImmutable


ERROR: MethodSignatureMismatch - src/functions.php:9:41 - Argument 1 of C::a has wrong type 'DateTimeInterface', expecting 'DateTimeImmutable' as defined by P::a (see https://psalm.dev/042)
    public function a(DateTimeInterface $d): DateTimeImmutable


------------------------------
2 errors found
------------------------------

Checks took 0.44 seconds and used 57.117MB of memory
Psalm was able to infer types for 100% of the codebase
%
% cat src/functions.php
<?php

abstract class P
{
    abstract public function a(DateTimeImmutable $d): DateTimeInterface;
}
class C extends P
{
    public function a(DateTimeInterface $d): DateTimeImmutable
    {
        return new DateTimeImmutable($d->format('y-m-d'));
    }
}

https://psalm.dev/r/52f57aeabe と結果が異なる。

テンプレートエンジンを使うのをやめたい

BladeOneをアップデートしたら、include時の変数割り当てのフローが変わったのか、既存変数が上書きされてエラーが出るようになってしまった。
本家Bladeではこういったことは起きない?
仕方がないのでバージョン固定した。

ソースを追っても何となく不毛なことになりそうなので、少しずつ外していくことを目指したい。
PHPの型が強化されてきたことだし、そろそろデータの受け渡しに配列や動的変数を使うことをやめたい。かなり減らしたけど、まだ@param mixedと書いている部分も多い。


素のPHPを使って、かつnamespaceを使って、型チェックもして、ビューからロジックはなるべく呼ばないようにして、などという設計に関する蓄積がない。
テンプレートエンジン無しっていうのは手を抜くためとか余計なものを入れないとかの理由が多かったように思う。


と思っていたのだけど
teratail.com
同じような人も居るには居るのか。