您的第一个 Laravel 9 应用程序

发布日期:作者:

Your first Laravel 9 Application image

如果您从未构建过 Laravel 应用程序,请让我带您从头开始进行一次演练 - 无需任何先验知识。在本教程中,我将逐步介绍如何创建一个新的 Laravel 应用程序,是对这篇文章的刷新,该文章发布于 2021 年年中。

自首次发布以来,Laravel 的发展速度惊人,最近还增加了两个全职员工来帮助发展其生态系统。它不会很快消失,所以我们不妨尝试学习它,对吧?如果您还没有学习。Laravel 一直以来都以开发人员为中心,专注于开发人员体验、性能和定制。如果您问任何 Laravel 开发人员他们为什么喜欢 Laravel - 几乎每次他们都会说开发人员体验。所以问题是,当 Laravel 写起来如此顺畅时,为什么要写其他东西呢?

本教程面向刚开始学习 Laravel 的用户。也许他们知道它是什么,或者尝试过安装它一两次,但因为感觉有点不知所措而停了下来。我们将从头开始逐步介绍如何创建一个新的 Laravel 应用程序,您只需要

  • 一个终端
  • 已安装的 PHP 8
  • 已安装的 Composer 并且在您的路径中可用
  • 已安装的 NPM 并且在您的路径中可用

那么我们要构建什么呢?我们将制作一个书签收集器,以便您抓取感兴趣的链接并存储它们。除此之外,我们还将允许您向书签添加标签,以便在您返回时对它们进行分类。

我们如何开始使用 Laravel?当然,我们首先需要创建一个新项目,为此有几种方法可以做到;Laravel 安装程序、Laravel Sail Build 或使用 composer create project。在本教程中,我将使用 composer create-project 方法:因为我希望它的需求尽可能少。因此,选择一个您希望应用程序所在的目录,然后运行以下 composer 命令

composer create-project laravel/laravel bookmarker

现在,在您选择的代码编辑器中打开新的 bookmarker 目录,以便我们开始。这是一个空白的 Laravel 项目,起点。我不会对您希望如何在本地查看此项目做出任何假设,因为有很多不同的选择。相反,我们将使用 artisan 来运行应用程序。运行以下 artisan 命令

php artisan serve

这将为您提供一个 URL 供您查看,因此请单击它并在您的浏览器中打开它。这应该是默认的 Laravel 屏幕。恭喜,您已经迈出了使用 Laravel 的第一步!接下来,我们可以继续了解应用程序的工作原理。

Laravel 将从 routes/web.php 加载所有 Web 路由,在路由方面,您有几个选择。当您不需要将数据传递给视图时,您可以使用 Route::view() 直接加载视图。您可以使用可调用对象|闭包|函数通过调用 Route::get('route', fn () => view('home')) 来实现,其中 get 是您要使用的 HTTP 动词。您还可以使用控制器,以便可以将逻辑隔离在单个类中 Route::get('route', App\Http\Controllers\SomeRouteController::class)

关于通过控制器加载路由,也有一些选择。您可以将它们声明为字符串并指向特定方法 Route::get('route', 'App\Http\Controllers\SomeController@methodName')。您可以声明路由资源,其中 Laravel 将假设一个标准的 Route::resource('route', 'App\Http\Controllers\SomeController'),这将为您提供 indexcreatestoreshoweditupdatedestroy 方法。这些在文档中有很好的解释。您还可以使用可调用控制器,它是一个具有 __invoke() 方法的单个类,该方法被视为闭包 Route::get('route', App\Http\Controllers\SomeController::clas)

在本教程中,我将使用可调用控制器,因为它们是我喜欢的使用方式 - 所以请随意跟随。我喜欢使用它们,因为它们使我的路由保持干净,有利于我的 IDE 进行点击操作,并将每个路由封装在一个单独的类中。

在开始任何新项目时,您必须考虑您希望它做什么。正如我们已经说过的,我们正在构建一个书签应用程序,所以我们可以假设我们希望这个应用程序做什么。让我们写下几个需求

  • 作为用户,我希望能够创建新的书签。
  • 作为用户,我希望能够查看我所有的书签。
  • 作为用户,我希望能够更新或删除我的书签。
  • 作为用户,我希望能够点击我的一个书签并查看网站。
  • 作为用户,我希望能够查看以特定方式标记的书签。

这些需求几乎是用户故事,所以我们可以逐步完成它们,并了解我们可能需要接触的区域。我们的第一步是设计我们需要在数据库中存储的数据。我们将使用 SQLite 来存储此应用程序的数据,以降低我们的需求。

要开始在您的 Laravel 应用程序中使用 SQLite,首先,我们需要通过在我们的终端(或者如果您更习惯在您的 IDE 中)运行以下命令来创建 SQLite 文件

touch database/database.sqlite

然后,我们需要打开我们的 .env 文件并修改我们的数据库块,以便我们的应用程序了解它。Laravel 使用 env 文件来配置您的本地环境,然后这些环境将通过 config/*.php 中的各种配置文件加载。每个文件都会配置应用程序的特定部分,所以请随意花点时间探索这些文件,看看配置是如何工作的。

目前,您的 env 中将有一个块,如下所示

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=

现在您需要做的是用以下内容替换此块,将 DB_DATABASE 路径替换为 /database/database.sqlite,您创建项目的任何地方(如果您需要帮助,可以使用 pwd bash 命令获取当前工作目录)

DB_CONNECTION=sqlite
DB_DATABASE=/Users/steve/code/sites/bookmarker/database/database.sqlite
DB_FOREIGN_KEYS=true

在这里,我们将数据库连接设置为 SQLite。数据库指向我们新创建的数据库文件,并且我们希望 SQLite 启用外键。

现在我们已经设置并配置了数据库,我们可以运行默认的数据库迁移来启动我们的应用程序。在 Laravel 中,数据库迁移用于更新应用程序数据库的状态。每次您想要更改数据库结构时,您都会创建一个新的迁移来创建表、添加列或删除列,甚至完全删除表。有关数据库迁移的文档非常出色,解释了所有可用的选项,因此当您有时间时,请务必阅读。Laravel 默认情况下附带一些用于用户、密码重置、失败作业和个人访问令牌的迁移。这些在 99% 的应用程序中都是有用的,因此我们将保留它们。

幸运的是,Laravel 已经准备好了一个 User 模型,因此我们不需要在那里编辑或更改任何内容。我们收集用户的姓名、电子邮件地址和密码,并在用户创建和最后更新时存储。因此,我们已经对数据模型进行了排序。接下来,我们需要考虑这个用户如何才能访问我们的应用程序。我们需要他们能够登录或注册新帐户。Laravel 为此提供了一些可用的包,或者您可以自己构建身份验证。但是,标准包非常出色且可定制,因此我们将使用它们。

在这个应用中,我们将使用一个名为 `Breeze` 的 Laravel 包,它是一个基本的认证脚手架,但也有一些其他选择,比如 `Jetstream`,它允许 2FA 和团队模型,多个用户可以协作。甚至还有一个名为 `Socialite` 的包,它可以让你配置和设置来自多个不同提供商的社交登录。但是,我们不需要这些,所以使用以下命令安装 Laravel Breeze

composer require laravel/breeze --dev

这使得 Laravel Breeze 成为应用程序的开发依赖项,它是一个开发依赖项,因为它需要被安装。安装完成后,该包将文件复制到你的应用程序中,用于路由、视图、控制器等。因此,让我们使用以下 artisan 命令安装该包

php artisan breeze:install

最后,我们需要使用 npm 安装并构建前端资源

npm install && npm run dev

这可能需要一段时间,因为它需要下载所有 JavaScript 或 CSS 包,然后为你运行一个构建过程。完成后,你将看到脚本停止执行。

现在所有这些都已安装并准备就绪。我们需要运行数据库迁移,以便我们的数据库处于特定状态,以便我们开始使用。你可以通过运行以下 artisan 命令来实现

php artisan migrate

这将运行你 `database/migrations` 目录中的每个迁移,并将它们应用到你的数据库中。因此,你的数据库状态可以直接与你的版本控制相绑定,使你的应用程序更智能、更具弹性。

让我们花点时间思考一下我们希望如何存储书签。每个书签都需要属于一个用户,具有唯一的标识符、可以访问的 URL 以及可选的描述,以防你在返回时想要写下关于它的说明。

现在,我们可以使用 artisan 命令行生成一个新的 eloquent 模型和迁移,所以在你的终端中运行以下命令

php artisan make:model Bookmark -m

我们要求 Laravel 创建一个名为 `Bookmark` 的新 eloquent 模型,`-m` 标志告诉命令也生成一个迁移。如果你需要创建新的模型和迁移,这是推荐的做法,因为它同时执行这两项操作。你可以将其他标志应用于此命令以生成模型工厂、种子器等,但我们不会在本入门教程中使用它们。

这将在 `database/migrations` 中为你创建一个新的迁移,它将有一个时间戳名称,后跟 `create_bookmarks_table`。在你的 IDE 中打开它,以便我们可以构建数据。在 `up` 方法中,用以下代码块替换内容

Schema::create('bookmarks', static function (Blueprint $table): void {
$table->id();
 
$table->string('name');
$table->string('url');
$table->text('description')->nullable();
 
$table->foreignId('user_id')
->index()->constrained()->cascadeOnDelete();
 
$table->timestamps();
});

从上面的代码中,你可以看到数据库迁移的描述性性质,我们如何逐步创建新表,以及描述我们希望如何构建它。现在,我们可以通过再次运行我们的迁移 artisan 命令将这些更改应用到我们的数据库中

php artisan migrate

接下来,让我们进入我们的 Eloquent 模型,并添加一些代码,使其了解数据库列以及它们可能具有的任何关系。在你的编辑器中打开 `app/Models/Bookmark.php`,并将内容替换为以下代码

declare(strict_types=1);
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
 
class Bookmark extends Model
{
use HasFactory;
 
protected $fillable = [
'name',
'url',
'description',
'user_id'
];
 
public function user(): BelongsTo
{
return $this->belongsTo(
related: User::class,
foreignKey: 'user_id',
);
}
}

我们将我们的 `fillable` 属性设置为与数据库中可用的列匹配。这将阻止任何与大规模分配属性相关的错误。然后,我们添加了用户方法,这是一个关系。一个 Bookmark `BelongsTo` 一个 User,使用 `user_id` 外键。我们也可以将此关系添加到我们的 User 模型中,因此在 Models 目录中,打开 `User.php` 文件,并将内容替换为以下代码

declare(strict_types=1);
 
namespace App\Models;
 
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
 
class User extends Authenticatable
{
use Notifiable;
use HasFactory;
use HasApiTokens;
 
protected $fillable = [
'name',
'email',
'password',
];
 
protected $hidden = [
'password',
'remember_token',
];
 
protected $casts = [
'email_verified_at' => 'datetime',
];
 
public function bookmarks(): HasMany
{
return $this->hasMany(
related: Bookmark::class,
foreignKey: 'user_id',
);
}
}

我们的用户现在了解了与书签的关系,因为一个 User `HasMany` Bookmarks。当我们开始在应用程序中构建逻辑时,我们将稍后使用这些关系。

最后,我们可以创建一个 Topic 模型。我们希望每个书签都与许多主题相关联。以 Laravel News 为例。我们可能希望用以下内容对其进行标记

  • Laravel
  • 新闻
  • 教程
  • 职位

因此,无论何时我们想查看关于这些标签中的任何一个的书签,Laravel News 都应该出现。像以前一样,我们将运行一个 artisan 命令来创建一个 Tag 模型

php artisan make:model Tag -m

现在,在你的编辑器中打开迁移文件,并再次替换 `up` 方法的内容

Schema::create('tags', static function (Blueprint $table): void {
$table->id();
$table->string('name');
$table->string('slug')->unique();
});

我们的标签有名称和别名,但这一次我们不希望有时间戳,因为它不是重要信息。我称之为元模型,用于分类,主要由系统使用,用户会创建它们,但它们不是重点。

所以让我们现在处理我们的 Eloquent 模型

declare(strict_types=1);
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
 
class Tag extends Model
{
use HasFactory;
 
protected $fillable = [
'name',
'slug',
];
 
public $timestamps = false;
}

到目前为止,我们没有任何关系,因为我们需要一个中间表来将这些标签与书签关联起来。在你的终端中运行以下命令来只生成一个迁移

php artisan make:migration create_bookmark_tag_table

Laravel 有一种约定,即对于中间表,你按照字母顺序并以单数形式命名它们,以及两个表。因此,我们想将书签和标签连接起来,所以我们将其命名为 `bookmark_tag`,因为标签可以属于许多不同的书签,而书签可以有许多其他标签。

让我们填写此迁移,看看它有什么不同,再次关注 `up` 方法

Schema::create('bookmark_tag', static function (Blueprint $table): void {
$table->foreignId('bookmark_id')->index()->constrained()->cascadeOnDelete();
$table->foreignId('tag_id')->index()->constrained()->cascadeOnDelete();
});

此表必须包含书签和标签主键的外键。现在我们有了此表的 Eloquent 模型,因此我们将关系添加到 Tag 和 Bookmark 模型中。

你的 Tag 模型现在应该看起来像这样

declare(strict_types=1);
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
 
class Tag extends Model
{
use HasFactory;
 
protected $fillable = [
'name',
'slug',
];
 
public $timestamps = false;
 
public function bookmarks(): BelongsToMany
{
return $this->belongsToMany(
related: Bookmark::class,
table: 'bookmark_tag',
);
}
}

你的 Bookmark 模型现在也应该看起来像这样

declare(strict_types=1);
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
 
class Bookmark extends Model
{
use HasFactory;
 
protected $fillable = [
'name',
'url',
'description',
'user_id'
];
 
public function user(): BelongsTo
{
return $this->belongsTo(
related : User::class,
foreignKey: 'user_id',
);
}
 
public function tags(): BelongsToMany
{
return $this->belongsToMany(
related: Tag::class,
table: 'bookmark_tag',
);
}
}

最后,运行迁移命令,以便我们的数据库状态可以发生变化

php artisan migrate

现在我们的 Bookmark 和 Tag 模型已经相互了解,我们可以开始构建我们的用户界面!在本教程中,我们不会专注于一个经过打磨的用户界面,所以请随意发挥你的创意。但是,我们将使用 tailwindcss

我们将把大多数与书签相关的工作放在 Laravel Breeze 创建的仪表板路由中,因此如果你查看 `routes/web.php`,你应该看到以下内容

declare(strict_types=1);
 
use Illuminate\Support\Facades\Route;
 
Route::get('/', function () {
return view('welcome');
});
 
Route::get('/dashboard', function () {
return view('dashboard');
})->middleware(['auth'])->name('dashboard');
 
require __DIR__.'/auth.php';

仪表板路由目前是一个闭包,我们可能想将其重构为控制器。所以让我们创建一个新的控制器来保存仪表板的逻辑,运行以下 artisan 命令

php artisan make:controller DashboardController --invokable

现在让我们重构这个路由文件,使其更简洁

declare(strict_types=1);
 
use Illuminate\Support\Facades\Route;
 
Route::view('/', 'welcome')->name('home');
 
Route::get(
'/dashboard',
App\Http\Controllers\DashboardController::class
)->middleware(['auth'])->name('dashboard');
 
require __DIR__.'/auth.php';

我们已将主页路由简化为一个 `view` 路由,而仪表板路由现在指向一个控制器。在你的编辑器中打开此控制器,以便我们可以复制之前的逻辑

declare(strict_types=1);
 
namespace App\Http\Controllers;
 
use App\Models\Bookmark;
use Illuminate\Contracts\View\View;
use Illuminate\Http\Request;
 
class DashboardController extends Controller
{
public function __invoke(Request $request): View
{
return view('dashboard', [
'bookmarks' => Bookmark::query()
->where('user_id', auth()->id())
->get()
]);
}
}

像以前一样,我们现在只需要返回一个视图。现在让我们测试一下,运行以下 artisan 命令来再次运行你的应用程序

php artisan serve

现在,如果你在浏览器中打开它,在右上角,你应该看到两个链接,分别为“登录”和“注册”。尝试注册一个帐户,然后等待它将你重定向到仪表板。你应该看到一条消息“你已登录!”。

到目前为止,你做得太棒了!你现在拥有一个 Laravel 应用程序,它处理身份验证并在后台具有数据模型,我们可以用它来创建和管理书签。

在前端方面,很难确定你希望它如何工作,有数百万个 JavaScript 库可供选择,或者你可以使用纯 PHP 和 blade。在本教程中,我们将专注于使用 Laravel blade,因为我们不想在学习初期添加太多复杂的功能或包。

当我们安装 Laravel Breeze 时,我们获得了几个额外的视图文件,这很好,因为它已经为成功做好了准备。对于新的书签表单,我将创建一个新的 Blade 组件,它是一个单独的视图,我们可以将其拉入并在多个地方使用。

创建一个新的 Blade 组件,一个匿名的组件,它只是一个视图文件,运行以下 artisan 命令

php artisan make:component bookmarks.form --view

然后在我们的 `resources/views/dashboard.blade.php` 中,我们可以将其重构为以下内容

<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Dashboard') }}
</h2>
</x-slot>
 
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<x-bookmarks.form :bookmarks="$bookmarks" />
</div>
</div>
</div>
</x-app-layout>

我们通过调用 `<x-bookmarks.form />` 来加载我们的 blade 组件,它的工作方式是:所有 blade 组件都可以通过在名称前加 `x-` 来加载。然后,如果它嵌入在目录中,我们将用 `.` 表示每个目录,因此查看 `x-bookmarks.form`,我们可以假设它存储在 `resources/views/components/bookmarks/form.blade.php` 中。在这里,我们将创建一个简单的用于添加新书签的方法。将以下(大量)代码片段添加到组件中

@props(['bookmarks'])
 
<div>
<div x-data="{ open: true }" class="overflow-hidden">
<div class="px-4 py-5 border-b border-gray-200 sm:px-6">
<div class="-ml-4 -mt-2 flex items-center justify-between flex-wrap sm:flex-nowrap">
<div class="ml-4 mt-2">
<h3 class="text-lg leading-6 font-medium text-gray-900">
Your Bookmarks
</h3>
</div>
<div class="ml-4 mt-2 flex-shrink-0">
<a x-on:click.prevent="open = ! open" class="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
<span x-show="! open" x-cloak>Show Form</span>
<span x-show="open" x-cloak>Hide Form</span>
</a>
</div>
</div>
</div>
 
<div x-show="open" x-cloak class="divide-y divide-gray-200 py-4 px-4">
<div class="pt-8">
<div>
<h3 class="text-lg leading-6 font-medium text-gray-900">
Create a new bookmark.
</h3>
<p class="mt-1 text-sm text-gray-500">
Add information about the bookmark to make it easier to understand later.
</p>
</div>
<form id="bookmark_form" method="POST" action="{{ route('bookmarks.store') }}" class="mt-6 grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">
@csrf
 
<div class="sm:col-span-3">
<label for="name" class="block text-sm font-medium text-gray-700">
Name
</label>
<div class="mt-1">
<input type="text" name="name" id="name" class="shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md">
</div>
@error('name')
<p class="mt-2 text-sm text-red-500">
{{ $message }}
</p>
@enderror
</div>
 
<div class="sm:col-span-3">
<label for="url" class="block text-sm font-medium text-gray-700">
URL
</label>
<div class="mt-1">
<input type="text" name="url" id="url" class="shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md">
</div>
@error('url')
<p class="mt-2 text-sm text-red-500">
{{ $message }}
</p>
@enderror
</div>
 
<div class="sm:col-span-6">
<label for="description" class="block text-sm font-medium text-gray-700">
Description
</label>
<div class="mt-1">
<textarea id="description" name="description" rows="3" class="shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border border-gray-300 rounded-md"></textarea>
</div>
<p class="mt-2 text-sm text-gray-500">
Write any notes about this bookmark.
</p>
@error('description')
<p class="mt-2 text-sm text-red-500">
{{ $message }}
</p>
@enderror
</div>
 
<div class="sm:col-span-6">
<label for="tags" class="block text-sm font-medium text-gray-700">
Tags
</label>
<div class="mt-1">
<input
type="text"
name="tags"
id="tags"
class="shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md"
/>
<p class="mt-2 text-sm text-gray-500">
Add a comma separated list of tags.
</p>
@error('tag')
<p class="mt-2 text-sm text-red-500">
{{ $message }}
</p>
@enderror
</div>
</div>
 
<div class="sm:col-span-6">
<div class="pt-5">
<div class="flex justify-end">
<a x-on:click.prevent="document.getElementById('bookmark_form').reset();" class="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Cancel
</a>
<button type="submit" class="ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Save
</button>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
 
@forelse ($bookmarks as $bookmark)
<div>
<a href="#" class="block hover:bg-gray-50">
<div class="px-4 py-4 sm:px-6">
<div class="flex items-center justify-between">
<p class="text-sm font-medium text-indigo-600 truncate">
{{ $bookmark->name }}
</p>
</div>
<div class="mt-2 sm:flex sm:justify-between">
<div class="flex space-x-4">
@foreach ($bookmark->tags as $tag)
<p class="flex items-center text-sm text-gray-500">
{{ $tag->name }}
</p>
@endforeach
</div>
</div>
</div>
</a>
</div>
@empty
<a href="#" class="relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z"></path></svg>
<span class="mt-2 block text-sm font-medium text-gray-900">
Create a new bookmark
</span>
</a>
@endforelse
</div>

我们有一个相当大的组件,它将处理我们在前端创建书签所需的所有逻辑。首先,我们的组件声明它希望向其发送道具,即组件可以使用的一些属性。这些只是我们传入的变量,以便我们的组件知道它们。然后,我们在顶部有一个部分,它是控制部分。它有一个标题和一个操作按钮。我们使用 Alpine.js 来实现我们需要的基本 JavaScript,即切换表单的可见性。我们的表单是一个标准的 HTML 表单,但我们将其发布到我们尚未创建的路由中,所以很快,我们将处理它。然后,我们在表单中添加了一个新的 blade 指令,该指令 `@csrf` 将向我们的表单添加一个跨站点请求伪造输入,以防止任何其他站点试图劫持我们的表单并进行干预。其余的代码只是用于视觉效果的标记,所以请随意根据你的喜好进行自定义。需要注意的是,目前我们使用逗号分隔的列表来添加标签。如果我们要使用更多 JavaScript 或 UI 库,我们可能会采用略有不同的方法。然后我们有一个取消按钮和一个保存按钮。我们的取消按钮将使用 JavaScript 重置表单,而我们的提交按钮,正如你所想象的那样,会提交我们的表单。

所以现在我们必须保存它,很有可能你的页面无法加载,因为路由尚未定义,这没关系。我们即将创建它。然而,首先,我们需要创建控制器,我们在其中将保存此数据,因此使用以下 artisan 命令,创建一个新的控制器

php artisan make:controller Bookmarks/StoreController --invokable

然后将以下内容添加到你的路由文件中

Route::post(
'bookmarks',
App\Http\Controllers\Bookmarks\StoreController::class,
)->middleware(['auth'])->name('bookmarks.store');

现在在控制器内部,我们需要做几件事。首先,我们希望验证请求,以便我们可以将任何信息与验证消息一起传递回去。然后,我们希望执行一个操作来创建一个新的书签,最后,我们将重定向回仪表板,在那里我们将看到新创建的书签。

在你的 `app/Http/Controllers/Bookmarks/StoreController.php` 中,我们将添加以下代码

declare(strict_types=1);
 
namespace App\Http\Controllers\Bookmarks;
 
use App\Http\Controllers\Controller;
use App\Models\Tag;
use Illuminate\Http\Request;
use Illuminate\Http\RedirectResponse;
 
class StoreController extends Controller
{
public function __invoke(Request $request): RedirectResponse
{
$this->validate($request, [
'name' => [
'required',
'string',
'min:1',
'max:255',
],
'url' => [
'required',
'url',
],
'description' => [
'nullable',
'string',
],
'tags' => [
'nullable',
'array',
]
]);
 
$bookmark = auth()->user()->bookmarks()->create([
'name' => $request->get('name'),
'url' => $request->get('url'),
'description' => $request->get('description'),
]);
 
foreach (explode(',', $request->get('tags')) as $tag) {
$tag = Tag::query()->firstOrCreate(
['name' => trim(strtolower($tag))],
);
 
$bookmark->tags()->attach($tag->id);
}
 
return redirect()->route('dashboard');
}
}

我们有一个 `__invoke` 方法,它将接受当前请求。这是由 Laravels DI 容器处理的,所以你不必担心。然后,我们可以调用 `$this->validate` 的原因是我们扩展了 Laravel 应用程序的主要控制器。我们设置了我们的验证规则。传递给验证的第一个参数是我们想要验证的数据。然后我们传递一个包含我们要遵循的验证规则的数组。我已经设置了我觉得合理的默认规则,请仔细查看它们,并随时查看 Laravel 文档中提供的可用验证选项:文档

接下来,我们开始创建书签。不过,我们不会在这里使用模型,因为我们可以通过获取已认证用户、获取书签关联方法并调用 create 来节省时间 - 这意味着我们不必传入 user_id,因为它直接来自用户。然后,我们循环遍历请求标签,获取第一个匹配的标签,或者根据输入名称创建一个新的标签(我们会修剪空格,并将其设置为小写字符串以确保一致性)。然后,我们将这个新标签附加到书签。最后,我们返回一个重定向,以查看新创建的书签。

这段代码很好,而且完全满足我们的需求,但是我们还能做得更好吗?我认为可以。

重构这段代码的第一步是将验证从我们的控制器中提取出来。将其保留在控制器中并没有什么问题。但是,如果验证失败,我们可以通过不实例化控制器来节省一些时间。为此,我们可以使用以下 artisan 命令创建一个新的表单请求:

php artisan make:request Bookmarks/StoreRequest

这将在 `app/Http/Requests/Bookmarks/StoreRequest.php` 中创建一个新的类,所以我们打开它,添加一些代码,并逐步讲解。

declare(strict_types=1);
 
namespace App\Http\Requests\Bookmarks;
 
use Illuminate\Foundation\Http\FormRequest;
 
class StoreRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
 
public function rules(): array
{
return [
'name' => [
'required',
'string',
'min:1',
'max:255',
],
'url' => [
'required',
'url',
],
'description' => [
'nullable',
'string',
],
'tags' => [
'nullable',
'array',
]
];
}
}

我们使用 `authorize` 方法来表示这是一个授权的请求。现在,这很好,但是如果你以后添加了角色和权限层,你可以确保已认证用户被允许对书签执行存储操作。然后是 `rules` 方法,它是一个包含验证规则的数组,类似于我们在控制器中使用的规则。现在,Laravel 会怎么做呢?当请求到来时,它会使用 DI 容器,在实例化新控制器之前,它会尝试构建表单请求。这样做会验证请求。如果验证失败,将抛出一个异常,Laravel 会捕获该异常,将其转换为 ErrorBag,并将其返回到上一个视图,该 ErrorBag 可以用于显示任何验证错误。这是一个非常棒的 Laravel 功能。但是,在这之前,我们需要告诉我们的控制器使用这个新的表单请求,因此,更改 `__invoke()` 方法的签名,使其如下所示:

public function __invoke(StoreRequest $request): RedirectResponse

现在,验证将自动完成。所以我们可以删除扩展 Laravel 基本控制器的要求,并删除手动验证。

declare(strict_types=1);
 
namespace App\Http\Controllers\Bookmarks;
 
use App\Http\Requests\Bookmarks\StoreRequest;
use App\Models\Tag;
use Illuminate\Http\RedirectResponse;
 
class StoreController
{
public function __invoke(StoreRequest $request): RedirectResponse
{
$bookmark = auth()->user()->bookmarks()->create([
'name' => $request->get('name'),
'url' => $request->get('url'),
'description' => $request->get('description'),
]);
 
foreach (explode(',', $request->get('tags')) as $tag) {
$tag = Tag::query()->firstOrCreate(
['name' => trim(strtolower($tag))],
);
 
$bookmark->tags()->attach($tag->id);
}
 
return redirect()->route('dashboard');
}
}

突然之间,我们的控制器变得小很多,也更容易理解。如果你需要在注释中添加一个备注来提醒自己验证正在处理,请随意添加,直到你进入一个你记住这种情况的工作流程。

我们可以保留在这里,因为它是合理的,但是 Laravel 中的一个标准是将这个创建逻辑移动到一个名为操作的新类中。没有创建操作的 artisan 命令,所以这需要手动完成。在这里创建一个新文件 `app/Actions/Bookmarks/CreateBookmarkAndTags`。

然后在编辑器中打开这个文件,以便我们添加以下代码块:

declare(strict_types=1);
 
namespace App\Actions\Bookmarks;
 
use App\Models\Bookmark;
use App\Models\Tag;
 
class CreateBookmarkAndTags
{
public function handle(array $request, int $id): void
{
$bookmark = Bookmark::query()->create([
'name' => $request['name'],
'url' => $request['url'],
'description' => $request['description'],
'user_id' => $id,
]);
 
if ($request['tags'] !== null) {
foreach (explode(',', $request['tags']) as $tag) {
$tag = Tag::query()->firstOrCreate(
['name' => trim(strtolower($tag))],
);
 
$bookmark->tags()->attach($tag->id);
}
}
}
}

我们有一个单一的 `handle` 方法,它接收请求数据和一个我们将用于用户 ID 的 ID,然后我们将控制器中的逻辑复制到其中,并做一些小的调整。我们可以在应用程序中的任何地方使用这个操作,例如从 UI、CLI 甚至 API,如果需要的话。我们创建了一个模块化的操作,可以轻松调用、测试,并具有可预测的结果。

所以现在我们可以进一步重构我们的控制器:

declare(strict_types=1);
 
namespace App\Http\Controllers\Bookmarks;
 
use App\Actions\Bookmarks\CreateBookmarkAndTags;
use App\Http\Requests\Bookmarks\StoreRequest;
use Illuminate\Http\RedirectResponse;
 
class StoreController
{
public function __invoke(StoreRequest $request): RedirectResponse
{
(new CreateBookmarkAndTags())->handle(
request: $request->all(),
id: auth()->id(),
);
 
return redirect()->route('dashboard');
}
}

现在,我们有一个单一的动作,我们在控制器中调用它,然后返回一个重定向。更干净,而且命名良好。当然,我们还可以进一步改进它,例如使用 Laravel 容器在构造函数中注入操作 - 允许我们调用操作。这将看起来像下面这样:

declare(strict_types=1);
 
namespace App\Http\Controllers\Bookmarks;
 
use App\Actions\Bookmarks\CreateBookmarkAndTags;
use App\Http\Requests\Bookmarks\StoreRequest;
use Illuminate\Http\RedirectResponse;
 
class StoreController
{
public function __construct(
protected CreateBookmarkAndTags $action,
) {}
 
public function __invoke(StoreRequest $request): RedirectResponse
{
$this->action->handle(
request: $request->all(),
id: auth()->id(),
);
 
return redirect()->route('dashboard');
}
}

这种方法在你的操作对构造函数有要求的情况下非常有用。例如,如果你正在使用存储库模式或其他模式 - 你可以在你的操作的构造函数中添加它,如果 Laravel 可以解析它,它会自动为你解析它。

所以现在我们可以列出和创建我们的书签,我们也可以在书签列表中添加简单的按钮来删除它们 - 现在没有必要创建过大的东西,对吧?

使用 artisan 命令创建一个新的控制器:

php artisan make:controller Bookmarks/DeleteController --invokable

现在,我们不需要为此操作创建动作,因为它只是一行代码,但是如果你想提供多种删除书签的方法,请遵循与上面相同的步骤,但这次是删除书签而不是创建书签。将以下代码添加到你的控制器中:

declare(strict_types=1);
 
namespace App\Http\Controllers\Bookmarks;
 
use App\Models\Bookmark;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
 
class DeleteController
{
public function __invoke(Request $request, Bookmark $bookmark): RedirectResponse
{
$bookmark->delete();
 
return redirect()->route('dashboard');
}
}

这里我们接受 Bookmark 模型作为参数,以便我们可以启用路由模型绑定,Laravel 会为你查找记录,并将其注入到你的方法中 - 如果失败,它会抛出一个 404 异常。然后,我们只需要对模型调用 `delete`,并返回一个重定向。接下来添加路由:

Route::delete(
'bookmarks/{bookmark}',
App\Http\Controllers\Bookmarks\DeleteController::class,
)->middleware(['auth'])->name('bookmarks.delete');

最后,我们可以回到我们的组件,并添加一个按钮:

@forelse ($bookmarks as $bookmark)
<div>
<a href="#" class="block hover:bg-gray-50">
<div class="px-4 py-4 sm:px-6">
<div class="flex items-center justify-between">
<p class="text-sm font-medium text-indigo-600 truncate">
{{ $bookmark->name }}
</p>
<div class="ml-2 flex-shrink-0 flex">
<form method="DELETE" action="{{ route('bookmarks.delete', $bookmark->id) }}">
@csrf
 
<button type="submit" class="ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-red-500 bg-gray-100 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Delete
</button>
</form>
</div>
</div>
<div class="mt-2 sm:flex sm:justify-between">
<div class="flex space-x-4">
@foreach ($bookmark->tags as $tag)
<p class="flex items-center text-sm text-gray-500">
{{ $tag->name }}
</p>
@endforeach
</div>
</div>
</div>
</a>
</div>
@empty
<a href="#" class="relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z"></path></svg>
<span class="mt-2 block text-sm font-medium text-gray-900">
Create a new bookmark
</span>
</a>
@endforelse

现在我们已经全部连接起来了。我们可以列出、创建和删除。最后我要做的事情是添加一个查看书签的方法。一个简单的方法是在新标签中打开链接,但是这会很无聊...

相反,我将重定向到书签,并附带一个推荐者,以便人们可以跟踪它的来源。为此,让我们再次使用 artisan 控制台命令创建一个新的控制器:

php artisan make:controller Bookmarks/RedirectController --invokable

现在添加这个 GET 路由:

Route::get(
'bookmarks/{bookmark}',
App\Http\Controllers\Bookmarks\RedirectController::class
)->middleware(['auth'])->name('bookmarks.redirect');

为了管理构建这个 URL,我们可以手动编写它。但是,我之前为这些情况创建了一个库,叫做 `juststeveking/uri-builder`,它可以让我构建一个 URI 并流畅地添加额外的部分。

declare(strict_types=1);
 
namespace App\Http\Controllers\Bookmarks;
 
use App\Http\Controllers\Controller;
use App\Models\Bookmark;
use Illuminate\Http\Request;
use JustSteveKing\UriBuilder\Uri;
 
class RedirectController extends Controller
{
public function __invoke(Request $request, Bookmark $bookmark)
{
$url = Uri::fromString(
uri: $bookmark->url,
)->addQueryParam(
key: 'utm_campaign',
value: 'bookmarker_' . auth()->id(),
)->addQueryParam(
key: 'utm_source',
value: 'Bookmarker App'
)->addQueryParam(
key: 'utm_medium',
value: 'website',
);
 
return redirect(
$url->toString(),
);
}
}

你不需要像我这里一样深入 - 这部分取决于你。最后,我们只需在 UI 中添加一个链接,就可以开始了。

<div class="flex items-center justify-between">
<p class="text-sm font-medium text-indigo-600 truncate">
{{ $bookmark->name }}
</p>
<div class="ml-2 flex-shrink-0 flex">
<a
href="{{ route('bookmarks.redirect', $bookmark->id) }}"
target="__blank"
rel="nofollow noopener"
class="ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-indigo-600 bg-gray-100 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>Visit</a>
<form method="POST" action="{{ route('bookmarks.delete', $bookmark->id) }}">
@csrf
@method('DELETE')
 
<button type="submit" class="ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-red-500 bg-gray-100 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Delete
</button>
</form>
</div>
</div>

你可以进一步扩展它,例如收集你查看次数最多的书签的统计数据,或者其他任何东西。你甚至可以允许你点击标签查看所有具有特定标签的书签 - 但是我认为本教程的内容已经足够多了。

你觉得怎么样?为不熟悉 Laravel 的人编写教程可能很棘手,我希望我能以足够清晰和详细的方式解释它,以便你能跟上。请在 Twitter 上告诉我你的想法!

Steve McDougall photo

技术作家,就职于 Laravel News,开发者倡导者,就职于 Treblle。API 专家,经验丰富的 PHP/Laravel 工程师。 YouTube 直播主.

Cube

Laravel 新闻

加入 40k+ 其他开发者,绝不错过新的技巧、教程等等。

Laravel Forge logo

Laravel Forge

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

Laravel Forge
Tinkerwell logo

Tinkerwell

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

Tinkerwell
No Compromises logo

没有妥协

Joel 和 Aaron,来自 “没有妥协” 播客的两位经验丰富的开发者,现在可以为你的 Laravel 项目提供服务。 ⬧ 固定费用为 7500 美元/月。 ⬧ 没有冗长的销售流程。 ⬧ 没有合同。 ⬧ 100% 退款保证。

没有妥协
Kirschbaum logo

Kirschbaum

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

Kirschbaum
Shift logo

Shift

正在运行旧版本的 Laravel?即时、自动化的 Laravel 升级和代码现代化,使你的应用程序保持新鲜。

Shift
Bacancy logo

Bacancy

只需每月 2500 美元,即可使用经验丰富的 Laravel 开发者(具有 4-6 年的经验)来增强你的项目。获得 160 小时的专职专业知识和 15 天的无风险试用。立即安排通话!

Bacancy
Lucky Media logo

Lucky Media

立即获得幸运 - 拥有十多年经验的 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

在你的 Laravel 应用程序中添加 Swagger UI

阅读文章
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 应用程序添加评论

阅读文章