您的第一个 Laravel 9 应用程序
发布日期:作者: 史蒂夫·麦克杜格尔
如果您从未构建过 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')
,这将为您提供 index
、create
、store
、show
、edit
、update
、destroy
方法。这些在文档中有很好的解释。您还可以使用可调用控制器,它是一个具有 __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=mysqlDB_HOST=127.0.0.1DB_PORT=3306DB_DATABASE=laravelDB_USERNAME=rootDB_PASSWORD=
现在您需要做的是用以下内容替换此块,将 DB_DATABASE
路径替换为 /database/database.sqlite
,您创建项目的任何地方(如果您需要帮助,可以使用 pwd
bash 命令获取当前工作目录)
DB_CONNECTION=sqliteDB_DATABASE=/Users/steve/code/sites/bookmarker/database/database.sqliteDB_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 上告诉我你的想法!
技术作家,就职于 Laravel News,开发者倡导者,就职于 Treblle。API 专家,经验丰富的 PHP/Laravel 工程师。 YouTube 直播主.