Open28

PHPの小ネタメモ

ピン留めされたアイテム
DaikiSuyamaDaikiSuyama

PHP系の調べて理解した小ネタを投稿していく
過去の分を何も残していないのがもったいない
スクラップなので、情報は信じすぎないでほしい

DaikiSuyamaDaikiSuyama

laravelのvalidationでemail:strictを指定したときに落ちるパターン(括弧が入ってると落ちる)
バリデーションのルールを深追いするのもめんどいので、email:filter,strict,dnsを指定して妥協
(filterは日本語を含んでいる場合を弾く、dnsはそのメールのドメイン的が存在するか的なで@example.comとかだと落ちる)
https://github.com/egulias/EmailValidator/blob/c81f18a3efb941d8c4d2e025f6183b5c6d697307/tests/EmailValidator/Validation/NoRFCWarningsValidationTest.php

DaikiSuyamaDaikiSuyama

Contextual bindingはServiceProviderで行うが、method injectionには対応しないし、開発者が対応させる気がない。
まあ、そもそもmethod injectionのbindをServiceProviderで行うのはアーキテクチャ的には綺麗だが、運用上bindを遠いところでするのは避けた方が良いという考えもある。
(おそらく唯一の)代案としてはconstructorで通常通りmethod injectionを変数込みでやる感じで、そのクラスのメソッドで使うものをここでbindさせるもの、意味を考えたらこれがむしろ適切まである。

https://github.com/laravel/framework/issues/6177

DaikiSuyamaDaikiSuyama

phpでarrayをforeach文を回すとき、直接値を書き換えたい時は参照渡しで行う(マニュアル参照)かkeyを元にarrayにアクセスして書き換えることが可能。
keyを書き換えたいときは書き換え先のkeyの要素を作成した後に書き換え元のkeyをunsetで削除する。
基本的にforeachの中で回してる配列の処理を行うのは悪手なので注意が必要。array_系の関数で対応した方が良い。
ちなみに、PHPではおそらくforeachで回したいキーのイテレータを先に作っておいている感じなので、keyを新しく追加してもその追加したキーについてはforeach内の処理は適用されない(無限ループの可能性を回避できる)。

https://www.php.net/manual/ja/control-structures.foreach.php

DaikiSuyamaDaikiSuyama

PHP8.0から8.1へのアップグレード面白ポイント

  • enum -> 熱い
  • 継承されたメソッド内でstatic変数を使うと親クラスと共有されるように仕様変更
    • staticプロパティの場合は共有してるので、挙動が同じになった感じ
  • 必須の引数の前にデフォルト値を持つ引数を指定すると、デフォルト値を持つ引数は必須の引数として扱われる
    • function a($b=“1”, $c)でa(2)とすると$cが未定義になる
  • 暗黙のfloat->intの変換を非推奨
  • トレイトのstaticなメソッドに直接アクセスするのを非推奨

https://www.php.net/manual/ja/migration81.new-features.php

DaikiSuyamaDaikiSuyama

PHP8.1から8.2へのアップグレード面白ポイント

  • トレイトの中でconst定義可能に
  • str_splitが空文字を渡したときに空配列を返すように
    • 今までは空文字列の要素を一つ返していた
  • call_user_funcで呼び出せないcallableが非推奨
DaikiSuyamaDaikiSuyama

実装上はvoidはnullとして実装されているため、return;とした時の返り値は実装上null。つまり、voidを型宣言したとしてもnullでの実装なので戻り値はnullで受け取れてしまう。ただ、型でvoidとした時に明示的にreturn nullとするとTypeError。

DaikiSuyamaDaikiSuyama

PHPの変数はコピーオンライトなので、参照渡しの場合はむしろ低速化したりメモリを浪費したりするパターンがある。
参照渡しについて理解して使うべきだし、まずは参照渡しを避けるような書き方を意識する。
最近の言語はこの辺りの高速化を隠蔽しているので検索して見つけるのすら難しいが、この辺りにも目を配れるようにすると良い。

https://qiita.com/kkam0907/items/836c8977776453746455#comment-c432a5c14d49c1a36f43

https://ja.wikipedia.org/wiki/コピーオンライト

DaikiSuyamaDaikiSuyama

Laravelの探したいソースコードを見ようとしてもインターフェース(Contracts)で抽象化されていて見つけられないことが多い、インターフェースまで発見できているのに…という時は、vendor/laravel/framework/src/illuminate/Contracts/YYY/XXX.phpを見ているので、vendor/laravel/framework/src/illuminate/YYY/XXX.phpに具象を発見できる

DaikiSuyamaDaikiSuyama

phpにはcheck_dateという関数でグレゴリオ暦における妥当性をチェックする機能がある
Carbonは日時の扱いでよく使われるが、下記Issueでcheck_date的なのをしてからCabonのインスタンスを作る静的メソッドが作られている。現時点での最新コミットの実装を見たら、下記の感じになっていた。strict modeをfalseにすると、falseが返るようになる。trueの場合はエラーをthrowする。

https://github.com/briannesbitt/Carbon/issues/450

https://github.com/briannesbitt/Carbon/blob/65d7a4d3fb184984445faf93fad348d3f6f752da/src/Carbon/Traits/Creator.php#L453

https://www.php.net/manual/ja/function.checkdate.php

DaikiSuyamaDaikiSuyama

UnitのsetUpメソッドはdataproviderで与えた全パターンで毎回再実行されるが、DBを毎回のパターンでいじらない場合は高速化のためにsetUp内で再シーディングしたくない。
setUpでフラグ管理して行う方法を自分で書いて他の記事等でも書いていたが、付属のメソッドがあるか調べ、setUpBeforeClassを使えることに気づいた。
setUpBeforeClassを使うと、そのクラスで初めにテストケースを通過する場合のみの実行を行うことができるが、ファサードを呼ぶことができない。
ただ、Artisanのファサードを使うのは楽なので、再シーディングについてはフラグ管理で妥協する。おそらく、依存解決ができるようにすれえばファサードは呼べるが、下記PRにもあるが非推奨っぽいのでやめておく。

https://github.com/laravel/framework/issues/8459

DaikiSuyamaDaikiSuyama

laravel 9.xからSoftDeleteの仕様が一部変更になっているので、使う人は注意

一応、9.xに対応する自分の実装を貼っておく

delete_flagという命名は元のDBのカラム名がこうなってしまっているため…、自分ではどうにもできません

実装
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\SoftDeletes;

trait SoftDeletesBoolean
{
    use SoftDeletes;
    use SoftDeletesBooleanDeleteFlag;

    protected static $DELETED_AT = 'delete_flag';

    /**
     * Boot the soft deleting trait for a model.
     *
     * @return void
     */
    public static function bootSoftDeletes()
    {
        static::addGlobalScope(new SoftDeletingBooleanScope);
    }

     /**
     * Initialize the soft deleting trait for an instance.
     *
     * @return void
     */
    public function initializeSoftDeletes()
    {
        if (! isset($this->casts[$this->getDeletedAtColumn()])) {
            $this->casts[$this->getDeletedAtColumn()] = 'integer';
        }
    }

    /**
     * Perform the actual delete query on this model instance.
     *
     * @return void
     */
    protected function runSoftDelete()
    {
        $query = $this->setKeysForSaveQuery($this->newModelQuery());

        $time = $this->freshTimestamp();

        $columns = [$this->getDeletedAtColumn() => static::$DELETED];

        $this->{$this->getDeletedAtColumn()} = static::$DELETED;

        if ($this->usesTimestamps() && ! is_null($this->getUpdatedAtColumn())) {
            $this->{$this->getUpdatedAtColumn()} = $time;

            $columns[$this->getUpdatedAtColumn()] = $this->fromDateTime($time);
        }

        $query->update($columns);

        $this->syncOriginalAttributes(array_keys($columns));

        $this->fireModelEvent('trashed', false);
    }

    /**
     * Restore a soft-deleted model instance.
     *
     * @return bool|null
     */
    public function restore()
    {
        // If the restoring event does not return false, we will proceed with this
        // restore operation. Otherwise, we bail out so the developer will stop
        // the restore totally. We will clear the deleted timestamp and save.
        if ($this->fireModelEvent('restoring') === false) {
            return false;
        }

        $this->{$this->getDeletedAtColumn()} = static::$DELETE_FLAG_NO;

        // Once we have saved the model, we will fire the "restored" event so this
        // developer will do anything they need to after a restore operation is
        // totally finished. Then we will return the result of the save call.
        $this->exists = true;

        $result = $this->save();

        $this->fireModelEvent('restored', false);

        return $result;
    }

    /**
     * Determine if the model instance has been soft-deleted.
     *
     * @return bool
     */
    public function trashed()
    {
        return $this->{$this->getDeletedAtColumn()} === static::$DELETED;
    }

    /**
     * Get the name of the "deleted at" column.
     *
     * @return string
     */
    public function getDeletedAtColumn()
    {
        return static::$DELETED_AT;
    }
}
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletingScope;

class SoftDeletingBooleanScope extends SoftDeletingScope
{
    use SoftDeletesBooleanDeleteFlag;
    /**
     * Apply the scope to a given Eloquent query builder.
     *
     * @param  \Illuminate\Database\Eloquent\Builder  $builder
     * @param  \Illuminate\Database\Eloquent\Model  $model
     * @return void
     */
    public function apply(Builder $builder, Model $model)
    {
        $builder->where($model->getQualifiedDeletedAtColumn(), self::$NOT_DELETED);
    }

    /**
     * Extend the query builder with the needed functions.
     *
     * @return void
     */
    public function extend(Builder $builder)
    {
        foreach ($this->extensions as $extension) {
            $this->{"add{$extension}"}($builder);
        }

        $builder->onDelete(function (Builder $builder) {
            $column = $this->getDeletedAtColumn($builder);

            return $builder->update([
                $column => self::$DELETED,
            ]);
        });
    }

    /**
     * Add the restore extension to the builder.
     *
     * @param  \Illuminate\Database\Eloquent\Builder  $builder
     * @return void
     */
    protected function addRestore(Builder $builder)
    {
        $builder->macro('restore', function (Builder $builder) {
            $builder->withTrashed();

            return $builder->update([$builder->getModel()->getDeletedAtColumn() => self::$NOT_DELETED]);
        });
    }

    /**
     * Add the without-trashed extension to the builder.
     *
     * @param  \Illuminate\Database\Eloquent\Builder  $builder
     * @return void
     */
    protected function addWithoutTrashed(Builder $builder)
    {
        $builder->macro('withoutTrashed', function (Builder $builder) {
            $model = $builder->getModel();

            $builder->withoutGlobalScope($this)->where($model->getQualifiedDeletedAtColumn(), self::$NOT_DELETED);

            return $builder;
        });
    }

    /**
     * Add the only-trashed extension to the builder.
     *
     * @param  \Illuminate\Database\Eloquent\Builder  $builder
     * @return void
     */
    protected function addOnlyTrashed(Builder $builder)
    {
        $builder->macro('onlyTrashed', function (Builder $builder) {
            $model = $builder->getModel();

            $builder->withoutGlobalScope($this)->where($model->getQualifiedDeletedAtColumn(), self::$DELETED);

            return $builder;
        });
    }
}
<?php

namespace App\Models;

trait SoftDeletesBooleanDeleteFlag
{
    protected static $NOT_DELETED  = 0;
    protected static $DELETED = 1;
}
DaikiSuyamaDaikiSuyama

laravelのソースコードでよく見かけるtapという関数
これは、laravelのヘルパであり、tapの第一引数に与えた引数を第二引数に渡したクロージャーの引数にわたし、そのクロージャーでの処理を行なってから第一引数を返すようにできる
何か作用させたい時に使える

https://readouble.com/laravel/8.x/ja/helpers.html