LoginSignup
8
7

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-12-19

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 からの更新結果レスポンスをインスタンスに反映することを想定

既存のライブラリ

8
7
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
7