• Время чтения ~13 мин
  • 21.08.2022

Меня часто спрашивают, как вы работаете с Laravel. Итак, в этом уроке я расскажу о своем типичном подходе к созданию приложения Laravel. Мы создадим API, потому что мне это нравится.

API, который мы создаем, представляет собой базовое приложение в стиле списка дел, в которое мы можем добавлять задачи и перемещать их между сделать и сделать.Я выбираю такой простой пример, потому что хочу, чтобы вы сосредоточились больше на процессе, чем на самой реализации. Итак, приступим.

Для меня все всегда начинается с простой команды:

laravel new todo-api --jet --git

Для В этом случае я обычно выбираю Livewire, так как он мне наиболее удобен.Если честно, веб-функции этого приложения будут предназначены только для управления пользователями и создания токенов API. Тем не менее, не стесняйтесь использовать Inertia, если вам удобнее и вы хотите следовать ей.

После того, как эта команда запущена и все настроено и готово для меня, я откройте этот проект в PHPStorm.PHPStorm — это моя любимая IDE, поскольку она предоставляет надежный набор инструментов для разработки PHP, которые помогают мне в моем рабочем процессе. Как только это появится в моей IDE, я смогу начать рабочий процесс.

The first step for me in every new application is to open the README file and start documenting what I want to achieve. This includes:
A general description of what I want to build.
Any data models that I know I will need
A rough design of the API endpoints I will need to create.

Давайте сначала рассмотрим модели данных, которые мне нужно создать. Обычно я документирую их в виде блоков кода YAML, так как это позволяет мне описать модель простым и понятным способом.

Модель задач будет относительно простой:

Task:
  attributes:
    id: int
    title: string
    description: text (nullable)
    status: string
    due_at: datetime (nullable)
    completed_at: datetime
  relationships:
    user: BelongTo
    tags: BelongsToMany

Затем у нас есть модель тегов, которая позволит мне добавить своего рода систему таксономии к моим задачам для облегчения сортировка и фильтрация.

Tag:
  attributes:
    id: int
    name: string
  relationships:
    tasks: BelongsToMany

После того, как я пойму свои модели данных, я начну просматривать зависимости, которые, как я знаю, мне понадобятся или я хочу использовать для этого приложения. Для этого проекта я буду использовать:

Laravel Sail
Laravel Pint
Larastan
JSON-API Resources
Laravel Query Builder
Fast Paginate
Data Object Tools

Эти пакеты настроили меня на создание API очень дружественным и простым способом. Отсюда я могу начать создавать именно то, что мне нужно.

Теперь, когда мое базовое приложение Laravel успешно настроено, я могу начать публиковать заглушки, которые я обычно использую, и настроить их, чтобы сэкономить время в процессе разработки.Я склонен удалять заглушки, которые, как я знаю, я не буду здесь использовать, и модифицировать только те, которые, как я знаю, буду использовать. Это экономит мне много времени на просмотр заготовок, которые мне не нужно менять.

Я обычно добавляю в эти заглушки следующие изменения:

Adding declare(strict_types=1); to each file.
Making all generated class final by default.
Ensure that response types are always there.
Ensure that parameters are type hinted.
Ensure that any Traits are loaded one per use case.

Как только этот процесс завершен, я просматриваю все файлы, находящиеся в настоящее время в приложении Laravel, и вношу такие же изменения, как и в заглушки. Это может занять некоторое время, но я считаю, что это того стоит, и я люблю строгий, последовательный код.

Как только я, наконец, Выполнив все вышеперечисленное, я могу начать добавлять свои модели Eloquent!

php artisan make:model Task -mf

Мой типичный рабочий процесс с моделированием данных состоит в том, чтобы начать с миграции базы данных, перейти к фабрикам и, наконец, к моделям Eloquent. Мне нравится организовывать миграцию данных определенным образом, поэтому я покажу вам пример переноса задач:

public function up(): void
{
    Schema::create('tasks', static function (Blueprint $table): void {
        $table->id();
 
        $table->string('name');
        $table->text('description')->nullable();
 
        $table->string('status');
 
        $table
		->foreignId('user_id')
		->index()
		->constrained()
		->cascadeOnDelete();
 
        $table->dateTime('due_at')->nullable();
        $table->dateTime('completed_at')->nullable();
        $table->timestamps();
    });
}

Эта структура работает следующим образом:

Identifiers
Text content
Castable properties
Foreign Keys
Timestamps

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

Я знаю, что мне понадобится для этого API, особенно в отношении задач, статус Enum, который я могу использовать. Однако то, как я работаю с Laravel, очень похоже на дизайн, управляемый доменом, поэтому мне нужно выполнить небольшую настройку заранее.

Внутри моего composer.json я создаю несколько новых пространств имен, которые имеют разные цели:

Domains - Where my Domain-specific implementation code lives.
Infrastructure - Where my Domain specific interfaces live.
ProjectName - Where code specific to overriding specific Laravel code lives; in this case, it is called Todo.

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

"autoload": {
    "psr-4": {
        "App\\": "app/",
        "Domains\\": "src/Domains/",
        "Infrastructure\\": "src/Infrastructure/",
        "Todo\\": "src/Todo/",
        "Database\\Factories\\": "database/factories/",
        "Database\\Seeders\\": "database/seeders/"
    }
},

Теперь, когда это сделано, я могу начать думать о доменах, которые я хочу использовать для этого относительно простого приложения. Кто-то скажет, что использовать что-то подобное для такого простого приложения — это излишество, но это означает, что если я добавлю к этому, мне не придется делать большие рефакторинги.Дополнительным преимуществом является то, что мой код всегда организован так, как я ожидаю, независимо от размера приложения.

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

Workflow; anything to do with tasks and units of work.
Taxonomy; anything to do with categorization.

Первое, что мне нужно сделать в моем проекте, это создать Enum для атрибута состояния задачи. Я создам это в Workflowдомен, так как это напрямую связано с задачами и рабочими процессами.

declare(strict_types=1);
 
namespace Domains\Workflow\Enums;
 
enum TaskStatus: string
{
    case OPEN = 'open';
    case CLOSED = 'closed';
}

Как видите, это довольно простое Enum, но ценное, если я когда-нибудь захочу расширить возможности приложения to-do. Отсюда я могу настроить фабрику моделей и саму модель, используя Arr::random для выбора случайного состояния для самой задачи.

< p>Теперь мы приступили к моделированию данных.Мы понимаем отношения между аутентифицированными пользователями и исходными ресурсами, которые им доступны. Пришло время подумать о дизайне API.

Этот API будет иметь несколько конечных точек, ориентированных на задачи, и, возможно, конечную точку поиска, чтобы мы могли фильтровать на основе теги, которые являются нашей таксономии.Обычно здесь я записываю API, который мне нужен, и выясняю, будет ли он работать:

`[GET] /api/v1/tasks` - Get all Tasks for the authenticated user.
`[POST] /api/v1/tasks` - Create a new Task for the authenticated user.
`[PUT] /api/v1/tasks/{task}` - Update a Task owned by the authenticated user.
`[DELETE] /api/v1/tasks/{task}` - Delete a Task owned by the authenticated user.
 
`[GET] /api/v1/search` - Search for specific tasks or tags.

Теперь, когда я понимаю структуру маршрутизации, которую хочу использовать для своего API – Я могу приступить к внедрению регистраторов маршрутов. В моей последней статье о регистраторах маршрутов я говорил о том, как добавить их в структуру Laravel по умолчанию.Однако это не стандартное приложение Laravel, поэтому я должен маршрутизировать вещи по-другому. В этом приложении для этого предназначено мое пространство имен Todo. Это то, что я бы классифицировал как системный код, необходимый для запуска приложения, но не слишком важный для приложения.

После того, как я добавил черты и интерфейс, необходимые для использования регистраторов маршрутов, я могу приступить к регистрации доменов, чтобы каждый из них мог регистрировать свои маршруты. Мне нравится создавать поставщика услуг домена в пространстве имен приложений, чтобы не загромождать конфигурацию моего приложения множеством поставщиков услуг. Этот поставщик выглядит следующим образом:

declare(strict_types=1);
 
namespace App\Providers;
 
use Domains\Taxonomy\Providers\TaxonomyServiceProvider;
use Domains\Workflow\Providers\WorkflowServiceProvider;
use Illuminate\Support\ServiceProvider;
 
final class DomainServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->register(
            provider: WorkflowServiceProvider::class,
        );
 
        $this->app->register(
            provider: TaxonomyServiceProvider::class,
        );
    }
}

Затем все, что мне нужно сделать, это добавить этот единственный провайдер в мой config/app.php, чтобы мне не приходилось разбивать кеш конфигурации каждый раз, когда я хочу внести изменения. Я внес необходимые изменения в файл app/Providers/RouteServiceProvider.php.поэтому я могу зарегистрировать регистраторов маршрутов для конкретных доменов, что позволяет мне контролировать маршрутизацию из моего домена, но приложение по-прежнему контролирует их загрузку.

Давайте рассмотрим посмотрите на TaskRouteRegistrar, который находится в домене рабочего процесса:

declare(strict_types=1);
 
namespace Domains\Workflow\Routing\Registrars;
 
use App\Http\Controllers\Api\V1\Workflow\Tasks\DeleteController;
use App\Http\Controllers\Api\V1\Workflow\Tasks\IndexController;
use App\Http\Controllers\Api\V1\Workflow\Tasks\StoreController;
use App\Http\Controllers\Api\V1\Workflow\Tasks\UpdateController;
use Illuminate\Contracts\Routing\Registrar;
use Todo\Routing\Contracts\RouteRegistrar;
 
final class TaskRouteRegistrar implements RouteRegistrar
{
    public function map(Registrar $registrar): void
    {
        $registrar->group(
            attributes: [
                'middleware' => ['api', 'auth:sanctum', 'throttle:6,1',],
                'prefix' => 'api/v1/tasks',
                'as' => 'api:v1:tasks:',
            ],
            routes: static function (Registrar $router): void {
                $router->get(
                    '/',
                    IndexController::class,
                )->name('index');
                $router->post(
                    '/',
                    StoreController::class,
                )->name('store');
                $router->put(
                    '{task}',
                    UpdateController::class,
                )->name('update');
                $router->delete(
                    '{task}',
                    DeleteController::class,
                )->name('delete');
            },
        );
    }
}

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

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

Во-первых, мне нужно будет создать TaskObject, который я могу использовать в контроллере для перехода к действию или фоновому заданию, которому требуется доступ к основным свойствам Task, но не ко всей модели. Обычно я держу свой объект данных в домене, поскольку он является классом домена.

declare(strict_types=1);
 
namespace Domains\Workflow\DataObjects;
 
use Domains\Workflow\Enums\TaskStatus;
use Illuminate\Support\Carbon;
use JustSteveKing\DataObjects\Contracts\DataObjectContract;
 
final class TaskObject implements DataObjectContract
{
    public function __construct(
        public readonly string $name,
        public readonly string $description,
        public readonly TaskStatus $status,
        public readonly null|Carbon $due,
        public readonly null|Carbon $completed,
    ) {}
 
    public function toArray(): array
    {
        return [
            'name' => $this->name,
            'description' => $this->description,
            'status' => $this->status,
            'due_at' => $this->due,
            'completed_at' => $this->completed,
        ];
    }
}

Мы хотим убедиться, что мы по-прежнему сохраняем уровень возможностей приведения для Data Object, поскольку мы хотим, чтобы он вел себя аналогично модели Eloquent. Мы хотим отделить поведение от него, чтобы иметь четкую цель. Теперь давайте посмотрим, как мы можем это использовать.

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

declare(strict_types=1);
 
namespace App\Http\Requests\Api\V1\Workflow\Tasks;
 
use Illuminate\Foundation\Http\FormRequest;
 
final class StoreRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }
 
    public function rules(): array
    {
        return [
            'name' => [
                'required',
                'string',
                'min:2',
                'max:255',
            ],
        ];
    }
}

В конечном итоге мы внедрим этот запрос в наш контроллер, но перед мы подошли к этому моменту — нам нужно создать действие, которое мы хотим внедрить в наш контроллер.Однако из-за того, как я пишу приложения Laravel, мне нужно будет создать интерфейс/контракт для использования и привязки к контейнеру, чтобы я мог разрешить действие из контейнера Laravel DI. Давайте посмотрим, как выглядит наш интерфейс/контракт:

declare(strict_types=1);
 
namespace Infrastructure\Workflow\Actions;
 
use App\Models\Task;
use Illuminate\Database\Eloquent\Model;
use JustSteveKing\DataObjects\Contracts\DataObjectContract;
 
interface CreateNewTaskContract
{
    public function handle(DataObjectContract $task, int $user): Task|Model;
}

Этот контроллер создает прочный контракт, которому мы должны следовать в нашей реализации.Мы хотим принять объект TaskObject, который мы только что создали, а также идентификатор пользователя, для которого мы создаем эту задачу. Затем мы возвращаем Task Model или Eloquent Model, что дает нам некоторую гибкость в нашем подходе. Теперь давайте посмотрим на реализацию:

declare(strict_types=1);
 
namespace Domains\Workflow\Actions;
 
use App\Models\Task;
use Illuminate\Database\Eloquent\Model;
use Infrastructure\Workflow\Actions\CreateNewTaskContract;
use JustSteveKing\DataObjects\Contracts\DataObjectContract;
 
final class CreateNewTask implements CreateNewTaskContract
{
    public function handle(DataObjectContract $task, int $user): Task|Model
    {
        return Task::query()->create(
            attributes: array_merge(
                $task->toArray(),
                ['user_id' => $user],
            ),
        );
    }
}

Мы используем модель Task Eloquent, открываем экземпляр Eloquent Query Builder и просим его создать новый экземпляр.Затем мы объединяем TaskObject в виде массива и идентификатор пользователя в массиве, чтобы создать задачу в формате, ожидаемом Eloquent.

Теперь, когда у нас есть реализация, мы хотим чтобы привязать это к контейнеру. Мне нравится делать это так, чтобы оставаться в домене, так что если мы отменим регистрацию домена, контейнер будет очищен от любых существующих привязок, специфичных для домена.Я создам нового поставщика услуг в своем домене и добавлю туда привязки, а затем попрошу своего поставщика услуг домена зарегистрировать для меня дополнительного поставщика услуг.

declare(strict_types=1);
 
namespace Domains\Workflow\Providers;
 
use Domains\Workflow\Actions\CreateNewTask;
use Illuminate\Support\ServiceProvider;
use Infrastructure\Workflow\Actions\CreateNewTaskContract;
 
final class ActionsServiceProvider extends ServiceProvider
{
    public array $bindings = [
        CreateNewTaskContract::class => CreateNewTask::class,
    ];
}

Все, что нам нужно здесь нужно связать интерфейс/контракт, который мы создали с реализацией, и позволить контейнеру Laravel обрабатывать все остальное.Затем мы регистрируем это внутри нашего поставщика услуг домена для домена рабочего процесса:

declare(strict_types=1);
 
namespace Domains\Workflow\Providers;
 
use Illuminate\Support\ServiceProvider;
 
final class WorkflowServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->register(
            provider: ActionsServiceProvider::class,
        );
    }
}

Наконец, мы можем посмотреть на контроллер хранилища, чтобы увидеть, как мы хотим достичь нашей цели.

declare(strict_types=1);
 
namespace App\Http\Controllers\Api\V1\Workflow\Tasks;
 
use App\Http\Requests\Api\V1\Workflow\Tasks\StoreRequest;
use Domains\Workflow\DataObjects\TaskObject;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Carbon;
use Infrastructure\Workflow\Actions\CreateNewTaskContract;
use JustSteveKing\DataObjects\Facades\Hydrator;
use JustSteveKing\StatusCode\Http;
 
final class StoreController
{
    public function __construct(
        private readonly CreateNewTaskContract $action
    ) {}
 
    public function __invoke(StoreRequest $request): JsonResponse
    {
        $task = $this->action->handle(
            task: Hydrator::fill(
                class: TaskObject::class,
                properties: [
                    'name' => $request->get('name'),
                    'description' => $request->get('description'),
                    'status' => strval($request->get('status', 'open')),
                    'due' => $request->get('due') ? Carbon::parse(
                        time: strval($request->get('due')),
                    ) : null,
                    'completed' => $request->get('completed') ? Carbon::parse(
                        time: strval($request->get('completed')),
                    ) : null,
                ],
            ),
            user: intval($request->user()->id),
        );
 
        return new JsonResponse(
            data: $task,
            status: Http::CREATED(),
        );
    }
}

Здесь мы используем Laravel DI Container для выполнения действия, которое хотим запустить из только что зарегистрированного контейнера, а затем вызываем наш контроллер.Используя действие, мы создаем новую модель задачи, передавая новый экземпляр TaskObject, который мы гидратируем с помощью созданного мной удобного пакета. При этом используется отражение, чтобы создать класс на основе его свойств и полезной нагрузки. Это приемлемое решение для создания новой задачи; однако меня беспокоит то, что все это делается синхронно. Давайте теперь преобразуем это в фоновое задание.

Вакансии в Laravel я стараюсь держать в пределах основного пространства имён приложения. Причина этого в том, что это что-то глубоко связанное с самим моим приложением. Однако логика Jobs может работать в реальном времени в наших действиях, которые живут в коде нашего домена. Давайте создадим новое задание:

php artisan make:job Workflow/Tasks/CreateTask

Затем мы просто переместим логику из контроллера в задание.Однако задание хочет принять объект задачи, а не запрос, поэтому нам нужно будет передать объект гидратов через это.

declare(strict_types=1);
 
namespace App\Jobs\Workflow\Tasks;
 
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Infrastructure\Workflow\Actions\CreateNewTaskContract;
use JustSteveKing\DataObjects\Contracts\DataObjectContract;
 
final class CreateTask implements ShouldQueue
{
    use Queueable;
    use Dispatchable;
    use SerializesModels;
    use InteractsWithQueue;
 
    public function __construct(
        public readonly DataObjectContract $task,
        public readonly int $user,
    ) {}
 
    public function handle(CreateNewTaskContract $action): void
    {
        $action->handle(
            task: $this->task,
            user: $this->user,
        );
    }
}

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

declare(strict_types=1);
 
namespace App\Http\Controllers\Api\V1\Workflow\Tasks;
 
use App\Http\Requests\Api\V1\Workflow\Tasks\StoreRequest;
use App\Jobs\Workflow\Tasks\CreateTask;
use Domains\Workflow\DataObjects\TaskObject;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Carbon;
use JustSteveKing\DataObjects\Facades\Hydrator;
use JustSteveKing\StatusCode\Http;
 
final class StoreController
{
    public function __invoke(StoreRequest $request): JsonResponse
    {
        dispatch(new CreateTask(
            task: Hydrator::fill(
                class: TaskObject::class,
                properties: [
                    'name' => $request->get('name'),
                    'description' => $request->get('description'),
                    'status' => strval($request->get('status', 'open')),
                    'due' => $request->get('due') ? Carbon::parse(
                        time: strval($request->get('due')),
                    ) : null,
                    'completed' => $request->get('completed') ? Carbon::parse(
                        time: strval($request->get('completed')),
                    ) : null,
                ],
            ),
            user: intval($request->user()->id)
        ));
 
        return new JsonResponse(
            data: null,
            status: Http::ACCEPTED(),
        );
    }
}

Когда дело доходит до Laravel, вся цель моего рабочего процесса — создать более надежный, безопасный и воспроизводимый подход к созданию моих приложений. Это позволило мне писать код, который не только прост для понимания, но и сохраняет контекст по мере прохождения через жизненный цикл любой бизнес-операции.

Как вы работаете с Ларавелем? Вы делаете что-то подобное?Расскажите нам в Твиттере о вашем любимом способе работы с кодом Laravel!

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