Help us understand the problem. What is going on with this article?

Laravel で Rails の抽象 ActiveModel みたいなやつが欲しくなった

TL;DR

車輪の再発明してしまったのでソースコードの墓場にする記事

内容

データベースを使わないけど, app/Services 配下に置くような外部 API サービスのレスポンスエンティティを Eloquent ライクに使いたいニーズがあった。

最初 Eloquent Model を継承することも考えたが,意味のないメソッドがごちゃごちゃ入ってくるのが鬱陶しいので

  • HasAttributes
  • HidesAttributes
  • GuardsAttributes

の3つのトレイトをベースに,足りないものや動作修正の必要があるものを適宜ケアして,最小限の記述で作ってみたのでここにぶん投げます。IDE でチェックしながらメソッド埋めやったのでエラーとかは起こらないはず…

<?php

namespace App\AbstractEntities;

use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Database\Eloquent\Concerns\GuardsAttributes;
use Illuminate\Database\Eloquent\Concerns\HasAttributes;
use Illuminate\Database\Eloquent\Concerns\HidesAttributes;
use Illuminate\Database\Eloquent\MassAssignmentException;
use Illuminate\Support\Collection;
use LogicException;

abstract class Entity implements Arrayable
{
    use HasAttributes, HidesAttributes, GuardsAttributes;

    public const CREATED_AT = 'created_at';
    public const UPDATED_AT = 'updated_at';

    /**
     * @var bool
     */
    public $timestamps = true;

    /**
     * @var bool
     */
    public $incrementing = true;

    /**
     * @var string
     */
    protected $primaryKey = 'id';

    /**
     * @var string
     */
    protected $keyType = 'int';

    /**
     * @var array
     */
    protected $relations = [];

    /**
     * Create an existing Entity instance.
     *
     * @param array $attributes
     * @return static
     */
    public static function hydrate(array $attributes = [])
    {
        return (new static())->forceFill($attributes)->syncOriginal();
    }

    /**
     * Create existing Entity instances.
     *
     * @param array $attributesArray
     * @return \Illuminate\Support\Collection|static[]
     */
    public static function hydrateMany(array $attributesArray = []): Collection
    {
        $collection = new Collection();
        foreach ($attributesArray as $key => $attributes) {
            $collection[$key] = static::hydrate($attributes);
        }
        return $collection;
    }

    /**
     * Create a new Entity instance.
     *
     * @param array $attributes
     */
    public function __construct(array $attributes = [])
    {
        $this->fill($attributes);
    }

    /**
     * Apply updates to an existing Entity instance.
     *
     * @param array $attributes
     * @return $this
     */
    public function apply(array $attributes)
    {
        return $this->forceFill($attributes)->syncOriginal();
    }

    /**
     * Fill the model with an array of attributes.
     *
     * @param array $attributes
     * @throws \Illuminate\Database\Eloquent\MassAssignmentException
     * @return $this
     */
    public function fill(array $attributes)
    {
        $totallyGuarded = $this->totallyGuarded();

        foreach ($this->fillableFromArray($attributes) as $key => $value) {
            if ($this->isFillable($key)) {
                $this->setAttribute($key, $value);
            } elseif ($totallyGuarded) {
                throw new MassAssignmentException(sprintf(
                    'Add [%s] to fillable property to allow mass assignment on [%s].',
                    $key,
                    get_class($this)
                ));
            }
        }

        return $this;
    }

    /**
     * Fill the model with an array of attributes. Force mass assignment.
     *
     * @param array $attributes
     * @return $this
     */
    public function forceFill(array $attributes)
    {
        return static::unguarded(function () use ($attributes) {
            return $this->fill($attributes);
        });
    }

    /**
     * Determine if the given relation is loaded.
     *
     * @param string $key
     * @return bool
     */
    public function relationLoaded(string $key): bool
    {
        return array_key_exists($key, $this->relations);
    }

    /**
     * Set the given relationship on the model.
     *
     * @param string $key
     * @param mixed $value
     * @return $this
     */
    public function setRelation(string $key, $value)
    {
        $this->relations[$key] = $value;

        return $this;
    }

    /**
     * Determine if the model uses timestamps.
     *
     * @return bool
     */
    public function usesTimestamps(): bool
    {
        return $this->timestamps;
    }

    /**
     * Get the value indicating whether the IDs are incrementing.
     *
     * @return bool
     */
    public function getIncrementing(): bool
    {
        return $this->incrementing;
    }

    /**
     * Get the primary key for the model.
     *
     * @return string
     */
    public function getKeyName(): string
    {
        return $this->primaryKey;
    }

    /**
     * Get the auto-incrementing key type.
     *
     * @return string
     */
    public function getKeyType(): string
    {
        return $this->keyType;
    }

    /**
     * Get the format for database stored dates.
     *
     * @return string
     */
    public function getDateFormat(): string
    {
        return $this->dateFormat ?? 'Y-m-d H:i:s';
    }

    /**
     * Get the database connection for the model.
     */
    public function getConnection(): void
    {
        throw new LogicException(static::class . ' is not an Eloquent Model; Database Connection is not available.');
    }

    /**
     * Dynamically retrieve attributes on the model.
     *
     * @param string $key
     * @return mixed
     */
    public function __get(string $key)
    {
        return $this->getAttribute($key);
    }

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

    /**
     * Determine if an attribute or relation exists on the model.
     *
     * @param string $key
     * @return bool
     */
    public function __isset(string $key): bool
    {
        return $this->getAttribute($key) !== null;
    }

    /**
     * Unset an attribute on the model.
     *
     * @param string $key
     */
    public function __unset(string $key): void
    {
        unset($this->attributes[$key]);
    }

    /**
     * Convert the model instance to an array.
     *
     * @return array
     */
    public function toArray(): array
    {
        return array_replace($this->attributesToArray(), $this->relationsToArray());
    }
}

機能制約

  • データベース絡む系は全部無し
    • リレーションは手動で setRelation() するのみ
  • APIレスポンスとしてそのまま返すことも無いと思うのでインタフェースは Jsonable とかは実装せず Arrayable のみ
    • ArrayAccess も基本使わないので無し
  • existing() で外部 API からの取得結果レスポンスからのインスタンス生成を想定
  • apply() で外部 API からの更新結果レスポンスをインスタンスに反映することを想定

既存のライブラリ

mpyw
古い記事はそのまま参考にしないようにご注意ください
synapse
Synapseは、オンラインサロンサービスにおけるパイオニアとして、かつて存在していたスタートアップです。
https://synapseam.github.io/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした