LaravelのEloquentModelを継承したクラスで定義できるアクセッサーはとても便利です。しかし、どうして、あたかもDBテーブルにある項目のようにアクセスできるのか不思議に思ったことありませんか? 

アクセッサー

例えば、以下のようにUserのモデルにgetEmailDomainAttribute()を定義します。

namespace App;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

// 注意:AuthenticatableのもとのUserクラスは、EloquentのModelクラスを継承したクラスです。
class User extends Authenticatable
{
    use Notifiable;
...
    // users.emailのDB項目のドメイン名を返す
    public function getEmailDomainAttribute()
    {
        $array = explode('@', $this->email);
        return array_pop($array);
    }
}

以下のように、getEmailDomainAttribute()ではなく、email_domainと、モデルの属性のようにアクセスできます。

$ php artisan tinker
Psy Shell v0.9.12 (PHP 7.2.16 — cli) by Justin Hileman
>>> factory(App\User::class)->create();
=> App\User {#3039
     name: "近藤 篤司",
     email: "manabu.takahashi@example.net",
     email_verified_at: "2020-02-13 04:27:15",
     updated_at: "2020-02-13 04:27:15",
     created_at: "2020-02-13 04:27:15",
     id: 1,
   }
>>> User::find(1)->email_domain;
[!] Aliasing 'User' to 'App\User' for this Tinker session.
=> "example.net"
>>> 

どうしたら、このようなことできるのでしょうか?

マジックメソッド

Laravelのアクセッサーのトリックは、実はPHPのマジックメソッドの__get()です。

まず、以下のようにFooというクラスで、__get()__set()の2つのマジックメソッドのメソッドを定義します

namespace App;

class Foo {

    protected $attributes = [];

    public function __get( $key )
    {
        return $this->attributes[ $key ];
    }

    public function __set( $key, $value )
    {
        $this->attributes[ $key ] = $value;
    }
}

さて、これをtinkerでテストしてみると、

$ php artisan tinker
Psy Shell v0.9.12 (PHP 7.2.16 — cli) by Justin Hileman
>>> $foo = new App\Foo;
=> App\Foo {#3018}
>>> $foo->name = '名前'; // Foo::__set()がコールされる
=> "名前"
>>> $foo->name; // Foo::__get()がコールされる
=> "名前"

Laravelはこのメカニズムを拡張したものです。以下のLaravelのModelの定義を見てください。

namespace Illuminate\Database\Eloquent;

use ArrayAccess;
use Exception;
use Illuminate\Contracts\Queue\QueueableCollection;
use Illuminate\Contracts\Queue\QueueableEntity;
use Illuminate\Contracts\Routing\UrlRoutable;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Contracts\Support\Jsonable;
use Illuminate\Database\ConnectionResolverInterface as Resolver;
use Illuminate\Database\Eloquent\Relations\Pivot;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection as BaseCollection;
use Illuminate\Support\Str;
use Illuminate\Support\Traits\ForwardsCalls;
use JsonSerializable;

abstract class Model implements Arrayable, ArrayAccess, Jsonable, JsonSerializable, QueueableEntity, UrlRoutable
{
    use Concerns\HasAttributes,
        Concerns\HasEvents,
        Concerns\HasGlobalScopes,
        Concerns\HasRelationships,
        Concerns\HasTimestamps,
        Concerns\HidesAttributes,
        Concerns\GuardsAttributes,
        ForwardsCalls;
...
    /**
     * 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);
    }
...

__get()__set()が定義されていますね。

__get()で使用されている、getAttribute()は、HasAttributesのトレイトで定義されています。
HasAttributeを見てましょう。

namespace Illuminate\Database\Eloquent\Concerns;

use Carbon\CarbonInterface;
use DateTimeInterface;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Database\Eloquent\JsonEncodingException;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Arr;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection as BaseCollection;
use Illuminate\Support\Facades\Date;
use Illuminate\Support\Str;
use LogicException;

trait HasAttributes
{
...
    /**
     * Get a plain attribute (not a relationship).
     *
     * @param  string  $key
     * @return mixed
     */
    public function getAttributeValue($key)
    {
        $value = $this->getAttributeFromArray($key);

        // If the attribute has a get mutator, we will call that then return what
        // it returns as the value, which is useful for transforming values on
        // retrieval from the model to a form that is more useful for usage.
        if ($this->hasGetMutator($key)) {
            return $this->mutateAttribute($key, $value);
        }

        // If the attribute exists within the cast array, we will convert it to
        // an appropriate native PHP type dependant upon the associated value
        // given with the key in the pair. Dayle made this comment line up.
        if ($this->hasCast($key)) {
            return $this->castAttribute($key, $value);
        }

        // If the attribute is listed as a date, we will convert it to a DateTime
        // instance on retrieval, which makes it quite convenient to work with
        // date fields without having to create a mutator for each property.
        if (in_array($key, $this->getDates()) &&
            ! is_null($value)) {
            return $this->asDateTime($value);
        }

        return $value;
    }
...
    /**
     * Get the value of an attribute using its mutator.
     *
     * @param  string  $key
     * @param  mixed  $value
     * @return mixed
     */
    protected function mutateAttribute($key, $value)
    {
        return $this->{'get'.Str::studly($key).'Attribute'}($value);
    }
...

上の、mutateAttribute()において、$keyがemail_domainなら、

return $this->getEmailDomainAttribute($value)

となります。

By khino