Laravel5-精要-全-
Laravel5 精要(全)
原文:
zh.annas-archive.org/md5/774629f62b04d78c887b149f95b68b9a译者:飞龙
前言
过去五年中,应用程序框架的受欢迎程度有所增长。从手动编写所有代码到利用这些强大的框架以及预构建的组件和功能,发生了巨大的转变。然而,随着任何流行事物的出现,现在有很多竞争性的选择,而且每个选择都是可行的。
虽然 CodeIgniter 是第一个广受欢迎的框架之一,但这种受欢迎程度在几年后变成了它的致命伤,因为其广泛的使用和低门槛意味着它无法在不失去向后兼容性的情况下利用 PHP 的新版本,这可能会破坏大量应用程序。这导致它被更快发展的替代品如 Symfony 和 FuelPHP 超越,后者是作为对 CodeIgniter 不愿接受变化的回应而开发的。
进入:Laravel。当已经有众多玩家加入框架领域时,Laravel 加入了这个场景。然而,Laravel 的开发者利用了这个时机,没有重蹈之前全栈框架所犯的所有问题和错误,而是在优秀的 Symfony 组件之上构建了一个强大、基于组件的框架。
Laravel 不是提供数十个不可灵活的库,而是提供合理的、基于驱动程序的组件,开发者可以使用这些组件以自己的方式构建应用程序,而不是试图将所有内容都塞入框架作者定义的布局中。这导致了 Laravel 的流行。它也是一个快速发展的框架,到版本 4 时,已成为 GitHub 上最受欢迎的框架,这是对其受欢迎程度的证明。
本书将带您游览 Laravel 及其核心功能。我们将探讨如何在同一台机器上管理多个 Laravel 应用程序,然后我们将从头开始构建自己的 Laravel 应用程序,直到完成。一旦我们有一个基本的应用程序,能够从数据库中读取和写入数据,我们将看看 Eloquent,Laravel 的 ORM,它使得从数据库中读取和写入变得容易,以及它提供的更高级功能。从那里,我们将探讨 Artisan,Laravel 的命令行工具,甚至如何定义我们自己的命令。然后,我们将学习如何为我们的应用程序编写自动化测试,以确保它按照我们的期望工作,即使在未来的发展中也是如此。最后,我们将探讨如何使用 Laravel 的用户认证组件构建登录和注册系统。
到本书结束时,你将拥有一个完整的 Laravel 应用程序,以及如何无辅助地构建基于 Laravel 的应用程序的工具和知识,以及在哪里继续学习该框架。
本书涵盖的内容
第一章,Laravel 简介,探讨了 PHP 应用程序框架的一般情况,Laravel 框架的近期历史,以及 Laravel 框架构建的原则。
第二章,设置开发环境,通过安装和配置 Homestead 虚拟机和 Composer 依赖管理器,为构建 Laravel 应用程序奠定基础。
第三章,你的第一个应用程序,从零开始构建一个完整的工作应用程序。从这里开始,乐趣开始了!
第四章,Eloquent ORM,探讨了 Laravel 附带的对象关系映射器 Eloquent,它允许你轻松查询数据库。
第五章,测试 – 比你想象的要简单,概述了测试你的 Laravel 应用程序的各种方法,以确保它们尽可能坚固,并且在添加新功能后仍然按预期工作。
第六章,名为 Artisan 的命令行伴侣,帮助我们了解 Artisan,它是 Laravel 的命令行实用工具。我们介绍了 Artisan 提供的内置命令,以及如何创建我们自己的命令行工具。
第七章,认证和安全,展示了保护你的 Laravel 应用程序免受常见攻击的各种方法,以及如何认证和授权访问你的应用程序的用户。
附录,工具箱,涵盖了 Laravel 提供的工具,这些工具在前面的章节中没有介绍。
你需要这本书什么
由于 Laravel 是一个基于 PHP 的应用程序框架,你需要一个具有 PHP 语法高亮的代码编辑器或 IDE。
我们将使用 Homestead 虚拟机,它需要在你的机器上安装 Vagrant 和 VirtualBox;这两者的安装说明将在本书的后续部分提供。
此外,如果你计划将应用程序部署到实时 Web 服务器,那么你需要一个 FTP 客户端或 SSH 访问远程 Web 服务器,以便将文件从你的本地机器移动到可访问的 Web 服务器。
这本书面向谁
本书主要面向对 Laravel 框架感兴趣的人,也许他们听说过它,但没有机会或时间熟悉它。因此,假设读者具备 PHP 和相关技术(如 MySQL)的知识,以及面向对象编程的知识。
习惯用法
在本书中,你将找到许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名应如下所示:“Illuminate命名空间不指向第三方库。”
代码块应如下设置:
sites:
- map: dev.furbook.com
to: /home/vagrant/Code/furbook.com/public
databases:
- furbook
当我们希望将你的注意力引到代码块的一个特定部分时,相关的行或项目将以粗体显示:
Route::post('cats', function() {
$cat = Furbook\Cat::create(Input::all());
return redirect('cats/'.$cat->id)
->withSuccess('Cat has been created.');
});
任何命令行输入或输出都应如下所示:
$ composer create-project laravel/laravel furbook.com --prefer-dist
新术语和重要词汇以粗体显示。屏幕上显示的单词,例如在菜单或对话框中,在文本中显示如下:“如果你现在尝试访问一个无效的 URL,nginx 将显示一个404 Not Found错误页面。”
注意
警告或重要注意事项以如下框的形式出现。
小贴士
技巧和窍门以如下形式出现。
读者反馈
我们始终欢迎读者的反馈。告诉我们你对这本书的看法——你喜欢什么或不喜欢什么。读者反馈对我们很重要,因为它帮助我们开发出你真正能从中获得最大价值的标题。
要向我们发送一般反馈,只需发送电子邮件至<feedback@packtpub.com>,并在邮件主题中提及书籍的标题。
如果你在一个主题上具有专业知识,并且你对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在你已经是 Packt 书籍的骄傲所有者,我们有一些事情可以帮助你从你的购买中获得最大价值。
下载示例代码
你可以从你购买的所有 Packt 出版物的账户中下载示例代码文件。www.packtpub.com。如果你在其他地方购买了这本书,你可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给你。
勘误
尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果你在我们的一本书中发现错误——可能是文本或代码中的错误——如果你能向我们报告这一点,我们将不胜感激。通过这样做,你可以帮助其他读者避免挫败感,并帮助我们改进本书的后续版本。如果你发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择你的书籍,点击勘误提交表单链接,并输入你的勘误详情。一旦你的勘误得到验证,你的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下的现有勘误列表中。
要查看之前提交的勘误表,请访问www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将显示在勘误部分。
侵权
互联网上对版权材料的盗版是一个持续存在的问题,涉及所有媒体。在 Packt,我们非常重视保护我们的版权和许可证。如果您在互联网上发现任何形式的我们作品的非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。
请通过 <copyright@packtpub.com> 联系我们,并提供涉嫌盗版材料的链接。
我们感谢您在保护我们作者和我们为您提供有价值内容的能力方面提供的帮助。
问题
如果您对本书的任何方面有问题,请通过 <questions@packtpub.com> 联系我们,我们将尽力解决问题。
第一章。Laravel 简介
PHP 框架并不新鲜,但最新的是 Laravel。自第 3 版以来,Laravel 在短时间内迅速崛起,成为最受欢迎和最广泛使用的 PHP 框架之一。在撰写本文时,GitHub 上的 Laravel 存储库的星标数量超过了其更成熟的同行,如 Symfony、CakePHP、CodeIgniter 和 Yii。那么,是什么让 Laravel 如此受欢迎呢?
在本章中,我们将涵盖以下主题:
-
如何通过使用框架来提高生产力
-
Laravel 的基本概念和关键特性
-
新建 Laravel 应用程序的一般结构和约定
-
介绍基于 Laravel 的模型-视图-控制器(MVC)设计模式
-
为 Laravel 旧版本用户提供的迁移技巧
我们将探讨其关键特性和它们是如何使 Laravel 成为许多 Web 开发人员不可或缺的工具。我们将比较使用和不使用框架编写 PHP 应用程序,并看看使用框架如何有助于编写更健壮和结构更好的 PHP 应用程序。然后,我们将更深入地研究 Laravel 应用程序的结构和它所利用的第三方包。阅读完本章后,您将拥有开始构建您的第一个 Laravel 应用程序所需的知识。
对框架的需求
在所有服务器端编程语言中,PHP 无疑具有最低的入门门槛。它几乎总是默认安装在即使是最低成本的 Web 托管服务上,而且它也非常容易在任何个人计算机上设置。对于有 HTML 和 CSS 网页编写经验的初学者来说,变量、内联条件和include语句的概念很容易理解。PHP 还提供了许多在开发动态网站时可能需要的常用函数。所有这些都为一些人所说的 PHP 的即时性做出了贡献。然而,这种即时的满足感是有代价的。它给初学者一种虚假的生产力感,他们几乎不可避免地会在添加更多功能和功能到他们的网站时,最终得到一团糟的意大利面代码。这主要是因为 PHP 本身并没有做很多来鼓励关注点的分离。
自制工具的限制
如果你已经有一些 PHP 项目经验,但之前没有使用过 Web 应用程序框架,那么你可能会积累了一些常用的函数和类,可以在新项目中使用。这些自制的实用工具可以帮助你完成常见任务,如清理数据、验证用户和动态包含页面。你可能还有一个预定义的目录结构,其中包含这些类以及你应用程序的其余代码。然而,所有这些都将完全独立存在;你将完全负责维护、添加新功能和编写文档。对于一个单独的开发者或人员不断变化的代理机构来说,这可能是一项繁琐且耗时的任务,更不用说如果你要与项目中的其他开发者合作,他们首先必须熟悉你构建应用程序的方式。
Laravel 来救命
这正是像 Laravel 这样的 Web 应用程序框架可以救命的地方。Laravel 重用和组装现有组件,为你提供一个统一的层,你可以在这个层上以更结构化和实用化的方式构建 Web 应用程序。Laravel 从不仅限于 PHP,还包括其他编程语言的流行框架中汲取灵感,提供了一套强大的工具和应用程序架构,它结合了许多像 CodeIgniter、Yii、ASP.NET MVC、Ruby on Rails、Sinatra 等其他框架的最佳特性。
大多数这些框架都使用模型-视图-控制器(MVC)范式或设计模式。如果你使用过上述工具或 MVC 模式,那么你会发现开始使用 Laravel 5 相当容易。
开发 PHP 应用程序的新方法
如前所述,PHP 由于许多编写不良的网站和 Web 应用程序以及与其他更成熟语言相比的不足,在多年中获得了坏名声。PHP 在命名不一致和关于其语法的有疑问的设计决策方面也臭名昭著。因此,许多人转向了用Ruby和Python编写的更可信的框架。由于这些语言在 Web 方面的功能远不如 PHP 丰富,例如 Ruby on Rails 和 Django 的创造者,不得不重新创建一些基本构建块,如类,以表示 HTTP 请求和响应,因此能够避免 PHP 在他们之前犯下的某些错误,因为从一张白纸开始有更多的自由。这些框架还迫使开发者遵守预定义的应用程序架构。
然而,现在是一个很好的时机去发现(或者重新爱上)PHP,因为在这过去的几年里,这门语言迅速发展,包括了新的特性,如闭包和 traits,以及事实上的包管理器 Composer。与其他语言相比,PHP 过去的抱怨现在正是如此,是过去的,PHP 正在缓慢但肯定地改变它长期遭受的坏名声。
一个更健壮的 HTTP 基础
经过多年人们开发自己独特的处理常见任务的方法,例如处理请求和响应,特别是针对他们自己的项目,一个框架采取了不同的方法,而不是开始创建可以在任何代码库中使用的组件,无论其基础是自建的还是基于框架的。Symfony 项目采用了这些原则,为 PHP 应用程序重新创建了一个更坚固、更灵活、更可测试的 HTTP 基础。与 Drupal 和 phpBB 的最新版本一样,Laravel 是众多使用此基础以及形成 Symfony 框架的几个其他组件的开源项目之一。
Laravel 是这样一个项目,它依赖于由 Symfony 创建的 HTTP 基础设施。它还依赖于由 Symfony 创建的其他组件,以及各种其他流行的库,例如 SwiftMailer 用于更直接的电子邮件发送,Carbon 用于更丰富的日期和时间处理,Doctrine 提供其屈折词和数据库抽象工具,以及一些其他工具来处理日志记录、类加载和错误报告。Laravel 决定不重新发明轮子,而是站在巨人的肩膀上,拥抱这些现有的成熟组件。
拥抱 PHP
Laravel 与其同行不同的一个方面是,它公开拥抱 PHP 的新特性,并相应地要求一个相当新的版本(至少 5.4)。以前,其他框架会构建对旧版本 PHP 的支持,以尽可能长时间地保持向后兼容性。然而,这种方法意味着那些相同的框架无法利用 PHP 新版本中的新特性,从而阻碍了 PHP 的发展。使用 Laravel 5,你将能够掌握 PHP 的一些新特性。如果你是 PHP 新手,或者一段时间后重返这门语言,那么你可以期待以下内容:
-
命名空间:比 PHP 更成熟的语言,如 Java 和 C#,都有命名空间。命名空间帮助开发者避免可能发生的命名冲突,比如两个不同的库有相同的函数或类名。在 PHP 中,命名空间由反斜杠分隔,这通常与目录结构相匹配,唯一的区别是在 Unix 系统中,根据 PSR-4 规范,使用斜杠。一个命名空间,如
<?php namespace Illuminate\Database\Eloquent,在文件顶部声明。要使用另一个命名空间中的代码,需要导入,这可以通过use关键字完成,然后指定命名空间,即use Illuminate\Database\Eloquent\Model。命名空间的另一个优点是可以为导入的类创建别名,以避免与另一个命名空间或全局命名空间中具有相同名称的类发生冲突。为此,你在use语句之后使用as关键字,例如use Foo\Logger as FooLogger; -
接口:接口指定了当实现该接口时类应该提供的方法。接口本身不包含任何实现细节,只包含方法(以及这些方法应该接受的参数)。例如,如果一个类实现了 Laravel 的
JsonableInterface接口,那么这个类也需要有一个toJson()方法。在 Laravel 中,接口通常被称为 契约。 -
匿名函数:这些也被称为
closures,PHP 5.3 中引入。它们与 JavaScript 有点相似,可以帮助你编写更短的代码。在构建 Laravel 应用程序时,你将广泛使用它们来定义路由、事件、过滤器等。这是一个附加到路由上的匿名函数示例:Route::get('/', function() { return 'Hello, world.'; });。在 Laravel 中,当请求网站的根路径时,这段代码创建了一个新的 路由。当请求发生时,闭包中的代码被执行并返回作为响应。 -
方法重载:也称为“动态”方法,它们允许你调用在类中未显式定义的方法,例如
whereUsernameOrEmail($name, $email)。这些调用由类中的__call()方法处理,该方法随后尝试解析名称以执行一个或多个已知方法。例如,->where('username', $username)->orWhere('email', $email)。 -
简化的数组语法:PHP 5.4 引入了简化的数组语法。现在,你可以只用方括号来表示数组,即
['primes'=>['1','3','5','7']],而不再需要像array('primes' =>array(1,3,5,7))这样写。如果你在 JavaScript 中使用过数组,你可能已经熟悉这种语法。
Laravel 的主要特性和灵感来源
那么,Laravel 5 默认提供了哪些功能?让我们来看看以下功能如何帮助提高你的生产力:
-
模块化:Laravel 建立在 20 多个不同的库之上,并且自身被分割成单独的模块。与Composer依赖管理器紧密集成,这些组件可以轻松更新。
-
可测试性:Laravel 从构建之初就是为了简化测试而设计的,它自带了几个辅助工具,允许您从测试中访问路由,抓取生成的 HTML,确保在特定的类上调用方法,甚至可以模拟认证用户以确保在正确的时间运行正确的代码。
-
路由:当您定义应用程序的路由时,Laravel 提供了很多灵活性。例如,您可以将一个简单的匿名函数手动绑定到一个带有 HTTP 动词的路由上,如
GET、POST、PUT或DELETE。这个功能受到了像Sinatra(Ruby)和Silex(PHP)这样的微框架的启发。 -
配置管理:通常情况下,您的应用程序将在不同的环境中运行,这意味着当您的应用程序在本地开发服务器上运行时,数据库或电子邮件服务器凭证的设置或错误信息的显示将与在生产服务器上运行时不同。Laravel 采用了一种一致的方法来处理配置设置,并且可以通过使用包含特定环境设置的
.env文件,在不同的环境中应用不同的设置。 -
查询构建器和 ORM:Laravel 自带了一个流畅的查询构建器,允许您使用 PHP 语法发出数据库查询,您只需链式调用方法而不是编写 SQL 语句。除此之外,它还为您提供了对象关系映射器(ORM)和ActiveRecord实现,称为Eloquent,这与您在 Ruby on Rails 中找到的类似,以帮助您定义相互关联的模型。查询构建器和 ORM 与不同的数据库兼容,例如 PostgreSQL、SQLite、MySQL 和 SQL Server。
-
模式构建器、迁移和填充:这些功能也受到了 Rails 的启发,允许您在 PHP 代码中定义数据库模式,并借助数据库迁移跟踪任何更改。迁移是一种简单的方式来描述模式更改及其回滚方式。填充允许您在运行迁移后填充数据库中选定的表,例如。
-
模板引擎:部分受到 ASP.NET MVC 中的Razor模板语言的启发,Laravel 自带了Blade,这是一种轻量级的模板语言,您可以使用它创建具有预定义块的分层布局,其中动态内容被注入。
-
电子邮件:通过其
Mail类,该类封装了流行的SwiftMailer库,Laravel 使得发送电子邮件变得非常简单,甚至可以发送包含丰富内容和附件的电子邮件。Laravel 还提供了对流行的电子邮件发送服务如 SendGrid、Mailgun 和 Mandrill 的驱动程序。 -
认证:由于用户认证是 Web 应用程序中如此常见的功能,因此 Laravel 默认提供了一种实现,用于注册、认证用户,甚至发送密码提醒。
-
Redis:这是一个以极快速度著称的内存键值存储。如果您为 Laravel 提供了一个它可以连接的
Redis实例,它可以用作会话和通用缓存,并为您提供直接与之交互的可能性。 -
队列:Laravel 集成了多个队列服务,如 Amazon SQS、Beanstalkd 和 IronMQ,允许您延迟执行资源密集型任务,例如向大量用户发送电子邮件,并在后台运行,而不是让用户等待任务完成。
-
事件和命令总线:尽管在版本 5 中并非新特性,但 Laravel 将命令总线提到了前台,这使得在应用程序的生命周期中的不同点轻松分发事件(表示应用程序中发生的事情的类),处理命令(表示应在应用程序中发生的事情的另一个类),并对这些事件采取行动。
表现力和简洁性
Laravel 的核心是其代码应简单且富有表现力的哲学。考虑以下代码示例:
<?php
Route::get('area/{area}', function($area) {
if (51 == $area && ! Auth::check()) {
return Redirect::guest('login');
} else {
return 'Welcome to Area '.$area;
}
})->where('area, '[0-9]+');
小贴士
下载示例代码
您可以从www.packtpub.com的账户下载您购买的所有 Packt Publishing 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
尽管我们还没有接触 Laravel 或介绍其路由函数,但您可能已经对这段代码的用途有一个大致的了解。具有表现力的代码对项目的新手来说更容易阅读,而且可能也更容易学习和记忆。
美化 PHP
除了美化 PHP 以及确保 Laravel 中的代码以有效传达其动作的纯英文命名外,Laravel 的作者们还将这些原则应用于现有的 PHP 语言函数。一个主要的例子是Storage类,它是为了简化文件操作而创建的:
-
更具表现力:要找出文件最后修改的时间,请使用
Storage::lastModified($path)而不是filemtime(realpath($path))。要删除文件,请使用Storage::delete($path)而不是unlink($path),后者是普通的 PHP 等效方法。 -
更一致:PHP 的一些原始文件操作函数以
file_为前缀,而其他则仅以file开头;一些被缩写,而其他则没有。使用 Laravel 的包装器,您不再需要猜测或参考 PHP 的文档。 -
更易于测试:许多原始函数由于它们抛出的异常以及它们更难以模拟,因此在测试中使用时可能会很棘手。
-
更完整的功能:这是通过添加之前不存在的功能来实现的,例如
File::copyDirectory($directory, $destination)。
非常罕见的情况下,会牺牲表达性以换取简洁性。这适用于常用的快捷函数,例如用于转义 HTML 实体的e()函数,或者可以用来停止脚本执行并转储一个或多个变量内容的dd()函数。
职责、命名和约定
在本章的开头,我们指出,标准 PHP 应用的主要问题之一是缺乏清晰的关注点分离;业务逻辑与表示层和数据层纠缠在一起。像许多其他更倾向于约定而非配置的框架一样,Laravel 为你提供了带有预定义代码放置位置的脚手架。为了帮助你消除琐碎的决定,它期望你以某种方式命名你的变量、方法或数据库表,尽管这些可以通过配置进行编辑。然而,它比 Ruby on Rails 这样的框架要少一些偏见,在路由等区域,通常有不止一种解决问题的方法。
你可能还记得我们提到过,Laravel 是一个基于 MVC 范式的框架。如果你之前没有使用过这种架构模式,请不要担心;简而言之,这是你需要了解的 MVC 知识,以便能够构建你的第一个 Laravel 应用:
-
模型:模型代表应用中的资源。大多数情况下,它们对应于数据存储中的记录,最常见的是数据库表。在这方面,你可以将模型视为代表应用中的实体,无论是用户、新闻文章还是事件等。在 Laravel 中,模型是通常扩展 Eloquent 基本
Model类的类,并以驼峰式命名(即NewsArticle)。这将对应于具有相同名称的数据库表,但以蛇形小写和复数形式(即news_articles)。默认情况下,Eloquent 还期望有一个名为id的主键,并且会查找并自动更新created_at和updated_at列。模型还可以描述它们与其他模型之间的关系。例如,一个NewsArticle模型可能与一个User模型相关联,因为一个User模型可能能够创建一个NewsArticle模型。然而,模型也可以引用来自其他数据源的数据,例如XML文件或来自网络服务或 API 的响应。 -
控制器或路由:控制器在最简单的形式下,接收一个请求,执行一些操作,然后发送一个适当的响应。控制器是实际数据处理的地方,无论是从数据库中检索数据,还是处理表单提交,并将数据保存回数据库。尽管在 Laravel 中创建控制器类时你不必遵守任何规则,但它确实提供了两种合理的做法:RESTful 控制器和资源控制器。RESTful 控制器允许你定义自己的操作以及它们应该响应的 HTTP 方法。资源控制器围绕一个实体构建,允许你根据使用的 HTTP 方法在该实体上执行常见操作。另一种选择是完全不使用控制器类,而是通过匿名函数在你的路由中编写逻辑。
-
视图或模板:视图负责以合适的格式显示控制器返回的响应,通常是 HTML 网页。它们可以通过使用 Blade 模板语言或简单地使用标准 PHP 方便地构建。视图的文件扩展名,无论是
.blade.php还是简单的.php,决定了 Laravel 是否将你的视图视为Blade模板。
以下图表展示了在典型 Web 应用中所有应用组件之间的交互:

当然,你可以违背 MVC 范式和框架的约定,按照自己的意愿编写代码,但这通常需要开发者付出更多努力,却得不到任何回报。
帮助你成为更好的开发者
Laravel 通过各种设计决策和哲学,如它倡导开发者编写框架无关的代码,并依赖于契约(接口)而不是实现,已经成为一种新的 PHP 应用开发方式的标准。它还建立了一个如此强大的社区,这无疑是其最强大的资产之一,也是其成功的主要贡献因素;你可以在几分钟内通过论坛、IRC 和像 Twitter 这样的社交网站等途径从其他用户那里获得答案。
然而,如果时间教会了我们什么,那就是框架的兴衰无常,很难预测 Laravel 何时会失去势头,被更好的或更受欢迎的框架所取代。尽管如此,Laravel 不仅会在短期内让你更高效,而且从长远来看,它还有潜力让你成为一名更好的开发者。通过用它来构建 Web 应用,你将间接地更熟悉以下概念,所有这些概念都高度可迁移到任何其他编程语言或框架。这包括 MVC 范式和面向对象编程(OOP)设计模式,依赖管理器的使用,测试和依赖注入,以及 ORMs 和数据库迁移的强大功能和局限性。
它还会激发你编写更具表达力的代码,使用描述性的DocBlock注释,这些注释有助于生成文档,以及无论是由你还是其他开发者进行的未来维护。
Laravel 应用的架构
在接下来的两章中,我们将安装 Laravel 并创建我们的第一个应用。像大多数框架一样,Laravel 一开始就为你提供了一个完整的目录树,你可以用它来组织你的代码,还包括一些占位符文件,你可以用作起点。以下是一个新的 Laravel 5 应用的目录结构:
./app/ # Your Laravel application
./app/Commands/ # Commands classes ./app/Console/
./app/Console/Commands/ # Command-line scripts
./app/Events/ # Events that your application can raise
./app/Exceptions/
./app/Handlers/ # Exception handlers
./app/Handlers/Commands # Handlers for command classes
./app/Handlers/Events # Handlers for event classes
./app/Http/
./app/Http/Controllers/ # Your application's controllers
./app/Http/Middleware/ # Filters applied to requests
./app/Http/Requests/ # Classes that can modify requests
./app/Http/routes.php # URLs and their corresponding handlers
./app/Providers # Service provider classes
./app/Services # Services used in your application
./bootstrap/ # Application bootstrapping scripts
./config/ # Configuration files
./database/
./database/migrations/ # Database migration classes
./database/seeds/ # Database seeder classes
./public/ # Your application's document root
./public/.htaccess # Sends incoming requests to index.php
./public/index.php # Starts Laravel application
./resources/
./resources/assets/ # Hold raw assets like LESS & Sass files
./resources/lang/ # Localization and language files
./resources/views/ # Templates that are rendered as HTML
./storage/
./storage/app/ # App storage, like file uploads etc
./storage/framework/ # Framework storage (cache)
./storage/logs/ # Contains application-generated logs
./tests/ # Test cases
./vendor/ # Third-party code installed by Composer
./.env.example # Example environment variable file
./artisan # Artisan command-line utility
./composer.json # Project dependencies manifest
./phpunit.xml # Configures PHPUnit for running tests
./server.php # A lightweight local development server
与 Laravel 的源代码一样,目录的命名也是表达性的,很容易猜测每个目录的作用。app目录是大多数应用的服务端代码所在的地方,它有子目录来处理应用如何被访问(Console和Http),以及用于组织可以在两种场景下使用的代码的子目录(例如Events和Services)。我们将在下一章中进一步探讨每个目录的职责。
服务容器和请求生命周期
无论你是 PHP 的初学者还是一个不同语言的资深开发者,可能并不总是明显地知道一个 HTTP 请求是如何到达 Laravel 应用的。确实,请求的生命周期与直接通过 URI 访问的纯 PHP 脚本(例如,GET http://example.com/about-us.php)在本质上是有区别的。
public/ 目录旨在充当文档根目录;换句话说,这是你的 web 服务器在处理每个传入请求时开始查找的目录。一旦 URL 重写设置正确,所有不匹配现有文件或目录的请求都会命中 /public/index.php 文件。此文件包含 Composer 自动加载文件,它加载依赖项(包括 Laravel 框架组件),并且也是查找你的应用程序代码的位置。然后你的应用程序启动,根据环境加载配置变量。完成此操作后,它实例化一个新的服务容器实例,该实例随后处理传入的请求,使用访问应用程序时使用的 HTTP 方法(例如 POST /comments),并将请求传递给正确的控制器操作或路由进行处理。
探索 Laravel
在本章中,我们只介绍 Laravel 的工作的一般机制,而不涉及详细的实现示例。对于大多数只想完成任务的开发者来说,这已经足够了。此外,一旦你已经构建了一些应用程序,深入研究 Laravel 的源代码会容易得多。尽管如此,这里有一些可能出现的异常抛出或源代码导航时可能需要回答的问题。这样做时,你可能会遇到官方指南中未记录的一些方法,甚至可能会激发你编写更好的代码的灵感。
浏览 API (laravel.com/api) 在一开始可能会有些令人畏惧。但通常这是了解特定方法底层工作原理的最好方式。以下是一些建议:
-
Illuminate命名空间并不指代第三方库。这是 Laravel 作者为构成 Laravel 的不同模块所选择的命名空间。它们中的每一个都旨在可重用,并且可以独立于框架使用。 -
当在源代码或 API 中搜索类定义,例如
Auth,时,你可能会遇到Facade,它几乎不包含任何有用的方法,仅作为真实类的代理。这是因为 Laravel 中几乎每个依赖项在实例化时都会注入到服务容器中。 -
包含在
vendor/目录中的大多数库都包含一个README文件,该文件详细说明了库中存在的功能(例如,vendor/nesbot/carbon/readme.md)。
版本 5 从版本 4 的变化
Laravel 5 最初是以 Laravel 4.3 的形式出现的,但当人们意识到这个新版本将彻底不同于框架的 4.0 版本时,它被提升到了自己的主要版本。Laravel 5 以 Laravel 4 为基础,但内置了诸如应用程序命名空间等功能,使得构建大型应用程序变得容易。要将 Laravel 4 应用程序迁移到 Laravel 5,需要进行相当多的工作。Laravel 5 中新增或更新的功能包括:
-
方法注入:在 Laravel 4 中,你可以通过类型提示(在构造函数中指定)来指定一个类所需的依赖项,Laravel 会自动从其容器中解析这些依赖项。现在,Laravel 5 将这一步更进一步,还将解析类方法和类构造函数中指定的依赖项。
-
表单请求:Laravel 5 引入了表单请求类。这些类可以被注入到你的控制器操作中。它们接受当前请求,并且你可以对它进行数据验证和清理,甚至用户授权(即检查当前登录的用户是否可以执行请求的操作)。这简化了验证过程,意味着你几乎不需要在你的控制器操作中进行数据验证。
-
Socialite:Laravel 5 新增了一个可选的包,名为 Socialite,你可以将其声明为 Composer 依赖项。它使得与第三方服务进行身份验证变得轻而易举,这意味着你可以通过几行代码轻松实现如 Facebook 登录等功能。
-
Elixir:Laravel 5 还致力于使前端开发更加容易。如今,许多开发者都在使用 LESS 和 Sass 等语言来创建他们的样式表,并将 JavaScript 文件合并成一个压缩的 JavaScript 文件,以减少 HTTP 请求并加快加载时间。Elixir 是一个围绕 Gulp 的包装器,Gulp 是一个基于 Node.js 的构建系统,它简化了上述任务。这大大减少了启动新应用程序所需的时间,因为你不需要从其他项目中安装 Node.js 模块或 Gulp 文件。你可以从一开始就免费获得它。
摘要
在本章中,我们向您介绍了 Laravel 5 以及它如何帮助你编写更好、更有结构的应用程序,同时减少样板代码的数量。我们还解释了 Laravel 使用的概念和 PHP 功能,你现在应该已经准备好开始编写你的第一个应用程序了!
在下一章中,你将学习如何设置一个环境,在这个环境中你可以开发 Laravel 应用程序,同时你还将被介绍到 Composer,用于管理依赖。
第二章. 设置开发环境
Laravel 不仅仅是一个框架:围绕它已经开发了一个完整的生态系统和工具集,以使构建 PHP 应用程序更快、更愉快。这些工具完全是可选的,并且了解它们并不是在 Laravel 中使用和构建项目所必需的,但它们与框架紧密相关,因此值得介绍。
在本章中,我们将涵盖以下主题:
-
认识 Composer,一个依赖管理器
-
Homestead 简介,以及如何使用它来管理 Laravel 项目
认识 Composer
在上一章中,你发现 Laravel 建立在几个第三方包之上。Laravel 不是将其外部依赖包含在其自己的源代码中,而是使用名为Composer的依赖管理器来下载它们并保持它们更新。由于 Laravel 由多个包组成,因此每次你创建一个新的 Laravel 项目时,它们也会被下载和安装。
强烈受到其他语言中流行的依赖管理器的启发,如 Ruby 的 Bundler 或 Node.js 的Node 包管理器(npm),Composer 将这些功能带到了 PHP,并迅速成为 PHP 事实上的依赖管理器。
几年前,你可能使用过PHP 扩展和应用仓库(PEAR)来下载库。PEAR 与 Composer 不同,因为 PEAR 会在系统级别上安装包,而依赖管理器,如 Composer,则是在项目级别上安装它们。使用 PEAR,你只能在系统上安装一个版本的包。Composer 允许你在不同的应用程序中使用同一包的不同版本,即使它们位于同一系统上。
使用命令行
如果你刚开始接触 Web 开发,你可能并不完全熟悉命令行界面(CLI)。在使用 Composer、虚拟机、Homestead 以及之后的 Artisan、Laravel 的 CLI 工具时,将需要与之进行一些交互。
下面是如何开始使用 CLI 的步骤:
-
在 Windows 上,寻找命令提示符程序。如果你找不到它,只需导航到开始 | 运行,并输入
cmd.exe。 -
在 Mac OS X 上,CLI 被称为终端,它可以在
/Applications/Utilities中找到。 -
在 Linux 上,根据你的 Linux 发行版,它可能被称为终端或Konsole,但如果你正在运行 Linux,你很可能已经熟悉它了。
你不需要任何高级命令行技能就能通过这本书并使用 Laravel 构建应用程序。然而,你将需要能够在运行命令之前在文件系统中导航到正确的目录。为此,只需输入cd命令,然后跟随你的代码目录路径。
注意
在大多数系统上,你还可以直接输入cd,然后跟一个空格,并将目录拖放到终端中,如图所示:
$ cd /path/to/your/code/directory
否则,你可以在 Windows 上运行以下命令行:
> cd C:\path\to\your\code\directory
如果路径包含空格,请确保将其包含在双引号中,以确保空格被转义:
> cd "C:\path\to\your\Laravel Projects"
在本书的其余部分,除非示例专门针对 Windows,否则我们将始终使用$字符表示 shell 命令,并使用斜杠作为目录分隔符。如果你正在运行 Windows,请确保相应地调整命令。
认识 Homestead
如果你想在你的个人电脑上开发 PHP 应用程序,你需要安装并运行一个本地网络服务器。PHP 被安装来解释你的脚本,以及你的网站可能需要的其他工具,例如与数据库交互。你的网站或网络应用甚至可能需要其他服务,如Memcached或Redis,这些是流行的缓存系统。这导致了像WampServer和MAMP这样的工具的兴起,它们可以创建开发动态网站的环境,而不需要连接到互联网,但这些工具需要配置。
在这些已安装的环境中构建,开发动态网站和应用程序的推荐做法是使用虚拟机(VMs)。这允许你在本地机器上模拟你的生产网络服务器的设置。你也可以为不同的项目使用不同配置的虚拟机——使用类似 WAMP 或 MAMP 的设置,每个项目都必须使用相同版本的 PHP、MySQL 以及其他你安装的任何东西。
Laravel 的创建者创建了一个名为Homestead的官方 Vagrant 箱。Vagrant是一种软件,它允许你在个人电脑上创建虚拟开发环境。你可以安装 Homestead 并立即开始创建 Laravel 项目。而且,如果你不再需要它,你只需从你的机器上移除它,而不会影响其他任何东西。最好的是,如果你目前正在使用全局安装的开发环境,如 WAMP 或 MAMP,Homestead 不会与之冲突。
安装 Homestead
Homestead 是一个基于 Vagrant 的虚拟机。因此,在使用它之前,你需要安装两个工具:
-
VirtualBox (
www.virtualbox.org/wiki/Downloads) -
Vagrant (
www.vagrantup.com/downloads.html)
它们都有 Windows、Mac OS X 和 Linux 的安装程序,将引导你完成安装过程。要安装 Homestead,请按照以下步骤操作:
-
Homestead 最好的事情之一是它托管在 Packagist 上。这意味着你可以通过使用以下命令通过 Composer 安装它:
> $ composer global require "laravel/homestead=~2.0" -
下载完成后,你需要使用以下命令初始化虚拟机(如果你正在使用 Windows,可能需要首先将 Homestead 添加到你的路径中):
> $ homestead init -
这将创建一个配置文件,你可以编辑它来指向你的项目,以及创建你可能需要的任何数据库。要编辑此文件,你可以运行以下命令:
> $ homestead edit -
文件将在您的默认文本编辑器中打开。当它打开时,您会看到文件组织成几个部分。为了启动运行,最重要的两个部分是
folders部分和sites部分:-
folders:这指定了您希望在虚拟机上共享的目录。 -
sites:这允许您将域名映射到 Homestead VM 上的文件夹,类似于 Apache 虚拟主机。
![安装 Homestead]()
-
-
一旦您已配置共享文件夹和网站,您可以使用以下命令启动虚拟机:
> $ homestead up -
最后,您需要将虚拟机的 IP 地址添加到您计算机的 hosts 文件中。此文件的位置取决于您使用的平台。
-
在 Windows 上,它位于
C:\Windows\System32\Drivers\etc\hosts。 -
在 *nix 系统(如 Mac OS X 和 Linux)中,它位于
/etc/hosts。
-
-
通常,您需要是管理员才能编辑此文件。在 Mac OS X 和 Linux 上,您可以使用以下命令以提升权限打开文件:
> $ sudo open /etc/hosts -
您可能会被提示输入管理员密码。在您的 hosts 文件中,将以下行添加到末尾:
192.168.10.10 homesteap.app![安装 Homestead]()
-
现在,如果在网页浏览器中尝试访问
http://homestead.app,浏览器将尝试在指定 IP 地址的机器上找到网站,而不是尝试找到具有该域名的网站。在这种情况下,IP 地址将属于您的 Homestead VM,并为您配置文件中为该域名配置的网站提供服务。
注意
关于 Homestead 和高级配置的更多信息,您可以在 laravel.com/docs/master/homestead 查看官方文档。
Homestead 的日常使用
您可以使用各种命令与您的 Homestead 虚拟机交互。例如,如果虚拟机正在运行,您如何停止它?有两种方法。
第一种方法是使用 $ homestead suspend 命令。这将保存您虚拟机的当前状态,并允许您在稍后的时间点使用 $ homestead resume 命令恢复。或者,您也可以使用 $ homestead halt 命令,这将关闭虚拟机并丢弃内存中的所有内容。您可以将这些差异视为将虚拟机置于休眠状态或完全关闭。要恢复已停止的虚拟机,您可以再次运行 $ homestead up 命令。
您还可以通过命令行与虚拟机交互并运行命令。您可以通过 SSH 连接到 Homestead,就像连接外部服务器一样。不过,您不需要记住虚拟机的主机名和凭证,因为有一个方便的 $ homestead ssh 命令可以连接到机器,并将您置于一个准备就绪的命令提示符中。如果您使用 Windows,有各种工具可以执行 SSH 命令,例如 PuTTY。
添加额外的网站
Homestead 的一个好处是您可以在其上托管多个应用程序。这意味着您可以在其上运行任意数量的 Laravel 应用程序(受限于磁盘空间等约束)。为每个站点设置的过程是在主机机器和虚拟机之间映射目录,并配置 nginx 在请求特定域名时查看此目录。手动完成此操作意味着需要编辑各种配置文件,如果我们需要定期这样做,这会变得很繁琐。幸运的是,Homestead 提供了一个方便的脚本,可以轻松添加新站点。
您首先需要启动一个新的 SSH 会话,如之前所述。然后,运行以下脚本:
serve example.app /home/vagrant/Code/example.app/public
将 example.app 替换为您想要使用的域名。然后,在您的 hosts 文件中添加一行新内容,您就可以开始了!
连接到您的数据库
Homestead 运行一个 MySQL 实例,其中包含您配置的所有应用程序的数据。Laravel 通过端口转发公开 MySQL 服务器的端口,这意味着您可以使用数据库管理工具(如 Navicat、Sequel Pro 或 MySQL Workbench)从您的主机机器连接到它。要连接,您只需指定以下参数:
-
主机:127.0.0.1
-
端口:33060
-
用户名:homestead
-
密码:secret
连接后,您就可以浏览 Homestead 虚拟机上的所有数据库、表和数据,就像 MySQL 服务器安装在本机上一样。
Homestead 还附带了一个 PostgreSQL 数据库服务器。连接方式与 MySQL 数据库服务器相同,但您需要使用端口 54320。
创建一个新的 Laravel 应用程序
在设置好 Homestead 之后,您现在拥有了一个开发环境,可以构建 Laravel 应用程序,稍后可以轻松移除,而不会干扰您的机器。您一定迫不及待地想要开始使用 Laravel,那么,我们开始吧!
在下一章中,我们将从头到尾构建一个简单的 Laravel 应用程序。因此,现在,我们将在 Homestead 中准备这个项目:
-
首先,使用以下命令检查 Homestead 是否正在运行:
> $ homestead status -
如果状态不是正在运行,请使用
homestead reload命令将其启动。我们现在需要添加我们的站点。它将是一个可浏览的猫的资料数据库,让我们称它为 "Furbook",并给它一个虚构的域名furbook.com。 -
我们可以通过编辑配置文件并添加以下映射来在 Homestead 中设置此环境:
sites: - map: dev.furbook.com to: /home/vagrant/Code/furbook.com/public databases: - furbook -
运行
$ homestead reload,这将重新配置站点并创建一个新的、空的数据库。 -
现在我们已经准备好了工作空间,我们需要实际创建一个新的 Laravel 项目。为此,我们需要 SSH 进入我们的运行中的 VM。Homestead 使这一过程变得非常简单:
> $ homestead ssh -
这将创建一个新的 SSH 会话,并将我们登录到正在运行的 Homestead VM 实例。现在,我们可以运行一个 Composer 命令来创建一个新的 Laravel 项目,如下所示:
$ cd /home/vagrant/Code/furbook.com $ composer create-project laravel/laravel . --prefer-dist
这将创建一个新的 Laravel 骨架项目,并下载构成 Laravel 框架的所有库。
摘要
在本章中,我们开始使用命令行。我们安装了 Composer,并了解了依赖管理器如 Composer 如何通过在我们的项目中使用预构建的包来帮助开发(其中 Laravel 框架就是一个例子)。我们探讨了用于开发的虚拟机概念,并查看并安装了官方的 Laravel VM:Homestead。
下一章将开始有趣的部分!现在我们已经设置了一个完整的发展环境和创建了一个新的 Laravel 应用程序,我们将逐步了解创建一个完整的 Laravel 应用程序所涉及的不同步骤。
第三章。您的第一个应用程序
在了解了 Laravel 的约定、使用 Composer 处理依赖关系以及使用 Homestead 设置开发环境之后,您现在可以准备构建您的第一个应用程序了!
在本章中,您将以实际的方式使用前两章中介绍的概念,并学习如何执行以下操作:
-
规划您应用程序的 URL 和实体
-
在开始时解决常见问题
-
定义路由及其操作,以及模型及其关系
-
准备您的数据库并学习如何使用 Eloquent 与其交互
-
使用 Blade 模板语言创建分层布局
创建网络应用程序的第一步是确定和定义其需求。然后,一旦确定了主要功能,我们就推导出主要实体以及应用程序的 URL 结构。有一套明确的需求和 URL 对于其他任务(如测试)也是必不可少的;这将在本书的后续章节中介绍。
本章介绍了许多新概念。如果您对某些内容有疑问或不确定某个代码片段应该放在哪里,您可以下载应用程序的注释源代码,从 packtpub.com/support,这将帮助您跟上进度。
规划我们的应用程序
我们将构建一个可浏览的猫档案数据库。访客将能够为他们的猫创建页面并填写基本信息,如每只猫的名字、出生日期和品种。此应用程序将实现默认的创建-检索-更新-删除(CRUD)操作。我们还将创建一个概述页面,用户可以选择按品种过滤猫。所有安全、身份验证和权限功能都故意省略,因为它们将在后续章节中介绍。
实体、关系和属性
首先,我们需要定义我们应用程序的实体。从广义上讲,实体是应用程序应该存储数据的东西(人、地点或对象)。根据需求,我们可以提取以下实体和属性:
-
猫:它们有一个数字标识符、一个名字、一个出生日期和一个品种
-
品种:它们只有一个标识符和一个名字
这些信息将帮助我们定义存储实体、关系、属性以及模型的数据库模式,这些模型是代表我们数据库中对象的 PHP 类。
我们应用程序的地图
我们现在需要考虑我们应用程序的 URL 结构。拥有干净且具有表达力的 URL 有很多好处。在可用性方面,应用程序将更容易导航,对用户来说看起来也不会那么令人生畏(描述性的 URL 比冗长的查询字符串更具吸引力)。对于频繁的用户来说,单独的页面更容易记住或收藏,如果它们包含相关关键词,它们通常在搜索引擎结果中排名更高。
为了满足初始的一组需求,我们需要在我们的应用程序中添加以下路由。路由是一个 URL 和 HTTP 方法,应用程序将对其做出响应。
| 方法 | 路由 | 描述 |
|---|---|---|
GET |
/ |
索引 |
GET |
/cats |
概览页面 |
GET |
/cats/breeds/:name |
特定品种的概览页面 |
GET |
/cats/:id |
单个猫页面 |
GET |
/cats/create |
创建新猫页面的表单 |
POST |
/cats |
处理创建新猫页面 |
GET |
/cats/:id/edit |
编辑现有猫页面的表单 |
PUT |
/cats/:id |
处理猫页面的更新 |
GET |
/cats/:id/delete |
确认删除页面的表单 |
DELETE |
/cats/:id |
处理猫页面的删除 |
你很快就会了解到 Laravel 如何帮助我们把这个路由草图变成实际的代码。如果你在没有框架的情况下编写过 PHP 应用程序,你可以简要地反思一下你将如何实现这样的路由结构。为了增加一些视角,这是第二个到最后一个 URL 可能看起来像的传统 PHP 脚本(没有 URL 重写):/index.php?p=cats&id=1&_action=delete&confirm=true。
上述表格可以使用笔和纸、电子表格编辑器,甚至在你最喜欢的代码编辑器中使用 ASCII 字符来准备。在初始开发阶段,这个路由表是一个重要的原型工具,它让你首先考虑 URL,并帮助你迭代地定义和细化应用程序的结构。
如果你与 REST API 合作过,这种路由结构对你来说将很熟悉。在 RESTful 术语中,我们有一个cats资源,它响应不同的 HTTP 动词,并提供了一组额外的路由来显示必要的表单。
另一方面,如果你没有与 RESTful 站点合作过,PUT和DELETE HTTP 方法可能对你来说是新的。尽管网络浏览器不支持这些方法进行标准 HTTP 请求,但 Laravel 使用了一种其他框架(如 Rails)使用的技巧,通过在表单中添加一个_method输入字段来模拟这些方法。这样,它们可以通过标准的POST请求发送,然后被委派到应用程序中的正确路由或控制器方法。
注意,表单提交的端点都不是用 GET 方法处理的。这主要是因为它们有副作用;当用户在使用浏览器历史记录时,可能会意外地触发相同的操作多次。因此,当这些路由被调用时,它们永远不会向用户显示任何内容。相反,它们在完成操作后会将用户重定向(例如,DELETE /cats/:id 将将用户重定向到 GET /cats)。
启动应用程序
现在我们有了应用程序的蓝图,让我们卷起袖子开始编写一些代码。
首先打开一个新的终端窗口并启动 Homestead:
$ homestead ssh
导航到您映射到 Homestead 的目录(默认情况下为 ~/Code):
$ cd ~/Code
然后使用 Composer 创建一个新的 Laravel 项目,如下所示:
$ composer create-project laravel/laravel furbook.com --prefer-dist
$ cd furbook.com
一旦 Composer 完成下载 Laravel 并解决其依赖关系,您将拥有与第一章中展示的相同的目录结构。
设置应用程序命名空间
Laravel 中的应用程序是命名空间的。默认情况下,这只是 App——Laravel 很棒,但它仍然无法猜测您应用程序的名称!为了将其设置为更合适的内容,我们可以使用 Artisan 命令:
$ php artisan app:name Furbook
这将更新我们应用程序的命名空间为 Furbook。
编写第一个路由
让我们从编写应用程序在 app/Http/routes.php 中的前两个路由开始。此文件已经包含了一些注释以及一些示例路由。在添加以下路由之前,请删除现有路由(但保留开头的 <?php 声明):
Route::get('/', function() {
return 'All cats';
});
Route::get('cats/{id}', function($id) {
return sprintf('Cat #%s', $id);
});
get 方法的第一个参数是 URI 模式。当模式匹配时,第二个参数中的闭包函数会执行,并带有从模式中提取的任何参数。请注意,模式中的斜线前缀是可选的;然而,您不应该有任何尾随斜线。您可以通过打开您的网络浏览器并访问 http://dev.furbook.com/cats/123 来确保您的路由可以正常工作。
限制路由参数
在第二个路由的模式中,{id} 当前匹配任何字符串或数字。为了限制它只匹配数字,我们可以在路由上链式调用一个 where 方法,如下所示:
Route::get('cats/{id}', function($id) {
sprintf('Cat #%d', $id);
})->where('id', '[0-9]+');
where 方法接受两个参数:第一个是参数的名称,第二个是需要匹配的正则表达式模式。
如果您现在尝试访问一个无效的 URL,nginx(服务应用程序的服务器软件)将显示一个 404 未找到 错误页面。
处理 HTTP 异常
当您的应用程序发生错误时,Laravel 会抛出异常。这对于 HTTP 错误也是如此,因为 Laravel 会抛出适当的 HTTP 异常。通常,当发生 HTTP 错误时,您可能希望显示一个响应,告知用户出了什么问题。在 Laravel 5 中,这很容易,您只需要在 resources/views/errors 目录中创建一个以您希望显示的 HTTP 状态码命名的视图。
例如,如果你想显示404 未找到错误的视图,那么你只需要在resources/views/errors/404.blade.php创建一个视图。
你可以使用这种方法处理其他 HTTP 错误,例如403 禁止访问错误;只需在resources/views/errors/403.blade.php创建一个视图即可。
我们将在本章后面介绍视图。在此期间,你可以在en.wikipedia.org/wiki/List_of_HTTP_status_codes找到 HTTP 状态码列表。
执行重定向
你也可以使用路由中的redirect()辅助函数重定向访客。例如,如果我们想当访客第一次访问应用程序时被重定向到/cats,我们会编写以下代码行:
Route::get('/', function() {
return redirect('cats');
});
现在,我们可以为我们要重定向到的 URL 创建路由:
Route::get('cats', function() {
return 'All cats';
});
返回视图
你从路由返回的最常见的对象是View对象。视图从路由(或控制器操作)接收数据并将其注入到模板中,因此帮助你将应用程序中的业务逻辑和展示逻辑分离。
要添加第一个视图,只需在resources/views中创建一个名为about.php的文件,并将其内容添加到其中:
<h2>About this site</h2>
There are over <?php echo $number_of_cats; ?> cats on this site!
然后,使用带有变量$number_of_cats的view()辅助函数返回视图:
Route::get('about', function() {
return view('about')->with('number_of_cats', 9000);
});
最后,在浏览器中访问/about以查看渲染的视图。这个视图是用纯 PHP 编写的;然而,Laravel 附带了一个强大的模板语言,称为 Blade,将在本章后面介绍。
准备数据库
在我们能够扩展路由的功能之前,我们需要定义应用程序的模型,准备必要的数据库模式,并在数据库中填充一些初始数据。
Homestead 内置了 MySQL 服务器,因此我们可以使用 MySQL 作为我们的数据库;然而,在我们可以使用应用程序中的 MySQL 数据库之前,需要先进行一些配置。
第一步是打开我们应用程序的配置文件,该文件应该在用 Composer 创建应用程序时创建,文件名为.env。找到写着DB_DATABASE=homestead的那一行,并将其更改为DB_DATABASE=furbook。
我们还可以将数据库名添加到我们的 Homestead 配置文件中,这样数据库就会自动为我们创建。从命令行打开文件,使用以下命令:
$ homestead edit
在databases部分下,添加一行新内容:
databases:
- homestead
- furbook
保存文件,然后运行homestead provision命令创建数据库。
创建 Eloquent 模型
第一步也是最简单的一步是定义我们的应用程序将要交互的模型。在本章的开始,我们确定了两个主要实体:猫和品种。Laravel 附带 Eloquent,这是一个强大的 ORM,允许你定义这些实体,将它们映射到相应的数据库表中,并使用 PHP 方法而不是原始 SQL 与之交互。按照惯例,它们以单数形式编写;名为Cat的模型将映射到数据库中的cats表,一个假设的Mouse模型将映射到mice表。你也可以使用名为$table的属性手动定义数据库表名,以防你的表名不符合 Laravel 期望的约定:
protected $table = 'custom_table_name';
存储在app/Cat.php的Cat模型将与Breed模型有一个belongsTo关系,这在以下代码片段中定义:
<?php namespace Furbook;
use Illuminate\Database\Eloquent\Model;
class Cat extends Model {
protected $fillable = ['name','date_of_birth','breed_id'];
public function breed() {
return $this->belongsTo('Furbook\Breed');
}
}
$fillable 数组定义了 Laravel 可以通过批量赋值填充的字段列表,这是一种方便地将属性分配给模型的方法。按照惯例,Laravel 将用于查找相关模型的列必须被命名为breed_id在数据库中。Breed模型,app/Breed.php,通过以下方式定义了反向的hasMany关系:
<?php namespace Furbook;
use Illuminate\Database\Eloquent\Model;
class Breed extends Model {
public $timestamps = false;
public function cats(){
return $this->hasMany('Furbook\Cat');
}
}
默认情况下,Laravel 期望数据库表中有一个created_at和updated_at的时间戳字段。由于我们对于存储品种的时间戳不感兴趣,我们在模型中将$timestamps属性设置为false来禁用它们:
protected $timestamps = false;
这是我们目前模型中所需的所有代码。随着我们在本书中的进展,我们将发现 Eloquent 的许多其他特性;然而,在本章中,我们将主要使用两种方法:all() 和 find()。为了说明它们的目的,以下是它们生成的 SQL 查询:
Furbook\Breed::all() => SELECT * FROM breeds;
Furbook\Cat::find(1) => SELECT * FROM cats WHERE id = 1;
Eloquent 模型的属性可以通过->运算符检索:$cat->name。同样,相关模型的属性也可以通过以下方式访问:$cat->breed->name。在幕后,Eloquent 将执行必要的 SQL 连接。
构建数据库模式
现在我们已经定义了我们的模型,我们需要创建相应的数据库模式。得益于 Laravel 对迁移和强大的模式构建器的支持,你将不必编写任何 SQL 代码,你还可以在版本控制系统中跟踪任何模式更改。要创建你的第一个迁移,打开一个新的终端窗口并输入以下命令:
$ php artisan make:migration create_breeds_table --create=breeds
这将在database/migrations/创建一个新的迁移。如果你打开新创建的文件,你会找到 Laravel 为你生成的某些代码。迁移总是有一个up()和down()方法,该方法定义了向上迁移或向下迁移时的模式更改。向上迁移是修改数据库模式(即,在以后日期添加一个表),而向下迁移则是撤销该模式更改的过程。按照惯例,表和字段名称以 snake_case 形式编写。此外,表名以复数形式书写。
我们的breeds表迁移将看起来像这样:
public function up() {
Schema::create('breeds', function($table) {
$table->increments('id');
$table->string('name');
});
}
public function down() {
Schema::drop('breeds');
}
我们可以通过重复此过程来创建我们的cats表模式:
public function up() {
Schema::create('cats', function($table) {
$table->increments('id');
$table->string('name');
$table->date('date_of_birth');
$table->integer('breed_id')->unsigned()->nullable();
$table->foreign('breed_id')->references('id')->on('breeds');
});
}
public function down() {
Schema::drop('cats');
}
date()和string()方法在数据库中创建对应类型的字段(在这种情况下,为DATE和VARCHAR),increments()创建一个自动增长的INTEGER主键,而timestamps()则添加 Eloquent 默认期望的created_at和updated_at DATETIME字段。nullable()方法指定该列可以具有NULL值。
Laravel 提供了以下方法来定义迁移:
| 命令 | 描述 |
|---|
|
$table->bigIncrements('name');
| 它创建了一个自动增长的整数列 |
|---|
|
$table->bigInteger('name');
它创建了一个BIGINT列 |
|---|
|
$table->binary('name');
它创建了一个BLOB列 |
|---|
|
$table->boolean('active');
它创建了一个BOOLEAN列 |
|---|
|
$table->char('name', 8);
它创建了一个指定长度的CHAR列 |
|---|
|
$table->date('birthdate');
它创建了一个DATE列 |
|---|
|
$table->dateTime('created_at');
它创建了一个DATETIME列 |
|---|
|
$table->decimal('amount', 5, 2);
它创建了一个具有指定精度和小数的DECIMAL列 |
|---|
|
$table->double('column', 10, 5);
它创建了一个带有 10 位总数和 5 位小数点的DOUBLE列 |
|---|
|
$table->enum('gender', ['Female', 'Male']);
它创建了一个ENUM列 |
|---|
|
$table->float('amount');
它创建了一个FLOAT列 |
|---|
|
$table->increments('id');
| 它创建了一个自动增长的整数列 |
|---|
|
$table->integer('rating');
它创建了一个INTEGER列 |
|---|
|
$table->json('options');
它创建了一个JSON列 |
|---|
|
$table->longText('description');
它创建了一个LONGTEXT列 |
|---|
|
$table->mediumInteger('name');
它创建了一个MEDIUMINT列 |
|---|
|
$table->mediumText('name');
它创建了一个MEDIUMTEXT列 |
|---|
|
$table->morphs('taggable');
它创建了两个列:INTEGER taggable_id和STRING taggable_type |
|---|
|
$table->nullableTimestamps();
这与时间戳(下一节)类似,但允许NULL值 |
|---|
|
$table->rememberToken();
它添加了一个remember_token VARCHAR列 |
|---|
|
$table->tinyInteger('name');
它创建了一个TINYINT列 |
|---|
|
$table->softDeletes();
它添加了一个用于软删除的deleted_at列 |
|---|
|
$table->string('name');
它创建了一个VARCHAR列 |
|---|
|
$table->string('name', 255);
它创建了一个指定长度的VARCHAR列 |
|---|
|
$table->text('name');
它创建了一个TEXT列 |
|---|
|
$table->time('name');
它创建了一个TIME列 |
|---|
|
$table->timestamp('name');
它创建了一个TIMESTAMP列 |
|---|
|
$table->timestamps();
它创建了created_at和deleted_at列 |
|---|
我们还在cats迁移中创建了一个外键。这会将breed_id列的值链接到breeds表中的一个 ID。这样我们就不必反复指定品种名称。我们只需引用breeds表中的一条记录。如果该记录被更新,那么所有链接到它的cats也会被更新。
要运行这两个迁移,请输入以下命令:
$ php artisan migrate
当它第一次运行时,此命令还将创建一个migrations表,Laravel 将使用它来跟踪已运行的所有迁移。然后它将运行任何未完成的迁移。在随后的运行中,命令将使用migrations表来确定是否有需要运行的迁移文件,如果有,则运行它们。
我们在cats表迁移之前创建了我们的breeds表迁移,因为我们cats表中有一个外键。如果我们尝试首先创建cats表,它将失败,因为它引用的列尚不存在。
初始化数据库
而不是手动填充我们的数据库,我们可以使用 Laravel 提供的初始化助手。这次,没有 Artisan 命令来生成文件,我们只需要在database/seeds/中创建一个名为BreedsTableSeeder.php的新类。这个类扩展了 Laravel 的Seeder类,并定义了以下run()方法:
class BreedsTableSeeder extends Seeder {
public function run() {
DB::table('breeds')->insert([
['id' => 1, 'name' => "Domestic"],
['id' => 2, 'name' => "Persian"],
['id' => 3, 'name' => "Siamese"],
['id' => 4, 'name' => "Abyssinian"],
]);
}
}
你可以在run()方法中批量插入一个数组,也可以插入任意代码来从 CSV 或 JSON 文件加载数据。还有第三方库可以帮助你生成大量测试数据来填充你的数据库,例如出色的 Faker。
为了控制初始化器的执行顺序,Laravel 允许你在database/seeds/DatabaseSeeder.php中单独调用它们。在我们的情况下,因为我们只有一个初始化器,我们只需要写以下这一行:
$this->call('BreedsTableSeeder');
然后,我们可以通过调用它来初始化数据库,使用以下命令:
$ php artisan db:seed
初始化对于最初填充数据库很有用。如果我们重新运行种子命令,实际上会得到一个错误,因为我们正在为我们的记录定义主键;如果我们尝试重新初始化,数据库将触发重复主键错误。我们可以首先截断表,但这在生产环境中部署时将是危险的,因为它将删除任何用户贡献的记录,以及你的种子数据定义!
掌握 Blade
现在我们已经在数据库中有了一些信息,我们需要定义将要显示这些信息的模板。Blade 是 Laravel 的轻量级模板语言,其语法非常容易学习。以下是一些 Blade 如何减少按键次数并提高模板可读性的示例:
| 标准 PHP 语法 | Blade 语法 |
|---|
|
<?php echo $var; ?>
|
{!! $var !!}
|
|
<?php echo htmlentities($var); ?>
|
{{ $var }}
|
|
<?php if ($cond): ?>…<?php endif; ?>
|
@if ($cond) … @endif
|
如果你使用默认的双大括号表示法,那么变量会被转义。这是为了防止 XSS 漏洞(在第七章中详细解释,认证和安全)。如果你确实需要未转义的变量原始值,那么你可以使用单大括号,并在每侧内部使用两个感叹号。你应该只在信任变量包含的值时这样做。
Blade 还支持 PHP 的所有主要结构来创建循环和条件:@for、@foreach、@while、@if 和 @elseif,允许您在模板的任何地方避免打开和关闭 <?php 标签。
创建主视图
Blade 允许您通过允许模板嵌套和扩展来构建分层布局。以下代码片段是我们将要用于我们应用程序的 master 模板。我们将将其保存为 resources/views/layouts/master.blade.php。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Furbook</title>
<link rel="stylesheet" href="{{ asset('css/bootstrap.min.css') }}">
</head>
<body>
<div class="container">
<div class="page-header">
@yield('header')
</div>
@if (Session::has('success'))
<div class="alert alert-success">
{{ Session::get('success') }}
</div>
@endif
@yield('content')
</div>
</body>
</html>
Bootstrap CSS 框架被包含进来以加快应用程序界面的原型设计。您可以从 getbootstrap.com 下载它,并将压缩后的 CSS 文件放置在 public/css/ 目录下。为了确保其路径前缀设置正确,即使 Laravel 从子目录运行,我们使用 asset() 辅助函数。您可以通过访问 laravel.com/docs/helpers 来查看您可用的完整的 Blade 模板辅助函数列表。
为了通知用户某些操作的结果,我们在页眉和页面内容之间准备了一个通知区域。这个 flash data(换句话说,仅对下一个请求可用的会话数据)是通过 Session 对象传递和检索的。
@yield 指令充当子视图可以填充和覆盖的不同部分的占位符。为了了解子模板如何重用它们,我们将通过将其扩展名更改为 .blade.php 并扩展我们的 master 布局模板来重新创建 about 视图:
@extends('layouts.master')
@section('header')
<h2>About this site</h2>
@stop
@section('content')
<p>There are over {{ $number_of_cats }} cats on this site!</p>
@stop
@section ... @stop 指令定义了将要注入到主模板中的内容块。您可以在以下图表中看到这是如何完成的:

如果您现在在您的网络浏览器中重新打开 /about 路由,而不更改您之前的路由定义,您将看到新的视图。Laravel 的视图查找器将简单地使用新文件,并且由于它的名称以 .blade.php 结尾,所以将其视为 Blade 模板。
返回到路由
现在我们有一个可以扩展和重用的主布局模板,我们就可以开始在 app/Http/routes.php 中创建我们应用程序的各个路由,以及将显示应用程序数据的不同视图。
概览页面
这是将要显示所有猫的 index 页面,我们将使用 cats.index 视图来重用这个视图,因为这两个路由几乎相同。请注意,Laravel 预期您使用点表示法(cats.index 而不是 cats/index)来引用位于子目录中的视图:
Route::get('cats', function() {
$cats = Furbook\Cat::all();
return view('cats.index')->with('cats', $cats);
});
Route::get('cats/breeds/{name}', function($name) {
$breed = Furbook\Breed::with('cats')
->whereName($name)
->first();
return view('cats.index')
->with('breed', $breed)
->with('cats', $breed->cats);
});
这些路由的唯一新特性是稍微更高级的 Eloquent 查询。虽然我们已经知道第一个路由中的 all() 方法从 cats 表中加载所有条目,但第二个路由使用了一个更复杂的查询。with('cats') 方法将加载任何相关的 cat 模型。whereName 是一个动态方法,它创建一个 WHERE SQL 子句,这将转换为 WHERE name = $name。这个长表达式将是 where('name', '=', $name)。最后,我们使用 first() 方法获取第一个品种记录(和相关猫模型)。
保存于 cats/index.blade.php 的模板将看起来像这样:
@extends('layouts.master')
@section('header')
@if (isset($breed))
<a href="{{ url('/') }}">Back to the overview</a>
@endif
<h2>
All @if (isset($breed)){{ $breed->name }}@endif Cats
<a href="{{ url('cats/create') }}" class="btn btn-primary pull-right">
Add a new cat
</a>
</h2>
@stop
@section('content')
@foreach ($cats as $cat)
<div class="cat">
<a href="{{ url('cats/'.$cat->id) }}">
<strong>{{ $cat->name }}</strong> - {{ $cat->breed->name }}
</a>
</div>
@endforeach
@stop
通过使用 foreach 循环,视图遍历从路由接收到的猫列表。由于我们将使用此视图来显示索引页面(/cats)以及品种概述页面(/cats/breeds/{breed}),我们在两个地方使用了 @if 指令来条件性地显示更多信息。
显示猫的页面
下一个路由用于显示单个猫。要按其 ID 查找猫,我们使用 Eloquent 的 find() 方法:
Route::get('cats/{id}', function($id) {
$cat = Furbook\Cat::find($id);
return view('cats.show) ->with('cat', $cat);
});
路由模型绑定
路由模型绑定是将路由参数自动转换为模型实例的方法,因此我们不必手动检索模型。由于这是一个非常常见的模式,Laravel 为你提供了一个自动将模型绑定到路由的方法,因此可以使你的代码更短、更易于表达。要将 $cat 变量绑定到 Cat 模型,打开 app/Providers/RouteServiceProvider.php。修改 boot() 方法,使其看起来像这样:
public function boot(Router $router) {
parent::boot($router);
$router->model('cat', 'Furbook\Cat');
}
这允许你缩短路由,并将一个 Cat 对象传递给它:
Route::get('cats/{cat}', function(Furbook\Cat $cat) {
return view('cats.show')->with('cat', $cat);
});
视图 cats/show.blade.php 不包含任何新的指令。它只是显示猫的名字,并提供编辑或删除的链接。在 content 部分,如果设置了品种,我们返回其年龄和品种;这将在以下代码片段中显示:
@extends('layouts.master')
@section('header')
<a href="{{ url('/') }}">Back to overview</a>
<h2>
{{ $cat->name }}
</h2>
<a href="{{ url('cats/'.$cat->id.'/edit') }}">
<span class="glyphicon glyphicon-edit"></span>
Edit
</a>
<a href="{{ url('cats/'.$cat->id.'/delete') }}">
<span class="glyphicon glyphicon-trash"></span>
Delete
</a>
<p>Last edited: {{ $cat->updated_at->diffForHumans() }}</p>
@stop
@section('content')
<p>Date of Birth: {{ $cat->date_of_birth }}</p>
<p>
@if ($cat->breed)
Breed:
{{ link_to('cats/breeds/'.$cat->breed->name, $cat->breed->name) }}
@endif
</p>
@stop
添加、编辑和删除猫
下一个系列的路由和视图将用于创建、编辑和删除一个猫页面。
直到版本 5,Laravel 附带了一个用于创建常见 HTML 和表单元素的包。在 Laravel 5 应用程序中,我们需要将此包重新投入使用。我们通过 Composer 来完成此操作。
在 composer.json 的 require 部分添加以下代码:
"laravelcollective/html": "5.0.*"
然后运行 $ composer update。这将安装该包。接下来,我们需要注册服务提供者和外观。打开 config/app.php 并将以下内容添加到 $providers 数组中:
'Collective\Html\HtmlServiceProvider',
然后将以下两行添加到 $facades 数组中:
'Form' => 'Collective\Html\FormFacade',
'HTML' => 'Collective\Html\HtmlFacade',
这现在为我们提供了许多有用的方法,我们可以用这些方法在我们的模板中构建表单。
尽管 Blade 模板是分层的,但在视图中仍然可以包含其他模板,就像你可能习惯于使用 PHP 中的 include() 或 require() 函数一样。我们将使用此功能来共享创建和编辑模板所需的形式字段。
在 resources/views/partials/forms/cat.blade.php 中添加以下内容:
<div class="form-group">
{!! Form::label('name', 'Name') !!}
<div class="form-controls">
{!! Form::text('name', null, ['class' => 'form-control']) !!}
</div>
</div>
<div class="form-group">
{!! Form::label('date_of_birth', 'Date of Birth') !!}
<div class="form-controls">
{!! Form::date('date_of_birth', null, ['class' => 'form-control']) !!}
</div>
</div>
<div class="form-group">
{!! Form::label('breed_id', 'Breed') !!}
<div class="form-controls">
{!! Form::select('breed_id', $breeds, null, ['class' => 'form-control']) !!}
</div>
</div>
{!! Form::submit('Save Cat', ['class' => 'btn btn-primary']) !!}
Form::select()辅助函数构建一个包含不同选择的<select>下拉菜单。它期望将选择列表传递到一个多维数组中。我们不需要将此数组绑定到每个路由,可以使用 Laravel 的另一个特性——视图组合器,它允许你每次将一个变量绑定到特定的视图。
我们可以通过将其添加到应用程序的服务提供者中来初始化一个视图组合器。打开app/Providers/AppServiceProvider.php并修改boot()方法,使其看起来像这样:
public function boot(ViewFactory $view) {
$view->composer('partials.forms.cat', 'App\Http\Views\Composers\CatFormComposer');
}
我们还需要解析ViewFactory。在文件顶部,在命名空间声明之下,添加以下行:
use Illuminate\Contracts\View\Factory as ViewFactory;
现在我们需要创建实际的视图组合器类。我们已经指定了路径,所以让我们创建文件并添加以下代码到其中:
<?php namespace Furbook\Http\Views\Composers;
use Furbook\Breed;
use Illuminate\Contracts\View\View;
class CatFormComposer {
protected $breeds;
public function __construct(Breed $breeds) {
$this->breeds = $breeds;
}
public function compose(View $view) {
$view->with('breeds', $this->breeds->lists('name', 'id');
}
}
现在当调用partials.forms.cat模板部分时,视图组合器开始工作。当 Laravel 实例化它时,它会读取构造函数并自动注入指定类型的实例。在我们的视图组合器构造函数中,我们指定需要一个Breed模型实例,然后将该实例存储为类属性。
一旦视图组合器初始化完成,就会调用compose()方法。这是数据绑定到视图的实际发生之处。由于我们的模型是 Eloquent 模型,我们可以使用lists()方法来获取所有记录的关联数组,这正是我们为我们的select列表所需要的。第一个参数是用户将看到的值(在这个例子中是品种名称)和第二个参数是将在<option>标签中用作value属性的值(品种 ID)。
现在我们有一个部分表单,当请求时自动注入品种选项,我们可以继续构建创建、编辑和删除视图。
创建视图很简单:我们扩展主布局,打开表单,然后包含我们刚刚创建的部分。在resources/views/cats/create.blade.php中添加以下代码:
@extends('layouts.master')
@section('header')
<h2>Add a new cat</h2>
@stop
@section('content')
{!! Form::open(['url' => '/cats']) !!}
@include('partials.forms.cat')
{!! Form::close() !!}
@stop
编辑模板(resources/views/cats/edit.blade.php)看起来类似,除了几个小的改动:
@extends('layouts.master')
@section('header')
<h2>Edit a cat</h2>
@stop
@section('content')
{!! Form::model($cat, ['url' => '/cats/'.$cat->id],
'method' => 'put') !!}
@include('partials.forms.cat')
{!! Form::close() !!}
@stop
在部分视图中,我们没有包含开始和结束的表单标签,因为我们需要根据操作改变动作 URL 和方法。此外,在编辑模板中,我们使用表单模型绑定将传递给模板的Cat实例绑定到表单上。这会自动将表单字段的值填充为我们的Cat模型实例中的属性值。
现在我们有了视图,我们可以创建相应的路由:
Route::get('cats/create', function() {
return view('cats.create');
});
Route::post('cats', function() {
$cat = Furbook\Cat::create(Input::all());
return redirect('cats/'.$cat->id)
->withSuccess('Cat has been created.');
});
Route::get('cats/{cat}/edit', function(Furbook\Cat $cat) {
return view('cats.edit')->with('cat', $cat);
});
Route::put('cats/{cat}', function(Furbook\Cat $cat) {
$cat->update(Input::all());
return redirect('cats/'.$cat->id)
->withSuccess('Cat has been updated.');
});
Route::delete('cats/{cat}', function(Furbook\Cat $cat) {
$cat->delete();
return redirect('cats')
->withSuccess('Cat has been deleted.');
});
您可能已经注意到,在先前的路由中,正在使用一个新的方法withSuccess()与我们的重定向一起使用。这不是一个明确定义的方法;相反,它是 Laravel 中重载的一个例子。在重定向的上下文中,Laravel 查看以with开头的调用方法;它将后一部分分配给会话闪存数据。这包括将在下一个请求中可用的会话变量,以及下一个请求仅有的变量。这使得它非常适合像成功消息这样的单次使用数据,正如我们之前所看到的。
如果您回顾我们的主布局模板,您可以看到我们提供了检查具有键success的任何会话变量的方法;如果存在,我们只需在 Bootstrap 成功警报中显示它。
应用程序接收到的任何输入数据,您通常通过$_GET或$_POST变量访问,现在可以通过使用Input::get()方法检索。您还可以使用Input::all()检索所有输入数据的数组。在POST /cats和PUT /cats/{cat}路由中,我们分别使用 Eloquent 的create()和update()方法,并将Input::all()作为它们的参数。这仅因为我们之前在Cat模型中指定了可填充的字段。
现在我们有一个可以工作的应用程序,用户可以添加、编辑和删除猫。
从简单路由到强大的控制器
到目前为止,我们一直在创建基于闭包的路由。这对于快速原型设计应用程序来说很棒,在像Silex和Slim这样的微框架中很常见;然而,随着应用程序的增长,这种方法可能会变得繁琐和限制。定义当请求路由时要执行的逻辑的替代(且推荐)方法是使用控制器,即 MVC 中的 C。
控制器通常是一个类,包含一个或多个方法,也称为动作。您通常有一个路由映射到控制器动作。
考虑以下示例:
Route::get('user/{id}', ['middleware' => ['auth'], function($id) {
// Perform some operations
return 'Something';
}]);
要使用控制器实现相同的功能并从路由中删除业务逻辑,请在新文件app/Http/Controllers/UserController.php中创建:
<?php namespace Furbook\Http\Controllers;
class UserController extends Controller {
public function __construct()
{
$this->middleware('auth');
}
public function show($id)
{
$this->doSomething();
return 'Something';
}
protected function doSomething()
{
// Perform some operations
}
}
这种方法可以大大提高代码的可重用性和可测试性,特别是如果您的理论doSomething()方法在多个控制器动作中使用。您可以在隔离的情况下测试它一次,然后依赖它。当您进入更高级的主题,如依赖注入时,您甚至可以在实例化控制器时替换整个类,但这里我们不会涉及这一点。
最后,为了告诉 Laravel 使用哪个控制器动作,只需将路由声明重写如下:
Route::get('user/{id}', ['uses' => 'UserController@show']);
根控制器命名空间(App\Http\Controllers)自动添加到控制器类名之前,以避免为每个路由指定它的任务。
资源控制器
Laravel 通过资源控制器极大地简化了 REST API 的创建。由于它们遵循约定,因此从控制器中可以执行的操作集合是有限的。实际上,我们之前创建的所有路由都可以重写如下:
Route::resource('cat', 'CatController');
这将注册以下路由:
| 动词 | 路径 | 操作 | 路由名称 |
|---|---|---|---|
GET |
/cat |
列表 | cat.index |
GET |
/cat/create |
创建 | cat.create |
POST |
/cat |
存储 | cat.store |
GET |
/cat/{id} |
显示 | cat.show |
GET |
/cat/{id}/edit |
编辑 | cat.edit |
PUT/PATCH |
/cat/{id} |
更新 | cat.update |
DELETE |
/cat/{id} |
删除 | cat.destroy |
然后,在你的CatController类中,你将拥有所有不同的操作:index、create、show、edit等等。这些操作将被连接到正确的路由和 HTTP 动词。
你可以使用以下 Artisan 命令创建一个资源控制器存根:
$ php artisan make:controller CatController
为什么不尝试将基于闭包的路由操作重写为你的新CatController类呢?
摘要
我们在本章中涵盖了大量的内容。你学习了如何定义路由、准备应用程序的模型以及与之交互。此外,我们还对 Eloquent、Blade 以及其他 Laravel 中方便的辅助功能进行了简要了解,这些功能可以帮助创建表单和输入字段——所有这些都在 200 行代码以下完成!
在下一章中,你将了解更多关于 Laravel 强大 ORM,Eloquent 的信息,它允许你无需编写一行 SQL 语句即可执行强大的数据库查询。
第四章:Eloquent ORM
在上一章中,我们提到了与 Laravel 一起提供的 对象关系映射器(ORM)Eloquent。Eloquent 在我们的应用程序中充当模型层(MVC 中的 M)。由于它是大多数在 Laravel 中构建的应用程序的一个重要部分,我们将更详细地探讨 Eloquent。
在本章中,我们将涵盖以下主题:
-
读取和写入数据库数据
-
模型之间的关系
-
查询作用域
-
模型事件和观察者
-
集合
Eloquent 约定
Eloquent 有一些约定,遵循这些约定可以使你的生活更轻松。这种方法被称为 约定优于配置,这意味着如果你遵循这些约定,你将几乎不需要进行配置,事情就能“自然而然”地工作。
Eloquent 模型包含在一个单独的类中,并且是数据库表名称的单数“大驼峰式”版本。大驼峰式类似于驼峰式,但第一个字母也是大写。所以如果你有一个名为 cats 的数据库表,那么你的模型类将被称为 Cat。
在文件系统中没有固定的位置来放置你的 Eloquent 模型;你可以自由地按你的意愿组织它们。你可以使用 Artisan 命令来创建一个模型 模板(一个具有 Eloquent 模型基本结构的简单类)。命令如下:
$ php artisan app:model Cat
默认情况下,Artisan 将新的模型类放在 app 目录中。你可以自由地移动你的模型类并将它们存储在你想要的任何目录中,只需确保更新文件顶部的命名空间声明以反映其新位置。
我们的模式模板类看起来像这样:
<?php namespace App;
use Illuminate\Database\Eloquent\Model;
class Cat extends Model {
//
}
这将默认尝试使用名为 cats 的表。
我们的模式类扩展了 Eloquent 的基本 Model 类,其中包含我们在本章中将使用到的所有功能。在创建模型后,你应该做的第一件事是定义它映射到的数据库表。在我们的例子中,数据库表将被称为 cats:
class Cat extends Model {
protected $table = 'cats';
}
这是一个简单的、可工作的 Eloquent 模型,你现在可以使用它来从你的数据库表中获取记录。
获取数据
Eloquent 提供了多种方式来从数据库中获取记录,每种方式都有其合适的用例。你可以一次性获取所有记录;基于主键获取单个记录;基于条件获取记录;或者获取所有或过滤记录的分页列表。
要获取所有记录,我们可以使用名为 all 的方法:
use App\Cat;
$cats = Cat::all();
要通过主键获取记录,你可以使用 find 方法:
$cat = Cat::find(1);
除了 first 和 all 方法之外,还有 聚合 方法。这些方法允许你从数据库表中检索聚合值(而不是记录集):
use App\Order;
$orderCount = Order::count();
$maximumTotal = Order::max('amount');
$minimumTotal = Order::min('amount');
$averageTotal = Order::avg('amount');
$lifetimeSales = Order::sum('amount');
过滤记录
Eloquent 还提供了一套功能丰富的查询构建器,允许你在代码中构建查询,而无需编写任何 SQL 代码。这个抽象层使得在需要时更容易切换数据库平台。使用 Laravel,你只需要更新你的数据库配置,你的应用程序将继续像以前一样运行。
Laravel 的查询构建器提供了常见的 SQL 类型的指令方法,如 WHERE、ORDER 和 LIMIT;以及更高级的概念,如连接。例如,之前的 find 示例可以表达为——尽管是冗长的——:
$cat = Cat::where('id', '=', 1)->first();
这将检索第一个记录 WHERE 'id' = 1。当我们基于主键进行查询时,我们只期望一个记录,所以使用 first 方法。如果我们有一个更开放的 WHERE 子句,我们期望可能有多条记录,我们可以使用 get 方法,就像我们在第一个代码示例中所做的那样,它将只返回匹配该子句的记录。
条件也可以链式使用。这允许你通过添加条件来构建复杂的查询条件。考虑以下示例代码:
use App\User;
$users = User::where('gender', '=', 'Male')
->where('birth_date', '>', '1989-02-12')
->all();
这将找到所有在 1989 年 2 月 12 日之后出生的男性用户。除了手动指定日期外,我们还可以使用 Carbon,一个日期和时间库。以下是一个使用 Carbon 查找所有 21 岁以上用户的示例:
use App\User;
use Carbon\Carbon;
$users = User::where('birth_date', '<', Carbon::now()- >subYears(21))
->all();
注意
你可以在 Carbon 的官方 GitHub 仓库中找到更多关于 Carbon 及其可用函数的信息 github.com/briannesbitt/Carbon。Carbon 的常见方法也在 附录 An Arsenal of Tools 中进行了介绍。
除了通过 WHERE 条件过滤记录外,你还可以使用 take 方法通过范围限制记录数量:
$women = User::where('gender', '=', 'Female')->take(5)->get();
这将获取前五个女性用户。你还可以使用 skip 方法指定偏移量:
$women = User::where('gender', '=', 'Female')->take(5)->skip(10)->get();
在 SQL 中,这看起来类似于以下:
SELECT * FROM users WHERE gender = 'Female' OFFSET 10 LIMIT 5
查询也可以通过使用 orderBy 方法进行排序:
$rankings = Team::orderBy('rating', 'asc')->get();
这将对应于一个看起来像这样的 SQL 语句:
SELECT * FROM teams ORDER BY rating ASC
保存数据
显示数据的应用程序很棒,但它们并不非常交互式。当允许用户提交数据时,乐趣才开始,无论这些用户是受信任的贡献者通过内容管理系统添加内容,还是来自像维基百科这样的网站的一般用户。
当你通过 Eloquent 获取记录时,你可以按照以下方式访问其属性:
$cat = Cat::find(1);
print $cat->name;
我们也可以以同样的方式更新属性值:
$cat->name = 'Garfield';
这将在模型实例中设置值,但我们需要将更改持久化到数据库中。我们通过之后调用 save 方法来完成此操作:
$cat->name = 'Garfield';
$cat->save();
如果你有一个包含许多列的表格,那么手动为每个属性分配值将会变得非常繁琐。为此,Eloquent 允许你通过传递一个包含值的关联数组,并使用键来表示列名,来填充模型。你可以在创建或更新模型时填充模型:
$data = [
'name' => 'Garfield',
'birth_date' => '1978-06-19',
'breed_id' => 1,
];
$cat->create($data);
然而,这将抛出MassAssignmentException错误。
批量赋值
上述示例是一个批量赋值的例子。这就是模型属性被盲目地批量更新。如果前一个例子中的$data数组来自用户表单提交,那么它们可以更新数据库中相同表中的任何和所有值。
假设你有一个名为users的表,其中有一个名为is_admin的列,该列确定用户是否可以查看你的网站管理区域。还假设你的网站公共侧面的用户可以更新他们的个人资料。如果在表单提交期间,用户还包含一个名为is_admin且值为1的字段,那么这将更新数据库表中的列值,并授予他们访问你的超级秘密管理区域——这是一个重大的安全问题,这正是批量赋值保护所防止的。
为了标记可以通过批量赋值安全设置的列(例如name、birth_date等),我们需要通过提供一个名为$fillable的新属性来更新我们的 Eloquent 模型。这只是一个包含可以通过批量赋值安全设置的属性名称的数组:
class Cat extends Model {
protected $table = 'cats';
protected $fillable = [
'name',
'birth_date',
'breed_id',
];
}
现在,我们可以通过传递一个数据数组的方式创建和更新模型,就像之前一样,而不会遇到抛出MassAssignmentException异常的情况。
除了创建新记录之外,还有一些你可以使用的兄弟方法。有firstOrCreate,你可以传递一个数据数组——Eloquent 会首先尝试找到具有匹配值的模型。如果找不到匹配项,它将创建记录。
还有类似命名的firstOrNew方法。然而,它不会立即将记录保存到数据库中,而是会返回一个新的 Eloquent 实例,其中设置了属性值,允许你在手动保存之前先设置其他任何值。
使用这些方法的好时机是当允许用户通过使用第三方服务(如 Facebook 或 Twitter)登录时。这些服务通常会返回识别用户的个人信息,例如电子邮件地址,允许你检查数据库中是否存在匹配的用户。如果存在,你可以简单地让他们登录,否则你可以为他们创建一个新的用户账户。
删除数据
删除记录有两种方式。如果你已经从数据库中获取了一个模型实例,那么你可以在其上调用delete方法:
$cat = Cat::find(1);
$cat->delete();
或者,你可以调用destroy方法,指定你想要删除的记录的 ID,而不需要先获取这些记录:
Cat::destroy(1);
Cat::destroy(1, 2, 3, 4, 5);
软删除
默认情况下,Eloquent 将从你的数据库中硬删除记录。这意味着一旦删除,它就永远消失了。如果你需要保留已删除的数据(即,用于审计),那么你可以使用软删除。当删除模型时,记录将保留在数据库中,但会设置一个 deleted_at 时间戳,并且当查询数据库时,将不包括设置此时间戳的任何记录。
软删除可以轻松添加到你的 Eloquent 模型中。你只需要包含以下特性:
use Illuminate\Database\Eloquent\SoftDeletes;
class Cat extends Model {
use SoftDeletes;
protected $dates = ['deleted_at'];
}
我们还指定了 deleted_at 列应被视为日期列。这将产生一个 Carbon 实例,并允许我们在需要时对其进行操作或以各种格式显示。
你还需要确保将 deleted_at 列添加到你的表迁移中。以下是一个此类迁移的示例:
public function up() {
$table->softDeletes();
}
包含已删除模型的结果
如果你发现需要在查询数据库时包含已删除的记录(例如,在管理区域),那么你可以使用 withTrashed 查询作用域。查询作用域只是你可以用于链式调用的方法:
$cats = Cat::withTrashed()->get();
这将混合已删除的记录和非删除的记录。如果你发现你需要检索仅已删除的记录,那么你可以使用 onlyTrashed 查询作用域:
$cats = Cat::onlyTrashed()->get();
如果你发现你需要“恢复”一条记录,那么 SoftDeletes 特性为你提供了一个新的 restore 方法来撤销此操作:
$cat->restore();
最后,如果你发现你真的需要从数据库中删除一条记录,可以使用 forceDelete 方法。正如其名所示,一旦使用此方法删除记录,它就真的消失了。
$cat->forceDelete();
查询作用域
上一节向您介绍了查询作用域的概念。这是基于查询构建器,允许您根据需要构建条件。然而,如果你需要某些条件适用于每个请求?或者一个实际上是多个 WHERE 子句组合的单个条件?这就是查询作用域发挥作用的地方。
查询作用域允许你在模型中一次性定义这些条件,然后无需手动定义构成该条件的子句即可重用它们。例如,假设我们需要在我们的应用程序的多个地方找到年龄超过 21 岁的用户。我们可以将此表示为一个查询作用域:
class User extends Model {
public function scopeOver21($query)
{
$date = Carbon::now()->subYears(21);
return $query->where('birth_date', '<', $date);
}
}
多亏了流畅的查询构建器,我们现在可以这样使用:
$usersOver21 = User::over21()->get();
如您所见,查询作用域是那些以“scope”一词开头的方法,它们接受当前查询作为参数,以某种方式修改它,然后返回修改后的查询,以便在另一个子句中使用。这意味着你可以像使用任何其他查询表达式一样链式调用查询作用域:
$malesOver21 = User::male()->over21()->get();
除了这些简单的查询作用域之外,我们还可以创建更“动态”的查询作用域,这些作用域接受参数,并且可以将它们传递给作用域的条件。考虑以下示例代码:
class Cat extends Model {
public function scopeOfBreed($query, $breedId)
{
return $query->where('breed_id', '=', $breedId);
}
}
然后,我们可以按照以下方式找到特定品种的猫:
$tabbyCats = Cat::ofBreed(1)->get();
关联
当我们在第三章中构建我们的应用程序时,您的第一个应用程序,我们使用了关系。在我们的应用程序中,每只猫都属于一个特定的品种。然而,我们并没有在每只猫旁边存储品种名称,并且可能重复多次,而是创建了一个单独的breeds表,每只猫的品种都是一个引用该表中记录 ID 的值。这为我们提供了一个关于两种关系类型的例子:猫属于一个品种,但一个品种可以拥有许多猫。这被定义为一对多关系。
Eloquent 为每种关系类型都提供了良好的支持:
-
一对一
-
多对多
-
通过多个模型关联
-
多态关系
-
多对多多态关系
在这里,我们将通过每个示例来查看它们。
一对一
有时,你可能想将数据分散在多个表中以便于管理,或者因为它们代表了一个实体的两个不同部分。一个常见的例子是用户及其个人资料。你可能有一个包含关于该用户核心信息的users表,例如他们的姓名、账户电子邮件地址和密码散列;然而,如果是一个社交网络网站,他们可能还有一个包含更多信息的个人资料,例如他们最喜欢的颜色。这些信息可以存储在一个单独的profiles表中,其中外键代表个人资料所属的用户。
在你的模型中,这种关系将看起来像这样:
class User extends Model {
public function profile()
{
return $this->hasOne('App\Profile');
}
}
在Profile模型中,关系将看起来像这样:
class Profile extends Model {
public function user()
{
return $this->belongsTo('App\User');
}
}
当查询用户时,我们也可以单独访问他们的个人资料:
$profile = User::find(1)->profile;
使用在模型中定义它的方法的名称来访问关系。由于在User模型中我们在一个名为profile的方法中定义了关系,因此这是我们用来访问相关模型数据的属性名称。
多对多
多对多关系比一对一(一个模型恰好属于另一个模型)或一对多关系(许多模型可以属于另一个模型)更复杂。正如其名所示,许多模型可以属于许多其他模型。为了实现这一点,除了涉及两个表之外,还需要引入第三个表。这可能会相当难以理解,所以让我们通过一个例子来看看。
想象你正在构建一个权限系统来限制每个用户可以执行的操作。你不必为每个用户分配权限,而是有角色,每个用户根据他们被分配的角色获得一组子权限。在这个描述中,我们识别了两个实体:用户和角色。在这个场景中,一个用户可以有多个角色,一个角色可以属于多个用户。为了将角色映射到用户,我们创建了一个第三个表,称为连接表。Laravel 将这些表称为枢纽表,如果你之前使用过电子表格,你可能已经听说过这个术语。
默认情况下,Eloquent 预期连接表包含两个目标表的单一名称,按字母顺序排列并用下划线分隔。所以在我们这个场景中,这将是一个 role_user。该表本身只包含两列(除了主键之外)。这些列代表 Role 模型和它正在创建关系的 User 模型的外键。再次按照约定优于配置的原则,这些名称应该是小写、单数,并附加 _id,即 role_id 和 user_id。
我们在 User 和 Role 模型中使用 belongsToMany 方法定义了这种关系:
class User extends Model {
public function roles()
{
return $this->belongsToMany('App\Role');
}
}
class Role extends Model {
public function users()
{
return $this->belongsToMany('App\User');
}
}
现在我们可以找出用户被分配了哪些角色:
$roles = User::find(1)->roles;
我们还可以找出具有特定角色的所有用户:
$admins = Role::find(1)->users;
如果你需要向用户添加新角色,你可以通过使用 attach 方法来实现:
$user = User::find(1);
$user->roles()->attach($roleId);
当然,attach 的反义词是 detach:
$user->roles()->detach($roleId);
attach 和 detach 方法也接受数组,允许你在一次操作中添加/删除多个关系。
或者,你也可以使用 sync 方法。与 sync 的区别在于,只有当操作完成后,传递的 ID 才会出现在连接表中,而不是从现有关系中添加/删除它们。
$user->roles()->sync(1, 2, 3, 4, 5);
存储在枢纽表中的数据
除了在枢纽表中存储相关模型的两个主键之外,你还可以存储额外的数据。想象一下,在一个应用程序中,我们有用户和组。许多用户可以属于许多组,但用户也可以是组的调解员。为了指示哪些用户是某个组的调解员,我们可以在枢纽表上添加一个 is_moderator 列。为了指定在调用 attach 方法时应存储在枢纽表中的额外数据,我们可以在调用 attach 方法时指定第二个参数:
$user->groups()->attach(1, ['is_moderator' => true]);
当使用 sync 方法时,我们也可以使用相同的方法:
$user->groups()->sync([1 => ['is_moderator' => true]]);
一对多通过
当你需要从与你当前正在操作的直接相关的模型中获取数据时,有相关数据的情况下事情很简单;但如果你想要获取与你当前模型相隔两个跳的数据会发生什么呢?
考虑一个简单的电子商务网站。你可能有一个 Product 模型,一个 Order 模型,以及一个 OrderItem 模型,它属于产品也属于订单。你被分配的任务是找出包含特定产品的所有订单。如果 Product 与 Order 没有直接关联,你该如何做呢?幸运的是,在我们的场景中,它们有一个共同的关系——OrderItem 模型。
我们可以使用 "一对多通过" 关系通过中间的 OrderItem 模型来访问产品所属的订单。我们在 Product 模型中设置关系,如下所示:
class Product extends Model {
public function orders()
{
return $this->hasManyThrough('App\Order', 'App\OrderItem');
}
}
hasManyThrough 方法中的第一个参数是目标模型,第二个参数是我们通过它到达的中间模型。现在我们可以轻松地列出产品所属的订单:
$product = Product::find(1);
$orders = $product->orders;
多态关系
多态关系一开始很难理解;然而,一旦你理解了它们,它们就非常强大。它们允许一个模型在单个关联中属于多个其他模型。
多态关系的一个常见用例是创建一个图像库,然后允许你的其他模型通过链接到图像库表中的相关记录来包含图片。一个基本的Image模型看起来像这样:
class Image extends Model {
public function imageable()
{
return $this->morphTo();
}
}
morphTo方法使得这个模型成为多态。现在,在我们的其他模型中,我们可以创建一个与Image模型的关联,如下所示:
class Article extends Model {
public function images()
{
$this->morphMany('App\Image', 'imageable');
}
}
你现在可以通过你的Article模型获取任何相关的Image模型:
$article = Article::find(1);
foreach ($article->images as $image) {
// Do something with image
}
你可能会认为这和一对一关系没有区别,但当你从另一边看这个关系时,差异就变得明显了。当你检索一个Image实例时,如果你访问imageable关系,你会收到一个属于任何“拥有”该图片的模型的实例。这可能是一个Article、一个Product或者你应用中的另一个模型类型。Eloquent 通过不仅存储外键值,还存储模型类名来实现这一点。在我们的Image模型的情况下,列将是imageable_id和imageable_type。在创建迁移时,有一个方法可以创建这两个列:
$table->morphs('imageable');
多对多多态关系
我们将要查看的最后一个关系类型是多对多多态关系,这是最复杂的。继续我们的图像库示例,我们可以看到它有一个缺点,一个Image一次只能属于另一个模型。所以,虽然我们可以看到我们应用中所有模型上传的图片,但我们不能像在真正的图像库中那样重复使用上传的图片。这就是多对多多态关系可以发挥作用的地方。
保持我们的images和articles表,我们需要引入第三个表,imageables。关系数据从images表中移除,并放置在这个新表中,这个新表还有一个外键列指向Image主键。这三个列是:
-
image_id -
imageable_id -
imageable_type
使用这个模式,一个Image可以有多个关系。也就是说,图片可以在多个模型中重复使用,无论是多个Article记录,还是不同类型的模型。我们的更新后的模型类现在是这样的:
class Article extends Model {
public function images()
{
return $this->morphedByMany('App\Image', 'imageable');
}
}
Image模型也被更新,包含其每个关系的操作方法:
class Image extends Model {
public function articles()
{
return $this->morphToMany('App\Article', 'imageable');
}
public function products()
{
return $this->morphToMany('App\Product', 'imageable');
}
// And any other relations
}
你仍然可以像在正常的多态关系一样访问images关系。
模型事件
Eloquent 在不同的点触发许多事件,例如当模型正在保存或删除时。以下是一个 Eloquent 模型可以触发的方法列表:
-
creating -
created -
updating -
updated -
saving -
saved -
deleting -
deleted -
restoring -
restored
这些名称是自解释的。过去和现在分词之间的区别在于,例如creating这样的事件在模型创建之前触发,而created在模型创建之后触发。因此,如果你在creating事件的处理器中停止执行,记录将不会被保存;而如果你在created事件的处理器中停止执行,记录仍然会被持久化到数据库中。
注册事件监听器
它非常开放,关于在哪里注册模型事件的监听器。一个地方是在EventServiceProvider类中的boot方法内:
public function boot(DispatcherContract $events)
{
parent::boot($events);
User::creating(function($user)
{
// Do something
});
}
确保在文件顶部导入DispatcherContract命名空间:
use Illuminate\Contracts\Bus\Dispatcher as DispatcherContract;
优雅的模型为每个事件提供了一个方法,你可以传递一个匿名函数。这个匿名函数接收一个模型的实例,然后你可以对其执行操作。所以如果你想在每次Article模型保存时创建一个 URL 友好的文章标题表示,你可以通过监听保存事件来实现这一点:
Article::saving(function($article)
{
$article->slug = Str::slug($article->headline);
});
模型观察者
随着你向EventServiceProvider类添加更多的模型事件处理器,你可能会发现它变得越来越拥挤且难以维护。这就是处理模型事件的替代方案——模型观察者。
模型观察者是独立类,你可以将其附加到模型上,并实现你需要的任何事件的方法。所以我们的 slug 创建函数可以被重构为模型观察者,如下所示:
use Illuminate\Support\Str;
class ArticleObserver {
public function saving($article)
{
$article->slug = Str::slug($article->headline);
}
}
我们可以在EventServiceProvider类中注册我们的观察者:
public function boot(DispatcherContract $events)
{
parent::boot($events);
Article::observe(new ArticleObserver);
}
集合
从历史上看,其他带有自己 ORM 和查询构建器的框架返回的结果集要么是多维数组,要么是普通的 PHP 对象(POPOs)。Eloquent 从其他更成熟的 ORM 中汲取灵感,而不是返回结果集为集合对象的实例。
集合对象非常强大,因为它不仅包含从数据库返回的数据,还包含许多辅助方法,允许你在向用户显示之前操纵这些数据。
检查集合中是否存在键
如果你需要确定某个特定的键是否存在于集合中,你可以使用contains方法:
$users = User::all();
if ($users->contains($userId))
{
// Do something
}
当查询模型时,任何关系也会作为子集合返回,允许你在关系上使用完全相同的方法:
$user = User::find(1);
if ($user->roles->contains($roleId))
{
// Do something
}
默认情况下,模型返回Illuminate\Database\Eloquent\Collection的实例。然而,这可以被覆盖以使用不同的类。如果我们想向集合添加额外的功能,这会很有用。
假设有一个角色集合,我们想要确定管理员是否是这些角色之一。如果我们想象管理员角色有一个主键值为1,我们可以创建一个新的方法,如下所示:
<?php namespace App;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
class RoleCollection extends EloquentCollection {
public function containsAdmin()
{
return $this->contains(1);
}
}
第二部分是告诉Role模型使用我们新的集合:
use App\RoleCollection;
class Role extends Model {
public function newCollection(array $models = array())
{
return new RoleCollection($models);
}
}
而不是实例化默认的 Eloquent 集合,它将创建我们RoleCollection类的新实例,并用查询结果填充它。这意味着每次我们请求角色时,我们都可以使用我们新的containsAdmin方法:
$user = User::find(1);
if ($user->roles->containsAdmin())
{
// Let user administrate something
}
else
{
// User does not have administrator role
}
Eloquent 集合还提供了许多其他有用的功能,允许您操作、过滤和迭代项目。您可以在laravel.com/docs/master/eloquent#collections查看这些方法的更多信息。
摘要
尽管我们在本章中已经涵盖了大量内容,但 Eloquent 功能丰富,遗憾的是,没有足够的空间来介绍其每一个特性。不过,我们已经涵盖了 Eloquent 最重要的方面,这将为您在保存和检索数据、在模型之间创建不同复杂度的关系以及处理模型生命周期中引发的各种事件方面打下坚实的基础。
下一章我们将继续学习如何测试我们的应用程序,以确保其尽可能的坚不可摧。
第五章:测试 – 比您想象的要简单
测试是 PHP 开发中经常被忽视的部分。与 Java 和 Ruby 等语言相比,在这些语言中测试被深深植根于开发者的工作流程中,PHP 在这方面落后了。这主要是因为简单的 PHP 应用程序往往耦合紧密,因此难以测试。然而,得益于标准化和模块化努力以及鼓励关注点分离的框架,PHP 测试变得更加容易接近,并且对其的态度正在慢慢改变。
Laravel 5 是一个从头开始构建以促进测试的框架。它包含所有必要的文件以开始,以及不同的辅助工具来测试您的应用程序,从而帮助初学者克服一些最大的障碍。
在本章中,我们将展示 Laravel 如何使您轻松开始测试,而无需强迫您采取测试优先的方法,或使您追求完整的测试覆盖率。在这个轻松的测试介绍中,我们将探讨以下主题:
-
为您的应用程序编写测试的优势
-
如何准备您的测试
-
Laravel 培养的软件设计模式
-
如何使用 Mockery 单独测试对象
-
方便测试的内置功能和辅助工具
测试的好处
如果您之前没有为您的 Web 应用程序编写过测试,测试的优势可能并不总是对您显而易见。毕竟,准备和编写测试需要大量的时间投入,对于短期原型或黑客马拉松项目来说,它们甚至可能看起来是完全浪费时间。然而,在几乎所有其他情况下,当您的项目可能变得复杂,或者当您与其他开发者合作时,测试有可能为您和其他人节省大量时间和头疼。
测试也会对您的流程带来一些变化。在开发阶段,您将不再需要在代码编辑器和 Web 浏览器之间来回切换。相反,如果您使用的是支持测试运行器的文本编辑器或 IDE,您可以将测试运行器绑定到一个键盘快捷键上。
一旦您证明某个功能部分工作正常,您将有一种快速确保它在以后日期代码更改后仍然按预期工作的方式。此外,它迫使您明确且无歧义地定义应用程序的预期行为,因此可以补充或替代大量文档。这特别有助于新开发者开始参与项目合作,如果您有一段时间没有接触过项目,这也对您自己特别有帮助。
测试的结构
你的应用程序测试将位于tests/目录中。在这个目录中,你将找到一个TestCase.php中的基本测试用例,它负责在测试环境中启动应用程序。这个类扩展了 Laravel 的主要TestCase类,该类反过来又扩展了PHPUnit_Framework_TestCase类,以及我们将在本章后面讨论的许多有用的测试方法。你所有的测试都将扩展这个第一个TestCase类,并定义一个或多个旨在测试应用程序一个或多个功能的函数。
在每个测试中,我们通常执行以下三个不同的任务:
-
我们安排或初始化一些数据。
-
我们执行一个函数来对这份数据进行操作。
-
我们断言或验证输出是否与预期相符。
假设我们有一个以下辅助类:
class Helper {public static function sum($arr) { return array_sum($arr); }}
一个示例测试用例,HelperTest.php,它展示了前面的三个步骤,看起来是这样的:
class HelperTest extends PHPUnit_Framework_TestCase {
public function testSum() {
$data = [1,2,3]; // 1) Arrange
$result = Helper::sum($data); // 2) Act
$this->assertEquals(6, $result); // 3) Assert
}
public function testSomethingElse() {
// ...
}
}
当执行前面的代码片段时,PHPUnit 将运行测试用例中的每个方法,并跟踪测试失败或通过的数量。如果你的系统上安装了 PHPUnit,你可以使用以下命令运行此测试:
$ phpunit --colors HelperTest.php
这将产生以下输出:

提示
大多数代码编辑器还提供了通过按快捷键直接在编辑器中运行测试的方法。此类编辑器的例子包括PhpStorm。甚至可以在每次提交之前或在你将代码部署到远程服务器之前自动运行它们。
使用 PHPUnit 进行单元测试
测试的一个积极影响是它迫使你将代码拆分成可管理的依赖项,这样你就可以单独测试它们。对这些单个类和方法的测试被称为单元测试。由于它依赖于 PHPUnit 测试框架,该框架已经提供了大量设置测试套件的工具,因此 Laravel 不需要为此类型测试提供任何额外的辅助函数。
了解任何框架的绝佳方式,同时了解它如何被测试的不同方式,是查看其作者如何为其编写测试。因此,我们的下一个示例将直接来自 Laravel 的测试套件,该套件位于vendor/laravel/framework/tests/。
使用断言定义你期望的内容
断言是单元测试的基本组成部分。简单来说,它们用于比较函数的预期输出与其实际输出。
要了解断言是如何工作的,我们将检查Str::is()辅助函数的测试,该函数用于检查给定的字符串是否与给定的模式匹配。
以下测试可以在Support/SupportStrTest.php文件的底部找到:
use Illuminate\Support\Str;
class SupportStrTest extends PHPUnit_Framework_TestCase {
// ...
public function testIs()
{
$this->assertTrue(Str::is('/', '/'));
$this->assertFalse(Str::is('/', ' /'));
$this->assertFalse(Str::is('/', '/a'));
$this->assertTrue(Str::is('foo/*', 'foo/bar/baz'));
$this->assertTrue(Str::is('*/foo', 'blah/baz/foo'));
}
}
前面的测试执行了五个断言,以测试当使用不同的参数调用方法时,它是否确实返回了预期的值。
PHPUnit 提供了许多其他断言方法,例如,你可以使用 assertGreaterThan() 测试数值,使用 assertEquals() 测试相等性,使用 assertInstanceOf() 测试类型,或者使用 assertArrayHasKey() 测试存在性。虽然还有更多可能的断言,但这些都是你可能会最频繁使用的。总而言之,PHPUnit 提供了大约 40 种不同的断言方法,所有这些方法都在官方文档中有详细描述,请参阅 phpunit.de/manual/。
准备场景和清理对象
如果你需要在每个测试方法之前运行一个函数来设置一些测试数据或减少代码重复,你可以使用 setUp() 方法。另一方面,如果你需要在每个测试之后运行一些代码来清除测试中创建的任何对象,你可以在 tearDown() 方法中定义它。一个很好的例子是在 setUp() 方法中插入到数据库中的任何记录。
预期异常
你也可以通过在函数上装饰 @expectedException DocBlock 来测试异常,就像 Laravel 在 Database/ DatabaseEloquentModelTest.php 中所做的那样:
/**
* @expectedException Illuminate\Database\Eloquent\MassAssignmentException
*/
public function testGlobalGuarded()
{
$model = new EloquentModelStub;
$model->guard(['*']);
$model->fill(['name' => 'foo', 'age' => 'bar', 'votes' => 'baz']);
}
在这个测试函数中,没有断言,但预期当它执行时将抛出异常。同时请注意 EloquentModelStub 对象的使用。存根创建了一个对象的实例,该对象提供了或模拟了我们类需要的某些方法——在这种情况下,是一个我们可以调用 guard() 和 fill() 方法的 Eloquent 模型。如果你查看测试中更下面的存根定义,你会看到它实际上并没有与数据库交互,而是提供了预定义的响应。
隔离测试相互依赖的类
除了我们在上一节中查看的存根之外,还有另一种方法可以用来隔离测试一个或多个相互依赖的类。这是通过使用 模拟 来实现的。在 Laravel 中,使用 Mockery 库创建模拟,它们有助于定义测试期间应该调用哪些方法,它们应该接收哪些参数,以及它们的返回值。Laravel 在其自己的测试中大量依赖模拟。一个例子可以在 AuthEloquentUserProviderTest 类中找到,其中 Hasher 类被模拟:
use Mockery as M;
class AuthEloquentUserProviderTest extends PHPUnit_Framework_TestCase {
public function tearDown(){
M::close();
}
// ...
public function getProviderMock() {
$hasher = m::mock('Illuminate\Contracts\Hashing\Hasher');
return $this->getMock('Illuminate\Auth\EloquentUserProvider', array('createModel'), array($hasher, 'foo'));
}
}
与存根不同,模拟允许我们定义需要调用哪些方法,它们应该被调用多少次,它们应该接收哪些参数,以及它们应该返回哪些参数。如果任何这些前提条件没有得到满足,测试将失败。
为了确保我们没有持久化的模拟对象实例,它可能会干扰未来的测试,Mockery 提供了一个 close() 方法,需要在每次测试后执行。多亏了这个模拟,我们可以在完全隔离的情况下测试这个类。
端到端测试
当我们确信所有相互依赖的类都按预期工作后,我们就可以进行另一种类型的测试。这包括模拟用户通过网页浏览器进行的交互。例如,这个用户会访问特定的 URL,执行某些操作,并期望从应用程序看到某种形式的反馈。
这可能是最直接的测试类型,因为它模仿了每次代码更改后刷新浏览器时手动执行的测试类型。当你开始时,只执行这种类型的测试是完全正常的。然而,你必须记住,如果发生任何错误,你仍然需要深入到代码中找到导致错误的精确组件。
测试 - 包含电池
当你使用 Laravel 开始一个新项目时,你将在项目的根目录中提供一个配置文件phpunit.xml,其中包含 PHPUnit 的合理默认设置,以及一个名为tests/的目录,你可以在其中保存你的测试。此目录甚至包含一个示例测试,你可以将其用作起点。
在这些设置到位后,从我们项目的根目录,我们只需要 SSH 到我们的 Homestead 虚拟机并运行以下命令:
$ phpunit
此命令将读取 XML 配置文件并运行我们的测试。如果在这一阶段,你收到一个错误消息告诉你 PHPUnit 找不到,你可能需要将phpunit命令添加到你的PATH变量中,或者使用 Composer 安装它。
Laravel 应用程序在composer.json文件的autoload-dev块中已经声明了 PHPUnit。在运行composer update之后,你将能够使用以下命令调用 PHPUnit:
$ vendor/bin/phpunit
框架断言
现在我们已经了解了两种主要的测试类型并且已经安装了 PHPUnit,我们将为我们在第三章中开发的程序编写一些测试,你的第一个应用程序。
这个第一次测试将验证访问者在首次访问我们的网站时是否被重定向到正确的页面:
public function testHomePageRedirection() {
$this->call('GET', '/');
$this->assertRedirectedTo('cats');
}
在这里,我们使用了call()方法来模拟对应用程序的请求,该请求通过 Laravel 的 HTTP 内核执行请求。然后,我们使用了 Laravel 提供的断言方法之一来确保响应是重定向到新位置。如果你现在运行phpunit命令,你应该看到以下输出:
OK (1 test, 2 assertions)
接下来,我们可以尝试编写一个测试来确保创建表单对未登录的用户不可用;这在上面的代码片段中显示:
public function testGuestIsRedirected() {
$this->call('GET', '/cats/create');
$this->assertRedirectedTo('login');
}'
模拟用户
有时候,你可能希望以注册用户身份运行测试。这可以通过使用be()方法和传递一个User实例或你使用的任何 Eloquent 模型到其中,以及 Laravel 的认证类来实现:
public function testLoggedInUserCanCreateCat() {
$user = new App\User([
'name' => 'John Doe',
'is_admin' => false,
]);
$this->be($user);
$this->call('GET', '/cats/create');
$this->assertResponseOk();
}
带数据库的测试
虽然一些开发者会建议不要编写针对数据库的测试,但通常这是一种简单而有效的方法,以确保所有组件按预期协同工作。然而,这应该在每个单独的单元测试之后才进行。我们也不应忘记 Laravel 支持迁移和数据填充;换句话说,它拥有从头开始重新创建相同数据结构所需的所有工具,在每次测试之前。
要编写依赖于数据库的测试,我们需要在我们的测试中覆盖setUp()方法,每次运行测试时都要迁移和填充数据库。运行父setUp()方法也很重要,否则测试用例将无法正确启动:
public function setUp(){
parent::setUp();
Artisan::call('migrate');
$this->seed();
}
然后,我们需要在config/database.php中配置测试数据库连接;如果应用程序不包含任何特定于数据库的查询,我们可以通过设置:memory:而不是数据库文件的路径来使用 SQLite 的内存功能。以下配置也有可能加快我们的测试速度:
'sqlite' => [
'driver' => 'sqlite',
'database' => ':memory:',
],
最后,由于我们将要测试编辑和删除功能,我们至少需要在数据库的cats表中有一行,因此我们准备了一个填充器,将插入一个强制id值为1的猫:
class CatsTableSeeder extends Seeder {
public function run(){
Cat::create(['id' => 1, 'name' => '''Tom', 'user_id' => 1]);
}
}
一旦完成,我们就可以按照以下方式测试删除功能:
public function testOwnerCanDeleteCat() {
$user = new App\User(['id' => 1, 'name' => 'User #1',
'is_admin' => false]);
$this->be($user);
$this->call('DELETE', '/cats/1');
$this->assertRedirectedTo('/cats');
$this->assertSessionHas('message');
}
注意,这次我们不需要启用过滤器,因为权限是由User模型中的方法检查的。由于数据库在每次测试后都会被清除并重新填充,所以我们不需要担心之前的测试删除了那个特定的猫。我们还可以编写一个测试来确保非管理员用户无法编辑其他人的猫资料:
public function testNonAdminCannotEditCat() {
$user = new App\User(['id' => 2, 'name' => 'User #2',
'is_admin' => false]);
$this->be($user);
$this->call('DELETE', '/cats/1');
$this->assertRedirectedTo('/cats/1');
$this->assertSessionHas('error');
}
检查渲染的视图
由于 Laravel 自带了 Symfony 的DomCrawler和CssSelector组件,因此可以检查渲染视图的内容。通过通过测试客户端实例发出请求$this->client->request(),可以使用 CSS 查询过滤其内容,如下所示:
public function testAdminCanEditCat() {
$user = new App\User(['id' => 3, 'name' => 'Admin',
'is_admin' => true));
$this->be($user);
$newName = 'Berlioz';
$this->call('PUT', '/cats/1', ['name' => $newName]);
$crawler = $this->client->request('GET', '/cats/1');
$this->assertCount(1, $crawler->filter('h2:contains("'.$newName.'")'));
}
DomCrawler组件的完整文档可以在symfony.com/doc/current/components/dom_crawler.html找到。如果你已经熟悉 jQuery,其语法将对你来说很熟悉。
摘要
虽然测试背后的主要思想很容易理解,但它们的实现往往可能成为障碍,尤其是在使用新框架时。然而,在阅读本章之后,你应该对如何测试你的 Laravel 应用程序有一个很好的概述。本章中介绍的技术将使你能够编写更健壮和面向未来的应用程序。
在下一章中,我们将探讨 Artisan 提供的可能性,它是 Laravel 的命令行实用工具。
第六章:名为 Artisan 的命令行伴侣
在最后几章中,我们使用了 Artisan 来执行各种任务,例如运行数据库迁移。然而,正如我们将在本章中看到的,Laravel 的命令行工具具有更强大的功能,可以用来运行和自动化各种任务。在接下来的页面中,你将学习 Artisan 如何帮助你:
-
检查和交互你的应用程序
-
提高应用程序的整体性能
-
编写你自己的命令
在这次 Artisan 功能之旅结束时,你将了解它如何成为你项目中的不可或缺的伴侣。
跟踪最新变化
Laravel 不断添加新功能。如果你首次安装它已经过去几天了,尝试从你的终端运行一个 composer update 命令。你应该能看到 Laravel 及其依赖的最新版本正在下载。既然你已经在终端里了,了解最新功能只需一个命令:
$ php artisan changes
这可以让你免于上网查找变更日志或阅读 GitHub 上漫长的提交历史。它还可以帮助你了解你之前不知道的功能。你还可以通过输入以下命令来找出你正在运行的 Laravel 版本:
$ php artisan --version
Laravel Framework version 5.0.16
所有 Artisan 命令都必须从你的项目根目录运行。
注意
通过使用 Artisan Anywhere 这样的简短脚本,它可以在 github.com/antonioribeiro/artisan-anywhere 找到,你也可以从项目的任何子目录中运行 Artisan。
检查和交互你的应用程序
使用 route:list 命令,你可以一目了然地看到你的应用程序将响应哪些 URL,它们的名称是什么,以及是否有任何中间件被注册来处理请求。这可能是熟悉别人构建的 Laravel 应用程序的最快方式。
要显示包含所有路由的表格,你只需输入以下命令:
$ php artisan route:list
例如,以下是我们构建在 第三章,你的第一个应用程序 中应用程序的样子:

注意
在某些应用程序中,你可能会看到 /{v1}/{v2}/{v3}/{v4}/{v5} 被附加到特定的路由上。这是因为开发者已经注册了一个具有隐式路由的控制器,Laravel 将尝试匹配并将最多五个参数传递给控制器。
玩转内部结构
在开发应用程序时,你有时需要运行短期的、一次性的命令来检查数据库的内容,向其中插入一些数据,或检查 Eloquent 查询的语法和结果。你可以这样做的一种方法是通过创建一个临时路由,该路由使用闭包来触发这些操作。然而,这不太实用,因为它要求你在代码编辑器和你的网络浏览器之间来回切换。
为了使这些小更改更容易,Artisan 提供了一个名为 tinker 的命令,该命令启动应用程序并允许你与之交互。只需输入以下命令:
$ php artisan tinker
这将启动一个类似于运行 php -a 命令时获得的 Read-Eval-Print Loop (REPL),该命令启动一个交互式 shell。在这个 REPL 中,你可以在应用程序的上下文中输入 PHP 命令并立即看到它们的输出:
> $cat = 'Garfield';
> App\Cat::create(['name' => $cat,'date_of_birth' => new DateTime]);
> echo App\Cat::whereName($cat)->get();
[{"id":"4","name":"Garfield 2","date_of_birth":…}]
> dd(Config::get('database.default'));
Laravel 5 版本利用了 PsySH,这是一个针对 PHP 的 REPL,它提供了一个更强大的 shell,支持键盘快捷键和历史记录。
关闭引擎
不论是因为你正在升级数据库还是等待将关键错误的修复推送到生产环境,你可能希望手动暂停你的应用程序以避免向访客提供损坏的页面。你可以通过输入以下命令来完成此操作:
$ php artisan down
这将使你的应用程序进入 维护 模式。你可以通过编辑位于 resources/views/errors/503.blade.php 的模板文件来确定当用户以这种方式访问你的应用程序时显示什么内容(因为维护模式向客户端发送 503 Service Unavailable 的 HTTP 状态码)。要退出维护模式,只需运行以下命令:
$ php artisan up
微调你的应用程序
对于每个传入的请求,Laravel 都必须加载许多不同的类,这可能会减慢你的应用程序,尤其是如果你没有使用 PHP 加速器,如 APC、eAccelerator 或 XCache。为了减少磁盘 I/O 并从每个请求中节省宝贵的时间,你可以运行以下命令:
$ php artisan optimize
这将修剪并合并许多常见的类到一个位于 storage/framework/compiled.php 内的文件中。optimize 命令是你可以,例如,包含在部署脚本中的内容。
默认情况下,如果 app.debug 设置为 true,Laravel 不会编译你的类。你可以通过在命令中添加 --force 标志来覆盖此设置,但请注意,这将使错误信息更难以阅读。
缓存路由
除了缓存类映射以改善应用程序的响应时间外,你还可以缓存应用程序的路由。这是你可以包含在你的部署过程中的另一项内容。命令?只需输入以下内容:
$ php artisan route:cache
缓存路由的优势是,由于路由已经被预编译,你的应用程序会稍微快一点。然而,由于路由过程现在引用一个缓存文件,任何新添加的路由都不会被解析。你需要通过再次运行 route:cache 命令来重新缓存它们。因此,这不适合开发阶段,因为路由可能会频繁更改。
生成器
Laravel 5 随带各种命令来生成不同类型的文件。在整个书中,我们已经使用了一些(即用于生成新的迁移文件),但还有其他一些。如果你在 make 命名空间下运行 $ php artisan list,你将找到以下条目:
-
make:command -
make:console -
make:controller -
make:event -
make:middleware -
make:migration -
make:model -
make:provider -
make:request
这些命令在你的 Laravel 应用程序适当的位置创建一个存根文件,其中包含你开始使用的样板代码。这节省了键位,从头开始创建这些文件。所有这些命令都需要指定一个名称,如下所示:
$ php artisan make:model Cat
这将在 app/Cat.php 创建一个名为 Cat 的 Eloquent 模型类,以及一个相应的迁移来创建 cats 表。如果你在创建模型时不需要创建迁移(例如,如果表已经存在),则可以按照以下方式传递 --no-migration 选项:
$ php artisan make:model Cat --no-migration
一个新的模型类看起来像这样:
<?php namespace App;
use Illuminate\Database\Eloquent\Model;
class Cat extends Model {
//
}
从这里,你可以定义自己的属性和方法。
其他命令可能有选项。最好的检查方法是命令名称后附加 --help,如下所示:
$ php artisan make:command --help
你会发现这个命令有 --handler 和 --queued 选项来修改创建的类存根。
滚出你自己的 Artisan 命令
在这个阶段,你可能正在考虑编写自己的定制命令。正如你将看到的,使用 Artisan 做这件事出奇地简单。如果你使用过 Symfony 的 Console 组件,你会很高兴地知道 Artisan 命令只是它的一个扩展,语法稍微更丰富一些。这意味着各种助手将提示输入,显示进度条,或格式化表格,所有这些都可以在 Artisan 中使用。
我们将要编写的命令取决于我们在 第三章 中构建的应用程序,你的第一个应用程序。它将允许你将数据库中现有的所有猫记录导出为带或不带标题行的 CSV 文件。如果没有指定输出文件,命令将简单地以格式化的表格形式将所有记录输出到屏幕上。
创建命令
创建命令只需要两个步骤。首先,你需要创建命令本身,然后你需要手动注册它。
我们可以使用以下命令来创建我们之前看到的控制台命令:
$ php artisan make:console ExportCatsCommand
这将在 app/Console/Commands 内部生成一个类。然后我们需要将此命令注册到控制台内核,位于 app/Console/Kernel.php:
protected $commands = ['App\Console\Commands\ExportCatsCommand',];
如果你现在运行 php artisan,你应该看到一个名为 command:name 的新命令。这个命令目前还没有做任何事情。然而,在我们开始编写功能之前,让我们简要地看看它是如何内部工作的。
命令的结构
在新创建的命令类内部,你会找到为你生成的某些代码。我们将遍历不同的属性和方法,看看它们的目的。
前两个属性是命令的名称和描述。这里没有什么激动人心的,这只是当你运行 Artisan 时将在命令行中显示的信息。冒号用于命名空间命令,如下所示:
protected $name = 'export:cats';
protected $description = 'Export all cats';
然后你会找到 fire 方法。这是当你运行特定命令时被调用的方法。从那里,你可以检索传递给命令的参数和选项,或者运行其他方法。
public function fire()
最后,有两个方法负责定义传递给命令的参数或选项列表:
protected function getArguments() { /* Array of arguments */ }
protected function getOptions() { /* Array of options */ }
每个参数或选项都可以有一个名称、一个描述和一个默认值,这个默认值可以是强制性的或可选的。此外,选项可以有快捷方式。
要理解参数和选项之间的区别,考虑以下命令,其中选项以两个连字符为前缀:
$ command --option_one=value --option_two -v=1 argument_one argument_two
在这个例子中,option_two 没有值;它仅用作标志。-v 标志只有一个连字符,因为它是一个快捷方式。在你的控制台命令中,你需要验证用户提供的任何选项和参数值(例如,如果你期望一个数字,以确保传递的值实际上是一个数值)。
可以使用 $this->argument($arg) 获取参数,而选项——没错——使用 $this->option($opt)。如果这些方法没有接收任何参数,它们将简单地返回完整的参数列表。你通过它们的名称来引用参数和选项,即 $this->argument('argument_name');。
编写命令
我们将首先编写一个方法,从数据库中检索所有猫并将它们作为数组返回:
protected function getCatsData() {
$cats = App\Cat::with('breed')->get();
foreach ($cats as $cat) {
$output[] = [
$cat->name,
$cat->date_of_birth,
$cat->breed->name,
];
}
return $output;
}
这里不应该有任何新内容。我们本可以使用 toArray() 方法,它将 Eloquent 集合转换为数组,但我们必须扁平化数组并排除某些字段。
然后我们需要定义我们的命令期望的参数和选项:
protected function getArguments() {
return [
['file', InputArgument::OPTIONAL, 'The output file', null],
];
}
要指定额外的参数,只需向数组中添加一个具有相同参数的额外元素:
return [
['arg_one', InputArgument::OPTIONAL, 'Argument 1', null],
['arg_two', InputArgument::OPTIONAL, 'Argument 2', null],
];
选项的定义方式类似:
protected function getOptions() {
return [
['headers', 'h', InputOption::VALUE_NONE, 'Display headers?',
null],
];
}
最后一个参数是如果未指定,参数和选项应该具有的默认值。在这两种情况下,我们希望它是 null。
最后,我们编写 fire 方法的逻辑:
public function fire() {
$output_path = $this->argument('file');
$headers = ['Name', 'Date of Birth', 'Breed'];
$rows = $this->getCatsData();
if ($output_path) {
$handle = fopen($output_path, 'w');
if ($this->option('headers')) {
fputcsv($handle, $headers);
}
foreach ($rows as $row) {
fputcsv($handle, $row);
}
fclose($handle);
} else {
$table = $this->getHelperSet()->get('table');
$table->setHeaders($headers)->setRows($rows);
$table->render($this->getOutput());
}
}
虽然这个方法的大部分内容相对简单,但也有一些新特性。第一个是使用$this->info()方法,它将一条信息性消息写入输出。如果您需要以不同颜色显示错误消息,可以使用$this->error()方法。
在代码的下方,您将看到一些用于生成表格的函数。正如我们之前提到的,Artisan 命令扩展了 Symfony 控制台组件,因此继承了所有其辅助函数。这些可以通过$this->getHelperSet()访问。然后只需传递表格的标题和行数组,并调用render方法即可。
要查看我们命令的输出,我们将运行以下命令:
$ php artisan export:cats
$ php artisan export:cats --headers file.csv
安排命令
传统上,如果您想要一个命令定期运行(每小时、每天、每周等),那么您必须在基于 Linux 的环境中设置 Cron 作业,或者在 Windows 环境中设置计划任务。然而,这也有一些缺点。它要求用户具有服务器访问权限并熟悉创建此类计划。此外,在基于云的环境中,应用程序可能不在单个机器上托管,或者用户可能没有创建 Cron 作业的权限。Laravel 的创建者认为这可以改进,并想出了一个表达性良好的安排 Artisan 任务的方法。
您的计划定义在app/Console/Kernel.php中,并且由于您的计划定义在这个文件中,它具有额外的优势,即存在于源控制中。
如果您打开 Kernel 类文件,您将看到一个名为schedule的方法。Laravel 默认提供这个方法作为示例:
$schedule->command('inspire')->hourly();
如果您以前设置过 Cron 作业,您会看到这比 crontab 等价物更容易阅读:
0 * * * * /path/to/artisan inspire
在代码中指定任务也意味着我们可以轻松地更改要运行的控制台命令,而无需更新 crontab 条目。
默认情况下,计划命令不会运行。要这样做,您需要一个单独的 Cron 作业,每分钟运行一次调度程序:
* * * * * php /path/to/artisan schedule:run 1>> /dev/null 2>&1
当调度程序运行时,它将检查任何与计划匹配的作业,然后运行它们。如果没有匹配的计划,则在该次运行中不运行任何命令。
您可以自由地安排您想要的任何数量的命令,并且有各种表达性和描述性的方法来安排它们:
$schedule->command('foo')->everyFiveMinutes();
$schedule->command('bar')->everyTenMinutes();
$schedule->command('baz')->everyThirtyMinutes();
$schedule->command('qux')->daily();
您还可以指定一个时间来运行计划中的命令:
$schedule->command('foo')->dailyAt('21:00');
或者,您还可以创建更频繁的计划命令:
$schedule->command('foo')->weekly();
$schedule->command('bar')->weeklyOn(1, '21:00');
第二个示例中的第一个参数是日期,其中0代表星期日,1到6代表星期一到星期六,第二个参数是时间,同样以 24 小时制指定。您还可以明确指定运行计划命令的日期:
$schedule->command('foo')->mondays();
$schedule->command('foo')->tuesdays();
$schedule->command('foo')->wednesdays();
// And so on
$schedule->command('foo')->weekdays();
如果您有一个可能运行时间较长的命令,那么您可以防止它重叠:
$schedule->command('foo')->everyFiveMinutes() ->withoutOverlapping();
除了计划,你还可以指定计划命令应在哪个环境下运行,如下面的命令所示:
$schedule->command('foo')->weekly()->environments('production');
你可以使用这个功能在生产环境中运行命令,例如,定期存档数据或运行报告。
默认情况下,如果启用维护模式,计划命令不会执行。这种行为可以很容易地覆盖:
$schedule->command('foo')->weekly()->evenInMaintenanceMode();
查看计划命令的输出
对于一些计划命令,你可能希望以某种方式查看输出,无论是通过电子邮件、记录到磁盘上的文件,还是发送回调到预定义的 URL。在 Laravel 中,所有这些场景都是可能的。
要通过以下命令通过电子邮件发送作业的输出:
$schedule->command('foo')->weekly()
->emailOutputTo('someone@example.com');
如果你希望将作业的输出写入磁盘上的文件,这也很简单:
$schedule->command('foo')->weekly()->sendOutputTo($filepath);
你也可以在作业运行后 ping 一个 URL:
$schedule->command('foo')->weekly()->thenPing($url);
这将向指定的 URL 发送一个GET请求,此时你可以向你的聊天客户端发送消息,通知你命令已运行。
最后,你可以将前面的命令链式调用以发送多个通知:
$schedule->command('foo')->weekly()
->sendOutputTo($filepath)
->emailOutputTo('someone@example.com');
然而,请注意,如果你希望同时发送电子邮件,你必须先将输出发送到文件。
摘要
在本章中,你学习了 Artisan 如何以不同的方式协助你在开发、调试和部署过程中的工作。我们还看到了如何轻松构建自定义 Artisan 命令并适应你的需求。
如果你相对较新于命令行,你将一瞥命令行工具的力量。另一方面,如果你是命令行的资深用户,并且使用其他编程语言编写过脚本,你一定能欣赏 Artisan 的简洁性和表达性。
在下一章中,我们将探讨 Laravel 为我们提供的安全应用程序的功能,以及用户认证和授权。
第七章. 认证和安全
在本章中,我们将通过添加简单的认证机制和解决现有代码库中的任何安全问题来改进我们在第三章中构建的应用程序,即你的第一个应用程序。通过这样做,你将了解:
-
配置和使用认证服务
-
中间件及其如何应用于特定路由
-
数据验证和表单请求
-
网络应用程序中最常见的安全漏洞
-
Laravel 如何帮助你编写更安全的代码
认证用户
允许用户注册和登录是网络应用程序中极其常见的功能。然而,PHP 并没有规定应该如何实现,也没有提供任何辅助函数来实现它。这导致了不同的、有时是不安全的用户认证方法和限制对特定页面的访问方法的产生。在这方面,Laravel 为你提供了不同的工具来使这些功能更安全且更容易集成。它是通过其认证服务和尚未覆盖的功能——中间件来实现的。
创建用户模型
首先,我们需要定义一个模型来表示我们应用程序的用户。Laravel 已经在config/auth.php中为你提供了合理的默认设置,你可以在那里更改用于存储用户账户的模型或表。
它还包含一个现有的User模型,位于app/User.php中。为了这个应用程序的目的,我们将对其进行轻微简化,删除某些类变量,并添加新方法,以便它可以与Cat模型交互如下:
namespace App;
use Illuminate\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Contracts\Auth\Authenticatable as uthenticableContract;
use Illumunate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
use App\Cat;
class User extends Model implements AuthenticatableContract, CanResetPasswordContract {
use Authenticable, CanResetPassword;
public function cats() {
return $this->hasMany('App\Cat');
}
public function owns(Cat $cat) {
return $this->id == $cat->user_id;
}
public function canEdit(Cat $cat) {
return $this->is_admin || $this->owns($cat);
}
}
首先要注意的是,这个模型实现了Authenticable接口。记住,接口并不提供任何实现细节。它不过是一个合同,它指定了一个类在实现接口时应定义的方法名称。在这种情况下,Authenticable接口强制要求实现以下方法:
-
getAuthIdentifier -
getAuthPassword -
getRememberToken -
setRememberToken -
getRememberTokenName
如果你打开app/User.php文件,你可能会想知道这些方法在哪里。实际上,这些方法是由Authenticable特性提供的。你可以在User类的大括号后面看到这个特性被包含进来:
use Authenticable, CanResetPassword;
特性允许在类中重用代码。这是为了弥补 PHP 语言的一个不足,即不允许类中的多重继承。因此,作为一个解决方案,你可以组合可能被放入多个类中的方法,这些类可能已经扩展了另一个基类。
在我们的User模型中,cats()方法简单地定义了与Cat模型的hasMany关系。最后两个方法将用于检查给定的Cat实例是否由当前User实例拥有或可编辑。
最后,让我们在 User 模型上创建一个辅助方法,以便我们可以检查我们是否有管理员权限。这个方法将被命名为 isAdministrator,如下所示:
public function isAdministrator()
{
return $this->getAttribute('is_admin');
}
如果使用 MySQL,这将返回一个由 0 或 1 组成的字符串(因为 MySQL 没有原生的布尔数据类型)。然而,我们可以将这个模型属性转换为布尔类型,以便值检查更好。在你的模型顶部添加以下代码:
protected $casts = [
'is_admin' => 'boolean',
];
在这个数组中,我们定义了属性以及我们实际上想要的数据类型。然后,当我们从模型中检索属性时,它将被转换为指定的数据类型。
其他可以转换为模型属性的 数据类型有如下:
-
string -
integer -
real -
float -
double -
integer
array 类型可用于包含序列化 JSON 字符串的列,它将反序列化数据并将其呈现为普通的 PHP 数组。
目前我们的 users 表中不存在 is_admin 属性,所以让我们修复这个问题。
创建必要的数据库模式
除了 User 模型外,Laravel 还预包装了两个迁移文件:一个用于创建 users 表,另一个用于创建 password_resets 表。
默认情况下,用户表迁移创建了一个包含每个用户的 ID、名称、密码、记住令牌以及创建时间和更新时间的列的表。我们需要通过添加一个新列来扩展该表,指定每个用户是否是应用程序的管理员。
要做到这一点,我们可以创建另一个迁移。迁移可以用来修改现有的表,也可以用来创建全新的表。在这个例子中,我们将创建一个迁移来向 users 表添加一个名为 is_admin 的布尔列。
运行以下命令以在 database/migrations 中创建迁移文件:
$ php artisan make:migration add_is_admin_column_to_users
然后将 up 方法更改为如下,以包含模式更改:
public function up() {
Schema::table('users', function(Blueprint $table) {
$table->boolean('is_admin')->default(false)
->after('password');
}
}
我们将默认值设置为 false,这样我们才必须显式地将任何我们希望成为管理员的用户设置为 true,而不是每个新用户(以及数据库表中的任何现有用户)在创建时自动获得管理员权限。
就像任何其他迁移一样,我们也必须提供 down 方法来回滚任何更改。由于我们已经创建了一个列,如果用户决定回滚迁移,我们需要将其删除:
public function down() {
Schema::table('users', function(Blueprint $table) {
$table->dropColumn('is_admin');
});
}
现在,我们还需要更新 cats 数据库表,添加一个与用户关联的列。通过遵循前面的步骤,我们创建了一个新的模式,如下所示,描述了更改:
$ php artisan make:migration add_user_id_column_to_cats
然后我们按照以下方式完成方法:
public function up() {
Schema::table('cats', function(Blueprint $table) {
$table->integer('user_id')->unsigned();
$table->foreign('user_id')->references('id')->on('users')
->onDelete('cascade');
});
}
使用前面的代码,我们修改 cats 表以包含一个 user_id 列,该列存储 Cat 拥有者的 id。在创建列之后,我们在表上创建一个 外键约束,以确保 user_id 值必须匹配 users 表中记录的主键。外键有助于你强制数据的一致性(例如,你将无法将 Cat 分配给不存在的用户)。级联删除还意味着当用户被删除时,其关联的猫记录也将被删除;否则,数据库将包含不再有任何所有者的猫!
反转此迁移的代码将简单地删除外键约束和列,然后删除 user_id 列:
public function down() {
Schema::table('cats', function(Blueprint $table) {
$table->dropForeign('cats_user_id_foreign');
$table->dropColumn('user_id');
});
}
接下来,我们准备一个数据库填充器来为我们的应用程序创建两个用户,其中一个将是管理员。
Use App\User;
class UsersTableSeeder extends Seeder {
public function run() {
User::create([
'username' =>'admin',
'password' => bcrypt('hunter2'),
'is_admin' => true,
]);
User::create([
'username' => 'scott',
'password' => bcrypt('tiger'),
'is_admin' => false,
]);
}
}
一旦你将此代码保存在一个名为 database/seeds/UsersTableSeeder.php 的新文件中,不要忘记在主 DatabaseSeeder 类中调用它。
注意
Laravel 预期所有密码都使用 bcrypt 辅助函数进行散列,该函数使用 Bcrypt 算法创建强散列。你不应该在 明文 中存储密码或使用弱算法(如 md5 或 sha1)对其进行散列。
要同时运行迁移和填充数据库,请输入以下命令:
$ php artisan migrate --seed
认证路由和视图
我们之前提到 PHP 没有标准的用户认证方式,但 Laravel 并非如此。Laravel 认识到最现代的 Web 应用程序将需要用户注册和登录,因此它自带控制器、路由和视图,以便从一开始就方便地完成这些操作。你可以找到主要的认证控制器在 app/Http/Controllers/Auth/AuthController.php。如果你打开该文件,你会看到它只包含一个构造函数,因为像 User 模型一样,它使用一个特质来提供功能,在这种情况下,是 AuthenticatesAndRegistersUsers:
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Contracts\Auth\Registrar;
use Illuminate\Foundation\Auth\AuthenticatesAndRegistersUsers;
class AuthController extends Controller {
use AuthenticatesAndRegistersUsers;
public function __construct(Guard $auth, Registrar $registrar) {
$this->auth = $auth;
$this->registrar = $registrar;
$this->middleware('guest', ['except' => 'getLogout']);
}
}
middleware 方法将应用访客中间件到控制器中的所有操作,除了 getLogout() 操作。我们将在本章的后面更深入地探讨中间件。
此控制器(以及用于处理密码重置的控制器)可以在应用程序的路由文件中找到:
$router->controllers([
'auth' => 'Auth\AuthController',
'password' => 'Auth\PasswordController',
]);
Laravel 还在 resources/views/auth 中包含了两个视图,login.blade.php 和 register.blade.php。
让我们看看如何将 Laravel 的 auth 视图集成到我们的应用程序中。我们将首先修改我们的主布局(resources/views/layouts/master.blade.php)以显示登录链接给访客,以及显示注销链接给已登录的用户。为了检查访客是否已登录,我们使用 Auth::check() 方法:
<div class="container">
<div class="page-header">
<div class="text-right">
@if (Auth::check())
Logged in as
<strong>{{ Auth::user()->username }}</strong>
{!! link_to('auth/logout', 'Log Out') !!}
@else
{!! link_to('auth/login', 'Log In') !!}
@endif
</div>
@yield('header')
</div>
@if (Session::has('message'))
<div class="alert alert-success">
{{ Session::get('message') }}
</div>
@endif
@if (Session::has('error'))
<div class="alert alert-warning">
{{ Session::get('error') }}
</div>
@endif
@yield('content')
</div>
我们可以在 resources/views/auth/login.blade.php 内替换登录视图,使用一个更简单的表单:
@extends('layouts.master')
@section('header')<h2>Log In</h2>@stop
@section('main')
{!! Form::open(['url' => 'auth/login') !!}
<div class="form-group">
{!! Form::label('username', 'Username', ['class' =>
'control-label') !!}
<div class="form-controls">
{!! Form::text('username', null, ['class' =>
'form-control']) !!}
</div>
</div>
<div class="form-group">
{!! Form::label('Password') !!}
<div class="form-controls">
{!! Form::password('password', ['class' =>
'form-control']) !!}
</div>
</div>
{!! Form::submit('Log in', ['class' => 'btn btn-primary') !!}
{!! Form::close() !!}
@stop
我们使用 Blade 语法从 HTML 和表单辅助函数中获取原始值({!! $value !!}),因为它们返回 HTML 标记,如果我们使用默认语法({{ $value }})来渲染这些,我们会在屏幕上打印 HTML 字符串。
中间件
如果你回顾一下AuthController,你会在构造方法中注意到以下行:
$this->middleware('auth', ['except' => 'getLogout']);
中间件包括可以附加到进入您应用程序的请求的类,并用于更改这些请求的结果。中间件是 Laravel 4 中发现的路由过滤器的替代品。
中间件可以在定义路由时注册,或者在前面提到的控制器中注册。前面的例子将auth中间件附加到所有将由AuthController处理的请求上,除了对getLogout方法的请求。
中间件类可以在app/Http/Middleware目录中找到。在这里,你可以找到默认的Authentication中间件类,以及另外两个:RedirectIfAuthenticated和VerifyCsrfToken。我们可以检查Authentication类来了解中间件类是如何工作的:
public function __construct(Guard $auth) {
$this->auth = $auth;
}
public function handle($request, Closure $next) {
if ($this->auth->guest()) {
if ($request->ajax()) {
return response('Unauthorized', 401);
} else {
return redirect()->guest('auth/login');
}
}
return $next($request);
}
有两个方法:构造方法和handle方法。在先前的例子中,该类正在检查当前用户是否已认证(使用Guard类上的guest()方法),如果是访客,则如果请求是通过 AJAX 进行的,则返回响应,或者将用户重定向到登录表单。因为响应是在那里返回的,所以请求将不会进一步处理。
我们可以使用这种方法来检查用户是否已认证,还可以检查他们是否是管理员。我们可以使用内置的 Artisan 生成器创建一个新的中间件类,如下所示:
$ php artisan make:middleware IsAdministrator
这将在app/Http/Middleware/IsAdministrator.php创建一个新文件。像Authentication类一样,我们需要Guard实现,所以添加一个构造函数,将依赖项的类型提示,以便服务容器自动注入它:
public function __construct(Guard $auth) {$this->auth = $auth;}
我们还需要在文件顶部导入完整的命名空间,如下所示:
use Illuminate\Contracts\Auth\Guard;
现在我们有一个Guard实例,并将其分配给一个类属性;我们现在可以完善handle方法,如下所示:
public function handle($request, \Closure $next) {
if ( ! $this->auth->user()->isAdministrator()) {
if ($this->request->ajax()) {
return response('Forbidden.', 403);
} else {
throw new AccessDeniedHttpException;
}
}
}
这次,我们从Guard中获取当前用户(这将产生一个User Eloquent 模型实例)。然后我们可以调用这个模型上的任何方法。在先前的例子中,我们调用了我们的isAdministrator()方法,这将返回一个布尔值,表示用户是否应该被视为管理员。如果不是——就像Authenticated类一样——如果请求是通过 AJAX 进行的,我们返回一个简单的字符串响应(以及适当的 HTTP 状态码);否则,我们抛出一个AccessDeniedHttpException。这个异常实际上是 Symfony HttpKernel库的一部分,所以我们需要在文件顶部导入类的完整命名空间:
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
创建中间件的最后一步是告诉 HTTP Kernel 类关于它。你可以在这个文件中找到它:app/Http/Kernel.php。通过打开这个文件,你会看到定义了两个属性:$middleware 和 $routeMiddleware。将类的完整命名空间添加到 $middleware 数组中会将中间件添加到每个请求中。我们不想这样做,因为如果我们这样做,就没有人能够访问登录页面了,因为他们在这个时候将不会被认证!相反,我们想要将一个条目添加到 $routeMiddleware 数组中,如下所示:
protected $routeMiddleware = [
'auth' => 'App\Core\Http\Middleware\Authenticate',
'auth.basic' => Illuminate\Auth\Middleware\AuthenticateWithBasicAuth',
'guest' => 'App\Core\Http\Middleware\RedirectIfAuthenticated',
'admin' => 'App\Http\Middleware\IsAdministrator',
];
数组中的键是我们可以在路由和控制器中使用的内容,并且当请求指定的资源时,将应用相应的类。
下面是一个将此应用于路由的示例:
Route::get('admin/dashboard', [
'middleware' => ['auth', 'admin'],
'uses' => '\Admin\DashboardController@index',
]);
如你所见,你可以指定多个中间件类来应用于单个请求。在先前的路由示例中,首先会调用 Authenticated 中间件类;如果一切顺利(并且用户没有被重定向到登录页面),它将被传递到 IsAdministrator 中间件类,该类将检查当前登录的用户是否是管理员。
验证用户输入
我们的应用程序仍然有一个重大缺陷——它不对用户提交的数据进行任何验证。虽然如果你用纯 PHP 做这件事,可能会得到一系列带有正则表达式的条件,但 Laravel 提供了一种更简单、更健壮的方式来实现这一点。
通过传递一个包含输入数据的数组和一个包含验证规则的数组到 Validator::make($data, $rules) 方法来执行验证。在我们的应用程序中,以下是我们可以编写的规则:
$rules = [
'name' => 'required|min:3', // Required, > 3 characters
'date_of_birth' => ['required, 'date'] // Must be a date
];
可以通过管道符号或作为数组传递来分隔多个验证规则(前述代码中展示了这两种示例)。Laravel 提供了超过 30 种不同的验证规则,它们都记录在这里:
laravel.com/docs/validation#available-validation-rules
下面是如何使用表单提交的数据来检查这些规则:
$validator = Validator::make($rules, Input::all());
然后,你可以根据 $validator->fails() 的输出使你的应用程序做出响应。如果这个方法调用返回 true,你将使用 $validator->messages() 获取包含所有错误信息的对象。如果你在一个控制器动作中验证数据,你可以将这个对象附加到重定向中,将用户送回表单:
return redirect()
->back()
->with('errors, $validatior->messages());
由于每个字段可以有零个或多个验证错误,你将使用条件语句和循环以及以下方法来显示这些消息:
if ($errors->has('name')) {
foreach ($errors->get('name') as $error) {
echo $error;
}
}
你也可以使用像 Ardent 这样的工具,它扩展了 Eloquent,并允许你直接在模型中编写验证规则。你可以从以下链接下载 Ardent:
表单请求
在 Laravel 4 中,你可以自由地将验证放在任何你想放的地方。这导致开发者以无数种方式实现验证,包括在控制器中或作为验证服务。在版本 5 中,Laravel 引入了一种标准化提交数据验证的方法,即通过表单请求。
表单请求是封装 Laravel 标准Request类的类,但实现了名为ValidatesWhenResolved的特质。这个特质包含一个Validator实例,并使用你在表单请求类中定义的规则来验证请求中的数据。如果验证器通过,那么应用到的控制器动作将正常执行。如果验证器失败,则用户将被重定向到之前的 URL,并在会话中显示错误。这意味着你不需要在控制器动作中定义验证流程,甚至可以在不同场景下提交相同数据但不同动作的控制器动作中重用它们。
让我们创建一个用于保存猫的详细信息的表单请求。同样,Artisan 为我们提供了一个生成器来创建一个新的表单请求类:
$ php artisan make:request SaveCatRequest
这将在app/Http/Requests/SaveCatRequest.php文件中创建文件。在文件中,你会找到两个方法:authorize和rules。
在执行验证之前,表单请求授权当前请求。实现细节取决于你。你可能希望当前用户登录或成为管理员。你可以在该方法中定义这个逻辑。默认情况下,它简单地返回false。这并不理想,因为它意味着没有人能够执行此请求。由于我们通过中间件处理用户身份验证,我们可以简单地将其更改为返回true。
第二个方法,rules,是我们提供要馈送给Validator实例的验证规则数组的地方。以先前的Validator示例为例,这可以更改为以下代码:
public function rules() {
return [
'name' => 'required|min:3',
'date_of_birth' => 'required|date',
];
}
规则被定义在方法中而不是简单地作为类属性,是为了允许条件验证。可能有些时候你只想验证某个字段,例如,如果另一个字段提供了值。想象一下电子商务网站上的结账表单,它会要求用户提供一个账单地址,如果与账单地址不同,则可选的送货地址。大多数在线商店都会有一个复选框,当勾选时,将显示输入送货地址的字段。如果我们为这种场景创建验证,那么它可能看起来像以下代码:
public function rules() {
$rules = [
'billing_address' => 'required',
];
if ($request->has('shipping_address_different') {
$rules['shipping_address'] = 'required';
}
return $rules;
}
之前的示例检查是否存在带有shipping_address_different(复选框)字段的字段,如果存在,则附加一个验证规则来指定shipping_address是必需的。正如你所见,这使得表单请求中的验证非常强大。
通过指定控制器动作的参数来实例化表单请求类。在我们的保存猫的例子中,这将对我们的CatsController类中的create和update方法都适用:
public function create(SaveCatRequest $request) {
// method body
}
public function update(SaveCatRequest $request) {
// method body
}
现在,每当请求这些动作中的任何一个时,SaveCatRequest类将被首先调用并检查数据是否有效。这意味着我们的控制器方法可以保持简洁,并且只处理将新数据持久化到数据库的实际操作。
保护你的应用程序(Securing your application)
在你将应用程序部署到充满无情机器人和恶意用户的敌对环境之前,你必须牢记一些安全考虑因素。在本节中,我们将介绍几个常见的针对 Web 应用程序的攻击向量,并了解 Laravel 是如何保护你的应用程序免受这些攻击的。由于框架不能保护你免受所有攻击,我们还将探讨要避免的常见陷阱。
跨站请求伪造(Cross-site request forgery)
跨站请求伪造(CSRF)攻击是通过针对具有副作用(即执行操作而不是仅显示信息)的 URL 来进行的。我们已经通过避免对具有永久效果的路线(如DELETE/cats/1)使用GET来部分缓解了 CSRF 攻击,因为这些操作无法通过简单链接访问或嵌入到<iframe>元素中。然而,如果攻击者能够将他的受害者引导到一个他控制的页面,他可以轻易地让受害者向目标域提交表单。如果受害者已经在目标域登录,应用程序将无法验证请求的真实性。
最有效的对策是在表单显示时发出一个令牌,然后在表单提交时检查该令牌。Form::open和Form::model都会自动插入一个隐藏的_token输入元素,并且中间件会应用于检查传入请求中提供的令牌是否与预期值匹配。
转义内容以防止跨站脚本(XSS)
跨站脚本(XSS)攻击发生在攻击者能够在其他用户查看的页面上放置客户端 JavaScript 代码时。在我们的应用程序中,假设我们的猫的名字没有被转义,如果我们将以下代码片段作为名字的值输入,那么每个访问者都会在我们的猫名字显示的每个地方看到一个警告消息:
Evil Cat <script>alert('Meow!')</script>
虽然这是一个相当无害的脚本,但很容易插入更长的脚本或链接到外部脚本,该脚本会窃取会话或 cookie 值。为了避免这种攻击,你不应该信任任何用户提交的数据或转义任何危险字符。你应该在 Blade 模板中优先使用双大括号语法({{ $value }}),并且只有在确定数据可以以原始格式安全显示时才使用{!! $value !!}语法。
避免 SQL 注入
当一个应用程序在 SQL 查询中插入任意且未经筛选的用户输入时,就存在SQL 注入漏洞。这种用户输入可能来自 cookie、服务器变量,或者最常见的是通过GET或POST输入值。这些攻击的目的是访问或修改通常不可用的数据,有时还会干扰应用程序的正常运行。
默认情况下,Laravel 会保护你免受此类攻击,因为查询构建器和 Eloquent 在幕后都使用PHP 数据对象(PDO)类。PDO 使用预处理语句,这允许你安全地传递任何参数,而无需对它们进行转义和清理。
在某些情况下,你可能想用 SQL 编写更复杂或数据库特定的查询。这可以通过使用DB::raw方法实现。当使用此方法时,你必须非常小心,不要创建任何像以下这样的易受攻击的查询:
Route::get('sql-injection-vulnerable', function() {
$name = "'Bobby' OR 1=1";
return DB::select(
DB::raw("SELECT * FROM cats WHERE name = $name"));
});
为了保护此查询免受 SQL 注入攻击,你需要通过在查询中用问号替换参数来重写它,然后将值作为一个数组作为raw方法的第二个参数传递:
Route::get('sql-injection-not-vulnerable', function() {
$name = "'Bobby' OR 1=1";
return DB::select(
DB::raw("SELECT * FROM cats WHERE name = ?", [$name]));
});
前面的查询被称为预处理语句,因为我们定义了查询和预期的参数,并且任何有害的参数都会被清理,以防止它们以未预期的方式更改查询或数据库中的数据。
谨慎使用质量分配
在第三章,你的第一个应用中,我们使用了质量分配,这是一个方便的特性,允许我们根据表单输入创建一个模型,而无需逐个分配每个值。
然而,这个特性应该谨慎使用。恶意用户可能会在客户端篡改表单并添加新的输入:
<input name="is_admin" value="1" />
然后,当表单提交时,我们尝试使用以下代码创建一个新的模型:
Cat::create(Request::all())
由于$fillable数组定义了可以通过质量分配填充的字段白名单,因此此方法调用将抛出质量分配异常。
你也可以做相反的事情,使用$guarded属性定义一个黑名单。然而,这个选项可能具有潜在的危险性,因为你可能会忘记在向模型添加新字段时更新它。
Cookie – 默认安全
Laravel 通过其Cookie类使创建、读取和过期 cookie 变得非常容易。
你还会很高兴地知道,所有 cookie 都会自动签名和加密。这意味着如果它们被篡改,Laravel 会自动丢弃它们。这也意味着你将无法使用 JavaScript 从客户端读取它们。
在交换敏感数据时强制使用 HTTPS
如果您通过 HTTP 提供服务,您需要记住,交换的每一比特信息,包括密码,都是以明文形式发送的。因此,同一网络上的攻击者可以拦截私人信息,例如会话变量,并以受害者身份登录。我们唯一能防止这种情况的方法是使用 HTTPS。如果您已经在您的 Web 服务器上安装了 SSL 证书,Laravel 提供了一些助手来在http://和https://之间切换并限制对某些路由的访问。例如,您可以定义一个https过滤器,将访客重定向到安全的路由,如下面的代码片段所示:
Route::filter('https', function() {
if ( ! Request::secure())
return Redirect::secure(URI::current());
});
摘要
在本章中,我们学习了如何利用 Laravel 的许多工具为网站添加认证功能、验证数据和避免常见的安全问题。现在您应该拥有创建、测试和确保 Laravel 应用程序所需的所有必要信息。
在附录中,An Arsenal of Tools,您将获得 Laravel 提供的许多其他有用功能的便捷参考。
附录 A. 工具箱
Laravel 提供了一些实用工具,可以帮助你执行特定任务,例如发送电子邮件、排队函数和文件操作。它自带了大量内部使用的实用工具;好消息是,你也可以在应用程序中使用它们。本章将介绍最有用的实用工具,这样你就不会重写框架中已经存在的函数!
本章的结构部分基于Jesse O'Brien的速查表,可在cheats.jesse-obrien.ca/找到。示例基于 Laravel 的测试以及其官方文档和 API。
数组辅助函数
对于任何处理数据的 Web 应用程序来说,数组都是基础。PHP 已经提供了近 80 个函数来执行数组上的各种操作,而 Laravel 通过一些受 Python 和 Ruby 中某些函数启发的实用函数来补充它们。
注意
Laravel 的几个类,包括 Eloquent 集合,实现了 PHP 的ArrayAccess接口。这意味着你可以在代码中使用它们像普通数组一样,例如,在foreach循环中迭代项目或使用这里描述的数组函数。
大多数函数支持点符号来引用嵌套值,这与 JavaScript 对象类似。例如,你不必编写$arr['foo']['bar']['baz'],而是可以使用array_get辅助函数并编写array_get($arr, 'foo.bar.baz');。
在以下使用示例中,我们将使用三个虚拟数组,并假设每个示例都会重置它们:
$associative = [
'foo' => 1,
'bar' => 2,
];
$multidimensional = [
'foo' => [
'bar' => 123,
],
];
$list_key_values = [
['foo' => 'bar'],
['foo' => 'baz'],
];
数组辅助函数的使用示例
我们现在将看看如何使用 Laravel 的数组辅助函数提取和操作这些数组的值:
-
要在键不存在时使用回退值检索值,我们使用
array_get函数,如下所示:array_get($multidimensional, 'foo.bar', 'default'); // Returns 123如果你引用的数组键可能存在或不存在(即在请求数据数组中),这将很有用。如果键不存在,则将返回默认值。
-
要使用点符号从数组中删除值,我们使用
array_forget函数,如下所示:array_forget($multidimensional, 'foo.bar'); // $multidimensional == ['foo' => []]; -
要从数组中删除值并返回它,我们使用
array_pull函数,如下所示:array_pull($multidimensional, 'foo.bar'); // Returns 123 and removes the value from the array -
要使用点符号设置嵌套值,我们使用
array_set函数,如下所示:array_set($multidimensional, 'foo.baz', '456'); // $multidimensional == ['foo' => ['bar' => 123, 'baz' => '456']]; -
要将多维关联数组展平,我们使用
array_dot函数,如下所示:array_dot($multidimensional); // Returns ['foo.bar' => 123]; array_dot($list_key_values); // Returns ['0.foo' => 'bar', '1.foo' => 'baz']; -
要从数组中返回所有键及其值(除了指定的那些),我们使用
array_except函数,如下所示:array_except($associative, ['foo']); // Returns ['bar' => 2]; -
要只从数组中提取一些键,我们使用
array_only函数,如下所示:array_only($associative, ['bar']); // Returns ['bar' => 2]; -
要返回包含所有嵌套值(键被丢弃)的展平数组,我们使用
array_fetch函数,如下所示:array_fetch($list_key_values, 'foo'); // Returns ['bar', 'baz']; -
要遍历数组并返回闭包返回 true 的第一个值,我们使用
array_first函数如下:array_first($associative, function($key, $value) { return $key == 'foo'; }); // Returns 1 -
要生成一个只包含在多维数组中找到的值的单维数组,我们使用
array_flatten函数如下:array_flatten($multidimensional); // Returns [123] -
要从键值对列表中提取值数组,我们使用
array_pluck函数如下:array_pluck($list_key_values, 'foo'); // Returns ['bar', 'baz']; -
要获取数组的第一个或最后一个项目(这也适用于函数返回的值),我们使用
head和last函数如下:head($array); // Aliases to reset($array) last($array); // Aliases to end($array)
字符串和文本操作
字符串操作函数位于Illuminate\Support命名空间中,并且可以在Str对象上调用。
大多数函数也有更短的snake_case别名。例如,Str::endsWith()方法与全局ends_with()函数相同。我们可以在应用程序中自由选择我们喜欢的任何一个。
布尔函数
以下函数返回true或false值:
-
is方法检查一个值是否与一个模式匹配。星号可以用作通配符,如下所示:Str::is('projects/*', 'projects/php/'); // Returns true -
如以下代码所示,
contains方法检查一个字符串是否包含给定的子字符串:Str::contains('Getting Started With Laravel', 'Python'); // returns false -
startsWith和endsWith方法,如以下代码所示,检查一个字符串是否以一个或多个子字符串开始或结束:Str::startsWith('.gitignore', '.git'); // Returns true Str::endsWith('index.php', ['html', 'php']); // Returns true
如前所述的示例所示,这些方法对于验证文件名和类似数据非常方便。
变换函数
在某些情况下,您需要在向用户显示或将其用于 URL 之前转换字符串。Laravel 提供了以下辅助函数来实现这一点:
-
此函数生成一个对 URL 友好的字符串:
Str::slug('A/B testing is fun!'); // Returns "ab-testing-is-fun" -
此函数生成一个每个单词都大写的标题:
Str::title('getting started with laravel'); // Returns 'Getting Started With Laravel' -
此函数将给定字符的实例添加到字符串的开头:
Str::finish('/one/trailing/slash', '/'); Str::finish('/one/trailing/slash/', '/'); // Both will return '/one/trailing/slash/' -
此函数限制字符串中的字符数量:
Str::limit($value, $limit = 100, $end = '...') -
此函数限制字符串中的单词数量:
Str::words($value, $words = 100, $end = '...')
逆变换函数
以下函数帮助您找出单词的复数或单数形式,即使它是不规则的形式:
-
此函数找出单词的复数形式:
Str::plural('cat'); // Returns 'cats' Str::plural('fish'); // Returns 'fish' Str::plural('monkey'); // Returns 'monkeys' -
此函数找出单词的单数形式:
Str::singular('elves'); // Returns 'elf'
文件处理
Laravel 5 包含了优秀的Flysystem项目,用于与应用程序文件系统以及流行的基于云的存储解决方案(如Amazon Simple Storage Service(Amazon S3)和Rackspace)交互。文件系统在config/filesystems.php文件中配置为磁盘。然后您可以使用一致的 API 来管理文件,无论它们位于本地还是外部云存储中。
直接在Storage外观上调用方法将在默认磁盘上调用这些方法,如下所示:
Storage::exists('foo.txt');
如果您配置了多个磁盘,您也可以显式指定要执行操作的磁盘,如下所示:
Storage::disk('local')->exists('foo.txt');
您可以按照如下方式读取和写入文件:
Storage::put('foo.txt', $contents);
$contents = Storage::get('foo.txt');
您也可以按照如下方式添加或附加数据:
Storage::prepend('foo.txt', 'Text to prepend.');
Storage::append('foo.txt', 'Text to append.');
你可以使用命名恰当的方法来复制和移动文件,如下所示:
Storage::copy($source, $destination);
Storage::move($source, $destination);
你还可以通过提供要删除的文件数组来删除文件,无论是单个文件还是一次删除多个文件,如下所示:
Storage::delete('foo.txt');
Storage::delete(['foo.txt', 'bar.txt']);
此外,还有一些其他有用的方法,允许你获取有关文件的有用信息,如下所示:
Storage::size('foo.txt');
Storage::lastModified('foo.txt');
除了处理文件外,你还可以处理目录。要列出特定目录中的所有文件,请使用以下代码:
Storage::files('path/to/directory');
前面的代码将只列出当前目录中的文件。如果你想要递归地列出所有文件(即当前目录及其子目录中的文件),那么你可以使用allFiles方法,如下所示:
Storage::allFiles('path/to/directory');
你可以按照以下方式创建目录:
Storage::makeDirectory('path/to/directory');
你也可以按照以下方式删除目录:
Storage::deleteDirectory('path/to/directory');
文件上传
在 Laravel 5 中处理文件上传很容易。第一步是创建一个在提交时发送文件的表单:
{!! Form::open(['files' => true) !!}
这将设置enctype属性为multipart/form-data。然后你需要一个 HTML 的file输入:
{!! Form::file('avatar') !!}
在提交时,你可以在控制器操作中按照以下方式从Request对象访问文件:
public function store(Request $request){$file = $request->file('avatar');}
从这里,你通常会将文件移动到你的选择目录:
public function store(Request $request)
{
$file = $request->file('avatar');
$file->move(storage_path('uploads/avatars'));
}
在前面的示例中,$file 是 Symfony\Component\HttpFoundation\File\UploadedFile 类的一个实例,它提供了一系列方便的方法来与上传的文件交互。
你可以按照以下方式获取文件的完整路径:
$path = $request->file('avatar')->getRealPath();
你可以按照以下方式获取用户上传的文件名:
$name = $request->file('avatar')->getClientOriginalName();
你也可以按照以下方式检索原始文件的扩展名:
$ext = $request->file('avatar')->getClientOriginalExtension();
发送电子邮件
Laravel 的Mail类扩展了流行的 Swift Mailer 包,这使得发送电子邮件变得轻而易举。电子邮件模板的加载方式与视图相同,这意味着你可以使用 Blade 语法并将数据注入到模板中:
-
要将一些数据注入到位于
resources/views/email/view.blade.php内的模板中,我们使用以下函数:Mail::send('email.view', $data, function($message) {}); -
要发送 HTML 和纯文本版本,我们使用以下函数:
Mail::send(array('html.view', 'text.view'), $data, $callback); -
要延迟邮件 5 分钟(这需要队列),我们使用以下函数:
Mail::later(5, 'email.view', $data, function($message) {});
在接收消息对象的 $callback 闭包内部,我们可以调用以下方法来修改要发送的消息:
-
$message->subject('Welcome to the Jungle'); -
$message->from('email@example.com', 'Mr. Example'); -
$message->to('email@example.com', 'Mr. Example');
一些不太常见的方法包括:
-
$message->sender('email@example.com', 'Mr. Example'); -
$message->returnPath('email@example.com'); -
$message->cc('email@example.com', 'Mr. Example'); -
$message->bcc('email@example.com', 'Mr. Example'); -
$message->replyTo('email@example.com', 'Mr. Example'); -
$message->priority(2);
要附加或嵌入文件,你可以使用以下方法:
-
$message->attach('path/to/attachment.txt'); -
$message->embed('path/to/attachment.jpg');
如果你已经在内存中有了数据,并且不想创建额外的文件,你可以使用attachData或embedData方法,如下所示:
-
$message->attachData($data, 'attachment.txt'); -
$message->embedData($data, 'attachment.jpg');
嵌入通常使用图像文件完成,你可以在消息体内部直接使用embed或embedData方法,如下面的代码片段所示:
<p>Product Screenshot:</p>
<p>{!! $message->embed('screenshot.jpg') !!}</p>
使用 Carbon 简化日期和时间处理
Laravel 捆绑 Carbon (github.com/briannesbitt/Carbon),它通过添加更多富有表现力的方法扩展并增强了 PHP 的本地DateTime对象。Laravel 主要使用它来为 Eloquent 对象的日期和时间属性(created_at、updated_at和deleted_at)提供更多富有表现力的方法。然而,由于库已经存在,不利用它在应用程序的其他代码部分中就显得有些可惜了。
实例化 Carbon 对象
Carbon 对象旨在像正常的DateTime对象一样进行实例化。然而,它们确实支持一些更多富有表现力的方法:
-
Carbon 对象可以使用默认构造函数进行实例化,该构造函数将使用当前日期和时间,如下所示:
$now = new Carbon();
-
它们可以使用给定时区的当前日期和时间进行实例化,如下所示:
$jetzt = new Carbon('Europe/Berlin');
-
它们可以使用以下富有表现力的方法进行实例化:
-
$yesterday = Carbon::yesterday(); -
$demain = Carbon::tomorrow('Europe/Paris');
-
-
它们可以使用精确参数进行实例化,如下所示:
-
Carbon::createFromDate($year, $month, $day, $tz); -
Carbon::createFromTime($hour, $minute, $second, $tz); -
Carbon::create($year, $month, $day, $hour, $minute, $second, $tz);
-
输出用户友好的时间戳
我们可以使用diffForHumans()方法生成人类可读的相对时间戳,例如5 分钟前、上周或一年后,如下所示:
$post = App\Post::find(123);
echo $post->created_at->diffForHumans();
布尔方法
碳也提供了一些简单且富有表现力的方法,这些方法在你的控制器和视图中会很有用:
-
$date->isWeekday(); -
$date->isWeekend(); -
$date->isYesterday(); -
$date->isToday(); -
$date->isTomorrow(); -
$date->isFuture(); -
$date->isPast(); -
$date->isLeapYear();
Carbon for Eloquent DateTime properties
要能够在数据库中存储为DATE或DATETIME类型的属性上调用 Carbon 的方法,你需要在模型中列出它们在$dates属性中:
class Post extends Model {
// ...
protected $dates = [
'published_at',
'deleted_at',
];
}
你不需要包含created_at或updated_at,因为这些属性会自动被视为日期。
不要再等待队列了
队列允许你延迟函数的执行而不阻塞脚本。它们可以用来运行各种函数,从给大量用户发邮件到生成 PDF 报告。
Laravel 5 与以下队列驱动程序兼容:
-
使用
pda/pheanstalk包的 Beanstalkd -
使用
aws/aws-sdk-php包的 Amazon SQS -
IronMQ,使用
iron-io/iron_mq包
每个队列系统都有其优点。Beanstalkd 可以安装在自己的服务器上;Amazon SQS 可能更经济实惠且维护成本更低,IronMQ 也是如此,它也是基于云的。后者还允许您设置推送队列,如果您无法在服务器上运行后台作业,这会非常好。
创建一个命令并将其推送到队列中
作业以命令的形式出现。命令可以是自我处理的,也可以不是。在后一种情况下,相应的处理器类将取命令类中的数据并对其采取行动。
命令类位于app/Commands目录中,命令处理器类可以在app/Handlers/Commands目录中找到。可以使用 Artisan 命令生成命令及其处理器的类,如下所示:
$ php artisan make:command CommandName --handler --queued
--handler选项告诉 Artisan 创建一个处理器类(省略此选项将仅创建一个自我处理的命令类),而--queued选项指定应将其添加到队列中,而不是同步处理。
然后,您可以使用Queue外观将命令添加到队列中:
Queue::push(new SendConfirmationEmail($order));
或者,您可以使用命令总线来调度命令。命令总线默认在控制器中使用DispatchesCommands特质设置。这意味着在您的控制器操作中,您可以使用dispatch方法:
public function purchase(Product $product)
{
// Create order
$this->dispatch(new SendConfirmationEmail($order));
}
命令是包含执行动作所需数据的简单类——处理器随后在稍后的阶段使用命令提供的数据执行实际处理。一个例子是在下单后发送确认电子邮件。此命令看起来如下所示:
<?php namespace App\Commands;
use App\Order;
use Illuminate\Contracts\Queue\ShouldBeQueued;
use Illuminate\Queue\InteractsBeQueued;
use Illuminate\Queue\SerializesModels;
class SendConfirmationEmail extends Command implements ShouldBeQueued {
use InteractsWithQueue, SerializesModels;
public $order;
public function __construct(Order $order) {
$this->order = $order;
}
}
当队列执行处理器时,将执行实际发送电子邮件的操作,将订单传递给电子邮件模板以显示客户购买详情,如下所示:
<?php namespace App\Handlers\Commands;
use App\Commands\SendConfirmationEmail;
use Illuminate\Contracts\Mail\Mailer;
use Illuminate\Queue\InteractsWithQueue;
class SendConfirmationEmailHandler {
public function __construct(Mailer $mail) {
$this->mail = $mail;
}
public function handle(SendConfirmationEmail $command) {
$order = $command->order;
$data = compact('order');
$this->mail->send('emails.order.confirmation', $data, function($message) use ($order) {
$message->subject('Your order confirmation');
$message->to(
$order->customer->email,
$order->customer->name
);
});
}
}
由于命令处理器是通过服务容器解析的,因此我们可以进行类型提示依赖项。在前面的例子中,我们需要邮件服务,因此我们类型提示合约以获取实现。然后我们可以使用邮件服务,通过从命令类接收到的订单数据向客户发送电子邮件。
注意
从 Laravel 5.1 开始,app/Commands目录将被重命名为app/Jobs,以表明它主要用于队列作业。
监听队列并执行作业
以下是用作监听队列和执行作业的函数:
-
我们可以如下监听默认队列:
$ php artisan queue:listen -
我们可以指定要监听的连接,如下所示:
$ php artisan queue:listen connection -
我们可以按优先级顺序指定多个连接,如下所示:
$ php artisan queue:listen important,not-so-important
queue:listen命令必须在后台运行,以便处理从队列中到达的作业。为了确保它永久运行,您必须使用进程控制系统,如forever (github.com/nodejitsu/forever)或supervisor (supervisord.org/)。
当作业失败时接收通知
要在作业失败时接收通知,我们使用以下函数和命令:
-
以下事件监听器用于查找失败的作业:
Queue::failing(function($job, $data) { // Send email notification }); -
任何失败的作业都可以存储在数据库表中,并使用以下命令查看:
$ php artisan queue:failed-table // Create the table $ php artisan queue:failed // View the failed jobs
没有后台进程的队列
推送队列不需要后台进程,但它们只与iron.io驱动程序一起工作。推送队列在接收到作业时将调用应用程序中的端点,而不是由持续运行的工人进程处理的队列。如果您无法定义在应用程序服务器上运行的过程(例如在共享主机套餐中),这将非常有用。在iron.io上注册账户并将凭据添加到app/config/queue.php后,您可以通过定义一个接收所有传入作业的POST路由来使用它们。此路由调用Queue::marshal(),这是负责触发正确作业处理程序的方法:
Route::post('queue/receive', function() {
return Queue::marshal();
});
此路由需要使用queue:subscribe命令注册为订阅者:
$ php artisan queue:subscribe queue_name http://yourapp.example.com/queue/receive
一旦 URL 在iron.io/上订阅,任何使用Queue::push()创建的新作业将通过POST请求从 Iron 发送回您的应用程序。
接下来去哪里?
以下是可以访问的资源站点列表,您可以访问它们以了解 Laravel 的最新更改:
-
在 Twitter 上关注
twitter.com/laravelphp以获取定期更新 -
Laravel 完整文档以获取更多信息
-
Laravel API 浏览器以查看可浏览的 API




浙公网安备 33010602011771号