我如何使用 Laravel 开发应用程序
发布时间 作者 史蒂夫·麦克道格
我经常被问到如何使用 Laravel 工作。因此,在本教程中,我将介绍我构建 Laravel 应用程序的典型方法。我们将创建一个 API,因为这是我最喜欢做的事情。
我们要构建的 API 是一个基本的待办事项风格应用程序,我们可以在其中添加任务并将它们在待办和已完成之间移动。我选择这样一个简单的示例,因为我希望您关注过程而不是实现本身。因此,让我们开始吧。
对我来说,它总是从一个简单的命令开始
laravel new todo-api --jet --git
为此,我通常会选择 Livewire,因为我对此最熟悉。说实话,这个应用程序的 Web 功能仅用于用户管理和 API 令牌创建。但是,如果您更熟悉并想继续学习,请随时使用 Inertia。
一旦此命令运行,并且一切准备就绪,我就在 PHPStorm 中打开此项目。PHPStorm 是我的首选 IDE,因为它为 PHP 开发提供了一套强大的工具,可以帮助我完成工作流程。一旦它在我的 IDE 中,我就可以开始工作流程。
我在每个新应用程序中的第一步是打开 README 文件并开始记录我想达成的目标。这包括: 对我想构建的内容的总体描述。 我知道我将需要的任何数据模型 我将需要创建的 API 端点的粗略设计。
让我们先探索一下我需要创建的数据模型。我通常将它们记录为 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 资源 Laravel 查询构建器 快速分页 数据对象工具
这些包为我构建了一个非常友好且易于构建的 API。从这里,我就可以开始构建我真正需要的东西。
现在,我的基本 Laravel 应用程序已经为成功做好了准备,我就可以开始发布我常用的存根,并对其进行自定义以节省我在开发过程中的时间。我倾向于删除我知道不会在此处使用的存根,并且只修改我知道会使用的存根。这节省了我大量时间,不必浏览那些不需要更改的存根。
我通常添加到这些存根的更改是
在每个文件开头添加 declare(strict_types=1);
。 默认情况下,使所有生成的类 final
。 确保始终存在响应类型。 确保参数是类型提示的。 确保每个用例都只加载一个 Trait。
完成此过程后,我将浏览 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(); });}
这种结构的工作方式是
标识符 文本内容 可转换属性 外键 时间戳
这使我能够查看任何数据库表并大致了解列可能位于何处,而无需搜索整个表。我称之为微优化。您不会从中获得实质性的时间收益,但它会开始迫使您拥有一个标准,并且能够立即了解事物的位置。
有一件事我知道我将需要这个 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/" }},
现在已经完成了,我就可以开始考虑我想用于这个相对简单的应用程序的域。有些人会说,对于这样一个简单的应用程序使用这种东西是过度设计,但它意味着如果我添加更多内容,就不必进行大型重构。额外的优势是,无论应用程序规模如何,我的代码始终按照我预期的方式进行组织。
我们想用于这个项目的域可以像下面这样设计
工作流程;与任务和工作单位相关的任何内容。 分类;与分类相关的任何内容。
我需要在我的项目中做的第一件事是为任务状态属性创建一个枚举。我将在 Workflow
域下创建它,因为这与任务和工作流程直接相关。
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 的路由结构,我就可以开始实现 路由注册器 了。在我的上一篇关于路由注册器的文章中,我谈到了如何将它们添加到默认的 Laravel 结构中。但是,这不是标准的 Laravel 应用程序,因此我必须以不同的方式进行路由。在这个应用程序中,这就是我的 Todo
命名空间的用处。我将其归类为系统代码,这是应用程序运行所必需的,但应用程序并不太关心它。
在添加了使用路由注册器所需的 Trait 和接口后,我就可以开始寻找要注册的域,以便每个域都可以注册其路由。我喜欢在 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,我可以在控制器中使用它,并将其传递给需要访问 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 的目的是加快响应速度,以便您可以将操作链接在一起,并创建比通过 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 容器解析动作。让我们看看我们的接口/契约是什么样的
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。然后,我们返回一个 Task 模型或一个 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 容器处理剩下的工作。接下来,我们在 Workflow 域的域服务提供者中注册它
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 控制器,看看我们想要如何实现我们的目标。
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 容器从我们刚刚注册的容器中解析我们想要运行的动作,然后调用我们的控制器。使用该动作,我们通过传递一个新的 TaskObject 实例来构建新的 Task 模型,我们使用我创建的一个方便的包来对其进行填充。它使用反射来根据其属性和有效负载创建类。这是一个创建新任务的可接受的解决方案;但是,让我困扰的是这一切都是同步完成的。现在让我们将其重构为后台作业。
我倾向于将 Laravel 中的作业保留在主 App 命名空间中。这样做的原因是因为它与我的应用程序本身紧密相关。但是,作业可以运行的逻辑可以存在于我们的动作中,我们的动作存在于我们的域代码中。让我们创建一个新的作业
php artisan make:job Workflow/Tasks/CreateTask
然后,我们只需将逻辑从控制器移动到作业中。但是,作业想要接受 Task 对象,而不是请求,因此我们需要将填充的对象传递给它。
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 代码的方式!