• Время чтения ~6 мин
  • 12.02.2023

Просто голова вверх эта статья старше 12 месяцев и может быть устаревшей!

Вы когда-нибудь хотели получить доступ к атрибуту модели Eloquent в качестве ценностного объекта, подобно тому, как Eloquent позволяет нам работать с датами через Carbon?

$user->address->calculateDistance($otherUser->address);

Большинство объектов значений имеют несколько атрибутов. Это часть того, что отделяет их от примитивных типов, таких как строки и целые числа. Некоторые из них могут иметь специальные форматы, которые позволяют нам представлять отдельные атрибуты в виде одной строки. Например, дата может представлять год, месяц и день как Y-m-d, но мы все равно можем разобрать ее, если это необходимо, и база данных знает, как запрашивать части по отдельности.

Однако нам не всегда может так повезти с нашими объектами ценности. Таким образом, у нас может возникнуть соблазн создать наши собственные конвенции. Однако база данных не сможет легко запрашивать части, и в зависимости от количества типов атрибутов она может стать громоздкой.

Мы могли бы создать выделенные столбцы в базе данных для каждого атрибута. В документах Laravel есть отличный пример того, как мы можем cast в несколько столбцов и из них в один объект значения.

Но в некоторых случаях нам может понадобиться вложенная структура, или, может быть, у нас есть много необязательных полей, которые мы не хотим загромождать структуру таблицы. Может быть, у нас есть коллекция предметов, которые не заслуживают отдельного стола. Существует множество причин, по которым вы можете рассмотреть столбец JSON.

Столбцы JSON эффективно дают нам преимущества (и недостатки) NoSQL/документированной базы данных внутри нашей реляционной базы данных. И современные ядра баз данных могут индексировать и нативно запрашивать внутри этих структур JSON довольно хорошо.

Итак, каковы наши варианты работы со столбцами JSON в Laravel?

Возможно, вы знакомы со следующим встроенным приведением:

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    protected $casts = [
        'address' => 'array'
    ];
}

Это автоматически приведет к приведению массива (ассоциативного или числового) в JSON и обратно. Очень удобно!

Возможно, вы также знакомы с:

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    protected $casts = [
        'address' => 'object'
    ];
}

Это делает то же самое, но для объекта stdClass. Это тоже круто, но на практике я обнаружил, что с ассоциативным массивом часто легче работать.

Теперь представьте себе приведение к экземпляру определенного класса ценностных объектов через custom casts laravel:

namespace App\Models;

use App\Casts\Address;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    protected $casts = [
        'address' => Address::class,
    ];
}

Теперь все становится довольно хорошо.

Но сделайте еще один шаг и сделайте класс объекта value литым, реализовав интерфейс Castable:

namespace App\Values;

use App\Casts\Address as AddressCast;
use Illuminate\Contracts\Database\Eloquent\Castable;

class Address implements Castable
{
    // ...

    public static function castUsing(array $arguments)
    {
        return AddressCast::class;
    }
}

И теперь мы можем привести к самому классу объекта value, а не к пользовательскому приведению, которое просто кажется немного приятнее:

use App/Values/Address;

class User extends Model
{
    protected $casts = [
        'address' => Address::class,
    ];
}

aw yes!

Что касается логики приведения, сериализация между JSON и массивами или объектами stdClass довольно проста с json_encode() и json_decode(). Наш пользовательский актерский состав может обернуть их и создать экземпляр нашего ценностного объекта, и все будет довольно сладко.

Как мы можем пойти еще дальше, за пределы документов?

Знаете ли вы, что делает работу с массивными структурами в PHP еще приятнее? Пакет Spatie Data Transfer Object (DTO), вот что!

composer require spatie/data-transfer-object

Это открывает несколько дополнительных

  • вещей: данные проверяются, чтобы убедиться, что они соответствуют определенной структуре и типам, в противном случае создается исключение.
  • Он обрабатывает приведение необработанного массива к экземпляру нашего пользовательского класса и из

него.Нам просто нужно определить свойства:

namespace App\Values;

use App\Casts\Address as AddressCast;
use Illuminate\Contracts\Database\Eloquent\Castable;
use Spatie\DataTransferObject\DataTransferObject;

class Address extends DataTransferObject implements Castable
{
    public string $street;
    public string $suburb;
    public string $state;

    public static function castUsing(array $arguments)
    {
        return AddressCast::class;
    }
}

Мне нравится хранить их в App\Values, потому что я обычно добавляю методы, специфичные для домена, которые делают его больше похожим на объект значения, чем на простой DTO. Не стесняйтесь хранить его в любом месте, например App\DataTransferObjects или даже в том же каталоге, что и ваши модели.

Также обратите внимание, что здесь мы не ограничиваемся примитивными типами - мы можем включить другие классы, включая вложенные объекты передачи данных. Мы также можем пометить свойства как нулевые, чтобы сделать их необязательными. Пакет Spatie даже позволяет нам определять массивы вещей, а также типы союзов! Некоторые из них будут родными в PHP 8. Ознакомьтесь с документами пакета объектов передачи данных data, чтобы узнать, как все они могут быть определены.

На этом этапе пользовательское приведение Address может выглядеть примерно так:

namespace App\Casts;

use App\Values\Address as AddressValue;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;

class Address implements CastsAttributes
{
    /**
     * Cast the stored value to an Address.
     */
    public function get($model, $key, $value, $attributes)
    {
        /*
         * We'll need this to handle nullable columns
         */
        if (is_null($value)) {
            return;
        }
        return new AddressValue(json_decode($value, true));
    }
    /**
     * Prepare the given value for storage.
     */
    public function set($model, $key, $value, $attributes)
    {
        /*
         * We'll need this to handle nullable columns
         */
        if (is_null($value)) {
            return;
        }
        /*
         * Allow the user to pass an array instead of the value object itself.
         * Similar to how we can pass a date string or a Carbon/DateTime object with a date cast.
         */
        if (is_array($value)) {
            $value = new AddressValue($value);
        }
        if (! $value instanceof AddressValue) {
            throw new InvalidArgumentException('Value must be of type Address, array, or null');
        }
        return json_encode($value->toArray());
    }
}

Но обратите внимание, что в нашем пользовательском приведении нет ничего действительно специфического для объекта значения Address, кроме самого имени класса. Если у нас есть несколько объектов передачи данных, мы могли бы сделать еще один шаг, указав класс DTO через конструктор, чтобы приведение можно было использовать повторно!

namespace App\Casts;

use Illuminate\Contracts\Database\Eloquent\CastsAttributes;

class DataTransferObject implements CastsAttributes
{
    protected string $class;

    /**
     * @param string $class The DataTransferObject class to cast to
     */
    public function __construct(string $class)
    {
        $this->class = $class;
    }
    /**
     * Cast the stored value to the configured DataTransferObject.
     */
    public function get($model, $key, $value, $attributes)
    {
        if (is_null($value)) {
            return;
        }
        return new $this->class(json_decode($value, true));
    }
    /**
     * Prepare the given value for storage.
     */
    public function set($model, $key, $value, $attributes)
    {
        if (is_null($value)) {
            return;
        }
        if (is_array($value)) {
            $value = new $this->class($value);
        }
        if (! $value instanceof $this->class) {
            throw new InvalidArgumentException("Value must be of type [$this->class], array, or null");
        }
        return json_encode($value->toArray());
    }
}

И теперь метод castUsing() нашего объекта значения Address можно обновить следующим образом:

public static function castUsing(array $arguments)
{
    return new DataTransferObject(Address::class);
}

Единственный другой рефакторинг, который мне нравится, - это создание класса CastableDataTransferObject, который может расширить мой класс Address и другие. Таким образом, им не нужно беспокоиться о том, чтобы сделать себя кастоируемыми:

namespace App\Values;

use App\Casts\DataTransferObject as DataTransferObjectCast;
use Illuminate\Contracts\Database\Eloquent\Castable;
use Spatie\DataTransferObject\DataTransferObject;
use function Safe\json_decode;
use function Safe\json_encode;

abstract class CastableDataTransferObject extends DataTransferObject implements Castable
{
    public static function castUsing()
    {
        return new DataTransferObjectCast(static::class);
    }
    public function toJson()
    {
        return json_encode($this->toArray());
    }
    public static function fromJson($json)
    {
        return new static(json_decode($json, true));
    }
}

toJson() и fromJson() являются более приятным дополнительным штрихом, чтобы инкапсулировать сериализацию DTO, а не помещать ее в актерский состав. Также не забудьте проверить Safe PHP, если вы еще этого не сделали.

Наш класс объекта значений Address теперь супер аккуратный:

namespace App\Values;

class Address extends CastableDataTransferObject
{
    public string $street;
    public string $suburb;
    public string $state;
}

И помните, что мы можем транслировать непосредственно к этому классу в нашей модели Eloquent:

protected $casts = [
    'address' => Address::class,
];

Когда все сказано и сделано, нам понадобятся только следующие классы:

app/Casts/DataTransferObject.php
app/Values/CastableDataTransferObject.php
app/Values/Address.php

И теперь мы можем делать классные вещи, такие как:

User::create([
    'name' => 'Emmett Brown',
    'address' => [
        'street' => '1640 Riverside Drive',
        'suburb' => 'Hill Valley',
        'state' => 'California',
    ]
])
$residents = User::where('address->suburb', 'Hill Valley')->get();

И, наконец, мы можем полностью реализовать наш объект ценности, создав богатый API для наших адресно-специфичных методов в нашем классе Address, который теперь является гибридным объектом передачи данных и объектом ценности. Объект значения передачи данных.

$user->address->toMapUrl();

$user->address->getCoordinates();

$user->address->getPostageCost($sender);

$user->address->calculateDistance($otherUser->address);

Если вы хотите использовать CastableDataTransferObject в своем проекте, я создал пакет, поэтому вам нужно только принести свои собственные классы!

Comments

No comments yet
Yurij Finiv

Yurij Finiv

Full stack

Про мене

Professional Fullstack Developer with extensive experience in website and desktop application development. Proficient in a wide range of tools and technologies, including Bootstrap, Tailwind, HTML5, CSS3, PUG, JavaScript, Alpine.js, jQuery, PHP, MODX, and Node.js. Skilled in website development using Symfony, MODX, and Laravel. Experience: Contributed to the development and translation of MODX3 i...

Об авторе CrazyBoy49z
WORK EXPERIENCE
Контакты
Ukraine, Lutsk
+380979856297