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

发布时间 作者

How I develop applications with Laravel image

我经常被问到如何使用 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 代码的方式!

Steve McDougall photo

Laravel News 的技术作家,Treblle 的开发者倡导者。API 专家,资深 PHP/Laravel 工程师。 YouTube 直播主.

归档于
Cube

Laravel 新闻

加入 40,000 多名其他开发者,永不错过新的技巧、教程等。

Laravel Forge logo

Laravel Forge

轻松创建和管理您的服务器,并在几秒钟内部署您的 Laravel 应用程序。

Laravel Forge
Tinkerwell logo

Tinkerwell

Laravel 开发人员必备的代码运行器。使用 AI、自动完成和对本地和生产环境的即时反馈来进行调试。

Tinkerwell
No Compromises logo

No Compromises

Joel 和 Aaron,来自 No Compromises 播客的两名经验丰富的开发者,现可为您的 Laravel 项目提供服务。 ⬧ 固定费率为每月 7500 美元。 ⬧ 没有冗长的销售流程。 ⬧ 没有合同。 ⬧ 100% 退款保证。

No Compromises
Kirschbaum logo

Kirschbaum

提供创新和稳定性,以确保您的 Web 应用程序取得成功。

Kirschbaum
Shift logo

Shift

正在运行旧版本的 Laravel?立即自动进行 Laravel 升级和代码现代化,以保持您的应用程序更新。

Shift
Bacancy logo

Bacancy

让经验丰富的 Laravel 开发人员(具有 4-6 年经验)以每月仅 2500 美元的价格为您的项目提供助力。获得 160 小时的专业知识和 15 天免费试用。立即安排电话会议!

Bacancy
Lucky Media logo

Lucky Media

立即获得 Lucky - Laravel 开发的理想选择,拥有超过十年的经验!

Lucky Media
Lunar: Laravel E-Commerce logo

Lunar: Laravel 电子商务

Laravel 的电子商务。一个开源包,将现代无头电子商务功能的功能带入 Laravel。

Lunar: Laravel 电子商务
LaraJobs logo

LaraJobs

官方 Laravel 招聘网站

LaraJobs
SaaSykit: Laravel SaaS Starter Kit logo

SaaSykit: Laravel SaaS 启动工具包

SaaSykit 是一个 Laravel SaaS 启动工具包,包含运行现代 SaaS 所需的所有功能。支付、漂亮结账、管理面板、用户仪表板、身份验证、现成组件、统计数据、博客、文档等。

SaaSykit: Laravel SaaS 启动工具包
Rector logo

Rector

您无缝进行 Laravel 升级的合作伙伴,降低成本,并为成功公司加速创新

Rector
MongoDB logo

MongoDB

通过 MongoDB 和 Laravel 的强大集成增强您的 PHP 应用程序,使开发人员能够轻松高效地构建应用程序。在使用熟悉的 Eloquent API 的同时,支持事务性、搜索、分析和移动用例。了解 MongoDB 的灵活、现代数据库如何改变您的 Laravel 应用程序。

MongoDB
Maska is a Simple Zero-dependency Input Mask Library image

Maska 是一个简单的无依赖项输入掩码库

阅读文章
Add Swagger UI to Your Laravel Application image

将 Swagger UI 添加到您的 Laravel 应用程序

阅读文章
Assert the Exact JSON Structure of a Response in Laravel 11.19 image

在 Laravel 11.19 中断言响应的准确 JSON 结构

阅读文章
Build SSH Apps with PHP and Laravel Prompts image

使用 PHP 和 Laravel Prompts 构建 SSH 应用程序

阅读文章
Building fast, fuzzy site search with Laravel and Typesense image

使用 Laravel 和 Typesense 构建快速、模糊的网站搜索

阅读文章
Add Comments to your Laravel Application with the Commenter Package image

使用 Commenter 包将评论添加到您的 Laravel 应用程序

阅读文章