我如何使用 Laravel 开发应用程序

Laravel

我被问到很多关于你如何使用 Laravel 的问题。因此,在本教程中,我将介绍构建 Laravel 应用程序的典型方法。我们将创建一个 API,因为这是我喜欢做的事情。

我们正在构建的 API 是一个基本的待办事项样式应用程序,我们可以在其中添加任务并在待办事项和已完成事项之间移动它们。我之所以选择这样一个简单的示例,是因为我希望你更关注过程而不是实现本身。那么让我们开始吧。

对我来说,它总是以一个简单的命令开始:

laravel new todo-api --jet --git

为此,我通常会选择 Livewire,因为我最喜欢它。老实说 - 此应用程序的 Web 功能将仅用于用户管理和 API 令牌创建。但是,如果您感觉更舒服并想跟随,请随意使用 Inertia。

一旦这个命令运行并且一切都为我准备好了,我在 PHPStorm 中打开这个项目。 PHPStorm 是我的首选 IDE,因为它为 PHP 开发提供了一套强大的工具,可以帮助我完成工作流程。一旦这在我的 IDE 中,我就可以开始工作流程了。

在每个新应用程序中,我的第一步是打开 README 文件并开始记录我想要实现的目标。这包括:
我要构建的内容的一般描述。
我知道我需要的任何数据模型
我需要创建的 API 端点的粗略设计。

让我们探索一下我首先需要创建的数据模型。我通常将这些记录为 YAML 代码块,因为它允许我以友好且简单的方式描述模型。

让我们先了解我需要创建的数据模型。我通常将这些代码块记录为 YAML 代码块,因为它允许我以一种友好而简单的方式描述模型。

任务模型将是相对 straightforward :

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 应用程序已经成功设置,我可以开始发布我常用的存根并自定义它们以节省我在开发过程中的时间。我倾向于删除我知道我不会在这里使用的存根,只修改我知道我会使用的存根。这为我节省了大量时间来处理我不需要更改的存根。

我通常添加到这些存根的更改是:

declare(strict_types=1); 添加到每个文件中。
默认情况下使所有生成的类final
确保响应类型始终存在。
确保参数是类型提示的。
确保为每个用例加载一个特征。

一旦这个过程完成,我将处理当前在 Laravel 应用程序中的所有文件–并进行与我对存根所做的类似的更改。现在,这可能需要一点时间,但我发现这是值得的,而且我喜欢严格、一致的代码。

一旦我最终完成了以上所有操作,我就可以开始添加我的雄辩模型!

php artisan make:model Task -mf

我进行数据建模的典型工作流程是从数据库迁移开始,转移到工厂,最后是雄辩模型。我喜欢以特定的方式组织我的数据迁移-所以我将向你展示任务迁移的示例:

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

这种结构的工作方式是:

身份标识
文字内容
浇注料特性
外键
时间戳

这使我可以查看任何数据库表并大致了解列可能位于的位置,而无需搜索整个表。这就是我所说的微优化。你不会从中获得可观的时间收益——但它会开始迫使您有一个标准并立即知道事情在哪里。

我知道我想要这个 API 的一件事,特别是关于任务,是我可以使用的状态枚举。然而,我使用 Laravel 的方式与领域驱动设计非常相似,所以我需要事先做一些设置。

在我的 composer.json 文件中,我创建了一些具有不同用途的新命名空间:

Domains - 我的特定领域实现代码所在的位置。
Infrastructure - 我的领域特定界面所在的位置。
ProjectName - 特定于覆盖特定 Laravel 代码的代码所在的位置;在这种情况下,它被称为 Todo

最终,你将拥有以下可用的命名空间:

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

现在已经完成了,我可以开始考虑我想为这个相对简单的应用程序使用的域。有人会说在这样一个简单的应用程序中使用这样的东西是多余的,但这意味着如果我添加它,我不必进行大型重构。额外的好处是,无论应用程序大小如何,我的代码总是按照我期望的方式组织。

我们将要用于该项目的域可以设计如下:

工作流程;与任务和工作单元有关的任何事情。
分类;任何与分类有关的事情。

我需要在项目中做的第一件事是为任务状态属性创建一个Enum。我将在“工作流”域下创建它,因为这与任务和workflows。

declare(strict_types=1);

namespace Domains\Workflow\Enums;

enum TaskStatus: string
{
    case OPEN = 'open';
    case CLOSED = 'closed';
}

如你所见,它是一个非常简单的枚举,但如果我想扩展待办事项应用程序的功能,它是一个很有价值的枚举。从这里,我可以设置模型工厂和模型本身,使用 Arr::random 为任务本身选择一个随机状态。

现在我们已经开始我们的数据建模了。我们了解经过身份验证的用户与他们可用的初始资源之间的关系。是时候开始考虑 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 的路由结构 - 我可以开始实施 Route Registrars。在我上一篇关于路由注册器的文章中,我谈到了如何将它们添加到默认的 Laravel 结构中。然而,这不是一个标准的 Laravel 应用程序,所以我必须以不同的方式路由。在这个应用程序中,这就是我的 Todo 命名空间的用途。这就是我将其归类为系统代码的内容,它是应用程序运行所必需的——但不是应用程序太关心的东西。

添加使用路由注册器所需的特征和接口后,我可以开始寻找注册域,以便每个人都可以注册其路由。我喜欢在 App 命名空间中创建一个域服务提供者,这样我的应用程序配置就不会被大量的服务提供者淹没。此提供程序如下所示:

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 进行了所需的更改,因此我可以注册特定于域的路由注册器,这使我可以控制来自我的域的路由,但应用程序仍然可以控制加载这些。

让我们看一下 Workflow 域下的 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,我可以在控制器中使用它来传递到需要访问任务的基本属性而不是整个模型本身的操作或后台作业。我通常将我的数据对象保存在域中,因为它们是域类。

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 的目的是加快响应速度,以便您可以将操作链接在一起并创建比通过 Web 界面更复杂的工作流。首先,我们要对传入的请求进行一些验证,因此我们将为此使用 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 容器中解析操作。让我们看看我们的 Interface/Contract 是什么样子的:

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,但也接受我们为其创建此任务的用户的 ID。然后我们返回一个任务模型,或一个 Eloquent 模型,这使我们的方法有一点灵活性。现在让我们看一个实现:

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 合并为一个数组,并将用户 ID 合并到一个数组中,以创建 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,
        );
    }
}

最后,我们可以查看 Store Controller 以了解我们希望如何实现我们的目标。

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 中的作业我倾向于保留在主 App 命名空间中。这样做的原因是因为它与我的应用程序本身紧密相关。但是,逻辑 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?你做过类似的事情吗?让我们知道你最喜欢在Twitter上使用Laravel代码的方式!

本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://laravel-news.com/how-i-develop-a...

译文地址:https://learnku.com/laravel/t/71188

本文为协同翻译文章,如您发现瑕疵请点击「改进」按钮提交优化建议
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!