如何用Laravel开发应用程序

248 阅读8分钟

我经常被问到关于你如何使用Laravel的问题.所以在这个教程中, 我将通过我的典型方法来建立一个Laravel应用程序.我们将创建一个API,因为这是我喜欢做的事情。

我们要建立的API是一个基本的待办事项风格的应用程序, 我们可以添加任务并在待办和已办之间移动。我选择这样一个简单的例子是因为我想让你把注意力集中在过程上而不是实现本身。所以让我们开始吧。

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

laravel new todo-api --jet --git

为此,我通常会选择Livewire,因为我对它最熟悉。如果我是诚实的--这个应用程序的网络功能将只是用于用户管理和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 Resources
Laravel Query Builder
Fast Paginate
Data Object Tools

这些软件包为我建立一个API提供了一个非常友好和容易构建的方式.从这里, 我可以开始构建我需要的东西.

现在我的基本Laravel应用已经设置好了, 我可以开始发布我常用的存根, 并定制它们以节省我在开发过程中的时间.我倾向于删除那些我知道我不会在这里使用的存根,只修改那些我知道我将会使用的存根。这为我节省了大量的时间去看那些我不需要改变的存根。

我通常对这些存根所做的修改是。

在每个文件中添加declare(strict_types=1);
使所有生成的类默认为final
确保响应类型总是存在的。
确保参数是有类型提示的。
确保任何Traits在每个用例中都被加载。

一旦这个过程完成了, 我就会检查目前Laravel应用程序中的所有文件 - 并做类似于我对存根所做的修改.现在, 这可能需要一点时间, 但我发现这是值得的, 而且我对严格, 一致的代码有兴趣.

一旦我最终完成了上述所有工作,我就可以开始添加我的Eloquent模型了。

php artisan make:model Task -mf

我在数据建模方面的典型工作流程是:从数据库迁移开始,到工厂,最后是Eloquent模型。我喜欢用一种特殊的方式来组织我的数据迁移--所以我将向你展示Tasks迁移的例子。

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 - 我的特定领域的实现代码所在的地方。- 放置我的领域特定的接口的地方。- 覆盖特定Laravel代码的地方; 在这种情况下, 它被称为 .
Infrastructure
ProjectName 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 命名空间的作用.这就是我所归类的系统代码, 它是应用程序运行所需要的 - 但不是应用程序太关心的东西.

在我添加了使用路由注册器所需的特质和接口之后,我可以开始寻找注册域名,以便每个域名都能注册其路由。我喜欢在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 ,这样我就可以注册特定于域的路由注册器,这样我就可以控制来自我的域的路由,但应用程序仍然控制着加载这些。

让我们看一下工作流域下的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的目的是加快响应速度,这样你就可以把行动串联起来,创建比你通过网页界面更复杂的工作流程。首先,我们要对传入的请求进行一些验证,所以我们将使用一个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],
            ),
        );
    }
}

我们使用任务Eloquent Model,打开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,
        );
    }
}

最后, 我们可以看一下商店控制器,看看我们要如何实现我们的目标。

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实例来建立新的任务模型, 我们使用我创建的一个方便的包将其水化。这使用了反射,使类基于其属性和有效载荷。对于创建一个新的任务来说,这是一个可以接受的解决方案;然而,让我感到不安的是,这一切都在同步进行。现在让我们把它重构为一个后台工作。

在Laravel中的工作,我倾向于保持在主App命名空间中。这样做的原因是它与我的应用程序本身紧密相连。然而, 乔布斯可以运行的逻辑是在我们的行动中, 而行动是在我们的领域代码中。让我们创建一个新的作业。

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时,我工作流程的全部目的是创建一个更可靠,安全和可复制的方法来构建我的应用程序。这使得我写的代码不仅容易理解,而且在任何业务操作的生命周期中都能保持上下文。