• Час читання ~2 хв
  • 21.08.2022

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

API, який ми створюємо, — це базова програма у стилі справ, у яку ми можемо додавати завдання та переміщати їх між робити і зроблено.Я вибираю такий простий приклад, тому що хотів би, щоб ви зосередилися на процесі більше, ніж на самій реалізації. Отже, почнемо.

Для мене це завжди починається з простої команди:

laravel new todo-api --jet --git

Для для цього я зазвичай вибираю Livewire, оскільки мені це зручно.Якщо я чесно, веб-функціональність цієї програми буде призначена лише для керування користувачами та створення маркерів API. Однак не соромтеся використовувати Inertia, якщо вам зручніше і ви хочете слідкувати за цим.

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

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, дуже схоже на Domain Driven Design, тому мені потрібно буде зробити невеликі налаштування заздалегідь.

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

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

Ми хочемо переконатися, що ми все ще зберігаємо рівень можливостей кастингу для об’єкта даних, оскільки хочемо, щоб він поводився подібно до моделі 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 Container. Давайте подивимося, як виглядає наш інтерфейс/контракт:

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

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

Тоді ми просто перемістимо логіку з контролера в завдання.Однак завдання хоче прийняти об’єкт завдання, а не запит, тому нам потрібно буде передати об’єкт hydrates.

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? Ви робите щось подібне?Розкажіть нам про ваш улюблений спосіб роботи з кодом Laravel у Twitter!

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