今回は、Laravelの動的プロパティの裏側を見てみます。つまり、どうしてインスタンスのプロパティがメソッドのコールとなるのか。

マジックメソッド

知って入ればなんのことはないですが、phpではマジックメソッドという関数をクラスで定義すれば、クラスで定義されているメソッドを、インスタンスのプロパティとしてアクセスすることができます。

簡単にマジックメソッドの説明をしましょう。

まずは、Personというクラスを作成します。Modelsのネームスペースに入れてありますが、デモのためにEloquentのModelクラスは継承しません。

namespace App\Models;

// 以下はModelを継承していないことに注意
class Person
{
    protected array $attributes = [];

    // プロパティの値を取得
    public function __get(string $key ) : ?mixed
    {
        if (isset($this->attributes[$key])) {
            return $this->attributes[$key];
        }

        return null;
    }

    // プロパティを設定
    public function __set(string $key, mixed $value)
    {
        $this->attributes[$key] = $value;
    }

    // すべてのプロパティをリスト
    public function getAttributes(): array
    {
        return $this->attributes;
    }
}

2つの_(アンダースコア)で始まるメソッド名、__get()__set()がマジックメソッドです。

tinkerで実行すると、以下のようにインスタンスに好きなプロパティ名で値を与えることができます。

> use App\Models\Person;
> $person = new Person;
= App\Models\Person {#5077}

> $person->first_name = 'Joe';
= "Joe"

> $person->last_name = 'Biden';
= "Biden"

裏では、上のクラスの定義の__set()、つまりミューテーターが実行されています。その証拠に、$attributeの中身を見ると、


> $person->getAttributes();
= [
    "first_name" => "Joe",
    "last_name" => "Biden",
  ]

と連想配列に値が入っています。

また、__get()の定義のために、

> $person->first_name;
= "Joe"

> $person->last_name;
= "Biden"

とそれぞれのプロパティの値を取り出すことが可能です。

しかし、$attributesにキーがないプロパティにアクセスすると、

> $person->full_name;
= null

とnullを返します。

プロパティがなかったらメソッドコール

さて、今度はプロパティが存在しないときにメソッドをコールしてみましょう。
プロパティがfull_nameならfullName()をコールするように対応してみました。

namespace App\Models;

use Illuminate\Support\Str;

class Person
{
    protected array $attributes = [];

    public function __get(string $key ) : mixed
    {
        if (isset($this->attributes[$key])) {
            return $this->attributes[$key];
        }

        // full_nameがキーなら、fullName()をコール
        if (method_exists($this, Str::camel($key))) {
            return $this->{Str::camel($key)}();
        }

        return null;
    }
    
    ...

    // 新規に定義
    protected function fullName()
    {
        return $this->attributes['first_name'].' '.$this->attributes['last_name'];
    }
}

実行すると、full_nameの動的プロパティの値が返ります。

> $person->full_name;
= "Joe Biden"

もちろん、LaravelのModelのクラスではいろいろなケースがあるので、その動的プロパティのメカニズムはこれよりはるかに複雑ですが、マジックメソッドを使用して対応するのは同じです。

Laravelでのマジックメソッド

さて、Laravelで本当にマジックメソッドが使用されているか確認してみましょう。

まずは、EloquentのModelクラスでは、以下のように、マジックメソッドが定義されています。

namespace Illuminate\Database\Eloquent;

...
abstract class Model implements Arrayable, ArrayAccess, CanBeEscapedWhenCastToString, HasBroadcastChannel, Jsonable, JsonSerializable, QueueableEntity, Stringable, UrlRoutable
{
...
   /**
     * Dynamically retrieve attributes on the model.
     *
     * @param  string  $key
     * @return mixed
     */
    public function __get($key)
    {
        return $this->getAttribute($key);
    }

    /**
     * Dynamically set attributes on the model.
     *
     * @param  string  $key
     * @param  mixed  $value
     * @return void
     */
    public function __set($key, $value)
    {
        $this->setAttribute($key, $value);
    }
...

このコードレベルではすっきりしていますが、興味ある方は奥までコードを辿ってみてください。いろいろなケースの対応がされています。

そして、Collectionでは、HigherOrderCollectionProxyのクラスの中で、マジックメソッドが定義されています。

namespace Illuminate\Support;

/**
 * @mixin \Illuminate\Support\Enumerable
 */
class HigherOrderCollectionProxy
{
...
    /**
     * Proxy accessing an attribute onto the collection items.
     *
     * @param  string  $key
     * @return mixed
     */
    public function __get($key)
    {
        return $this->collection->{$this->method}(function ($value) use ($key) {
            return is_array($value) ? $value[$key] : $value->{$key};
        });
    }

    /**
     * Proxy a method call onto the collection items.
     *
     * @param  string  $method
     * @param  array  $parameters
     * @return mixed
     */
    public function __call($method, $parameters)
    {
        return $this->collection->{$this->method}(function ($value) use ($method, $parameters) {
            return $value->{$method}(...$parameters);
        });
    }
}

他のどのクラスでマジックメソッドが使用されているか、vendor/laravel/のディレクトリで、grepしてみました。いろいろありますね。

framework/src/Illuminate/Routing/Route.php
framework/src/Illuminate/View/InvokableComponentVariable.php
framework/src/Illuminate/Database/Eloquent/Model.php
framework/src/Illuminate/Database/Eloquent/Builder.php
framework/src/Illuminate/Bus/Batch.php
framework/src/Illuminate/Conditionable/HigherOrderWhenProxy.php
framework/src/Illuminate/Auth/GenericUser.php
framework/src/Illuminate/Mail/Events/MessageSent.php
framework/src/Illuminate/Queue/Jobs/DatabaseJobRecord.php
framework/src/Illuminate/Support/ViewErrorBag.php
framework/src/Illuminate/Support/ValidatedInput.php
framework/src/Illuminate/Support/Optional.php
framework/src/Illuminate/Support/Fluent.php
framework/src/Illuminate/Support/Stringable.php
framework/src/Illuminate/Http/Request.php
framework/src/Illuminate/Http/Resources/DelegatesToResource.php
framework/src/Illuminate/Testing/TestResponse.php
framework/src/Illuminate/Testing/TestComponent.php
framework/src/Illuminate/Container/Container.php
framework/src/Illuminate/Collections/HigherOrderCollectionProxy.php
framework/src/Illuminate/Collections/Enumerable.php
framework/src/Illuminate/Collections/Traits/EnumeratesValues.php

参照

混乱してはいけないLaravelの動的プロパティ – Eloquent編
混乱してはいけないLaravelの動的プロパティ – Collection編
混乱してはいけないLaravelの動的プロパティ – PHP8.2で廃止となった編

By khino