• Czas czytania ~2 min
  • 21.08.2022

Często jestem pytany o to, jak pracujesz z Laravelem. W tym samouczku omówię moje typowe podejście do budowania aplikacji Laravela. Stworzymy API, ponieważ jest to coś, co kocham robić.

Budowany przez nas interfejs API jest podstawową aplikacją typu „to-do”, w której możemy dodawać zadania i przenosić je między zrobić i zrobić.Wybieram taki prosty przykład, ponieważ chciałbym, abyś skupił się bardziej na procesie niż na samej realizacji. Zacznijmy więc.

Dla mnie zawsze zaczyna się od prostego polecenia:

laravel new todo-api --jet --git

Dla mnie to zwykle wybrałbym Livewire, ponieważ jest mi z tym najbardziej komfortowy.Jeśli mam być szczery - webowa funkcjonalność tej aplikacji będzie służyła tylko do zarządzania użytkownikami i tworzenia tokenów API. Możesz jednak użyć Inertia, jeśli czujesz się bardziej komfortowo i chcesz kontynuować.

Gdy to polecenie zostanie uruchomione i wszystko będzie dla mnie skonfigurowane i gotowe, ja otwórz ten projekt w PHPStorm.PHPStorm to moje IDE, ponieważ zapewnia solidny zestaw narzędzi do programowania PHP, które pomagają mi w przepływie pracy. Gdy to jest w moim IDE, mogę rozpocząć proces pracy.

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.

Przeanalizujmy najpierw modele danych, które muszę utworzyć. Zazwyczaj dokumentuję je jako bloki kodu YAML, ponieważ pozwala mi to opisać model w przyjazny i łatwy sposób.

Model zadań będzie stosunkowo prosty:

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

Następnie mamy model tagów, który będzie dla mnie sposobem na dodanie pewnego rodzaju systemu taksonomii do moich zadań w celu ułatwienia sortowanie i filtrowanie.

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

Po zrozumieniu moich modeli danych zaczynam przeglądać zależności, których będę potrzebować lub chcę używać w tej aplikacji. W tym projekcie użyję:

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

Te pakiety przygotowały mnie do budowania API w bardzo przyjazny i łatwy do zbudowania sposób. Od tego momentu mogę zacząć budować dokładnie to, czego potrzebuję.

Teraz, gdy moja podstawowa aplikacja Laravel jest skonfigurowana na sukces, mogę zacząć publikować skróty, których często używam i dostosuj je, aby zaoszczędzić mi czasu podczas procesu tworzenia.Mam tendencję do usuwania skrótów, o których wiem, że nie będę tutaj używał, i modyfikuję tylko te, o których wiem, że będę używał. Oszczędza mi to dużo czasu na przeglądaniu odcinków, których nie muszę zmieniać.

Zmiany, które zwykle dodaję do tych odcinków, to:

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.

Po zakończeniu tego procesu pracuję nad wszystkimi plikami znajdującymi się obecnie w aplikacji Laravel - i wprowadzam podobne zmiany, jak w przypadku skrótów. Może to zająć trochę czasu, ale uważam, że jest tego warte, i mam ochotę na ścisły, spójny kod.

Kiedy w końcu to zrobię przeszedłem przez wszystkie powyższe, mogę zacząć dodawać moje elokwentne modele!

php artisan make:model Task -mf

Mój typowy przepływ pracy z modelowaniem danych to rozpoczęcie od migracji baz danych, przejście do fabryk, a na końcu modeli elokwentnych. Lubię organizować migracje danych w określony sposób - dlatego pokażę Ci przykład migracji zadań:

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

Sposób działania tej struktury to:

Identifiers
Text content
Castable properties
Foreign Keys
Timestamps

Dzięki temu mogę spojrzeć na dowolną tabelę bazy danych i z grubsza wiedzieć, gdzie może znajdować się kolumna, bez przeszukiwania całej tabeli. To jest coś, co nazwałbym mikrooptymalizacją. Nie jest to coś, z czego odniesiesz znaczne korzyści czasowe – ale zacznie to zmuszać Cię do posiadania standardu i natychmiastowego poznania, gdzie rzeczy są.

Jedną rzeczą, o której wiem, że będę potrzebować tego interfejsu API, szczególnie w odniesieniu do zadań, jest status Enum, którego mogę użyć. Jednak sposób, w jaki pracuję z Laravel, jest bardzo podobny do projektowania opartego na domenie, więc muszę wcześniej wykonać małą konfigurację.

Wewnątrz mojego kompozytor.json, tworzę kilka nowych przestrzeni nazw, które mają różne cele:

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.

W końcu będziesz mieć dostępne następujące przestrzenie nazw:

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

Teraz, gdy już to zrobię, mogę zacząć myśleć o domenach, których chcę używać w tej stosunkowo prostej aplikacji. Niektórzy powiedzieliby, że użycie czegoś takiego do tak prostej aplikacji to przesada, ale oznacza to, że jeśli dodam do tego, nie muszę robić dużych refaktorów.Dodatkową korzyścią jest to, że mój kod jest zawsze zorganizowany zgodnie z moimi oczekiwaniami, bez względu na rozmiar aplikacji.

Domeny, których będziemy chcieli w tym celu użyć projekt można zaprojektować w następujący sposób:

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

Pierwszą rzeczą, którą muszę zrobić w moim projekcie, jest utworzenie Enum dla atrybutu Status zadania. Utworzę to w Workflowdomena, ponieważ jest to bezpośrednio związane z zadaniami i przepływami pracy.

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

Jak widać, jest to dość proste Enum, ale cenne, jeśli kiedykolwiek będę chciał rozszerz możliwości aplikacji do zrobienia. W tym miejscu mogę skonfigurować fabrykę modeli i sam model, używając Arr::random, aby wybrać losowy stan dla samego zadania.

< p>Teraz rozpoczęliśmy modelowanie danych.Rozumiemy relacje między uwierzytelnionymi użytkownikami a początkowymi zasobami, do których mają dostęp. Czas zacząć myśleć o projekcie interfejsu API.

Ten interfejs API będzie miał kilka punktów końcowych skoncentrowanych na zadaniach i być może punkt końcowy wyszukiwania, który pozwoli nam filtrować na podstawie tagi, czyli nasza taksonomia.Zwykle w tym miejscu zapisuję interfejs API, którego chcę i sprawdzam, czy będzie działać:

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

Teraz, gdy rozumiem strukturę routingu, której chcę użyć dla mojego interfejsu API - Mogę rozpocząć wdrażanie Rejestratorów trasy. W moim ostatnim artykule o rejestratorach tras mówiłem o tym, jak dodać ich do domyślnej struktury Laravel.Nie jest to jednak standardowa aplikacja Laravela, więc muszę inaczej pokierować. W tej aplikacji do tego służy moja przestrzeń nazw Todo. To właśnie zaklasyfikowałbym jako kod systemowy, który jest wymagany do działania aplikacji – ale nie jest to coś, o co aplikacja zbytnio się troszczy.

Po dodaniu cechy i interfejsu wymaganych do korzystania z rejestratorów tras, mogę zacząć rejestrować domeny, aby każda z nich mogła zarejestrować swoje trasy. Lubię tworzyć dostawcę usług domenowych w przestrzeni nazw aplikacji, aby nie zalewać konfiguracji mojej aplikacji mnóstwem dostawców usług. Ten dostawca wygląda tak:

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

Następnie wszystko, co muszę zrobić, to dodać tego jednego dostawcę do mojego config/app.php, abym nie musiał niszczyć pamięci podręcznej konfiguracji za każdym razem, gdy chcę dokonać zmiany. Wprowadziłem wymagane zmiany w app/Providers/RouteServiceProvider.phpwięc mogę zarejestrować rejestratorów tras specyficznych dla domeny, co pozwala mi kontrolować routing z mojej domeny, ale aplikacja nadal kontroluje ich ładowanie.

Weźmy spójrz na TaskRouteRegistrar, który znajduje się w domenie 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');
            },
        );
    }
}

Rejestrowanie moich tras w ten sposób pozwala mi zachować porządek i zawartość domeny Potrzebuję ich.Moje kontrolery nadal znajdują się w aplikacji, ale są oddzielone przestrzenią nazw łączącą się z domeną.

Teraz, gdy mam kilka tras, z których mogę korzystać, mogę zacząć myśleć o akcje, które chcę wykonać w samej domenie zadań i jakich obiektów danych mogę potrzebować, aby upewnić się, że kontekst jest utrzymywany między klasami.

Po pierwsze, będę musiał utworzyć TaskObject, którego mogę użyć w kontrolerze, aby przejść do zadania akcji lub zadania w tle, które wymaga dostępu do podstawowych właściwości zadania, ale nie do samego modelu. Zazwyczaj przechowuję mój obiekt danych w domenie, ponieważ są one klasą domeny.

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

Chcemy mieć pewność, że nadal utrzymujemy poziom możliwości rzutowania dla obiektu danych, ponieważ chcemy, aby zachowywał się podobnie do modelu Eloquent. Chcemy usunąć z niego zachowanie, aby mieć jasny cel. Teraz spójrzmy, jak możemy tego użyć.

Przyjrzyjmy się tutaj jako przykład tworzeniu nowego punktu końcowego API zadań.Chcemy zaakceptować żądanie i wysłać przetwarzanie do zadania w tle, aby uzyskać względnie natychmiastowe odpowiedzi z naszego interfejsu API. Celem interfejsu API jest przyspieszenie odpowiedzi, dzięki czemu można łączyć ze sobą akcje i tworzyć bardziej skomplikowane przepływy pracy niż za pośrednictwem interfejsu internetowego.Najpierw będziemy chcieli przeprowadzić pewną walidację przychodzącego żądania, więc użyjemy w tym celu 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',
            ],
        ];
    }
}

W końcu wprowadzimy to żądanie do naszego kontrolera, ale wcześniej dochodzimy do tego punktu - musimy stworzyć akcję, którą chcemy wstrzyknąć do naszego kontrolera.Jednak ze względu na sposób, w jaki piszę aplikacje Laravel, będę musiał utworzyć interfejs/kontrakt do użycia i powiązania z kontenerem, aby móc rozwiązać akcję z Laravel DI Container. Przyjrzyjmy się, jak wygląda nasz interfejs/umowa:

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

Ten kontroler tworzy dla nas solidną umowę, której będziemy przestrzegać podczas wdrażania.Chcemy zaakceptować TaskObject, który właśnie zaprojektowaliśmy, ale także ID użytkownika, dla którego tworzymy to zadanie. Następnie zwracamy model zadania lub model elokwentny, co pozwala nam na trochę elastyczności w naszym podejściu. Przyjrzyjmy się teraz implementacji:

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

Używamy Task Eloquent Model, otwieramy instancję Eloquent Query Builder i prosimy o utworzenie nowej instancji.Następnie łączymy TaskObject jako tablicę i identyfikator użytkownika w tablicy, aby utworzyć zadanie w formacie, którego oczekuje Eloquent.

Teraz, gdy mamy naszą implementację, chcemy aby związać to w pojemniku. Sposób, w jaki lubię to robić, polega na pozostaniu w domenie, aby w przypadku wyrejestrowania domeny z kontenera usunięto wszelkie istniejące powiązania specyficzne dla domeny.Utworzę nowego dostawcę usług w mojej domenie i dodam tam powiązania, a następnie poproszę mojego dostawcę usług domenowych o zarejestrowanie dla mnie dodatkowego dostawcy usług.

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

Wszystko, czego potrzebujemy do zrobienia tutaj jest powiązanie interfejsu/kontraktu, który stworzyliśmy z implementacją i umożliwienie kontenerowi Laravel obsłużenie reszty.Następnie rejestrujemy to u naszego dostawcy usług domeny dla domeny przepływu pracy:

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

Na koniec możemy spojrzeć na kontroler sklepu, aby zobaczyć, jak chcemy osiągnąć nasz cel.

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

Tutaj używamy Laravel DI Container, aby rozwiązać akcję, którą chcemy uruchomić z właśnie zarejestrowanego kontenera, a następnie wywołujemy nasz kontroler.Za pomocą akcji budujemy nowy Task Model, przekazując nową instancję TaskObject, którą uwadniamy za pomocą poręcznego pakietu, który stworzyłem. Używa odbicia, aby klasa była oparta na jej właściwościach i ładunku. Jest to akceptowalne rozwiązanie do tworzenia nowego zadania; jednak przeszkadza mi to, że wszystko odbywa się synchronicznie. Zmieńmy to teraz na pracę w tle.

Zadania w Laravel, które zwykle trzymam w głównej przestrzeni nazw aplikacji. Powodem tego jest to, że jest to coś głęboko związanego z samą moją aplikacją. Jednak logika Jobs może działać na żywo w ramach naszych działań, które znajdują się w kodzie naszej domeny. Utwórzmy nowe zadanie:

php artisan make:job Workflow/Tasks/CreateTask

Następnie przenosimy logikę ze sterownika do zadania.Zadanie jednak chce zaakceptować obiekt zadania, a nie żądanie - więc będziemy musieli przekazać do tego obiekt hydratów.

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

Na koniec możemy dokonać refaktoryzacji nasz kontroler usuwa akcję synchroniczną - w zamian otrzymujemy szybszy czas reakcji i zadania, które można ponowić, co zapewnia nam lepszą nadmiarowość.

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

Całym celem mojego przepływu pracy, jeśli chodzi o Laravela, jest stworzenie bardziej niezawodnego, bezpiecznego i powtarzalnego podejścia do tworzenia moich aplikacji. To pozwoliło mi napisać kod, który jest nie tylko łatwy do zrozumienia, ale także zachowuje kontekst w trakcie cyklu życia każdej operacji biznesowej.

Jak pracujesz z Laravelem? Czy robisz coś podobnego?Poinformuj nas o swoim ulubionym sposobie pracy z kodem Laravel na Twitterze!

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