Laravel 启动指南(三)
原文:
zh.annas-archive.org/md5/d0c72cd35a2ef551cf4f36bed0d4e4e2译者:飞龙
第六章:前端组件
Laravel 主要被认为是一个 PHP 框架,但它也是 全栈 的,意味着它有一系列组件和约定,专注于生成前端代码。其中一些组件,如分页和消息包,是针对前端的 PHP 帮助程序,但 Laravel 还提供基于 Vite 的前端构建系统,一些非 PHP 资产的约定以及几个起始套件。
Laravel 起始套件
在开箱即用的情况下,Laravel 提供了一个完整的构建系统,我们很快就会介绍,但它还包括易于安装的起始套件,其中包含模板、认证、样式、JavaScript 和用户注册及管理工作流程。
Laravel 的两个起始套件称为 Breeze 和 Jetstream。
Breeze 是一个更简单的选择;它提供了 Laravel 认证系统所需的所有路由、视图和样式,包括注册、登录、密码重置、密码确认、电子邮件确认以及“编辑个人资料”页面。Breeze 包含 Tailwind 样式,您可以选择使用 Blade 模板或者 Inertia 配合 React 或 Vue。
Jetstream 更加复杂和强大;它提供了 Breeze 所有的功能,但还增加了双因素认证、会话管理、API 令牌管理和团队管理功能。Jetstream 包含 Tailwind 样式,您可以选择 Livewire 或者 Inertia 配合 Vue。
注意
Inertia 是一个前端工具,允许您在 JavaScript 中构建单页面应用程序,同时使用 Laravel 路由和控制器为每个视图提供路由和数据,就像传统的服务器渲染应用程序一样。了解更多,请访问 inertiajs.com。
如果您刚开始使用 Laravel,Breeze 更容易理解,并且可以仅使用 Blade。大多数 Laravel 应用程序仅使用 Breeze 就能正常工作。
Jetstream 没有仅限于 Blade 的选项,也没有 React 的选项;您需要使用某种前端框架。您的选择是 Vue/Inertia 或者 Livewire,后者允许您主要编写后端代码,但在 Laravel 应用程序中获取前端交互性。然而,Jetstream 更为强大,因此如果您熟悉 Laravel 并且了解 Livewire 或 Inertia,并且您的项目需要这些额外的功能,Jetstream 可能是您的最佳选择。
Laravel Breeze
Laravel Breeze 是一个简单的起始套件,为普通的 Laravel 应用程序提供了一切所需,允许用户注册、登录和管理他们的个人资料。
安装 Breeze
Breeze 旨在安装在新应用程序上,因此通常是您启动新应用程序时安装的第一个内容:
laravel new myProject
cd myProject
composer require laravel/breeze --dev
一旦将Breeze添加到您的项目中,您将运行其安装程序:
php artisan breeze:install
一旦运行安装程序,您将提示选择一个堆栈:Blade、Inertia 配合 React、Inertia 配合 Vue 或者 API,后者用于支持像 Next.js 这样的非 Inertia 前端。这些堆栈在下一节中有详细解释。
安装了 Breeze 后,请确保运行您的迁移并构建您的前端:
php artisan migrate
npm install
npm run dev
Breeze 所带来的内容
Breeze 自动注册了用于注册、登录、注销、密码重置、电子邮件验证和密码确认页面的路由。这些路由位于新的 routes/auth.php 文件中。
Breeze 的非 API 形式还为用户仪表板和“编辑个人资料”页面注册了路由,并直接添加到 routes/web.php 文件中。
Breeze 的非 API 形式还发布了用于“编辑个人资料”页面、电子邮件验证、密码重置以及其他几个与身份验证相关的功能的控制器。此外,它还添加了 Tailwind、Alpine.js 和 PostCSS(用于 Tailwind)。除了这些共享的文件和依赖项外,每个堆栈还根据自身需求添加了独特的文件:
Breeze Blade
Breeze Blade 包含一系列 Blade 模板,涵盖了上述所有功能,你可以在 resources/views/auth、resources/view/components、resources/views/profile 等位置找到它们。
Breeze Inertia
两种 Inertia 堆栈都引入了 Inertia、Ziggy(用于在 JavaScript 中生成到 Laravel 路由的 URL 的工具)、Tailwind 的“forms”组件,以及使它们各自的前端框架功能正常运行所需的 JavaScript 包。它们还都发布了一个基本的 Blade 模板,该模板加载了 Inertia,以及在 resources/js 目录中发布页面的一系列 React/Vue 组件。
Breeze API
Breeze 的 API 堆栈安装的代码和包明显较少,但它也删除了所有新 Laravel 应用程序的现有引导文件。API 堆栈旨在准备一个应用程序仅作为独立的 Next.js 应用程序的 API 后端,因此它删除了 package.json、所有 JavaScript 和 CSS 文件,以及所有前端模板。
Laravel Jetstream
Jetstream 延续了 Breeze 的功能,并增加了更多用于启动新应用的工具;但是,它的设置更复杂,配置选项更少,因此在选择 Jetstream 而不是 Breeze 之前,您需要知道自己确实需要它。
与 Breeze 类似,Jetstream 也发布路由、控制器、视图和配置文件。与 Breeze 一样,Jetstream 使用 Tailwind,并提供不同的技术“堆栈”选项。
然而,与 Breeze 不同,Jetstream 需要互动性,因此没有仅限于 Blade 的堆栈。相反,您有两个选择:Livewire(这是带有一些由 PHP 驱动的 JavaScript 互动功能的 Blade)或 Inertia/Vue(Jetstream 没有 React 形式)。
Jetstream 还通过引入团队管理功能、双因素认证、会话管理和个人 API 令牌管理扩展了 Breeze 的功能。
安装 Jetstream
Jetstream 旨在安装到新的 Laravel 应用程序中,您可以使用 Composer 安装它:
laravel new myProject
cd myProject
composer require laravel/jetstream
一旦将 Jetstream 添加到您的项目中,您将运行其安装程序。与 Breeze 不同的是,您不会被要求选择堆栈;相反,您需要将堆栈 (livewire 或 inertia) 作为第一个参数传入。
php artisan jetstream:install livewire
如果你想在 Jetstream 安装中添加团队管理功能,请在安装步骤中加入--teams标志:
php artisan jetstream:install livewire --teams
安装完 Jetstream 后,请确保运行你的迁移并构建你的前端:
php artisan migrate
npm install
npm run dev
Jetstream 包含了什么
Jetstream 发布了大量的代码;以下是一个快速总结:
-
为用户模型添加双因素认证和个人资料照片功能(并添加/修改所需的迁移)
-
登录用户的仪表板
-
Tailwind, Tailwind forms, Tailwind typography
-
Laravel Fortify,Jetstream 构建在其上的后端身份验证组件
-
app/Actions 中的 Fortify 和 Jetstream 的“操作”
-
resources/markdown 中的条款和政策页面的 Markdown 文本
-
一个庞大的测试套件
Fortify
Fortify 是一个无头身份验证系统。它为 Laravel 所需的所有身份验证功能提供路由和控制器,从登录和注册到密码重置等,供你选择的任何前端消费。
Jetstream 建立在 Fortify 之上,因此你实际上可以将 Jetstream 视为 Fortify 的众多可能前端之一。Jetstream 还添加了后端功能,因此显示了 Fortify 支持的身份验证系统可以有多强大。
Jetstream 的 Livewire 和 Inertia 配置分别具有稍有不同的依赖项和模板位置:
Jetstream Livewire
Jetstream 的 Livewire 模板为你的应用程序设置了与 Livewire 和 Alpine 协作的基础,并发布了前端的 Livewire 组件。它提供了:
-
Livewire
-
Alpine.js
-
app/View/Components 中的 Livewire 组件
-
resources/views 中的前端模板
Jetstream Inertia
Jetstream 的 Inertia 模板为你的应用程序设置了与 Inertia 和 Vue 协作的基础,并发布了前端的 Vue 组件。它提供了:
-
Inertia
-
Vue
-
resources/js 中的 Vue 模板
自定义你的 Jetstream 安装
Jetstream 构建在 Fortify 基础之上,因此有时自定义 Jetstream 就意味着要自定义 Fortify。你可以在 config/fortify.php, config/jetstream.php, FortifyServiceProvider 和 JetstreamServiceProvider 中更新任何配置设置。
虽然 Breeze 为你发布控制器以修改其行为,Jetstream 发布了动作,每个动作都是一个一次性的行为块,名称如 ResetUserPassword.php 和 DeleteUser.php。
更多 Jetstream 特性
Jetstream 使你的应用程序能够管理团队、个人 API 令牌、双因素认证以及跟踪和断开所有活动会话。你还可以在自己的代码中使用 Jetstream 的一些 UI 精美功能,如自定义闪存横幅。
要了解更多关于这一切是如何工作的信息,请查看详尽的 Laravel Jetstream 文档。
Laravel 的 Vite 配置
Vite 是一个本地前端开发环境,结合了开发服务器和基于 Rollup 的构建工具链。这听起来可能很多,但在 Laravel 中,主要用于将 CSS 和 JavaScript 资源捆绑在一起。
Laravel 提供了一个 NPM 插件和一个 Blade 指令,使得与 Vite 协作变得容易。它们默认包含在 Laravel 应用程序中,还有一个配置文件:vite.config.js。
看一下 Example 6-1 来查看默认 vite.config.js 文件的内容。
示例 6-1. 默认的 vite.config.js
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
export default defineConfig({
plugins: [
laravel({
input: ['resources/css/app.css', 'resources/js/app.js'],
refresh: true,
}),
],
});
我们定义插件应该从中构建的文件 (input),并说我们希望启用 “每次保存视图文件时刷新我的页面” 的功能 (refresh)。
默认情况下,Vite 从 Example 6-1 中列出的两个文件中拉取,并在这些文件夹中的任何文件更改时自动刷新:
-
app/View/Components/
-
lang/
-
resources/lang/
-
resources/views/
-
routes/
现在,我们已经将 Vite 配置指向我们的 CSS 和 JavaScript 入口文件,我们将使用 @vite Blade 指令引用这些文件,正如你在 Example 6-2 中所看到的。
示例 6-2. 使用 @vite Blade 指令
<html>
<head>
@vite(['resources/css/app.css', 'resources/js/app.js'])
就这样!接下来,让我们看看如何使用 Vite 打包文件。
注意
如果你的本地开发域名是安全的(HTTPS),你需要修改你的 vite.config.js 文件,指向你的凭据。如果你使用 Valet,这里有一个特殊的配置选项:
// ...
export default defineConfig({
plugins: [
laravel({
// ...
valetTls: 'name-of_my-app-here.test',
}),
],
});
使用 Vite 打包文件
最后,是时候打包我们的资产了。使用 Vite 有两种打包资产的方式:“build” 和 “dev”。
如果你想构建文件一次,无论是交付到生产环境还是进行本地测试,运行 npm run build,Vite 将会打包你的资产。然而,如果你在本地开发,可能更喜欢让 Vite 启动一个进程,监视你的视图文件变化,每当检测到视图文件变化时重新触发构建,并在浏览器中刷新页面。这就是 npm run dev 为你做的事情。
你的构建文件将会存放在应用的 public/build/assets 文件夹中,同时还有一个位于 public/build/manifest.json 的文件,它告诉 Laravel 和 Vite 如何从非构建路径引用到每一个构建文件。
注意
默认情况下,Laravel 的 .gitignore 忽略 public/build 文件夹,所以确保在部署过程中运行 npm run build。
Vite 开发服务器
当你运行 npm run dev 时,你会启动一个由 Vite 提供支持的实际 HTTP 服务器。Vite Blade 辅助程序会重写你的资产 URL,指向开发服务器上的相同位置,而不是你的本地域名,这使得 Vite 能够更快地更新和刷新你的依赖。
这意味着,如果你编写以下 Blade 调用:
@vite(['resources/css/app.css', 'resources/js/app.js'])
在你的生产应用程序上看起来会是这样:
<link rel="preload" as="style"
href="http://my-app.test/build/assets/app-1c09da7e.css" />
<link rel="modulepreload"
href="http://my-app.test/build/assets/app-ea0e9592.js" />
<link rel="stylesheet"
href="http://my-app.test/build/assets/app-1c09da7e.css" />
<script type="module"
src="http://my-app.test/build/assets/app-ea0e9592.js"></script>
但是,如果你的 Vite 服务器在本地运行,情况会是这样的:
<script type="module" src="http://127.0.0.1:5173/@vite/client"></script>
<link rel="stylesheet" href="http://127.0.0.1:5173/resources/css/app.css" />
<script type="module" src="http://127.0.0.1:5173/resources/js/app.js"></script>
使用静态资产和 Vite
到目前为止,我们只覆盖了使用 Vite 加载 JavaScript 和 CSS。但是 Laravel 的 Vite 配置可以处理和版本化你的静态资产(如图像)。
如果你在 JavaScript 模板中工作,Vite 将会抓取任何相对静态资产的链接,并处理和版本化它们。任何绝对静态资产 Vite 都会忽略。
这意味着如果它们在 JavaScript 模板中,以下图像将会接受不同的处理。
<!-- Ignored by Vite -->
<img src="/resources/images/soccer.jpg">
<!-- Processed by Vite -->
<img src="../resources/images/soccer.jpg">
如果你在 Blade 模板中工作,你需要采取两步来让 Vite 处理你的静态资产。首先,你需要使用Vite::asset门面调用来链接你的资产:
<img src="{{ Vite::asset('resources/images/soccer.jpg') }}">
其次,你需要在resources/js/app.js文件中添加配置步骤,向 Vite 展示要导入的文件或文件夹:
import.meta.glob([
// Imports all the files in /resources/images/
'../images/**',
]);
警告
如果你使用npm run dev运行 Vite 服务器,服务器可以加载你的静态资产,无需你添加import.meta.glob配置。这意味着你可能认为它会出现,但在你的生产构建中将会失败。
与 JavaScript 框架和 Vite 一起工作
如果你想要与 Vue、React、Inertia 和/或单页面应用程序(SPA)一起工作,你可能需要引入一些特定的插件或设置一些特定的配置项。这里是你在最常见场景下所需的基本内容。
Vite 和 Vue
要与 Vite 和 Vue 一起工作,首先安装 Vite 的 Vue 插件:
npm install --save-dev @vitejs/plugin-vue
然后,你需要修改你的vite.config.js文件来调用 Vue 插件,并向其传递两个配置设置。第一个,template.transformAssetUrls.base=null,允许 Laravel 插件而不是 Vue 插件处理重写 URL。第二个,template.transformAssetUrls.includeAbsolute=false,允许 Vue 模板内的 URL 引用公共目录中的文件:
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [
laravel(['resources/js/app.js']),
vue({
template: {
transformAssetUrls: {
base: null,
includeAbsolute: false,
},
},
}),
],
});
Vite 和 React
要与 Vite 和 React 一起工作,首先安装 Vite 的 React 插件:
npm install --save-dev @vitejs/plugin-react
然后,你需要修改你的vite.config.js文件来调用 React 插件:
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [
laravel(['resources/js/app.js']),
react(),
],
});
最后,在你导入 JavaScript 文件之前,在模板中添加@viteReactRefresh Blade 指令:
@viteReactRefresh
@vite('resources/js/app.jsx')
Vite 和 Inertia
如果你正在自行设置 Inertia,你需要 Inertia 能够解析你的页面组件。
这里是你可能会在resources/js/app.js文件中编写的代码示例,但你最好的选择是使用 Breeze、Jetstream 或 Inertia 文档安装 Inertia。
import { createApp, h } from 'vue'
import { createInertiaApp } from '@inertiajs/vue3'
createInertiaApp({
resolve: name => {
const pages = import.meta.glob('./Pages/**/*.vue', { eager: true })
return pages[`./Pages/${name}.vue`]
},
setup({ el, App, props, plugin }) {
createApp({ render: () => h(App, props) })
.use(plugin)
.mount(el)
},
})
Vite 和 SPAs
如果你正在构建 SPA,请从你的vite.config.js文件中移除resources/css/app.css,这将它从入口点中移除。
相反,通过在resources/js/app.js文件中在引入 bootstrap 之下添加这一行,将你的 CSS 导入到 JavaScript 中:
import './bootstrap';
import '../css/app.css';
在 Vite 中使用环境变量
如果你想在你的 JavaScript 文件中使用环境变量,请以VITE_为前缀变量名,就像你在示例 6-3 中看到的那样。
示例 6-3. 在 vite.config.js 中引用环境变量
// .env
VITE_BASE_URL=http://local-development-url.test
// resources/js/app.js
const baseUrl = import.meta.env.VITE_BASE_URL;
每次运行npm run dev或npm run build时,它都会从*.env*中加载该环境变量,并将其注入到你的脚本中。
分页
尽管在 Web 应用程序中分页是如此常见,但实现起来仍然可能非常复杂。幸运的是,Laravel 默认已经集成了分页的概念,并且默认已经与 Eloquent 结果和路由器关联起来。
分页数据库结果
您最常见到分页的地方是当您显示数据库查询结果并且结果太多以至于无法在单个页面中显示时。Eloquent 和查询构建器都从当前页面请求的 page 查询参数中读取,并使用它在任何结果集上提供一个 paginate() 方法;您应该传递给 paginate() 的单个参数指示您希望每页显示多少结果。查看示例 6-4 了解其工作原理。
示例 6-4. 对查询构建器响应进行分页
// PostController
public function index()
{
return view('posts.index', ['posts' => DB::table('posts')->paginate(20)]);
}
示例 6-4 指定该路由应每页返回 20 篇文章,并根据 URL 的 page 查询参数定义当前用户所在的“页面”结果。所有 Eloquent 模型都具有相同的 paginate() 方法。
当您在视图中显示结果时,您的集合现在将具有一个 links() 方法,该方法将输出分页控件。(参见示例 6-5,我已将其简化以便包含在本书中。)
示例 6-5. 在模板中呈现分页链接
// posts/index.blade.php
<table>
@foreach ($posts as $post)
<tr><td>{{ $post->title }}</td></tr>
@endforeach
</table>
{{ $posts->links() }}
// By default, $posts->links() will output something like this:
<div class="...">
<div>
<p class="...">
Showing
<span class="...">1</span>
to
<span class="...">2</span>
of
<span class="...">5</span>
results
</p>
</div>
<div>
<span class="...">
<span aria-disabled="true" aria-label="&laquo; Previous">
<!-- SVG here for the ... ellipsis -->
</span>
<span class="...">1</span>
<a href="http://myapp.com/posts?page=2" class="..." aria-label="...">
2
</a>
<a href="http://myapp.com/posts?page=3" class="..." aria-label="...">
3
</a>
<a href="http://myapp.com/posts?page=2" class="..."
rel="next" aria-label="Next &raquo;">
<!-- SVG here for the ... ellipsis -->
</a>
</span>
</div>
</div>
分页器使用 TailwindCSS 进行默认样式设置。如果您想使用 Bootstrap 样式,请在 AppServiceProvider 中调用 Paginator::useBootstrap():
use Illuminate\Pagination\Paginator;
public function boot(): void
{
Paginator::useBootstrap();
}
自定义分页链接的数量
如果您希望控制当前页面两侧显示多少链接,您可以使用 onEachSide() 方法轻松自定义此数字:
DB::table('posts')->paginate(10)->onEachSide(3);
// Outputs:
// 5 6 7 [8] 9 10 11
手动创建分页器
如果您不使用 Eloquent 或查询构建器,或者正在使用复杂的查询(例如使用 groupBy 的查询),您可能会发现自己需要手动创建分页器。幸运的是,您可以使用 Illuminate\Pagination\Paginator 或 Illuminate\Pagination\LengthAwarePaginator 类来实现这一点。
这两个类的区别在于 Paginator 只提供上一页和下一页按钮,但没有每一页的链接;LengthAwarePaginator 需要知道完整结果的长度,以便可以为每个单独的页面生成链接。您可能会发现在大结果集上使用 Paginator 是有用的,这样您的分页器不必了解可能昂贵运行的大量结果数量。
Paginator 和 LengthAwarePaginator 都要求您手动提取您希望传递给视图的内容子集。查看示例 6-6 了解示例。
示例 6-6. 手动创建分页器
use Illuminate\Http\Request;
use Illuminate\Pagination\Paginator;
Route::get('people', function (Request $request) {
$people = [...]; // huge list of people
$perPage = 15;
$offsetPages = $request->input('page', 1) - 1;
// The Paginator will not slice your array for you
$people = array_slice(
$people,
$offsetPages * $perPage,
$perPage
);
return new Paginator(
$people,
$perPage
);
});
消息包
Another common-but-painful feature in web applications is passing messages between various components of the app, when the end goal is to share them with the user. Your controller, for example, might want to send a validation message: “The email field must be a valid email address.” However, that particular message doesn’t just need to make it to the view layer; it actually needs to survive a redirect and then end up in the view layer of a different page. How do you structure this messaging logic?
The Illuminate\Support\MessageBag class is tasked with storing, categorizing, and returning messages that are intended for the end user. It groups all messages by key, where the keys are likely to be something like errors or messages, and it provides convenience methods for getting either all its stored messages or only those for a particular key, and outputting these messages in various formats.
You can spin up a new instance of MessageBag manually like in Example 6-7. To be honest, though, you likely won’t ever do this manually—this is just a thought exercise to show how it works.
Example 6-7. Manually creating and using a message bag
$messages = [
'errors' => [
'Something went wrong with edit 1!',
],
'messages' => [
'Edit 2 was successful.',
],
];
$messagebag = new \Illuminate\Support\MessageBag($messages);
// Check for errors; if there are any, decorate and echo
if ($messagebag->has('errors')) {
echo '<ul id="errors">';
foreach ($messagebag->get('errors', '<li><b>:message</b></li>') as $error) {
echo $error;
}
echo '</ul>';
}
Message bags are also closely connected to Laravel’s validators (you’ll learn more about these in “Validation”): when validators return errors, they actually return an instance of MessageBag, which you can then pass to your view or attach to a redirect using redirect('route')->withErrors($messagebag).
Laravel passes an empty instance of MessageBag to every view, assigned to the variable $errors; if you’ve flashed a message bag using withErrors() on a redirect, it will get assigned to that $errors variable instead. That means every view can always assume it has an $errors MessageBag that it can check wherever it handles validation, which leads to Example 6-8 as a common snippet developers place on every page.
Example 6-8. Error bag snippet
// partials/errors.blade.php
@if ($errors->any())
<div class="alert alert-danger">
<ul>
@foreach ($errors as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
Missing $errors Variable
If you have any routes that aren’t under the web middleware group, they won’t have the session middleware, which means they won’t have this $errors variable available.
Sometimes you need to differentiate message bags not just by key (notices versus errors) but also by component. Maybe you have a login form and a signup form on the same page; how do you differentiate them?
When you send errors along with a redirect using withErrors(), the second parameter is the name of the bag: redirect('dashboard')->withErrors($validator, 'login'). Then, on the dashboard, you can use $errors->login to call all of the methods you saw before: any(), count(), and more.
String Helpers, Pluralization, and Localization
As developers, we tend to look at blocks of text as big placeholder divs, waiting for the client to put real content into them. Seldom are we involved in any logic inside these blocks.
但在一些情况下,你会感谢 Laravel 提供的字符串操作工具。
字符串助手和复数形式
Laravel 提供了一系列用于操作字符串的助手。它们作为 Str 类的方法可用(例如,Str::plural())。
Laravel 字符串和数组全局助手
旧版本的 Laravel 包括全局辅助函数,它们是 Str 和 Arr 方法的别名。这些全局的 str_ 和 array_ 辅助函数在 Laravel 6 版本中被移除,并导出到一个单独的包中。如果你愿意,你可以通过 Composer 安装 laravel/helpers 包:composer require laravel/helpers。
Laravel 的 文档 详细介绍了所有字符串助手,但以下是一些常用的助手:
e()
html_entities() 的快捷方式;对所有 HTML 实体进行编码,以确保安全性。
Str::startsWith(), Str::endsWith(), Str::contains()
检查字符串(第一个参数)是否以另一个字符串(第二个参数)开头、结尾或包含。
Str::is()
检查字符串(第二个参数)是否与特定模式(第一个参数)匹配 —— 例如,foo* 将匹配 foobar 和 foobaz。
Str::slug()
将字符串转换为带连字符的 URL 类型的 slug。
Str::plural(*word*, *count*), Str::singular()
使单词变为复数或单数;仅支持英语(例如,Str::plural('dog') 返回 dogs; Str::plural('dog,' 1')) 返回 dog)。
Str::camel(), Str::kebab(), Str::snake(), Str::studly(), Str::title()
将提供的字符串转换为不同的大小写 "case"。
Str::after(), Str::before(), Str::limit()
对字符串进行修剪并提供子字符串。Str::after() 返回给定字符串之后的所有内容,而 Str::before() 返回给定字符串之前的所有内容(两者都接受完整的字符串作为第一个参数,以及作为第二个参数用于切割的字符串)。Str::limit() 将字符串(第一个参数)截断为指定数量的字符(第二个参数)。
Str::markdown(*string*, *options*)
将 Markdown 转换为 HTML。您可以在 PHP League 网站 上阅读有关您可以传递的选项的更多信息。
Str::replace(*search*, *replace*, *subject*, *caseSensitive*)
在主题字符串中查找搜索字符串出现的位置,并用替换字符串替换它。如果 caseSensitive 参数为 true,则只有当出现与搜索案例匹配时才进行替换(例如,Str::replace('Running', 'Going', 'Laravel Up and Running', true) 返回 'Laravel Up and Going')。
本地化
本地化使您能够定义多种语言,并将任何字符串标记为翻译目标。您可以设置一个回退语言,甚至处理复数形式的变化。
在 Laravel 中,您需要在页面加载过程中的某个时刻设置“应用程序区域设置”,以便本地化助手知道从哪个翻译桶中获取翻译。每个“区域设置”通常与一个翻译相关联,通常看起来像“en”(英语)。您可以使用App::setLocale($localeName)来实现这一点,您可能会将其放在服务提供程序中。现在,您可以将其放在AppServiceProvider的boot()方法中,但如果您最终拥有多个与区域设置相关的绑定,可能需要创建一个LocaleServiceProvider。
您可以在config/app.php中定义您的回退区域设置,那里应该有一个fallback_locale键。这允许您为应用程序定义一个默认语言,在无法找到请求的区域设置的翻译时,Laravel 将使用它。
基本本地化
那么,我们如何调用翻译字符串呢?有一个辅助函数,__($key),它将为传递的键在当前区域设置中获取字符串,或者如果不存在,则从默认区域设置中获取。在 Blade 中,您还可以使用@lang()指令。示例 6-9 演示了基本翻译的工作方式。我们将使用“返回仪表板”链接的示例作为详细页面顶部的示例。
示例 6-9. __() 的基本使用
// Normal PHP
<?php echo __('navigation.back'); ?>
// Blade
{{ __('navigation.back') }}
// Blade directive
@lang('navigation.back')
假设我们现在正在使用es区域设置。首先,我们需要发布用于修改的lang文件:
php artisan lang:publish
此命令会将默认的 Laravel lang文件发布到应用程序的根目录。您需要创建一个文件来定义与导航相关的翻译,lang/en/navigation.php,并返回一个包含键名为back的 PHP 数组,如示例 6-10 所示。
示例 6-10. 示例 lang/en/navigation.php 文件
<?php
return [
'back' => 'Return to dashboard',
];
现在,为了使其可翻译,让我们在lang下创建一个es目录,并创建其自己的navigation.php文件,正如您在示例 6-11 中所见。
示例 6-11. 示例 lang/es/navigation.php 文件
<?php
return [
'back' => 'Volver al panel',
];
现在让我们尝试在我们的应用程序中使用该翻译键,在示例 6-12 中。
示例 6-12. 使用翻译
// routes/web.php
Route::get('/es/contacts/show/{id}', function () {
// Set the locale manually, for this example, instead of in a service provider
App::setLocale('es');
return view('contacts.show');
});
// resources/views/contacts/show.blade.php
<a href="/contacts">{{ __('navigation.back') }}</a>
本地化中的参数
前面的示例比较简单。让我们深入了解一些更复杂的内容。如果我们想要定义我们要返回的哪一个仪表板呢?看看示例 6-13。
示例 6-13. 翻译中的参数
// lang/en/navigation.php
return [
'back' => 'Back to :section dashboard',
];
// resources/views/contacts/show.blade.php
{{ __('navigation.back', ['section' => 'contacts']) }}
如您所见,用冒号(:section)标记一个单词,将其视为可以替换的占位符。__()的第二个可选参数是一个替换占位符的值数组。
本地化中的复数形式
我们已经讨论了复数形式,现在想象一下您正在定义自己的复数形式规则。有两种方法可以做到这一点;我们将从最简单的方法开始,如示例 6-14 所示。
示例 6-14. 定义具有复数形式选项的简单翻译
// lang/en/messages.php
return [
'task-deletion' => 'You have deleted a task|You have successfully deleted tasks',
];
// resources/views/dashboard.blade.php
@if ($numTasksDeleted > 0)
{{ trans_choice('messages.task-deletion', $numTasksDeleted) }}
@endif
正如您所见,我们有一个trans_choice()方法,它将受影响项目的计数作为其第二个参数;从中,它将确定要使用哪个字符串。
你还可以使用与 Symfony 的更复杂的Translation组件兼容的任何翻译定义;参见示例 6-15 作为示例。
示例 6-15. 使用 Symfony Translation组件
// lang/es/messages.php
return [
'task-deletion' => "{0} You didn't manage to delete any tasks.|" .
"[1,4] You deleted a few tasks.|" .
"[5,Inf] You deleted a whole ton of tasks.",
];
使用 JSON 存储默认字符串作为键
本地化的一个常见难点是确保有一个良好的定义键命名空间的系统——例如,记住三四级嵌套的键或不确定站点中使用两次的短语应该使用哪个键。
与基于 slug 键/字符串值对系统的另一种选择是使用主语言字符串作为键存储您的翻译,而不是使用虚构的 slug。您可以通过在lang目录中以 JSON 格式存储翻译文件,并使用反映区域设置的文件名来指示 Laravel,以表明您正在这样工作(参见示例 6-16)。
示例 6-16. 使用 JSON 翻译和__()助手
// In Blade
{{ __('View friends list') }}
// lang/es.json
{
'View friends list': 'Ver lista de amigos'
}
这是利用了__()翻译助手的一个事实,如果它找不到当前语言的匹配键,它将只显示键。如果您的键是应用程序默认语言中的字符串,那么这比如widgets.friends.title之类的做法更合理。
测试
在本章中,我们主要关注了 Laravel 的前端组件。这些组件不太可能成为单元测试的对象,但有时可能会在集成测试中使用。
测试消息和错误包
传递消息和错误包的消息的主要测试方法有两种。首先,您可以在应用程序测试中执行一个行为,设置最终将在某个地方显示的消息,然后重定向到该页面,并断言显示适当的消息。
其次,对于错误(这是最常见的用例),您可以使用$this->assertSessionHasErrors($bindings = [])断言会话存在错误。看一下示例 6-17,看看可能的显示方式。
示例 6-17. 断言会话存在错误
public function test_missing_email_field_errors()
{
$this->post('person/create', ['name' => 'Japheth']);
$this->assertSessionHasErrors(['email']);
}
为了使示例 6-17 通过,您需要在该路由上添加输入验证。我们将在第七章中介绍这个。
翻译和本地化
测试本地化的最简单方法是使用应用程序测试。设置适当的上下文(无论是通过 URL 还是会话),使用get()“访问”页面,并断言您看到适当的内容。
在测试中禁用 Vite
如果您想在测试期间禁用 Vite 的资产解析,可以通过在测试顶部调用withoutVite()方法完全禁用 Vite:
public function test_it_runs_without_vite()
{
$this->withoutVite();
// Test stuff
}
TL;DR
作为一个全栈框架,Laravel 提供了用于前端和后端的工具和组件。
Vite 是一个构建工具和开发服务器,Laravel 在其上构建,帮助处理、压缩和版本化 JavaScript、CSS 和静态资源(如图像)。
Laravel 还提供了针对前端的其他内部工具,包括用于实现分页、消息和错误包以及本地化的工具。
第七章:收集和处理用户数据
像 Laravel 这样的框架受益于的网站通常不仅提供静态内容。许多处理复杂和混合数据源,其中最常见(也最复杂)的是各种形式的用户输入:URL 路径、查询参数、POST 数据和文件上传。
Laravel 提供了一组工具,用于收集、验证、规范化和过滤用户提供的数据。我们将在这里看看这些工具。
注入请求对象
在 Laravel 中访问用户数据最常见的工具是注入 Illuminate\Http\Request 对象的实例。它为您提供了轻松访问用户在您的站点上提供输入的所有方式:POST 表单数据或 JSON、GET 请求(查询参数)和 URL 段。
其他访问请求数据的选项
还有一个 request() 全局助手和一个 Request 门面,两者都公开相同的方法。每个选项都公开了整个 Illuminate Request 对象,但现在我们只会涵盖与用户数据特别相关的方法。
因为我们计划注入一个 Request 对象,让我们快速看一下如何获取我们将在其上调用所有这些方法的 $request 对象:
Route::post('form', function (Illuminate\Http\Request $request) {
// $request->etc()
});
$request->all()
就像名字所暗示的那样,$request->all() 提供了一个包含用户从每个来源提供的所有输入的数组。假设出于某种原因,您决定让一个表单 POST 到一个带有查询参数的 URL——例如,向 myapp.com/signup?utm=… 发送一个 POST。查看 Example 7-1 来看看从 $request->all() 中得到了什么。($request->all() 也包含有关上传的任何文件的信息,但我们将在本章后面介绍这部分内容。)
Example 7-1. $request->all()
<!-- GET route form view at /get-route -->
<form method="post" action="/signup?utm=12345">
@csrf
<input type="text" name="first_name">
<input type="submit">
</form>
// routes/web.php
Route::post('signup', function (Request $request) {
var_dump($request->all());
});
// Outputs:
/**
* [
* '_token' => 'CSRF token here',
* 'first_name' => 'value',
* 'utm' => 12345,
* ]
*/
$request->except() 和 ->only()
$request->except() 提供与 $request->all() 相同的输出,但您可以选择排除一个或多个字段——例如 _token。您可以将其传递为字符串或字符串数组。
Example 7-2 显示了在我们使用 $request->except() 在 Example 7-1 中相同表单时的情况。
Example 7-2. $request->except()
Route::post('post-route', function (Request $request) {
var_dump($request->except('_token'));
});
// Outputs:
/**
* [
* 'firstName' => 'value',
* 'utm' => 12345
* ]
*/
$request->only() 是 $request->except() 的反义词,如您在 Example 7-3 中所见。
Example 7-3. $request->only()
Route::post('post-route', function (Request $request) {
var_dump($request->only(['firstName', 'utm']));
});
// Outputs:
/**
* [
* 'firstName' => 'value',
* 'utm' => 12345
* ]
*/
$request->has() 和 ->missing()
使用 $request->has(),您可以检测特定的用户输入是否可用,而不管输入中是否实际包含值。查看 Example 7-4 来查看前面示例中我们的 utm 查询字符串参数的分析示例。
Example 7-4. $request->has()
// POST route at /post-route
if ($request->has('utm')) {
// Do some analytics work
}
$request->missing() 是它的反义词。
$request->whenHas()
使用 $request->whenHas(),您可以定义当请求提供了字段或未提供字段时的行为。第一个闭包参数在字段存在时返回,第二个在字段不存在时返回。
查看带有utm查询字符串参数的示例 7-5。
示例 7-5. $request->whenHas()
// POST route at /post-route
$utm = $request->whenHas('utm', function($utm) {
return $utm;
}, function() {
return 'default';
});
$request->filled()
使用$request->filled()方法,可以检查请求中是否存在并填充了特定字段。filled()与has()相同,但它还要求字段中实际存在值。在示例 7-6 中,您可以看到如何使用此方法的示例。
示例 7-6. $request->filled()
// POST route at /post-route
if ($request->filled('utm')) {
// Do some analytics work
}
$request->whenFilled()
与whenHas()方法类似,$request->whenFilled()方法允许您在字段填充或未填充时定义值。第一个闭包参数在字段填充时运行,第二个在未填充时运行。请参阅示例 7-7 了解如何使用此方法的示例。
示例 7-7. $request->whenFilled()
// POST route at /post-route
$utm = $request->whenFilled('utm', function ($utm) {
return $utm;
}, function() {
return 'default';
});
$request->mergeIfMissing()
使用mergeIfMissing()方法,您可以在请求中添加字段,当字段不存在时定义其值。例如,当字段来自复选框时,只有在选中时才存在。您可以在示例 7-8 中看到一个实现。
示例 7-8. $request->mergeIfMissing()
// POST route at /post-route
$shouldSend = $request->mergeIfMissing('send_newsletter', 0);
$request->input()
而$request->all()、$request->except()和$request->only()操作于用户提供的完整输入数组上,$request->input()允许您仅获取单个字段的值。示例 7-9 提供了一个例子。注意第二个参数是默认值,所以如果用户没有传递值,您可以有一个合理(且不会中断流程)的回退。
示例 7-9. $request->input()
Route::post('post-route', function (Request $request) {
$userName = $request->input('name', 'Matt');
});
$request->method() 和 ->isMethod()
$request->method()返回请求的 HTTP 动词,$request->isMethod()检查它是否与指定的动词匹配。示例 7-10 说明了它们的用法。
示例 7-10. $request->method() 和 $request->isMethod()
$method = $request->method();
if ($request->isMethod('patch')) {
// Do something if request method is PATCH
}
$request->integer()、->float()、->string()和->enum()
当您分别使用这些方法时,它们将直接将输入转换为整数、浮点数、字符串或枚举。查看示例 7-11 获取使用示例。
示例 7-11. $request->integer()、$request->float()、$request->string()和$request->enum()
dump(is_int($request->integer('some_integer'));
// true
dump(is_float($request->float('some_float'));
// true
dump(is_string($request->string('some_string'));
// true
dump($request->enum('subscription', SubscriptionStatusEnum::class));
// 'active', assuming that's a valid status for the SubscriptionStatusEnum
$request->dump() 和 ->dd()
$request->dump() 和 $request->dd() 是用于展示请求的辅助方法。对于两者,您可以通过不传递任何参数来展示整个请求,或者通过传递数组来展示选择的字段。$request->dump()展示后继续执行,而$request->dd()展示后停止脚本的执行。示例 7-12 展示了它们的用法。
示例 7-12. $request->dump() 和 $request->dd()
// dumping the whole request
$request->dump()
$request->dd();
// dumping just two fields
$request->dump(['name', 'utm']);
$request->dd(['name', 'utm']);
数组输入
Laravel 还提供了方便的帮助程序,用于访问来自用户提供的数组输入的数据。只需使用“点”符号来指示进入数组结构,例如在示例 7-13 中。
示例 7-13. 用于访问用户数据中数组值的点符号表示法
<!-- GET route form view at /employees/create -->
<form method="post" action="/employees/">
@csrf
<input type="text" name="employees[0][firstName]">
<input type="text" name="employees[0][lastName]">
<input type="text" name="employees[1][firstName]">
<input type="text" name="employees[1][lastName]">
<input type="submit">
</form>
// POST route at /employees
Route::post('employees', function (Request $request) {
$employeeZeroFirstName = $request->input('employees.0.firstName');
$allLastNames = $request->input('employees.*.lastName');
$employeeOne = $request->input('employees.1');
var_dump($employeeZeroFirstname, $allLastNames, $employeeOne);
});
// If forms filled out as "Jim" "Smith" "Bob" "Jones":
// $employeeZeroFirstName = 'Jim';
// $allLastNames = ['Smith', 'Jones'];
// $employeeOne = ['firstName' => 'Bob', 'lastName' => 'Jones'];
JSON 输入(和 $request->json())
到目前为止,我们已经涵盖了从查询字符串(GET)和表单提交(POST)获取输入的内容。但是随着 JavaScript SPA 的出现,还有一种更常见的用户输入形式:JSON 请求。它本质上只是一个 POST 请求,但其主体设置为 JSON,而不是传统的表单 POST。
让我们看一下向 Laravel 路由提交 JSON 的情况以及如何使用 $request->input() 提取数据(参见 示例 7-14)。
示例 7-14. 使用 $request->input() 从 JSON 中获取数据
POST /post-route HTTP/1.1
Content-Type: application/json
{
"firstName": "Joe",
"lastName": "Schmoe",
"spouse": {
"firstName": "Jill",
"lastName":"Schmoe"
}
}
// Post-route
Route::post('post-route', function (Request $request) {
$firstName = $request->input('firstName');
$spouseFirstname = $request->input('spouse.firstName');
});
由于 $request->input() 足够智能,可以从 GET、POST 或 JSON 中提取用户数据,你可能会想知道为什么 Laravel 还提供 $request->json()。你可能更喜欢 $request->json() 的两个原因。首先,你可能希望对项目中其他程序员更明确地表明你期望数据来自何处。其次,如果 POST 没有正确的 application/json 头部,$request->input() 将无法将其识别为 JSON,但 $request->json() 可以。
路由数据
当你想象“用户数据”时,URL 可能不是你首先想到的,但是在本章中,URL 与其他任何内容一样都是用户数据。
从 URL 获取数据的两种主要方式是通过 Request 对象和路由参数。
来自请求
注入的 Request 对象(以及 Request 外观和 request() 助手)有几种方法可用于表示当前页面 URL 的状态,但现在让我们专注于获取关于 URL 片段的信息。
URL 中域名后的每组字符称为 片段。因此,http://www.myapp.com/users/15 具有两个片段:users 和 15。
正如你可能猜到的,我们有两种可用的方法:$request->segments() 返回所有片段的数组,并且 $request->segment($segmentId) 允许我们获取单个片段的值。请注意,片段是基于 1 的索引返回的,因此在上面的示例中,$request->segment(1) 将返回 users。
Request 对象、Request 外观和 request() 全局助手提供了更多方法来帮助我们从 URL 中获取数据。要了解更多,请参阅 第十章。
来自路由参数
获取关于 URL 的另一种主要方法是从路由参数中获取,这些参数被注入到正在服务当前路由的控制器方法或闭包中,如 示例 7-15 所示。
示例 7-15. 从路由参数获取 URL 详细信息
// routes/web.php
Route::get('users/{id}', function ($id) {
// If the user visits myapp.com/users/15/, $id will equal 15
});
要了解更多关于路由和路由绑定的信息,请参阅 第三章。
上传的文件
我们已经讨论了与用户文本输入的不同交互方式,但还有要考虑的文件上传问题。Request对象使用$request->file()方法提供对任何上传文件的访问,该方法以文件的输入名称作为参数,并返回Symfony\Component\HttpFoundation\File\UploadedFile的实例。让我们通过一个示例来说明。首先,我们的表单在示例 7-16 中。
示例 7-16. 用于上传文件的表单
<form method="post" enctype="multipart/form-data">
@csrf
<input type="text" name="name">
<input type="file" name="profile_picture">
<input type="submit">
</form>
现在让我们看看运行$request->all()后我们得到什么,如示例 7-17 所示。请注意,$request->input('profile_picture')将返回null;我们需要使用$request->file('profile_picture')。
示例 7-17. 提交表单后的输出在示例 7-16 中
Route::post('form', function (Request $request) {
var_dump($request->all());
});
// Output:
// [
// "_token" => "token here",
// "name" => "asdf",
// "profile_picture" => UploadedFile {},
// ]
Route::post('form', function (Request $request) {
if ($request->hasFile('profile_picture')) {
var_dump($request->file('profile_picture'));
}
});
// Output:
// UploadedFile (details)
Laravel 还提供了文件特定的验证规则,允许您要求文件上传匹配特定的 mime 类型、文件大小或长度等。查看验证文档以了解更多信息。
Symfony 的UploadedFile类通过允许您轻松检查和操作文件的方法扩展了 PHP 的本机SplFileInfo。这个列表并不详尽,但它让您体验到了您可以做什么的一部分:
-
guessExtension() -
getMimeType() -
store(*$path*, *$storageDisk = default disk*) -
storeAs(*$path*, *$newName*, *$storageDisk = default disk*) -
storePublicly(*$path*, *$storageDisk = default disk*) -
storePubliclyAs(*$path*, *$newName*, *$storageDisk = default disk*) -
move(*$directory*, *$newName = null*) -
getClientOriginalName() -
getClientOriginalExtension() -
getClientMimeType() -
guessClientExtension() -
getClientSize() -
getError() -
isValid()
如您所见,大多数方法与获取上传文件的信息有关,但有一个您可能比其他所有方法都更常用:store(),它接受通过请求上传的文件,并将其存储在服务器上指定的目录中。它的第一个参数是目标目录,可选的第二个参数是要用于存储文件的存储磁盘(s3,local等)。您可以在示例 7-18 中看到一个常见的工作流程。
示例 7-18. 常见文件上传工作流程
if ($request->hasFile('profile_picture')) {
$path = $request->profile_picture->store('profiles', 's3');
auth()->user()->profile_picture = $path;
auth()->user()->save();
}
如果需要指定文件名,您可以使用storeAs()代替store()。第一个参数仍然是路径;第二个是文件名,可选的第三个参数是要使用的存储磁盘。
文件上传的正确表单编码
如果在尝试从请求中获取文件的内容时得到null,可能是您忘记在表单上设置编码类型。确保在表单上添加属性 enctype="multipart/form-data":
<form method="post" enctype="multipart/form-data">
验证
Laravel 有很多方法可以验证传入的数据。我们将在下一节中讨论表单请求,因此我们现在有两个主要选项:手动验证或在Request对象上使用validate()方法。让我们从更简单、更常见的validate()方法开始。
在请求对象上进行验证()
Request 对象有一个 validate() 方法,提供了最常见的验证工作流的便捷方式。 请参阅示例 7-19。
示例 7-19. 请求验证的基本用法
// routes/web.php
Route::get('recipes/create', [RecipeController::class, 'create']);
Route::post('recipes', [RecipeController::class, 'store']);
// app/Http/Controllers/RecipeController.php
class RecipeController extends Controller
{
public function create()
{
return view('recipes.create');
}
public function store(Request $request)
{
$request->validate([
'title' => 'required|unique:recipes|max:125',
'body' => 'required'
]);
// Recipe is valid; proceed to save it
}
}
我们在这里只有四行代码运行我们的验证,但它们确实做了很多事情。
首先,我们明确定义我们期望的字段,并分别应用规则(这里用管道字符|分隔)。
接下来,validate()方法检查来自$request的传入数据,并确定其是否有效。
如果数据有效,则validate()方法结束,我们可以继续使用控制器方法保存数据或其他操作。
但是,如果数据无效,则会抛出ValidationException。 这包含有关如何处理此异常的路由器指令。 如果请求来自 JavaScript(或者请求 JSON 作为响应),则异常将创建一个包含验证错误的 JSON 响应。 如果不是,则异常将返回一个重定向到前一页的页面,以及所有用户输入和验证错误,非常适合重新填充失败的表单并显示一些错误。
更多关于 Laravel 验证规则的信息
在我们这里的示例中(就像文档中一样),我们使用“管道”语法:'*fieldname*': '*rule*|*otherRule*|*anotherRule*'。 但是,您也可以使用数组语法来执行相同的操作:'*fieldname*': ['*rule*', '*otherRule*', '*anotherRule*']。
此外,您还可以验证嵌套属性。 如果您使用 HTML 的数组语法,这就很重要,该语法允许您例如,在 HTML 表单上具有多个“用户”,每个用户都有一个关联的名称。 以下是验证方法:
$request->validate([
'user.name' => 'required',
'user.email' => 'required|email',
]);
我们没有足够的空间来涵盖这里的每一个可能的验证规则,但以下是一些最常见规则及其功能:
要求字段
required; required_if:*anotherField,equalToThisValue*;
required_unless:*anotherField,equalToThisValue*
排除请求输出中的字段
exclude_if:*anotherField,equalToThisValue*;
exclude_unless:*anotherField,equalToThisValue*
字段必须包含某些类型的字符
alpha; alpha_dash; alpha_num; numeric; integer
字段必须包含特定的模式
email; active_url; ip
日期
after:*date*; before:*date*(*date*可以是strtotime()可以处理的任何有效字符串)
数字
between:*min*,*max*; min:*num*; max:*num*; size:*num*(size测试字符串长度、整数值、数组计数或文件大小(KB))
图像尺寸
dimensions:min_width=*XXX*; 也可以与max_width、min_height、max_height、width、height和ratio结合使用
数据库
exists:*tableName*; unique:*tableName*(期望在与字段名相同的表列中查找;请查看验证文档以了解如何自定义)
你可以在数据库验证规则中指定 Eloquent 模型,而不是表名:
'name' => 'exists:App\Models\Contact,name',
'phone' => 'unique:App\Models\Contact,phone',
手动验证
如果您不在控制器中工作,或者由于某些其他原因,先前描述的流程不适合,您可以使用Validator门面手动创建Validator实例,并像示例 7-20 中那样检查成功或失败。
示例 7-20. 手动验证
Route::get('recipes/create', function () {
return view('recipes.create');
});
Route::post('recipes', function (Illuminate\Http\Request $request) {
$validator = Validator::make($request->all(), [
'title' => 'required|unique:recipes|max:125',
'body' => 'required'
]);
if ($validator->fails()) {
return redirect('recipes/create')
->withErrors($validator)
->withInput();
}
// Recipe is valid; proceed to save it
});
如您所见,我们通过将输入作为第一个参数和验证规则作为第二个参数传递来创建验证器的实例。验证器公开了一个fails()方法,我们可以进行检查,并可以将其传递给重定向的withErrors()方法。
使用验证数据
在验证数据后,您可以从请求中提取数据,确保只使用验证过的数据。有两个主要选项:validated()和safe()。您可以在$request对象上运行这些方法,或者如果您创建了手动验证器,则在$validator实例上运行。
validated()方法返回已验证数据的数组,如示例 7-21 所示。
示例 7-21. 使用validated()获取验证数据
// Both return an array of validated user input
$validated = $request->validated();
$validated = $validator->validated();
另一方面,safe()方法返回一个对象,该对象使您可以访问all()、only()和except()方法,正如您在示例 7-22 中所看到的。
示例 7-22. 使用safe()获取验证数据
$validated = $request->safe()->only(['name', 'email']);
$validated = $request->safe()->except(['password']);
$validated = $request->safe()->all();
自定义规则对象
如果 Laravel 中不存在您需要的验证规则,您可以创建自己的规则。要创建自定义规则,请运行php artisan make:rule *RuleName*,然后编辑位于app/Rules/{RuleName}.php中的文件。
您将获得默认提供的validate()方法。validate()方法应接受属性名称作为第一个参数,用户提供的值作为第二个参数,并在验证失败时接受一个闭包;您可以在消息中使用:attribute作为属性名称的占位符。
请查看示例 7-23 作为示例。
示例 7-23. 示例自定义规则
class AllowedEmailDomain implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if(! in_array(Str::after($value, '@'), ['tighten.co'])){
$fail('The :attribute field is not from an allowed email provider.');
}
}
}
要使用此规则,只需将规则对象的实例传递给您的验证器:
$request->validate([
'email' => new AllowedEmailDomain,
]);
显示验证错误消息
我们已经在第六章中详细介绍了这一点,但这里是如何从验证中显示错误的快速复习。
请求上的validate()方法(以及它依赖的重定向的withErrors()方法)将任何错误闪存到会话中。这些错误可以在您重定向到的视图中通过$errors变量访问。请记住,作为 Laravel 魔术的一部分,即使是空的,每次加载视图时$errors变量也会可用,因此您不必使用isset()来检查其是否存在。
这意味着您可以在每个页面上执行类似示例 7-24 的操作。
示例 7-24. 回显验证错误
@if ($errors->any())
<ul id="errors">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
@endif
您还可以有条件地回显单个字段的错误消息。为此,您将使用@error Blade 指令来检查给定字段是否存在错误。
@error('first_name')
<span>{{ $message }}</span>
@enderror
表单请求
在构建应用程序时,您可能会注意到控制器方法中出现了一些模式。有些模式是重复的——例如,输入验证、用户身份验证和授权以及可能的重定向。如果您希望有一种结构来规范化和提取这些常见行为,以便从控制器方法中提取出来,您可能会对 Laravel 的表单请求感兴趣。
表单请求是一个自定义的请求类,旨在映射到表单的提交,请求负责验证请求、授权用户,并在验证失败时可选择重定向用户。每个表单请求通常但并非总是显式映射到单个 HTTP 请求,例如,“创建评论”。
创建表单请求
您可以通过命令行创建一个新的表单请求:
php artisan make:request CreateCommentRequest
现在你可以在 app/Http/Requests/CreateCommentRequest.php 中使用表单请求对象。
每个表单请求类都提供一个或两个公共方法。第一个是 rules(),它需要返回此请求的验证规则数组。第二个(可选)方法是 authorize();如果返回 true,则用户被授权执行此请求,如果返回 false,则用户被拒绝。查看 示例 7-25 以查看一个表单请求的样本。
示例 7-25. 样本表单请求
<?php
namespace App\Http\Requests;
use App\BlogPost;
use Illuminate\Foundation\Http\FormRequest;
class CreateCommentRequest extends FormRequest
{
public function authorize(): bool
{
$blogPostId = $this->route('blogPost');
return auth()->check() && BlogPost::where('id', $blogPostId)
->where('user_id', auth()->id())->exists();
}
public function rules(): array
{
return [
'body' => 'required|max:1000',
];
}
}
示例 7-25 的 rules() 部分非常简单明了,但让我们简要了解一下 authorize()。
我们正在从名为 blogPost 的路由中获取段落。这暗示了该路由的定义可能看起来像这样:Route::post('blogPosts/*`blogPost`*', function () *`{ // Do stuff }`*)。正如您所见,我们将路由参数命名为 blogPost,这使得它可以在我们的 Request 中通过 $this->route('blogPost') 访问。
然后我们检查用户是否已登录,如果是,则查看是否存在以该标识符标记的任何博客文章,这些文章是当前登录用户拥有的。您已经学会了一些更简单的方法来检查所有权,例如 第五章 中的方法,但我们在这里保持更加明确以保持清洁。我们将很快讨论这样做的影响,但重要的是要知道,返回 true 表示用户被授权执行指定的操作(在本例中是创建评论),而 false 表示用户未被授权。
使用表单请求
现在我们已经创建了一个表单请求对象,那么我们如何使用它呢?这是 Laravel 的一点魔法。任何路由(闭包或控制器方法),如果将表单请求作为其参数类型提示,将从该表单请求的定义中受益。
让我们试试,在 示例 7-26 中。
示例 7-26. 使用表单请求
Route::post('comments', function (App\Http\Requests\CreateCommentRequest $request) {
// Store comment
});
您可能想知道我们何时调用表单请求,但 Laravel 会为我们执行这一步骤。它验证用户输入并授权请求。如果输入无效,它会像 Request 对象的 validate() 方法一样操作,重定向用户到前一页并保留其输入,同时传递适当的错误消息。如果用户未经授权,则 Laravel 将返回“403 禁止访问”错误并不执行路由代码。
Eloquent 模型大规模赋值
到目前为止,我们一直在控制器级别进行验证,这绝对是开始的最佳位置。但您也可以在模型级别过滤传入的数据。
将表单的整体输入直接传递给数据库模型是一种常见(但不推荐)的模式。在 Laravel 中,可能看起来像 Example 7-27。
示例 7-27. 将表单的整体内容传递给 Eloquent 模型
Route::post('posts', function (Request $request) {
$newPost = Post::create($request->all());
});
我们在这里假设最终用户是善良的,而非恶意的,并且仅保留了我们希望他们编辑的字段——也许是帖子的 title 或 body。
但是,如果我们的最终用户能够猜测或识别出我们在 posts 表中有一个 author_id 字段,该怎么办呢?如果他们使用浏览器工具添加了一个 author_id 字段,并将 ID 设置为别人的 ID,然后冒充其他人创建假的博客文章呢?
Eloquent 有一个称为“大规模赋值”的概念,允许您通过将数组传递给 create() 或 update() 方法,要么定义应填充的字段列表(使用模型的 $fillable 属性),要么定义不应填充的字段列表(使用模型的 $guarded 属性)。有关更多信息,请参阅“大规模赋值”。
在我们的示例中,我们可能想要像 Example 7-28 中那样填充模型,以确保我们的应用程序安全。
示例 7-28. 保护 Eloquent 模型免受恶意的大规模赋值攻击
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
// Disable mass assignment on the author_id field
protected $guarded = ['author_id'];
}
通过将 author_id 设置为 guarded,我们确保恶意用户将无法通过将其手动添加到发送给我们应用程序的表单内容中,覆盖此字段的值。
双重保护使用 $request->only()
尽管我们需要保护我们的模型免受大规模赋值的攻击,但在赋值端也值得小心。与其使用 $request->all(),不如考虑使用 $request->only(),这样您可以指定要传递到模型中的字段:
Route::post('posts', function (Request $request) {
$newPost = Post::create($request->only([
'title',
'body',
]));
});
{{ 与 {!!
每当在网页上显示由用户创建的内容时,您都需要防范恶意输入,例如脚本注入。
假设您允许用户在您的网站上撰写博客文章。您可能不希望他们能够注入恶意的 JavaScript 代码,从而在您的访客浏览器中运行,对吗?因此,您需要转义任何显示在页面上的用户输入,以避免这种情况发生。
幸运的是,这几乎完全为您覆盖了。如果您使用 Laravel 的 Blade 模板引擎,默认的“echo”语法({{ *$stuffToEcho* }})会自动通过 htmlentities()(PHP 最佳的安全输出用户内容的方式)运行输出。实际上,您需要额外的工作来避免转义输出,方法是使用 {!! *$stuffToEcho* !!} 语法。
测试
如果您对测试用户输入交互感兴趣,您可能最关心的是模拟有效和无效的用户输入,并确保如果输入无效,则用户被重定向,如果输入有效,则进入正确的位置(例如数据库)。
Laravel 的应用程序测试框架使得这变得简单。
Laravel Dusk 用于测试用户交互
这些测试测试的是您应用程序的 HTTP 层,但并非实际的表单字段和交互。如果您想要测试页面上特定的用户交互及其与您的表单的交互,您将需要引入 Laravel 的 Dusk 测试包。
查看 “使用 Dusk 进行测试” 了解如何在您的测试中安装和使用 Dusk。
让我们从一个无效的路由开始,我们预期会被拒绝,就像 示例 7-29 中一样。
示例 7-29. 测试无效输入是否被拒绝
public function test_input_missing_a_title_is_rejected()
{
$response = $this->post('posts', ['body' => 'This is the body of my post']);
$response->assertRedirect();
$response->assertSessionHasErrors();
}
在这里,我们断言在无效输入后,用户会被重定向,并附加错误信息。您可以看到我们在这里使用了一些 Laravel 添加的自定义 PHPUnit 断言。
那么,我们如何测试我们路由的成功?请查看 示例 7-30。
示例 7-30. 测试处理有效输入
public function test_valid_input_should_create_a_post_in_the_database()
{
$this->post('posts', ['title' => 'Post Title', 'body' => 'This is the body']);
$this->assertDatabaseHas('posts', ['title' => 'Post Title']);
}
请注意,如果您正在使用数据库进行测试,您需要了解更多关于数据库迁移和事务的知识。关于这方面的更多内容请参见 第 12 章。
简而言之
有很多方法可以获取相同的数据:使用 Request 门面、使用 request() 全局助手函数以及注入 Illuminate\Http\Request 实例。每个都提供了获取所有输入、部分输入或特定数据片段的能力,并且对于文件和 JSON 输入可能存在一些特殊考虑。
URL 路径段也是用户输入的一个可能来源,并且它们也可以通过请求工具访问。
可以使用 Validator::make() 手动执行验证,也可以使用 validate() 请求方法或表单请求自动执行验证。每个自动工具在验证失败时都会将用户重定向到上一页,并传递所有旧输入和错误信息。
视图和 Eloquent 模型也需要保护免受恶意用户输入的影响。您可以通过使用双花括号语法({{ }})来转义用户输入来保护 Blade 视图。您可以通过仅将特定字段传递给模型的批量方法(使用 $request->only())并在模型本身上定义批量赋值规则来保护模型。
第八章:Artisan 和 Tinker
从安装开始,现代 PHP 框架期望在命令行上进行许多交互。Laravel 提供了三种主要的命令行交互工具:Artisan,一组内置命令行操作,具有添加更多功能的能力;Tinker,用于应用程序的 REPL 或交互式 shell;以及安装程序,在第二章中已经介绍过。
一个关于 Artisan 的介绍
如果您已经逐章阅读本书,已经学会如何使用 Artisan 命令。它们看起来像这样:
php artisan make:controller PostController
如果您查看应用程序的根文件夹,您会看到artisan实际上只是一个 PHP 文件。这就是为什么您要以php artisan开头进行调用;您将该文件传递给 PHP 进行解析。之后的所有内容只是作为参数传递给 Artisan。
Symfony Console 语法
实际上,Artisan 是建立在Symfony Console 组件之上的一层;因此,如果您熟悉编写 Symfony Console 命令,那么您应该会感觉如同在家一样。
由于应用程序的 Artisan 命令列表可能会被包或特定应用程序代码更改,因此值得检查您遇到的每个新应用程序以查看可用的命令。
要获取所有可用 Artisan 命令的列表,可以从项目根目录运行php artisan list(尽管如果只运行php artisan而不带参数,它将执行相同的操作)。
基本的 Artisan 命令
这里没有足够的空间来涵盖所有的 Artisan 命令,但我们将涵盖其中的许多命令。让我们从基本命令开始:
clear-compiled
移除 Laravel 的编译类文件,这类似于内部 Laravel 缓存;当事情出现问题并且您不知道原因时,首先尝试运行此命令。
down,up
将您的应用程序置于“维护模式”中,以便您可以修复错误,运行迁移或其他操作,并将应用程序从维护模式恢复。
dump-server
启动转储服务器(参见“Laravel Dump Server”)以收集和输出转储的变量。
env
显示 Laravel 当前运行的环境;这相当于在应用中回显app()->environment()。
help
为命令提供帮助;例如,php artisan help *commandName*。
migrate
运行所有数据库迁移。
optimize
清除并刷新配置和路由文件。
serve
在localhost:8000上启动 PHP 服务器。(您可以使用--host和--port自定义主机和/或端口。)
tinker
启动 Tinker REPL,我们将在本章后面介绍它。
stub:publish
发布所有可用于自定义的存根。
docs
为您提供快速访问 Laravel 文档的途径;传递一个参数,您将被提示打开这些文档的 URL,或者不传递参数,您将能够浏览文档主题列表以选择。
about
显示项目环境、通用配置、包等的概述。
Artisan 命令列表随时间变化
Laravel 生命周期内 Artisan 命令及其名称略有变化。在撰写本书时,此列表尽可能是最新的。但是,了解可用内容的最佳方法是从您的应用程序中运行 php artisan。
选项
在我们介绍其余命令之前,让我们看一下您在运行 Artisan 命令时可以随时传递的一些显著选项:
-q
抑制所有输出
-v、-vv 和 -vvv
指定输出详细程度(正常、详细和调试)
--no-interaction
抑制交互式问题,因此命令不会中断正在运行它的自动化过程
--env
允许您定义 Artisan 命令应在哪个环境中运行(local、production 等)。
--version
显示您的应用程序正在运行的 Laravel 版本
您可能已经从这些选项中猜到,Artisan 命令的使用方式类似于基本的 shell 命令:您可以手动运行它们,但它们也可以作为某些自动化过程的一部分运行。
例如,许多自动化部署流程可能会从某些 Artisan 命令中受益。每次部署应用程序时,您可能希望运行 php artisan config:cache。像 -q 和 --no-interaction 这样的标志确保您的部署脚本可以顺利运行,而无需人类干预。
分组命令
提供了默认情况下可用的其余命令,这些命令根据上下文进行分组。我们不会在此处详细介绍所有命令,但我们将广泛涵盖每个上下文:
auth
这里仅有 auth:clear-resets,该命令从数据库中清除所有过期的密码重置令牌。
cache
cache:clear 清除缓存,cache:forget 从缓存中删除单个项,并且 cache:table 如果您计划使用 database 缓存驱动程序,则创建数据库迁移。
config
config:cache 缓存您的配置设置以加快查找速度;要清除缓存,请使用 config:clear。
db
db:seed 如果已配置数据库填充器,则向数据库中填充数据。
event
event:list 列出应用程序中的所有事件和监听器,event:cache 缓存该列表,event:clear 清除该缓存,event:generate 根据 EventServiceProvider 中的定义构建缺失的事件和事件监听器文件。您将在 第十六章 中了解更多关于事件的信息。
key
key:generate 在您的 .env 文件中创建一个随机的应用程序加密密钥。
重新生成 artisan key:generate 意味着丢失某些加密数据
如果在你的应用程序上多次运行php artisan key:generate,每个当前已登录的用户都将被注销。此外,任何您手动加密的数据将无法解密。要了解更多信息,请查看由同事 Tightenite Jake Bathman 撰写的文章“APP_KEY and You”。
make
每个make:操作都从一个存根创建一个单独的项目,并具有相应变化的参数。要了解有关任何单个命令参数的更多信息,请使用help来阅读其文档。
例如,您可以运行php artisan help make:migration,了解到可以传递--create=*tableNameHere*来创建一个已经包含创建表语法的迁移文件,如下所示:php artisan make:migration create_posts_table --create=posts。
migrate
之前提到的用于运行所有迁移的migrate命令,请参阅“Running Migrations”以获取有关所有与迁移相关的命令的详细信息。
notifications
notifications:table 生成一个创建数据库通知表的迁移。
package
Laravel 通过其“autodiscover”功能生成的清单。这在您首次安装第三方包时为您注册服务提供程序。package:discover 重新构建 Laravel 的“已发现”清单,其中包含来自外部包的服务提供程序。
queue
我们将在第十六章介绍 Laravel 的队列,但基本思想是您可以将作业推送到远程队列,由工作进程依次执行。此命令组提供了与队列交互所需的所有工具,如queue:listen用于开始监听队列,queue:table用于创建支持数据库的队列的迁移,queue:flush用于刷新所有失败的队列作业。还有更多命令,您将在第十六章中了解到。
route
如果运行route:list,您将看到应用程序中定义的每个路由的定义,包括每个路由的动词、路径、名称、控制器/闭包动作和中间件。您可以使用route:cache缓存路由定义以加快查找速度,并使用route:clear清除缓存。
schedule
我们将在第十六章介绍 Laravel 的类似于 cron 的调度器,但为了使其工作,您需要设置系统 cron 每分钟运行schedule:run一次:
* * * * * php /home/myapp.com/artisan schedule:run >> /dev/null 2>&1
正如您所看到的,此 Artisan 命令旨在定期运行,以支持 Laravel 核心服务。
session
session:table 为使用数据库支持会话的应用程序创建迁移。
storage
storage:link 创建一个符号链接,将public/storage链接到storage/app/public。这是 Laravel 应用程序中的常见约定,可以轻松地将用户上传的文件(或其他通常保存在storage/app中的文件)放在可以通过公共 URL 访问的地方。
vendor
一些特定于 Laravel 的包需要“发布”它们的一些资源,这样它们可以从你的 public 目录提供或者你可以修改它们。无论哪种方式,这些包都会向 Laravel 注册这些“可发布的资源”,当你运行 vendor:publish 时,它们就会发布到指定的位置。
view
Laravel 的视图渲染引擎会自动缓存你的视图。通常它处理自己的缓存失效工作做得不错,但如果你注意到有时候卡住了,可以运行 view:clear 来清除缓存。
编写自定义 Artisan 命令
现在我们已经讨论了 Laravel 开箱即用的 Artisan 命令,让我们来谈谈如何编写你自己的命令。
首先,你应该知道:有一个专门的 Artisan 命令来处理这个!运行 php artisan make:command *YourCommandName* 会在 app/Console/Commands/{YourCommandName}.php 中生成一个新的 Artisan 命令。
你的第一个参数应该是命令的类名,你还可以选择性地传递一个 --command 参数来定义终端命令将是什么(例如 appname:action)。所以,让我们来做吧:
php artisan make:command WelcomeNewUsers --command=email:newusers
查看 示例 8-1 以查看你将得到什么。
示例 8-1. Artisan 命令的默认骨架
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class WelcomeNewUsers extends Command
{
/**
* The name and signature of the console command
*
* @var string
*/
protected $signature = 'email:newusers';
/**
* The console command description
*
* @var string
*/
protected $description = 'Command description';
/**
* Execute the console command.
*/
public function handle(): void
{
//
}
}
正如你所看到的,定义命令签名、命令列表中显示的帮助文本以及命令执行时的行为 (handle()) 非常简单。
一个示例命令
在本章中我们还没有涵盖邮件或 Eloquent(查看 第十五章 获取邮件和 第五章 获取 Eloquent),但 示例 8-2 中的示例 handle() 方法应该读起来很清晰。
示例 8-2. 一个样例 Artisan 命令 handle() 方法
// ...
class WelcomeNewUsers extends Command
{
public function handle(): void
{
User::signedUpThisWeek()->each(function ($user) {
Mail::to($user)->send(new WelcomeEmail);
});
}
现在每当你运行 php artisan email:newusers 命令时,该命令将获取本周注册的每个用户,并发送给他们欢迎邮件。
如果你更喜欢注入你的邮件和用户依赖项而不是使用门面模式,你可以在命令构造函数中使用类型提示,当命令实例化时,Laravel 的容器会自动注入它们给你。
查看 示例 8-3 以查看使用依赖注入和将其行为提取到服务类中的 示例 8-2 是什么样子的。
示例 8-3. 同一个命令,重构后
...
class WelcomeNewUsers extends Command
{
public function __construct(UserMailer $userMailer)
{
parent::__construct();
$this->userMailer = $userMailer
}
public function handle(): void
{
$this->userMailer->welcomeNewUsers();
}
参数和选项
新命令的 $signature 属性看起来可能只包含命令名称。但这个属性也是你定义命令的参数和选项的地方。你可以使用特定而简单的语法来向你的 Artisan 命令添加参数和选项。
在我们深入研究语法之前,先看一个例子来获得一些上下文:
protected $signature = 'password:reset {userId} {--sendEmail}';
参数 — 必填、可选、或者带有默认值
要定义一个必填参数,用大括号括起来:
password:reset {userId}
要使参数变成可选的,添加一个问号:
password:reset {userId?}
要使它可选并提供默认值,使用:
password:reset {userId=1}
选项 — 必填值、默认值和快捷方式
选项类似于参数,但它们以 -- 作为前缀,并且可以不带值使用。要添加基本选项,请用大括号括起来:
password:reset {userId} {--sendEmail}
如果您的选项需要一个值,请在其签名中添加一个 =:
password:reset {userId} {--password=}
如果要传递默认值,请在 = 之后添加它:
password:reset {userId} {--queue=default}
数组参数和数组选项
无论是对于参数还是选项,如果要接受数组作为输入,请使用 * 字符:
password:reset {userIds*}
password:reset {--ids=*}
使用数组参数和参数看起来有点像 8-4 示例。
示例 8-4. 在 Artisan 命令中使用数组语法
// Argument
php artisan password:reset 1 2 3
// Option
php artisan password:reset --ids=1 --ids=2 --ids=3
数组参数必须是最后一个参数。
由于数组参数捕获其定义后的每个参数,并将它们作为数组项添加,因此数组参数必须是 Artisan 命令签名中的最后一个参数。
输入描述
还记得内置的 Artisan 命令如何在使用 artisan help 时能为我们提供有关其参数的更多信息吗?我们可以为我们的自定义命令提供相同的信息。只需在大括号中加上冒号和描述文本,就像 8-5 示例中一样。
示例 8-5. 为 Artisan 参数和选项定义描述文本
protected $signature = 'password:reset
{userId : The ID of the user}
{--sendEmail : Whether to send user an email}';
使用输入
现在我们已经提示了这个输入,那么我们如何在命令的 handle() 方法中使用它呢?我们有两套方法来检索参数和选项的值。
argument() 和 arguments()
$this->arguments() 返回所有参数的数组(第一个数组项将是命令名称)。没有参数调用的 $this->argument() 返回相同的响应;我更喜欢复数形式的方法,仅仅是为了更好的可读性。
要仅获取单个参数的值,请将参数名称作为参数传递给 $this->argument(),如示例 8-6 所示。
示例 8-6. 在 Artisan 命令中使用 $this->arguments()
// With definition "password:reset {userId}"
php artisan password:reset 5
// $this->arguments() returns this array
[
"command": "password:reset",
"userId": "5",
]
// $this->argument('userId') returns this string
"5"
option() 和 options()
$this->options() 返回一个包含所有选项的数组,其中一些默认为 false 或 null。没有参数调用的 $this->option() 返回相同的响应;我更喜欢复数形式的方法,仅仅是为了更好的可读性。
要仅获取单个选项的值,请将参数名称作为参数传递给 $this->option(),如示例 8-7 所示。
示例 8-7. 在 Artisan 命令中使用 $this->options()
// With definition "password:reset {--userId=}"
php artisan password:reset --userId=5
// $this->options() returns this array
[
"userId" => "5",
"help" => false,
"quiet" => false,
"verbose" => false,
"version" => false,
"ansi" => false,
"no-ansi" => false,
"no-interaction" => false,
"env" => null,
]
// $this->option('userId') returns this string
"5"
8-8 示例展示了在其 handle() 方法中使用 argument() 和 option() 的 Artisan 命令。
示例 8-8. 从 Artisan 命令获取输入
public function handle(): void
{
// All arguments, including the command name
$arguments = $this->arguments();
// Just the 'userId' argument
$userid = $this->argument('userId');
// All options, including some defaults like 'no-interaction' and 'env'
$options = $this->options();
// Just the 'sendEmail' option
$sendEmail = $this->option('sendEmail');
}
提示
在执行命令期间,有几种方法可以从 handle() 代码中获取用户输入:
ask()
提示用户输入自由格式文本:
$email = $this->ask('What is your email address?');
secret()
提示用户输入自由格式文本,但用星号隐藏输入:
$password = $this->secret('What is the DB password?');
confirm()
提示用户回答是或否,并返回布尔值:
if ($this->confirm('Do you want to truncate the tables?')) {
//
}
除了 y 或 Y 之外的所有答案都将被视为“否”。
anticipate()
提示用户输入自由格式文本,并提供自动完成建议。仍然允许用户输入他们想要的任何内容:
$album = $this->anticipate('What is the best album ever?', [
"The Joshua Tree", "Pet Sounds", "What's Going On"
]);
choice()
提示用户从提供的选项中选择一个。如果用户没有选择,则使用最后一个参数作为默认值:
$winner = $this->choice(
'Who is the best football team?',
['Gators', 'Wolverines'],
0
);
请注意,最后一个参数,默认应为数组键。由于我们传递了非关联数组,因此 Gators 的键是 0。如果您愿意,也可以对数组进行键分配:
$winner = $this->choice(
'Who is the best football team?',
['gators' => 'Gators', 'wolverines' => 'Wolverines'],
'gators'
);
输出
在执行命令期间,您可能希望向用户写入消息。实现这一最基本的方法是使用 $this->info() 输出基本的绿色文本:
$this->info('Your command has run successfully.');
您还可以使用 comment()(橙色)、question()(高亮青色)、error()(高亮红色)、line()(未着色)和 newLine()(未着色)方法在命令行输出。
请注意,确切的颜色可能因机器而异,但它们试图符合本地机器与最终用户之间的标准沟通。
表格输出
table() 方法使得创建包含数据的 ASCII 表格变得简单。查看 示例 8-9。
示例 8-9. 使用 Artisan 命令输出表格
$headers = ['Name', 'Email'];
$data = [
['Dhriti', 'dhriti@amrit.com'],
['Moses', 'moses@gutierez.com'],
];
// Or, you could get similar data from the database:
$data = App\User::all(['name', 'email'])->toArray();
$this->table($headers, $data);
注意 示例 8-9 包含两组数据:标题和数据本身。每行都包含两个“单元格”;每行的第一个单元格是名称,第二个单元格是电子邮件。这样,来自 Eloquent 调用的数据(限制为仅提取名称和电子邮件)与标题相匹配。
查看 示例 8-10 以查看表格输出的样子。
示例 8-10. Artisan 表格的示例输出
+---------+--------------------+
| Name | Email |
+---------+--------------------+
| Dhriti | dhriti@amrit.com |
| Moses | moses@gutierez.com |
+---------+--------------------+
进度条
如果您曾经运行过 npm install,您之前见过命令行进度条。让我们在 示例 8-11 中构建一个。
示例 8-11. Artisan 进度条示例
$totalUnits = 350;
$this->output->progressStart($totalUnits);
for ($i = 0; $i < $totalUnits; $i++) {
sleep(1);
$this->output->progressAdvance();
}
$this->output->progressFinish();
我们在这里做了什么?首先,我们告诉系统需要处理多少“单位”。也许一个单位是一个用户,您有 350 个用户。进度条然后将屏幕上可用的整个宽度除以 350,并且每次运行 progressAdvance() 时递增 1/350。完成后,请运行 progressFinish() 以通知它已完成显示进度条。
编写基于闭包的命令
如果您更喜欢保持命令定义过程简单,可以将命令编写为闭包而不是类,方法是在 routes/console.php 中定义和注册它们。本章中讨论的所有内容都将同样适用,但您将在该文件中的单个步骤中定义和注册命令,如 示例 8-12 所示。
示例 8-12. 使用闭包定义 Artisan 命令
// routes/console.php
Artisan::command(
'password:reset {userId} {--sendEmail}',
function ($userId, $sendEmail) {
$userId = $this->argument('userId');
// Do something...
}
);
在普通代码中调用 Artisan 命令
虽然 Artisan 命令设计用于从命令行运行,但您也可以从其他代码中调用它们。
最简单的方法是使用 Artisan 门面。您可以使用 Artisan::call() 调用命令(这将返回命令的退出代码),或者使用 Artisan::queue() 将命令排队。
两者都接受两个参数:第一个是终端命令(password:reset);第二个是要传递给它的参数数组。查看示例 8-13 以了解如何使用参数和选项。
示例 8-13. 从其他代码调用 Artisan 命令
Route::get('test-artisan', function () {
$exitCode = Artisan::call('password:reset', [
'userId' => 15,
'--sendEmail' => true,
]);
});
如您所见,参数通过键名传递给参数名,没有值的选项可以传递true或false。
使用字符串语法调用 Artisan 命令
您还可以通过将与命令行中相同的字符串传递到Artisan::call()中,更自然地从您的代码中调用 Artisan 命令:
Artisan::call('password:reset 15 --sendEmail')
您还可以从其他命令中使用$this->call()调用 Artisan 命令(与Artisan::call()相同),或者使用$this->callSilent(),它们的作用相同,但抑制了所有输出。参见示例 8-14 作为示例。
示例 8-14. 从其他 Artisan 命令调用 Artisan 命令
public function handle(): void
{
$this->callSilent('password:reset', [
'userId' => 15,
]);
}
最后,您可以注入Illuminate\Contracts\Console\Kernel合同的一个实例,并使用它的call()方法。
Tinker
Tinker 是一个 REPL(交互式环境),或者读取-求值-打印循环。REPL 会给您一个提示符,类似于命令行提示符,模仿应用程序的“等待”状态。您在 REPL 中键入命令,按回车键,然后期待您键入的内容进行评估并打印响应。
示例 8-15 提供了一个快速示例,让您了解它的工作方式及其可能的用处。我们使用php artisan tinker启动 REPL,然后看到一个空白提示符(>>>);每个命令的响应都打印在以=>为前缀的行上。
示例 8-15. 使用 Tinker
$ php artisan tinker
>>> $user = new App\User;
=> App\User: {}
>>> $user->email = 'matt@mattstauffer.com';
=> "matt@mattstauffer.com"
>>> $user->password = bcrypt('superSecret');
=> "$2y$10$TWPGBC7e8d1bvJ1q5kv.VDUGfYDnE9gANl4mleuB3htIY2dxcQfQ5"
>>> $user->save();
=> true
如您所见,我们创建了一个新用户,设置了一些数据(使用bcrypt()对密码进行了哈希处理以确保安全),并将其保存到数据库中。这是真实的情况。如果这是一个生产应用程序,我们会在系统中创建一个全新的用户。
这使得 Tinker 成为一个用于简单数据库交互、尝试新想法以及在应用程序源文件中找不到放置位置时运行代码片段的绝佳工具。
Tinker 由Psy Shell提供支持,因此请查看它,看看您还可以使用 Tinker 做什么。
Laravel 转储服务器
在开发过程中,调试数据状态的一种常见方法是使用 Laravel 的dump()助手函数,它对任何您传递给它的内容运行装饰过的var_dump()。这很好用,但它经常会遇到视图问题。
您可以启用 Laravel 转储服务器,它会捕获那些dump()语句,并在控制台中显示它们,而不是将它们渲染到页面中。
要在本地控制台中运行转储服务器,请导航至项目的根目录并运行php artisan dump-server:
$ php artisan dump-server
Laravel Var Dump Server
=======================
[OK] Server listening on tcp://127.0.0.1:9912
// Quit the server with CONTROL-C.
现在,请尝试在您的代码中某个地方使用dump()助手函数。要测试它,请在您的routes/web.php文件中尝试以下代码:
Route::get('/', function () {
dump('Dumped Value');
return 'Hello World';
});
没有 dump 服务器,你会同时看到 dump 和你的“Hello World”。但是有 dump 服务器运行时,你只会在浏览器中看到“Hello World”。在你的控制台中,你会看到 dump 服务器捕捉到了 dump(),你可以在那里检查它:
GET http://myapp.test/
--------------------
------------ ---------------------------------
date Tue, 18 Sep 2018 22:43:10 +0000
controller "Closure"
source web.php on line 20
file routes/web.php
------------ ---------------------------------
"Dumped Value"
自定义生成器存根
任何生成文件的 Artisan 命令(例如 make:model 和 make:controller)都使用“存根”文件,命令会复制并修改这些文件以创建新生成的文件。你可以在你的应用程序中自定义这些存根。
要在你的应用程序中自定义存根,请运行 php artisan stub:publish,它将把存根文件导出到一个 stub/ 目录中,你可以在那里自定义它们。
测试
由于你知道如何从代码中调用 Artisan 命令,因此在测试中执行这些操作并确保你期望的行为已经正确执行很容易,就像 示例 8-16 中展示的那样。在我们的测试中,我们使用 $this->artisan() 而不是 Artisan::call(),因为它具有相同的语法但添加了一些与测试相关的断言。
示例 8-16. 在测试中调用 Artisan 命令
public function test_empty_log_command_empties_logs_table()
{
DB::table('logs')->insert(['message' => 'Did something']);
$this->assertCount(1, DB::table('logs')->get());
$this->artisan('logs:empty'); // Same as Artisan::call('logs:empty');
$this->assertCount(0, DB::table('logs')->get());
}
你可以链式调用一些新的断言到你的 $this->artisan() 调用中,这使得测试 Artisan 命令变得更加容易——不仅仅是它们对你的应用程序的影响,还有它们的实际操作。看看 示例 8-17 来看看这种语法的一个示例。
示例 8-17. 对 Artisan 命令的输入和输出进行断言
public function testItCreatesANewUser()
{
$this->artisan('myapp:create-user')
->expectsQuestion("What's the name of the new user?", "Wilbur Powery")
->expectsQuestion("What's the email of the new user?", "wilbur@thisbook.co")
->expectsQuestion("What's the password of the new user?", "secret")
->expectsOutput("User Wilbur Powery created!");
$this->assertDatabaseHas('users', [
'email' => 'wilbur@thisbook.co'
]);
}
TL;DR
Artisan 命令是 Laravel 的命令行工具。Laravel 自带了一些命令,但也很容易创建你自己的 Artisan 命令并从命令行或你自己的代码中调用它们。
Tinker 是一个 REPL,使得进入你的应用程序环境并与真实代码和真实数据交互变得简单,而 dump 服务器允许你在不停止代码执行的情况下调试你的代码。