Value Object /Объект-Значение в Laravel

Источник: «value-objects-in-laravel»
Как уменьшить количество багов, сделать личную и командную работу более эффективной и улучшить качество кода в целом — Объект-Значение один из подходов для достижения этого.

Объект-значение (Value Object) — понятие в программирование относящееся к неизменному объекту, основной целью которого является хранение значений или данных, а не представление идентичности объекта. В объектно-ориентированном программировании Объект-Значение представляет собой простой небольшой объект, описывающий определённый аспект или атрибут сущности, например имя человека, адрес или номер телефона.

Как использовать Объект-Значение в Laravel приложении

Предположим, у нас есть модель, одним из параметров которой является номер телефона, и мы хотим, чтобы этот параметр всегда был в определённом формате. Нам нужно обезопасить себя от того, что кто-то при разработке нового функционала может начать хранить эту модель с другим номером телефона в другом формате. Если бы использовали строковое значение для хранения номера телефона, это было бы вполне возможно.

У нас есть конечная точка API и команда CLI:

<?php

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class UserController extends Controller
{
public function store(Request $request): JsonResponse
{
$user = new User();
$user->name = $request->get('name');
$user->phone = $request->get('phone');//Value: '+12124567890'
$user->save();

return new JsonResponse(status: Response::HTTP_CREATED);
}
}
<?php

namespace App\Console\Commands;

use App\Models\User;
use Illuminate\Console\Command;

class UserCreateCommand extends Command
{
protected $signature = 'users:create {name : User name} {phone : User phone}';

protected $description = 'Create user with name and phone';

public function handle(): int
{
$user = new User();

$user->name = $this->argument('name');
$user->phone = $this->argument('phone');//Значение: '212.456.7890'

$user->save();

$this->info('User successfully created!');

return self::SUCCESS;
}
}

В этом случае мы можем сохранить номер телефона как строковое значение в любом формате. Можно добавить дополнительную валидацию в обоих местах и потом как-то её поддерживать, но есть гораздо более простой и аккуратный вариант — добавляем Объект-Значение для Phone:

<?php

namespace App\ValueObjects;

use InvalidArgumentException;

class Phone
{
private function __construct(private readonly string $phone)
{
}

public static function fromString(string $phone): Phone
{
if (!preg_match('/^\+1\d{10}$/', $phone)) {
throw new InvalidArgumentException('It is not valid phone value');
}

return new self($phone);
}

public function toString(): string
{
return $this->phone;
}
}

У нас есть проверка preg_match для формата телефона, которая будет использоваться везде.

Теперь нам нужно добавить механику Cast для этого свойства в нашу модель:

php artisan make:cast PhoneCast
<?php

namespace App\Casts;

use App\ValueObjects\Phone;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
use InvalidArgumentException;

class PhoneCast implements CastsAttributes
{
/**
* Приведение заданного значения.
*
* @param array<string, mixed> $attributes
*/

public function get(Model $model, string $key, mixed $value, array $attributes): Phone
{
return Phone::fromString($value);
}

/**
* Подготовка данного значения для хранения.
*
* @param array<string, mixed> $attributes
*/

public function set(Model $model, string $key, mixed $value, array $attributes): string
{
if (!$value instanceof Phone) {
throw new InvalidArgumentException('The given value is not an Phone instance.');
}

return $value->toString();
}
}

И мы должны добавить этот Cast в модель:

<?php

namespace App\Models;

use App\Casts\PhoneCast;
use App\ValueObjects\Phone;
use Carbon\CarbonInterface;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

/**
* @property int $id
* @property string $name
* @property Phone $phone
* @property CarbonInterface $created_at
*/

class User extends Model
{
use HasFactory;

protected $casts = [
'phone' => PhoneCast::class
];
}

Теперь мы можем использовать этот Объект-Значение (Value Object)

В Контроллере:

<?php

namespace App\Http\Controllers;

use App\Models\User;
use App\ValueObjects\Phone;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class UserController extends Controller
{
public function store(Request $request): JsonResponse
{
$user = new User();
$user->name = $request->get('name');
$user->phone = Phone::fromString($request->get('phone'));
$user->save();

return new JsonResponse(status: Response::HTTP_CREATED);
}
}

В команде:

<?php

namespace App\Console\Commands;

use App\Models\User;
use App\ValueObjects\Phone;
use Illuminate\Console\Command;
use InvalidArgumentException;

class UserCreateCommand extends Command
{
protected $signature = 'users:create {name : User name} {phone : User phone}';

protected $description = 'Create user with name and phone';

public function handle(): int
{
$name = $this->argument('name');

try {
$phone = Phone::fromString($this->argument('phone'));
} catch (InvalidArgumentException $e) {
$this->error($e->getMessage());

return self::FAILURE;
}

$user = new User();

$user->name = $name;
$user->phone = $phone;

$user->save();

$this->info('User successfully created!');

return self::SUCCESS;
}
}

Давайте проверим, как это работает

С требуемым форматом телефона:

php artisan users:create Alex +12124567890
User successfully created!

С неправильным форматом телефона:

php artisan users:create Alex 1-212-456-7890
It is not valid phone value

Заключение

Таким образом, Объект-Значение — это хороший подход к описанию определённого правила для одного или нескольких свойств, вы можете использовать его как встроенное свойство для Модели или просто как способ описания определённого формата данных для определённого параметра метода.

Больше информации об Объектах-Значениях можно найти в статье на википедии Предметно-ориентированное программирование

Дополнительные материалы

Предыдущая Статья

Профилирование Сервис Контейнера Laravel

Следующая Статья

Laravel аналитика. Зачем и как я сделал свой пакет