🐘

Laravel 10.9.0がリリースされたので新機能や変更点の紹介

2023/05/06に公開

日本時間の昨日(2023 年 4 月 26 日)に Laravel の v10.9.0 がリリースされました。

https://github.com/laravel/framework/releases

詳細なリリース内容は上記のリリースノートにまとまっていますが、ざっと紹介していきたいと思います。

🎉 ADD

新規追加は以下の通りです。

  • 新しい HTTP ステータスのアサーションが追加された(#46841
  • キャンセルされたまたは未完了のキューバッチをすべて削除することができるようになった(#46833
  • ServeCommand.php の$passthroughVariables に IGNITION_LOCAL_SITES_PATH が追加された(#46857
  • ミドルウェアのための名前付き static メソッドが追加された((#46362

新しい HTTP ステータスのアサーションが追加された(#46841

HTTP レスポンスステータスを判定するアサーションが追加されています。

例えばこういうもの。

/**
 * Assert that the response has a 410 "Gone" status code.
 *
 * @return $this
 */
public function assertGone()
{
    return $this->assertStatus(410);
}
/**
 * Assert that the response has a 500 "Internal Server Error" status code.
 *
 * @return $this
 */
public function assertInternalServerError()
{
    return $this->assertStatus(500);
}

/**
 * Assert that the response has a 503 "Service Unavailable" status code.
 *
 * @return $this
 */
public function assertServiceUnavailable()
{
    return $this->assertStatus(503);
}

プルリクに記載されている使い方をそのまま載せるのですが、以下のように使うことでサーバーエラーの判定を行います。

$response = $this->get('/');
$response->assertGone();

$response = $this->get('/');
$response->assertInternalServerError();

$response = $this->get('/');
$response->assertServiceUnavailable();

キャンセルされたまたは未完了のキューバッチをすべて削除することができるようになった(#46833

これまでは php artisan queue:prune-batches がフラグ--unfinished=0 --cancelled=0 を処理する方法にバグがあったため、キャンセルまたは未完了のキューを削除することができなかった。

それがこのリリースで解消された。

ServeCommand.php の$passthroughVariables に IGNITION_LOCAL_SITES_PATH が追加された(#46857

修正タイトルだけ見ると、「なにが変わったんだ」という感じなのでプルリクを確認するとさらに詳細が 別のプルリク に委譲されていたので、そちらを確認しました。

そこで問題の本質が以下であることが確認できました。

Ignition エラーページには、リンクをクリックするだけで、エラーが発生したファイルを好みの IDE で開くことができる機能があります。ただし、以下のデモンストレーションビデオで確認できるように、Docker を使用する場合(少なくとも Sail の現在のセットアップでは)、Ignition はプロジェクトがホストマシン上のどこにあるかを把握していないため、/var/www/html にリンクを生成します。

どういうことかというと、みなさんも見たことがあるであろう Laravel のエラーページにはファイルをエディタで開くためのリンクがあります。(下の画像の赤枠部分)

これが Docker の場合だとファイルパスが把握できないので、ローカルマシンの/var/www/html にリンクを生成しようとするというものです。

(別のプルリクを参照すると、事象が動画付きで解説されているのでとてもわかりやすいです)

この事象が、章題のように対応することで解消されたようです。

ミドルウェアのための名前付き static メソッドが追加された(#46362

いくつかのミドルウェアに対してusingという名前の static メソッドが追加されました。

例えば src/Illuminate/Auth/Middleware/Authenticate.php の場合は以下の通りです。

src/Illuminate/Auth/Middleware/Authenticate.php
/**
 * Specify the guards for the middleware.
 *
 * @param  string  $guard
 * @param  string  $others
 * @return string
 */
public static function using($guard, ...$others)
{
    return static::class.':'.implode(',', [$guard, ...$others]);
}

使い方としてはこういうイメージみたいです。

Route::get('users', UserController::class)
    ->middleware([
        Authenticate::class, // default.
        Authenticate::class:, // specify a guard.
        Authenticate::using('web', 'another'), // specify multiple guards.
    ]);

これはこう書いたのと同じです。

Route::get('users', UserController::class)
    ->middleware([
        Authenticate::class, // default.
        "Illuminate\Auth\Middleware\Authenticate:web", // specify a guard.
        "Illuminate\Auth\Middleware\Authenticate:web,another", // specify multiple guards.
    ]);

🛠 Fixed

一点のみです。

  • 日付フォーマットのルールが ValueError をスローする問題を修正 (#46824)

日付フォーマットのルールが ValueError をスローする問題を修正 (#46824)

タイトルだけみると根本解決したのかと思ったのですが、コードを見ると単に try-catch で例外を捕まえられるようにしただけみたいです。

src/Illuminate/Validation/Concerns/ValidatesAttributes.php
foreach ($parameters as $format) {
    - $date = DateTime::createFromFormat('!'.$format, $value);
    + try {
    +     $date = DateTime::createFromFormat('!'.$format, $value);

    - if ($date && $date->format($format) == $value) {
    -     return true;
    +     if ($date && $date->format($format) == $value) {
    +         return true;
    +     }
    + } catch (ValueError) {
    +     return false;
    }
}

♻️ 変更点

  • Filestore のロックに別のディレクトリを使用できるようになった (#46811)
  • whereMorphedTo メソッドで null モデルを扱えるようになった (#46821)
  • Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable::addTimestampsToAttachment()で Carbon の代わりに pivotModel の fromDateTime を使うようになった(#46822)
  • FormRequest の rules メソッドがオプションになった (#46846)
  • mimetype がサポートされていない場合、FileFactory@image()を呼び出すと LogicException をスローするようになった (#46859)
  • ジョブリリースメソッドが Date インスタンスを受け入れるように改善した (#46854)
  • モデルが HasUlids トレイトを使用している場合、foreignIdFor を呼び出すと foreignUlid が使用されるようになった (#46876)

Filestore のロックに別のディレクトリを使用できるようになった (#46811)

php artisan cache:clear を実行するか、\Cache::flush();を呼び出すと、ファイルのロックが解除されるため、同時に実行された他のリクエストがアクセスできるようになります。

これは、アプリケーションが競合状態に陥り、予期しない結果が生じる原因となります。

この修正では以下のようにlock_pathというオプションを追加することで、この問題を回避しているようです。

(ロック処理もこのオプションで指定したパスを使用するように修正されている)

'file' => [
            'driver' => 'file',
            'path'   => storage_path().'/framework/cache',
            'lock_path' => storage_path().'/framework/lock',
        ],

whereMorphedTo メソッドで null モデルを扱えるようになった (#46821)

章題の通りです。null を指定すると、$this->whereNull($relation->getMorphType(), $boolean);の結果を返すよう修正されたようです。

src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php
public function whereMorphedTo($relation, $model, $boolean = 'and')
{
    if (is_string($relation)) {
        $relation = $this->getRelationWithoutConstraints($relation);
    }

+    if (is_null($model)) {
+        return $this->whereNull($relation->getMorphType(), $boolean);
+    }
+
    if (is_string($model)) {
        $morphMap = Relation::morphMap();

        if (! empty($morphMap) && in_array($model, $morphMap)) {
            $model = array_search($model, $morphMap, true);
        }
        return $this->where($relation->getMorphType(), $model, null, $boolean);
    }
    return $this->where(function ($query) use ($relation, $model) {
        $query->where($relation->getMorphType(), $model->getMorphClass())
            ->where($relation->getForeignKeyName(), $model->getKey());
    }, null, null, $boolean);
}

Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable::addTimestampsToAttachment()で Carbon の代わりに pivotModel の fromDateTime を使うようになった(#46822)

修正がニッチすぎて自分もイマイチピンときていないのですが、プルリクの内容を読んでみるとコミッターの方は$fresh->format で Carbon を利用することを避けて、pivotModel が従来から持っている fromDateTime を使うよう修正されたようです。

src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable.php
/**
 * Set the creation and update timestamps on an attach record.
 *
 * @param  array  $record
 * @param  bool  $exists
 * @return array
 */
protected function addTimestampsToAttachment(array $record, $exists = false)
{
    $fresh = $this->parent->freshTimestamp();
    if ($this->using) {
        $pivotModel = new $this->using;

        $fresh = $fresh->format($pivotModel->getDateFormat());
        $fresh = $pivotModel->fromDateTime($fresh);
    }

    if (! $exists && $this->hasPivotColumn($this->createdAt())) {
        $record[$this->createdAt()] = $fresh;
    }
    if ($this->hasPivotColumn($this->updatedAt())) {
        $record[$this->updatedAt()] = $fresh;
    }
    return $record;
}

FormRequest の rules メソッドがオプションになった (#46846)

この変更により、authorize メソッドのみ使用される場合、FormRequest オブジェクトの rules メソッドがオプションになりました。

こういうことが可能になるみたいです。(サンプルはテストコードの一部)

public function testRequestCanPassWithoutRulesMethod()
{
    $request = $this->createRequest([], FoundationTestFormRequestWithoutRulesMethod::class);

    $request->validateResolved();

    $this->assertEquals([], $request->all());
}

class FoundationTestFormRequestWithoutRulesMethod extends FormRequest
{
    public function authorize()
    {
        return true;
    }
}

mimetype がサポートされていない場合、FileFactory@image()を呼び出すと LogicException をスローするようになった (#46859)

章題のままです。修正は以下の通りです。

src/Illuminate/Http/Testing/FileFactory.php
public function image($name, $width = 10, $height = 10)
{
    return new File($name, $this->generateImage(
        $width, $height, pathinfo($name, PATHINFO_EXTENSION)
    ));
}

/**
 * Generate a dummy image of the given width and height.
 *
 * @param  int  $width
 * @param  int  $height
 * @param  string  $extension
 * @return resource
 *
 * @throws \LogicException
 */
protected function generateImage($width, $height, $extension)
{
+    if (! function_exists('imagecreatetruecolor')) {
+        throw new LogicException('GD extension is not installed.');
+    }

    return tap(tmpfile(), function ($temp) use ($width, $height, $extension) {
        ob_start();

        $extension = in_array($extension, ['jpeg', 'png', 'gif', 'webp', 'wbmp', 'bmp'])
            ? strtolower($extension)
            : 'jpeg';

        $image = imagecreatetruecolor($width, $height);

-        call_user_func("image{$extension}", $image);
+        if (! function_exists($functionName = "image{$extension}")) {
+            ob_get_clean();
+
+            throw new LogicException("{$functionName} function is not defined and image cannot be generated.");
+        }
+
+        call_user_func($functionName, $image);

        fwrite($temp, ob_get_clean());
    });
}

ジョブリリースメソッドが Date インスタンスを受け入れるように改善した (#46854)

こちらも章題のままです。修正は以下の通りです。

src/Illuminate/Queue/InteractsWithQueue.php
/**
 * Release the job back into the queue after (n) seconds.
 *
- * @param  int  $delay
+ * @param  \DateTimeInterface|\DateInterval|int  $delay
 * @return void
 */
public function release($delay = 0)
{
+    $delay = $delay instanceof DateTimeInterface
+        ? $this->secondsUntil($delay)
+        : $delay;
+
    if ($this->job) {
        return $this->job->release($delay);
    }
}

モデルが HasUlids トレイトを使用している場合、foreignIdFor を呼び出すと foreignUlid が使用されるようになった (#46876)

こちらも章題のままです。修正は以下の通りです。

src/Illuminate/Database/Schema/Blueprint.php
/**
 * Create a foreign ID column for the given model.
 *
 * @param  \Illuminate\Database\Eloquent\Model|string  $model
 * @param  string|null  $column
 * @return \Illuminate\Database\Schema\ForeignIdColumnDefinition
 */
public function foreignIdFor($model, $column = null)
{
    if (is_string($model)) {
        $model = new $model;
    }

-    return $model->getKeyType() === 'int' && $model->getIncrementing()
-                ? $this->foreignId($column ?: $model->getForeignKey())
-                : $this->foreignUuid($column ?: $model->getForeignKey());
+    $column = $column ?: $model->getForeignKey();
+
+    if ($model->getKeyType() === 'int' && $model->getIncrementing()) {
+        return $this->foreignId($column);
+    }
+
+    $modelTraits = class_uses_recursive($model);
+
+    if (in_array(HasUlids::class, $modelTraits, true)) {
+        return $this->foreignUlid($column);
+    }
+
+    return $this->foreignUuid($column);
}

🏁 Fine

初めてリリースノートを読み解いた感想ですが、知らなかった機能が増えたり、一方で理解が浅い部分がわかったりして結構良い勉強になることがわかりました。

また、テストコードの重要性を一層理解しました。というのは、修正内容をプルリクや issue で読んでも「?」となっているのであれば、テストコードを見ればどう使うことを想定しているのかがわかるからです。

これはどんなドキュメントよりも一番シンプルな実行例として理解の助けになると思いました。

Laravel10 系のマイナーリリースはだいたい 1 週間ごとなので、週一の Laravel リーディングとしていい題材かもしれないですね。来週以降も引き続きやっていこうと思います。

メンバー募集中!

サーバーサイド Kotlin コミュニティを作りました!

Kotlin ユーザーはぜひご参加ください!!

https://serverside-kt.connpass.com/

また関西在住のソフトウェア開発者を中心に、関西エンジニアコミュニティを一緒に盛り上げてくださる方を募集しています。

よろしければ Conpass からメンバー登録よろしくお願いいたします。

https://blessingsoftware.connpass.com/

Discussion