• Час читання ~6 хв
  • 12.02.2023

Просто голові цієї статті більше 12 місяців і вона може бути застарілою!

Ви коли-небудь хотіли отримати доступ до атрибута красномовної моделі як об'єкта цінності, подібно до того, як Красномовний дозволяє нам працювати з датами через Carbon?

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

Більшість об'єктів значень мають кілька атрибутів. Це частина того, що відокремлює їх від примітивних типів, таких як рядки та цілі числа. Деякі можуть мати спеціальні формати, які дозволяють представити окремі атрибути у вигляді одного рядка. Наприклад, дата може представляти рік, місяць і день як Y-m-d, але ми все одно можемо розірвати її, якщо потрібно, і база даних знає, як запитувати деталі окремо.

Однак нам не завжди може так пощастити з нашими ціннісними об'єктами. Тож у нас може виникнути спокуса створити власні конвенції. Однак база даних не зможе легко запитувати деталі, і залежно від кількості типів атрибутів вона може стати громіздкою.

Ми могли б створити спеціальні стовпці в базі даних для кожного атрибута. Документи Laravel мають чудовий приклад того, як ми можемо перекинути до та з декількох стовпців в один об'єкт значення.

Але в деяких випадках нам може знадобитися вкладена структура, або, можливо, у нас є багато необов'язкових полів, які ми не хочемо захаращувати структуру таблиці. Можливо, у нас є колекція предметів, які не заслуговують на власний стіл. Існує безліч причин, чому ви можете розглянути колонку 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. Це теж круто, але на практиці я виявив, що з асоціативним масивом часто легше працювати.

А тепер уявіть собі кастинг до екземпляра певного класу об'єктів значень за допомогою користувацьких зліпків:

namespace App\Models;

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

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

Тепер все стає досить приємним.

Але зробіть ще один крок і зробіть клас об'єкта значення трансльованим, реалізувавши інтерфейс 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;
    }
}

І тепер ми можемо перейти до самого класу об'єктів значення, замість користувацького акторського складу, який просто відчуває себе трохи приємніше:

use App/Values/Address;

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

aw yes!

Що стосується логіки акторського складу, то серіалізація між JSON і масивами або об'єктами stdClass досить пряма з json_encode() і json_decode(). Наш індивідуальний акторський склад міг би обернути їх і миттєво закарбувати наш ціннісний об'єкт, і речі були б досить солодкими.

Як ми можемо зробити це ще далі, за межами документів?

Чи знаєте ви, що робить роботу зі структурами масивів на PHP ще приємнішою? Пакет Spatie Об'єкт передачі даних (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. Ознайомтеся з документами пакету об'єктів передачі даних , щоб побачити, як усі вони можуть бути визначені.

На цьому етапі власний акторський склад адреси може виглядати приблизно так:

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());
    }
}

Але зверніть увагу, як у нашому користувацькому зліпку немає нічого насправді специфічного для об'єкта значення адреси, крім самого імені класу. Якщо у нас є кілька об'єктів передачі даних, ми могли б зробити ще один крок, вказавши клас 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() нашого об'єкта значення адреси можна оновити так:

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

Єдиний інший рефактор, який мені подобається, - це створити клас CastableDataTransferObject, який може розширити мій клас адреси та інші. Таким чином, їм не потрібно турбуватися про те, щоб зробити себе акторськими:

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));
    }
}

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

Наш клас об'єктів значення адреси тепер надзвичайно охайний:І

namespace App\Values;

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

пам'ятайте, що ми можемо перейти безпосередньо до цього класу в нашій красномовній моделі:

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 для наших методів для конкретної адреси на нашому класі адреси, який зараз є гібридним об'єктом передачі даних і об'єктом цінності. Об'єкт значення передачі даних.

$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