Просто голова вверх эта статья старше 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
в своем проекте, я создал пакет, поэтому вам нужно только принести свои собственные классы!