• Czas czytania ~6 min
  • 12.02.2023

Tylko uprzedzam to, ten artykuł ma ponad 12 miesięcy i może być nieaktualny!

Czy kiedykolwiek chciałeś uzyskać dostęp do atrybutu modelu Eloquent jako obiektu wartości, podobnie jak Eloquent pozwala nam pracować z datami za pomocą Carbon?

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

Większość obiektów wartości ma wiele atrybutów. To część tego, co odróżnia je od typów pierwotnych, takich jak ciągi i liczby całkowite. Niektóre mogą mieć specjalne formaty, które pozwalają nam reprezentować poszczególne atrybuty jako pojedynczy ciąg. Na przykład data może reprezentować rok, miesiąc i dzień jako Y-m-d, ale nadal możemy ją rozdzielić, jeśli to konieczne, a baza danych wie, jak zapytać o części indywidualnie.

Nie zawsze jednak mamy tyle szczęścia z naszymi wartościowymi przedmiotami. Możemy więc ulec pokusie tworzenia własnych konwencji. Jednak baza danych nie będzie w stanie łatwo wysyłać zapytań do części, a w zależności od liczby typów atrybutów może stać się nieporęczna.

Moglibyśmy utworzyć dedykowane kolumny w bazie danych dla każdego atrybutu. Dokumenty Laravel mają doskonały przykład tego, jak możemy < href = "https://laravel.com/docs/8.x/eloquent-mutators#value-object-casting" > rzut do i z wielu kolumn na obiekt pojedynczej wartości.

Ale w niektórych przypadkach możemy potrzebować zagnieżdżonej struktury, a może mamy wiele opcjonalnych pól, których nie chcemy zaśmiecać struktury tabeli. Może mamy kolekcję przedmiotów, które nie zasługują na własny stół. Istnieje wiele powodów, dla których warto rozważyć kolumnę JSON.

Kolumny JSON skutecznie dają nam zalety (i wady) bazy danych opartej na NoSQL / dokumentach w naszej relacyjnej bazie danych. Nowoczesne silniki baz danych mogą całkiem dobrze indeksować i natywnie wyszukiwać wewnątrz tych struktur JSON.

Jakie są więc nasze opcje pracy z kolumnami JSON w Laravel?

Być może znasz następującą wbudowaną obsadę:

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

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

Spowoduje to automatyczne rzutowanie tablicy (asocjacyjnej lub liczbowej) na JSON i z powrotem. Bardzo przydatne!

Być może znasz również:

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

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

To robi to samo, ale dla obiektu stdClass. To też jest fajne, ale w praktyce odkryłem, że tablica asocjacyjna jest często łatwiejsza w obsłudze.

Teraz wyobraź sobie, że rzutujesz do instancji określonej klasy obiektu wartości za pomocą rzuty niestandardowe:

namespace App\Models;

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

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

Teraz robi się całkiem nieźle.

Ale zrób kolejny krok i uczyń klasę obiektu wartości rzutowalną, implementując interfejs 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;
    }
}

A teraz możemy rzutować na samą klasę obiektu value, zamiast do niestandardowej rzuty, która jest po prostu nieco przyjemniejsza:

use App/Values/Address;

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

aw tak!

Jeśli chodzi o logikę rzutowania, serializacja między JSON a tablicami lub obiektami stdClass jest dość prosta w przypadku json_encode() i json_decode(). Nasza niestandardowa obsada mogłaby je owinąć i stworzyć nasz obiekt wartości, a rzeczy byłyby całkiem słodkie.

Jak możemy pójść jeszcze dalej, poza dokumentację?

Czy wiesz, co sprawia, że praca ze strukturami tablicowymi w PHP jest jeszcze przyjemniejsza? Pakiet Spatie < href="https://github.com/spatie/data-transfer-object/">Data Transfer Object (DTO), to jest to!

composer require spatie/data-transfer-object

Odblokowuje to kilka dodatkowych rzeczy:

  • Dane są sprawdzane, aby upewnić się, że są zgodne ze zdefiniowaną strukturą i typami, w przeciwnym razie zostanie zgłoszony wyjątek.
  • Obsługuje rzutowanie surowej tablicy do i z instancji naszej klasy niestandardowej.Musimy

tylko zdefiniować właściwości:

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

Lubię przechowywać je w App\Values, ponieważ zazwyczaj dodaję metody specyficzne dla domeny, które sprawiają, że bardziej przypomina obiekt wartości niż zwykłą DTO. Możesz przechowywać go w dowolnym miejscu, na przykład App\DataTransferObjects, a nawet w tym samym katalogu, co modele.

Należy również pamiętać, że nie jesteśmy tutaj ograniczeni do typów pierwotnych - możemy uwzględnić inne klasy, w tym zagnieżdżone obiekty transferu danych. Możemy również oznaczyć właściwości jako nullable, aby uczynić je opcjonalnymi. Pakiet Spatie'a pozwala nam nawet definiować tablice rzeczy, a także typy związków! Niektóre z nich będą natywne w PHP 8. Sprawdź dokumentację pakietu obiektów transferu danych < href="https://github.com/spatie/data-transfer-object/> aby zobaczyć, jak można je wszystkie zdefiniować.

W tym momencie rzutowanie niestandardowe Adres może wyglądać mniej więcej tak:

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

Zauważ jednak, że w naszej niestandardowej rzutacji nie ma nic specyficznego dla obiektu wartości adresu, z wyjątkiem samej nazwy klasy. Jeśli mamy wiele obiektów transferu danych, możemy wykonać ten kolejny krok, określając klasę DTO za pomocą konstruktora, aby rzutowanie można było ponownie wykorzystać!

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

Teraz metoda castUsing() naszego obiektu wartości Address może zostać zaktualizowana w następujący sposób:

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

Jedyną inną refaktoryzacją, którą lubię, jest utworzenie klasy CastableDataTransferObject, którą moja klasa Address i inne mogą rozszerzyć. W ten sposób nie muszą się martwić o to, że staną się odlewalni: toJson()

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

i fromJson() są milszym dodatkowym akcentem do hermetyzacji serializacji DTO zamiast umieszczania jej w obsadzie. Pamiętaj również, aby sprawdzić Safe PHP, jeśli jeszcze tego nie zrobiłeś.

Nasza klasa obiektu wartości adresu jest teraz bardzo uporządkowana:I

namespace App\Values;

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

pamiętaj, że możemy rzutować bezpośrednio do tej klasy w naszym modelu Eloquent:

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

Kiedy wszystko zostanie powiedziane i zrobione, powinniśmy potrzebować tylko następujących klas:I

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

możemy teraz robić fajne rzeczy, takie jak:

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

I wreszcie, możemy w pełni zrealizować nasz obiekt wartości, tworząc bogate API dla naszych metod specyficznych dla adresu w naszej klasie TAddress, który jest teraz hybrydowym obiektem transferu danych i obiektem wartości. Obiekt wartości transferu danych.

$user->address->toMapUrl();

$user->address->getCoordinates();

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

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

Jeśli chcesz użyć CastableDataTransferObject w swoim projekcie, stworzyłem pakiet, więc musisz tylko przynieść własne klasy!

Comments

No comments yet
Yurij Finiv

Yurij Finiv

Full stack

O

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...

O autorze CrazyBoy49z
WORK EXPERIENCE
Kontakt
Ukraine, Lutsk
+380979856297