精通-Laravel(全)
精通 Laravel(全)
原文:
zh.annas-archive.org/md5/d10bf45da1cebf8f2b06a9600172079d
译者:飞龙
前言
PHP 是一种免费开源的编程语言,正在持续复兴,而 Laravel 处于前沿。Laravel 5 被证明是最适合新手和专家程序员的可用框架。遵循现代 PHP 的面向对象最佳实践,可以减少上市时间,并构建强大的 Web 和 API 驱动的移动应用程序,可以自动测试和部署。
您将学习如何使用 Laravel 5 PHP 框架快速开发软件应用程序。
这本书涵盖了什么
第一章,使用 phpspec 进行正确设计,讲述了如何配置 Laravel 5 以使用 phpspec 进行现代单元测试,如何使用 phpspec 设计类,以及执行单元和功能测试。
第二章,自动化测试-迁移和填充数据库,涵盖了数据库迁移,其背后的机制以及如何为测试创建种子。
第三章,构建服务、命令和事件,讨论了 Model-View-Controller 以及它如何演变为服务、命令和事件,以解耦代码并实践关注点分离。
第四章,创建 RESTful API,带您了解如何创建 RESTful API:基本的 CRUD 操作(创建、读取、更新和删除),以及讨论一些最佳实践和超媒体控制(HATEOAS)。
第五章,使用表单生成器,带您进入 Web 界面的一面,展示如何利用 Laravel 5 的一些最新功能来创建 Web 表单。这里还将讨论反向路由。
第六章,使用注解驯服复杂性,专注于注解。当应用程序变得复杂时,routes.php
文件很容易变得混乱。在控制器内部使用注解,可以大大提高代码的可读性;然而,除了优点之外,还存在一些缺点。
第七章,使用中间件过滤请求,向您展示如何创建可在控制器之前或之后调用的可重用过滤器。
第八章,使用 Eloquent ORM 查询数据库,帮助您学习如何以一种方式使用 ORM 来减少编码错误的概率,增加安全性并减少 SQL 注入的可能性,以及学习如何处理 Eloquent ORM 的限制。
第九章,扩展 Laravel,讲述了如何将应用程序扩展到基于云的架构。讨论了读写主/从配置,并引导读者进行配置。
第十章,使用 Elixir 构建、编译和测试,介绍了 Elixir。Elixir 基于 gulp,是一个任务运行器,是一系列构建脚本,可以自动化 Laravel 软件开发工作流程中的常见任务。
这本书需要什么
我们需要以下软件:
-
Apache/Nginx
-
PHP 5.4 或更高版本
-
MySQL 或类似软件
-
Composer
-
phpspec
-
Node.js
-
npm
这本书适合谁
如果您是一位经验丰富的新手或者是一位有能力的 PHP 程序员,对现代 PHP(至少版本 5.4)的概念有基本的了解,那么这本书非常适合您。
需要基本的面向对象编程和数据库知识。您应该已经熟悉 Laravel,或者至少已经尝试过这个框架。
约定
在本书中,您会发现一些文本样式,用于区分不同类型的信息。以下是一些示例以及它们的含义解释。
文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名显示如下:“新的artisan
命令如下运行”
代码块设置如下:
protected function schedule(Schedule $schedule)
{
$schedule->command('inspire')
->hourly();
$schedule->command('manage:waitinglist')
->everyFiveMinutes();
}
任何命令行输入或输出都是这样写的:
**$ php artisan schedule:run**
新术语和重要单词以粗体显示。您在屏幕上看到的单词,例如菜单或对话框中的单词,会以这种方式出现在文本中:“如下截图所示,迁移表现在这里。”
注意
警告或重要说明会出现在这样的框中。
提示
提示和技巧会出现在这样。
第一章:使用 phpspec 正确设计
自 2011 年 Laravel 谦虚的开始以来,发生了许多事情。Taylor Otwell,一名.NET 程序员,寻求使用 PHP 来进行一项副业项目,因为他被告知托管 PHP 便宜且无处不在。最初作为 CodeIgniter 的扩展开始,最终成为自己的代码。将代码库从 CodeIgniter 的 PHP 5.2 的限制中释放出来,可以使用 PHP 5.3 提供的所有新功能,如命名空间和闭包。版本 1 和 3 之间的时间跨度仅为一年。版本 3 后,事情发生得非常迅速。在其爆炸式的流行之后,即版本 4 发布时,它迅速开始从其他流行框架(如 CodeIgniter、Zend、Symfony、Yii 和 CakePHP)那里夺取市场份额,最终占据了领先地位。除了其表达性语法、出色的文档和充满激情的创始人外,还有大型社区的主要支柱 IRC 和 Slack 聊天室、Laravel 播客和 Laracasts 教学视频网站。此外,新创建的商业支持,如提供100%正常运行时间的 Envoyer,也意味着 Laravel 也受到了企业的欢迎。随着 Laravel 4.2 的发布,最低要求的 PHP 版本提高到了 5.4,以利用现代 PHP 特性,如traits。
使用 Laravel 的特性以及新的语法,比如[]数组快捷方式,使编码变得轻松。Laravel 的表达性语法,再加上这些现代 PHP 特性,使它成为任何希望构建强大应用的开发者的绝佳选择。
Laravel 在 Google 趋势报告中的成功崛起
一个新时代
2014 年底,Laravel 历史上第二个最重要的时刻发生了。原定的 4.3 版本改变了许多 Laravel 的核心原则,社区决定将其成为 5.0 版本。
Laravel 5 的到来带来了许多在构建软件时使用它的方式的变化。从诸如 CodeIgniter 等框架继承的内置 MVC 架构已被放弃,以更具动态性、模块化甚至大胆的框架不可知性为代价。许多组件已尽可能解耦。Laravel 历史上最重要的部分将是 Laravel 5.1 版本的到来,它将有长期支持(LTS)。因此,Laravel 在企业中的地位将更加稳固。此外,最低的 PHP 要求将更改为 5.5 版本。因此,对于任何新项目,建议使用 PHP 5.5,甚至 PHP 5.6,因为升级到 PHP 7 版本将更加容易。
一个更精简的应用程序
/app
目录变得更加精简,只留下了应用程序中最基本的部分。诸如config
、database
、storage
和tests
等目录已经从app
目录中移出,因为它们是辅助应用程序本身的。最重要的是,测试工具的集成已经大大成熟。
PSR
由于框架互操作性组(PHP-FIG)的努力,PHP 标准推荐(PSR)的开发者,框架代码的阅读、编写和格式化变得更加容易。它甚至允许开发者更容易地在多个框架中工作。Laravel 是 FIG 的一部分,并继续将其建议纳入框架中。例如,Laravel 5.1 将采用 PSR-2 标准。有关 PHP FIG 和 PSR 的更多信息,请访问 PHP-FIG 网站www.php-fig.org
。
安装和配置 Laravel
安装 Laravel 的最新更新说明始终可以在 Laravel 网站laravel.com
找到。要在开发环境中开始使用 Laravel,当前的最佳实践建议使用以下方法:
-
Vagrant:这提供了一种方便的方式来管理虚拟机,如 Virtualbox。
-
PuPHPet:这是一个可以用来创建各种类型虚拟机的优秀工具。有关 PuPHPet 的更多信息,请访问
puphpet.com
。 -
Phansible:这是 PuPHPet 的另一种选择。有关 Phansible 的信息,请访问
phansible.com
。 -
Homestead:这是由 Laravel 社区维护的,是专门为 Laravel 创建的虚拟机,使用的是 NGINX 而不是 Apache。有关 Homestead 的更多信息,请访问
github.com/laravel/homestead
。
安装
基本过程涉及下载和安装 Composer,然后将 Laravel 添加为依赖项。一个重要的细节是,存储目录,它位于/app
目录的平行位置,需要以可写的方式设置,以便允许 Laravel 5 执行诸如写日志文件之类的操作。还很重要的是确保使用$ php artisan key:generate
生成一个用于哈希的 32 字符密钥,因为自 PHP 5.6 发布以来,Mcrypt 对其要求更为严格。对于 Laravel 5.1,OpenSSL 将取代 Mcrypt。
配置
在 Laravel 4 中,环境是以服务器或开发机器的主机名配置的,这相当牵强。相反,Laravel 5 使用一个.env
文件来设置各种环境。该文件包含在.gitignore
中。因此,每台机器都应该从源代码控制之外的源接收其配置。
因此,例如,可以使用以下代码来设置本地开发:
APP_ENV=local
APP_DEBUG=true
APP_KEY=SomeRandomString
DB_HOST=localhost
DB_DATABASE=example
DB_USERNAME=DBUser
DB_PASSWORD=DBPass
CACHE_DRIVER=file
SESSION_DRIVER=file
命名空间
Laravel 的一个很好的新功能是,它允许您将最高级别的命名空间设置为诸如MyCompany
之类的内容,通过app:name
命令。这个命令实际上会将/app
目录中所有相关文件的命名空间从 App 更改为MyCompany
,例如。然后,这个命名空间存在于/app
目录中。这将命名空间化到几乎每个文件中,而在之前的 4.x 版本中,这是可选的。
正确的 TDD
测试驱动开发的文化并不新鲜。相反,甚至在肯特·贝克(Kent Beck)在 1990 年代编写 SUnit 之前就已经存在。源自 SUnit 的 xUNIT 系列单元测试框架已经发展成为为 PHP 提供测试解决方案。
PHPUnit
PHP 端口的 PHP 测试软件名为 PHPUnit。然而,在 PHP 语言中进行测试驱动开发是一个相当新的概念。例如,在他的书《The Grumpy Programmer's Guide To Building Testable PHP Applications》中,Chris Hartjes在 2012 年底出版,写道“我开始研究围绕 CodeIgniter 的测试文化。它比新生儿还弱。”
自 Laravel 3 版本以来,测试一直是 Laravel 框架的一部分,使用 PHPUnit 单元测试工具,因此 Laravel 包含phpunit.xml
文件是在努力鼓励开发人员接受测试驱动开发的努力中迈出的重要一步。
phpspec
另一个测试工具 RSpec 在 2007 年出现在 Ruby 社区,并对测试驱动开发进行了改进。它具有行为驱动开发(BDD)。phpspec 工具将 RSpec 的 BDD 移植到 PHP 中,正在迅速增长。它的共同创始人 Marcello Duarte 多次表示“BDD 是正确的 TDD”。因此,BDD 只是对 TDD 的改进或演变。Laravel 5 现在巧妙地将 phpspec 包含为一种突出按规范设计行为驱动开发范式的方式。
由于在构建 Laravel 5 应用程序的基本步骤是指定要创建的实体,因此在安装和配置 Laravel 5 后,开发人员可以立即通过运行 phpspec 作为设计工具开始设计。
实体创建
让我们创建一个示例 Web 应用程序。如果客户要求我们为旅游结构构建预订系统,那么系统可能包含诸如住宿(例如酒店和早餐客栈)、房间、价格和预订等实体。
简化的数据库架构如下所示:
MyCompany 数据库架构
数据库架构有以下假设:
-
一个住宿有很多房间
-
预订仅适用于单个用户
-
预订可能包括多个房间
-
预订有一个开始日期和一个结束日期
-
价格从开始日期到结束日期对一个房间有效
-
一个房间有很多设施
-
预订的开始日期必须在结束日期之前
-
预订不能超过十五天
-
预订不能包括超过四个房间
使用 phpspec 进行设计
现在,让我们开始使用 phpspec 作为设计工具来构建我们的实体。
如果顶级命名空间是MyCompany
,那么使用 phpspec,只需输入以下命令:
**# phpspec describe MyCompany/AccommodationRepository**
在输入上述命令后,将创建spec/AccommodationSpecRepository.php
:
<?php
namespace spec\MyCompany;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class AccommodationRepositorySpec extends ObjectBehavior
{
function it_is_initializable()
{
$this->shouldHaveType('MyCompany\AccommodationRepository');
}
<?php
namespace MyCompany;
class AccommodationRepository
{
}
提示
应将 phpspec 的路径添加到.bashrc
或.bash_profile
文件中,以便可以直接运行 phpspec。
然后,输入以下命令:
**# phpspec run**
在输入上述命令后,开发人员将显示如下:
**class MyCompany\AcccommodationRepository does not exist.**
**Do you want me to create 'MyCompany\AccommodationRepository' for you? [Y/n]**
输入Y后,将创建AccommodationRepository.php
类,如下所示:
<?php
namespace MyCompany;
class AccommodationRepository
{}
提示
下载示例代码
您可以从www.packtpub.com
的帐户中下载示例代码文件,用于您购买的所有 Packt Publishing 图书。如果您在其他地方购买了本书,您可以访问www.packtpub.com/support
并注册,以便直接通过电子邮件接收文件。
phpspec 的美妙之处在于其简单性和加速类的创建,这些类与规范一起。
使用 phpspec 描述和创建类的基本步骤
使用 phpspec 进行规范说明
phpspec 的核心在于允许我们指定实体的行为并同时对其进行测试。通过简单地指定客户给出的业务规则,我们可以轻松为每个业务规则创建测试。然而,phpspec 的真正力量在于它如何使用表达自然语言的语法。让我们来看看之前给我们关于预订的业务规则:
-
预订的开始日期必顶在结束日期之前
-
预订不能超过十五天
-
预订不能包括超过四个房间
运行以下命令:
**# phpspec describe**
**MyCompany/Accommodation/ReservationValidator**
phpspec 将为上述命令产生以下输出:
<?php
namespace spec\MyCompany\Accommodation;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class ReservationSpec extends ObjectBehavior
{
function it_is_initializable()
{
$this->shouldHaveType('MyCompany\Accommodation\Reservation');
}
}
然后,使用以下命令运行 phpspec:
**# phpspec run**
phpspec 将像往常一样以以下输出做出响应:
**Do you want me to create**
**'MyCompany\Accommodation\ReservationValidator' for you?**
然后,phpspec 将创建ReservationValidator
类,如下所示:
<?php namespace MyCompany\Accommodation;
class ReservationValidator {
}
让我们创建一个validate()
函数,它将采用以下参数:
-
确定预订开始的开始日期字符串
-
确定预订结束的结束日期字符串
-
要添加到预订的
room
对象数组
以下是创建validate()
函数的代码片段:
<?php
namespace MyCompany\Accommodation;
use Carbon\Carbon;
class ReservationValidator
{
public function validate($start_date, $end_date, $rooms)
{
}
}
我们将包括Carbon
类,这将帮助我们处理日期。对于第一个业务规则,即预订的开始日期必须在结束日期之前,我们现在可以在ReservationValidatorSpec
类中创建我们的第一个规范方法,如下所示:
function its_start_date_must_come_before_the_end_date ($start_date,$end_date,$room)
{
$rooms = [$room];
$start_date = '2015-06-03';
$end_date = '2015-06-03';
$this->shouldThrow('\InvalidArgumentException')->duringValidate( $start_date, $end_date, $rooms);
}
在前面的函数中,phpspec 以it
或its
开始规范。phpspec 使用蛇形命名法以提高可读性,而start_date_must_be_less_than_the_end_date
则是规范的精确副本。这不是很棒吗?
当传入$start_date
,$end_date
和room
时,它们会自动被模拟。不需要其他任何东西。我们将创建一个有效的$rooms
数组。然而,我们将设置$start_date
和$end_date
,使它们具有相同的值,以导致测试失败。表达式语法如前面的代码所示。shouldThrow
出现在during
之前,然后采用方法名Validate
。
我们已经给了 phpspec 自动为我们创建validate()
方法所需的东西。我们将指定$this
,即ReservationValidator
类,将抛出InvalidArgumentException
。运行以下命令:
**# phpspec run**
再次,phpspec 问我们以下问题:
**Do you want me to create 'MyCompany\Accommodation\Reservation::validate()'**
**for you?**
只需在提示处简单地输入Y,方法就会在ReservationValidator
类中创建。就是这么简单。当再次运行 phpspec 时,它会因为方法尚未抛出异常而失败。所以现在需要编写代码。在函数内部,我们将从格式为"2015-06-02"的字符串创建两个Carbon
对象,以便能够利用 Carbon 强大的日期比较功能。在这种情况下,我们将使用$date1->diffInDays($date2);
方法来测试$end
和$start
之间的差异是否小于一。如果是这样,我们将抛出InvalidArgumentException
并显示用户友好的消息。现在,当我们重新运行 phpspec 时,测试将通过:
$end = Carbon::createFromFormat('Y-m-d', $end_date);
$start = Carbon::createFromFormat('Y-m-d', $start_date);
if ($end->diffInDays($start)<1) {
throw new \InvalidArgumentException('Requires end date to be greater than start date.');
}
红,绿,重构
测试驱动开发的规则要求红,绿,重构,这意味着一旦测试通过(绿色),我们应该尝试重构或简化方法内的代码,而不改变功能。
看一下if
测试:
if ( $end->diffInDays($start) < 1 ) {
前面的代码不太可读。我们可以以以下方式重构它:
if (!$end->diffInDays($start)>0)
然而,即使前面的代码也不太易读,我们还在代码中直接使用整数。
将0
移入一个常量中。为了提高可读性,我们将其更改为预订所需的最少天数,如下所示:
const MINIMUM_STAY_LENGTH = 1;
让我们将比较提取到一个方法中,如下所示:
/**
* @param $end
* @param $start
* @return bool
*/
private function endDateIsGreaterThanStartDate($end, $start)
{
return $end->diffInDays($start) >= MINIMUM_STAY_LENGTH;
}
我们现在可以这样写if
语句:
if (!$this->endDateIsGreaterThanStartDate($end, $start))
前面的陈述更加表达和可读。
现在,对于下一个规则,即预订不能超过十五天,我们需要以以下方式创建方法:
function it_cannot_be_made_for_more_than_fifteen_days(User $user, $start_date, $end_date, Room $room)
{
$start_date = '2015-06-01';
$end_date = '2015-07-30';
$rooms = [$room];
$this->shouldThrow('\InvalidArgumentException')
->duringCreateNew( $user,$start_date,$end_date,$rooms);
}
在这里,我们设置$end_date
,使其被分配一个比$start_date
晚一个月以上的日期,以导致方法抛出InvalidArgumentException
。再次执行phpspec
命令后,测试将失败。让我们修改现有方法来检查日期范围。我们将向方法添加以下代码:
if ($end->diffInDays($start)>15) {
throw new \InvalidArgumentException('Cannot reserve a room
for more than fifteen (15) days.');
}
再次,phpspec 愉快地成功运行所有测试。重构后,我们将再次提取if
条件并创建常量,如下所示:
const MAXIMUM_STAY_LENGTH = 15;
/**
* @param $end
* @param $start
* @return bool
*/
private function daysAreGreaterThanMaximumAllowed($end, $start)
{
return $end->diffInDays($start) > self::MAXIMUM_STAY_LENGTH;
}
if ($this->daysAreGreaterThanMaximumAllowed($end, $start)) {
throw new \InvalidArgumentException ('Cannot reserve a room for more than fifteen (15) days.');
}
整理一下
我们可以把事情留在这里,但是让我们清理一下,因为我们有测试。由于endDateIsGreaterThanStartDate($end, $start)
和daysAreGreaterThanMaximumAllowed($end, $start)
函数分别检查最小和最大允许的停留时间,我们可以从另一个方法中调用它们。
我们将endDateIsGreaterThanStartDate()
重构为daysAreLessThanMinimumAllowed($end, $start)
,然后创建另一个方法来检查最小和最大停留长度,如下所示:
private function daysAreWithinAcceptableRange($end, $start)
{
if ($this->daysAreLessThanMinimumAllowed($end, $start)
|| $this->daysAreGreaterThanMaximumAllowed($end, $start)) {
return false;
} else {
return true;
}
}
这样我们只剩下一个函数,而不是两个,在createNew
函数中,如下所示:
if (!$this->daysAreWithinAcceptableRange($end, $start)) {
throw new \InvalidArgumentException('Requires a stay length from '
. self::MINIMUM_STAY_LENGTH . ' to '. self::MAXIMUM_STAY_LENGTH . ' days.');
}
对于第三条规则,即预订不能包含超过四个房间,流程是一样的。创建规范,如下:
it_cannot_contain_than_four_rooms
这里的改变将在参数中。这次,我们将模拟五个房间,以便测试失败,如下所示:
function it_cannot_contain_than_four_rooms(User $user, $start_date, $end_date, Room $room1, Room $room2, Room $room3, Room $room4, Room $room5)
五个房间对象将被加载到$rooms
数组中,测试将会失败,如下所示:
$rooms = [$room1, $room2, $room3, $room4, $room5];
$this->shouldThrow('\InvalidArgumentException')->duringCreateNew($user,$start_date,$end_date,$rooms);
}
在添加代码以检查数组大小后,最终类将如下所示:
<?php
namespace MyCompany\Accommodation;
use Carbon\Carbon;
class ReservationValidator
{
const MINIMUM_STAY_LENGTH = 1;
const MAXIMUM_STAY_LENGTH = 15;
const MAXIMUM_ROOMS = 4;
/**
* @param $start_date
* @param $end_date
* @param $rooms
* @return $this
*/
public function validate($start_date, $end_date, $rooms)
{
$end = Carbon::createFromFormat('Y-m-d', $end_date);
$start = Carbon::createFromFormat('Y-m-d', $start_date);
if (!$this->daysAreWithinAcceptableRange($end, $start)) {
throw new \InvalidArgumentException('Requires a stay length from '
. self::MINIMUM_STAY_LENGTH . ' to '. self::MAXIMUM_STAY_LENGTH . ' days.');
}
if (!is_array($rooms)) {
throw new \InvalidArgumentException('Requires last parameter rooms to be an array.');
}
if ($this->tooManyRooms($rooms)) {
throw new \InvalidArgumentException('Cannot reserve more than '. self::MAXIMUM_ROOMS .' rooms.');
}
return $this;
}
/**
* @param $end
* @param $start
* @return bool
*/
private function daysAreLessThanMinimumAllowed($end, $start)
{
return $end->diffInDays($start) < self::MINIMUM_STAY_LENGTH;
}
/**
* @param $end
* @param $start
* @return bool
*/
private function daysAreGreaterThanMaximumAllowed($end, $start)
{
return $end->diffInDays($start) > self::MAXIMUM_STAY_LENGTH;
}
/**
* @param $end
* @param $start
* @return bool
*/
private function daysAreWithinAcceptableRange($end, $start)
{
if ($this->daysAreLessThanMinimumAllowed($end, $start)
|| $this->daysAreGreaterThanMaximumAllowed($end, $start)) {
return false;
} else {
return true;
}
}
/**
* @param $rooms
* @return bool
*/
private function tooManyRooms($rooms)
{
return count($rooms) > self::MAXIMUM_ROOMS;
}
public function rooms(){
return $this->belongsToMany('MyCompany\Accommodation\Room')->withTimestamps();
}
}
这种方法非常干净。只有两个if
语句——第一个用于验证日期范围是否有效,另一个用于验证房间数量是否在有效范围内。常量很容易访问,并且可以根据业务需求进行更改。显然,将 phpspec 添加到开发工作流程中,将之前需要两个步骤——使用 PHPUnit 编写断言,然后编写代码——合并在一起。现在,我们将离开 phpspec,转而使用 Artisan,开发人员对此很熟悉,因为它是 Laravel 先前版本的一个特性。
控制器
接下来,我们将创建一些示例控制器。在撰写本书时,我们需要同时使用 Artisan 和 phpspec。让我们为room
实体创建一个控制器,如下所示:
$ php artisan make:controller RoomController
<?php namespace MyCompany\Http\Controllers;
use MyCompany\Http\Requests;
use MyCompany\Http\Controllers\Controller;
use Illuminate\Http\Request;
class RoomController extends Controller {
/**
* Display a listing of the resource.
*
* @return Response
*/
public function index()
{}
/**
* Show the form for creating a new resource.
*
* @return Response
*/
public function create()
{}
/**
* Store a newly created resource in storage.
*
* @return Response
*/
public function store()
{}
….
}
注意
请注意,这将在app/Http/Controllers
目录中创建,这是 Laravel 5 的新位置。新的 HTTP 目录包含控制器、中间件和请求目录,将与 HTTP 请求或实际请求相关的文件分组在一起。此外,此目录配置是可选的,路由可以调用任何自动加载的位置,通常通过命名空间 PSR-4 结构。
命令总线
Laravel 5 采用了命令总线模式,创建的命令存储在app/Commands
目录中。而在 Laravel 4 中,命令被认为是命令行工具,而在 Laravel 5 中,命令被认为是一个类,其方法可以在应用程序内部使用,从而实现代码的优秀重用。这里的命令概念是需要完成的任务,或者在我们的例子中,是为用户预订的房间。总线的范式然后使用新的DispatchesCommands
特性传输命令,该特性用于基本控制器类中。Artisan 创建的每个控制器都扩展了这个类到一个处理程序方法,实际工作在其中执行。
为了使用 Laravel 的命令总线设计模式,我们现在将使用 Artisan 创建一些命令。我们将在未来的章节中详细介绍命令,但首先,我们将输入以下命令:
**$ php artisan make:commandReserveRoomCommand --handler**
输入此命令将创建一个用于预订房间的命令,可以从代码的任何位置调用,将业务逻辑与控制器和模型隔离,并允许以异步模式执行命令。
<?php namespace MyCompany\Commands;
use MyCompany\Commands\Command;
class ReserveRoomCommand extends Command {
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
//
}
}
填写完命令的细节后,该类现在看起来是这样的:
<?php namespace MyCompany\Commands;
use MyCompany\Commands\Command;
use MyCompany\User;
class ReserveRoomCommand extends Command {
public $user;
public $rooms;
public $start_date;
public $end_date;
/**
* Create a new command instance.
*
* @return void
*/
public function __construct(User $user, $start_date, $end_date, $rooms)
{
$this->rooms = $rooms;
$this->user = $user;
$this->start_date = $start_date;
$this->end_date = $end_date;
}
}
--handler
参数创建了一个额外的类ReserveRoomCommandHandler
,其中包含一个构造函数和一个 handle 方法,该方法注入了ReserveRoomCommand
。此文件将存在于app/Handlers/Commands
目录中。如果未使用--handler
标志,则ReserveRoomCommand
类将包含自己的handler
方法,并且不会创建单独的处理程序类:
<?php namespace MyCompany\Handlers\Commands;
use MyCompany\Commands\ReserveRoomCommand;
use Illuminate\Queue\InteractsWithQueue;
class ReserveRoomCommandHandler {
/**
* Create the command handler.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Handle the command.
*
* @paramReserveRoomCommand $command
* @return void
*/
public function handle(ReserveRoomCommand $command)
{
//
}
}
我们将填写处理预订验证的 handle 方法,如下所示:
public function handle(ReserveRoomCommand $command)
{
$reservation = new \MyCompany\Accommodation\ReservationValidator();
$reservation->validate(
$command->start_date, $command->end_date, $command->rooms);
}
总结
phpspec 为软件的业务逻辑方面添加了成熟、健壮、测试驱动和示例驱动的规范方法。再加上模型、控制器、命令、事件和事件处理程序的轻松创建,使得 Laravel 成为 PHP 框架竞争中的佼佼者。此外,它还采用了许多行业最佳程序员使用的最佳实践。
在本章中,我们学习了如何使用 phpspec 轻松地从命令行设计类及其相应的测试。这种工作流程,加上 Artisan,使得设置 Laravel 5 应用程序的基本结构变得非常容易。
在下一章中,我们将介绍数据库迁移、其背后的机制以及创建用于测试的种子的方法。
第二章:自动化测试-迁移和种子数据库
到目前为止,我们已经创建了一些基本模型和数据库的概要。现在,我们需要创建数据库迁移和种子。传统上,数据库“dump”文件被用作传递表结构和数据的方式,包括初始或预定义记录,如默认值;不变的列表,如城市或国家;以及用户,如“admin”。这些包含 SQL 的转储文件可以提交到源代码控制。这并不总是维护数据库完整性的最佳方式;因为每当开发人员添加记录或修改数据库时,团队中的所有开发人员都需要手动添加或删除数据、表、行、列或索引,或者删除并重新创建数据库。迁移允许数据库以代码形式存在,实际上驻留在 Laravel 项目内,并在源代码控制中进行版本控制。
迁移是从命令行运行的,也可以自动化,以在需要时自动创建数据库(如果不存在),或删除并重新创建表并填充表(如果已存在)。迁移在 Laravel 中已经存在一段时间,因此它们在 Laravel 5 中的存在并不令人惊讶。
使用 Laravel 的迁移功能
第一步是运行artisan
命令:
**$ php artisan migrate:install**
这将创建一个名为migration
的表,其中包含两列:migration
是 MySQL 中的 varchar 255,batch
是整数。这个表将被 Laravel 用来跟踪已运行的迁移。换句话说,它维护了所有已执行操作的历史记录。以下是主要操作的列表:
-
install
:如前所述,此操作安装 -
refresh
:此操作重置并重新运行所有迁移 -
reset
:此操作回滚所有迁移 -
rollback
:此操作是一种“撤消”类型,只是回滚上一个操作 -
status
:此操作生成迁移的类似表格的输出,并指出它们是否已运行
迁移示例
Laravel 5 在/database/migrations
目录中包含两个迁移。
第一个迁移创建了users
表。
第二个创建password_resets
表,正如你可能已经猜到的,用于恢复丢失的密码。除非指定,迁移操作的是在/config/database.php
配置文件中配置的数据库:
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateUsersTable extends Migration {
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('users', function(Blueprint $table)
{
$table->smallIncrements('id')->unsigned();
$table->string('name');
$table->string('email')->unique();
$table->string('password', 60);
$table->rememberToken();
$table->timestamps();
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('users');
}
}
迁移扩展了Migration
类并使用Blueprint
类。
有两种方法:up
和down
,分别在使用migrate
命令和rollback
命令时使用。Schema::create()
方法以表名作为第一个参数调用,并以函数回调作为第二个参数,接受Blueprint
对象的实例作为参数。
创建表
$table
对象有一些方法,执行任务,如创建索引,设置自增字段,指定应创建的字段类型,并将字段名称作为参数传递。
第一个命令用于创建自增字段id
,这将是表的主键。然后,创建字符串字段,如name
、email
和password
。请注意,unique
方法链接到email
字段的create
语句,说明email
字段将用作登录名/用户 ID,这是大多数现代 Web 应用程序的常见做法。rememberToken
用于允许用户在每个会话中保持身份验证。此令牌在每次登录和注销时重置,保护用户免受潜在的恶意劫持尝试。
Laravel 迁移魔法
Laravel 迁移还能够创建时间戳字段,用于自动存储每个模型的创建和更新信息。
$table->timestamps();
以下代码告诉迁移自动在表中创建两列,即 created_at
和 updated_at
,这是 Laravel 的 Eloquent 对象关系映射 (ORM) 自动使用的,以便应用程序知道对象何时创建和何时更新:
$table->timestamps()
在下面的示例中,字段更新如下:
/*
* created_at is set with timestamps
*/
$user = new User();
$user->email = "johndoe@acmewidgets.com";
$user->name = "John Doe";
$user->save(); // created_at is set with timestamps
/*
* updated_at is set with timestamps
*/
$user = User::find(1); //where 1 is the $id
$user->email = "johndoe@acmeenterprise.com";
$user->save(); //updated_at is updated
另一个很棒的 Laravel 功能是软删除字段。这提供了一种回收站,允许数据在以后可选地恢复。
这个功能简单地向表中添加了另一列,以允许软删除数据。要添加到迁移中的代码如下所示:
$table->softDeletes();
这在 数据库, deleted_at,
中添加了一列,它的值可以是 null
,也可以是一个时间戳,表示记录被删除的时间。这在您的数据库应用程序中构建了一个回收站功能。
运行以下命令:
**$ php artisan migrate**
迁移已启动并创建了表。现在出现了迁移表,如下截图所示:
users
表的结构如下截图所示:
要回滚迁移,请运行以下命令:
**$ php artisan migrate:rollback**
rollback
命令使用迁移表来确定要回滚的操作。在这种情况下,运行后的 migrations
表现在是空的。
从模式到迁移
在开发过程中经常发生的一种情况是创建了一个模式,然后我们需要从该模式创建一个迁移。在撰写本文时,Laravel 核心中没有官方工具可以做到这一点,但有几个可用的包。
其中一个这样的包是 migrations-generator
包。
首先,在 composer.json
文件的 require-dev
部分中添加以下行,以在 composer.json
文件中要求 migrations-generator
依赖项:
"require-dev": {
"phpunit/phpunit": "~4.0",
"phpspec/phpspec": "~2.1",
"xethron/migrations-generator": "dev-feature/laravel-five-stable",
"way/generators": "dev-feature/laravel-five-stable"
},
还需要在根级别的 composer.json
文件中添加以下文本:
"repositories": [
{
"type": "git",
"url": "git@github.com:jamisonvalenta/Laravel-4-Generators.git"
}],
Composer 的 require-dev 命令
require-dev
命令与 require
相反,是 composer 的一种机制,允许只在开发阶段需要的某些包。大多数测试工具和迁移工具只会在本地开发机器、QA 机器和/或持续集成环境中使用,而不会在生产环境中使用。这种机制可以使您的生产安装不受不必要的包的影响。
Laravel 的提供者数组
Laravel 的 providers
数组在 config/app.php
文件中列出了 Laravel 随时可用的提供者。
我们将添加 way generator
和 Xethron migration
服务提供者:
'providers' => [
/*
* Laravel Framework Service Providers...
*/
Illuminate\Foundation\Providers\ArtisanServiceProvider::class,
Illuminate\Auth\AuthServiceProvider::class,
Illuminate\Broadcasting\BroadcastServiceProvider::class,
...
'Way\Generators\GeneratorsServiceProvider',
'Xethron\MigrationsGenerator\MigrationsGeneratorServiceProvider'
]
composer update 命令
composer update
命令是一种简单而强大的方式,确保一切都在适当的位置,并且没有错误。运行此命令后,我们现在准备运行迁移。
生成迁移
只需输入以下命令:
**$ php artisan**
artisan
命令将显示所有可能的命令列表。migrate:generate
命令应该包含在有效命令列表中。如果此命令不在列表中,则说明某些配置不正确。
确认 migrate:generate
命令存在于列表中后,只需运行以下命令:
**$ php artisan migrate:generate**
这将启动该过程。
在这个例子中,我们使用了 MySQL 数据库。在提示时输入 Y
,进程将开始,输出应该显示为数据库中的每个表创建了一个迁移文件。
这是您的命令提示符在最后应该显示的样子:
**Using connection: mysql**
**Generating migrations for: accommodations, amenities, amenity_room, cities, countries, currencies, locations, rates, reservation_room, reservations, rooms, states, users**
**Do you want to log these migrations in the migrations table? [Y/n] Y**
**Migration table created successfully.**
**Next Batch Number is: 1\. We recommend using Batch Number 0 so that it becomes the "first" migration [Default: 0]**
**Setting up Tables and Index Migrations**
**Created: /var/www/laravel.example/database/migrations/2015_02_07_170311_create_accommodations_table.php**
**Created: /var/www/laravel.example/database/migrations/2015_02_07_170311_create_amenities_table.php**
**Created: /var/www/laravel.example/database/migrations/2015_02_07_170311_create_amenity_room_table.php**
**Created: /var/www/laravel.example/database/migrations/2015_02_07_170311_create_cities_table.php**
**Created: /var/www/laravel.example/database/migrations/2015_02_07_170311_create_countries_table.php**
**Created: /var/www/laravel.example/database/migrations/2015_02_07_170311_create_currencies_table.php**
**Created: /var/www/laravel.example/database/migrations/2015_02_07_170311_create_locations_table.php**
**Created: /var/www/laravel.example/database/migrations/2015_02_07_170311_create_rates_table.php**
**Created: /var/www/laravel.example/database/migrations/2015_02_07_170311_create_reservation_room_table.php**
**Created: /var/www/laravel.example/database/migrations/2015_02_07_170311_create_reservations_table.php**
**Created: /var/www/laravel.example/database/migrations/2015_02_07_170311_create_rooms_table.php**
**Created: /var/www/laravel.example/database/migrations/2015_02_07_170311_create_states_table.php**
**Created: /var/www/laravel.example/database/migrations/2015_02_07_170311_create_users_table.php**
**Finished!**
迁移解剖
考虑迁移文件中的一行的示例;我们可以看到表对象在一系列方法中使用。迁移文件的以下行设置了位置优雅属性中的状态属性在locations
表中:
$table->smallInteger('state_id')->unsigned()->index('state_id');
列出表
通常需要创建或导入通常保持不变的有限项目列表,例如城市、州、国家和类似项目。让我们称这些列表表或查找表。在这些表中,ID 通常应为正数。这些列表可能会增长,但通常不会删除或更新任何数据。smallInteger
类型用于保持表的小型,并且表示属于有限列表的值,这些值不会自然增长。下一个方法unsigned
表示限制将为 65535。这个值应该足以表示大多数州、省或类似类型的地理区域,酒店可能位于其中。链中的最后一个方法向数据库列添加索引。这在这样的列表表中是必不可少的,这些列表表用于select
语句或read
语句中。Read
语句将在第九章扩展 Laravel中讨论。使用 unsigned 很重要,因为它将正限制加倍,否则将是 32767。使用索引,我们可以加快查找时间并访问表中数据的缓存版本。
软删除和时间戳属性
关于列表表的softDeletes
和timestamps
,这取决于。如果表不是很大,跟踪更新、插入或删除不会太有害;但是,如果列表包含国家,其中更改不经常发生且非常小,最好省略softDeletes
和timestamps
。因此,整个表可能适合内存,并且速度非常快。要省略时间戳,需要添加以下代码行:
public $timestamps = false;
创建种子
要创建我们的数据库 seeder,我们将修改扩展Seeder
的DatabaseSeeder
类。文件的名称是database/seeds/DatabaseSeeder.php
。文件的内容将如下所示:
<?php
use Illuminate\Database\Seeder;
use Illuminate\Database\Eloquent\Model;
class DatabaseSeeder extends Seeder {
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
Model::unguard();
//create a user
$user = new \MyCompany\User();
$user->id=1;
$user->email = "testing@tester.com";
$user->password = Hash::make('p@ssw0rd');
$user->save();
//create a country
$country = new \MyCompany\Accommodation\Location\State;
$country->name = "United States";
$country->id = 236;
$country->save();
//create a state
$state = new \MyCompany\Accommodation\Location\State;
$state->name = "Pennsylvania";
$state->id = 1;
$state->save();
//create a city
$city = new \MyCompany\Accommodation\Location\City;
$city->name = "Pittsburgh";
$city->save();
//create a location
$location = new \MyCompany\Accommodation\Location;
$location->city_id = $city->id;
$location->state_id = $state->id;
$location->country_id = 236;
$location->latitude = 40.44;
$location->longitude = 80;
$location->code = '15212';
$location->address_1 = "100 Main Street";
$location->save();
//create a new accommodation
$accommodation = new \MyCompany\Accommodation;
$accommodation->name = "Royal Plaza Hotel";
$accommodation->location_id = $location;
$accommodation->description = "A modern, 4-star hotel";
$accommodation->save();
//create a room
$room1 = new \MyCompany\Accommodation\Room;
$room1->room_number= 'A01';
$room1->accommodation_id = $accommodation->id;
$room1->save();
//create another room
$room2 = new \MyCompany\Accommodation\Room;
$room2->room_number= 'A02';
$room2->accommodation_id = $accommodation->id;
$room2->save();
//create the room array
$rooms = [$room1,$room2];
}
}
seeder 文件设置了可能的最基本的场景。对于初始测试,我们不需要将每个国家、州、城市和可能的位置都添加到数据库中;我们只需要添加必要的信息来创建各种场景。例如,要创建一个新的预订;我们将创建每个用户、国家、州、城市、位置和住宿模型的实例,然后创建两个房间,这些房间将添加到房间数组中。
让我们为预订创建一个实现非常简单的存储库接口的存储库:
<?php
namespace MyCompany\Accommodation;
interface RepositoryInterface {
public function create($attributes);
}
现在让我们创建ReservationRepository
,它实现RepositoryInterface
:
<?php
namespace MyCompany\Accommodation;
class ReservationRepository implements RepositoryInterface {
private $reservation;
function __construct($reservation)
{
$this->reservation = $reservation;
}
public function create($attributes)
{
$this->reservation->create($attributes);
return $this->reservation;
}
}
现在,我们将创建所需的方法来创建预订,并填充reservation_room
的中间表:
public function create($attributes)
{
$modelAttributes= array_except($attributes, ['rooms']);
$reservation = $this->reservationModel->create($modelAttributes);
if (isset($attributes['rooms']) ) {
$reservation->rooms()->sync($attributes['rooms']);
}
return $reservation;
}
提示
array_except()
Laravel 助手用于返回attributes
数组,除了$rooms
数组之外,该数组将用于sync()
函数。
在这里,我们将模型的每个属性设置为方法中设置的属性。我们需要添加将建立预订和房间之间多对多关系的方法:
public function rooms(){
return $this->belongsToMany('MyCompany\Accommodation\Room')->withTimestamps();
}
在这种情况下,我们需要向关系添加withTimestamps()
,以便时间戳将被更新,指示关系何时保存在reservation_room
中。
使用 PHPUnit 进行数据库测试
PHPUnit 与 Laravel 5 集成良好,就像与 Laravel 4 一样,因此设置测试环境相当容易。测试的一个好方法是使用 SQLite 数据库,并将其设置为驻留在内存中,但是您需要修改config/database.php
文件,如下所示:
'default' => 'sqlite',
'connections' => array(
'sqlite' => array(
'driver' => 'sqlite',
'database' => ':memory:',
'prefix' => '',
),
),
然后,我们需要修改phpunit.xml
文件以设置DB_DRIVER
环境变量:
<php>
<env name="APP_ENV" value="testing"/>
<env name="CACHE_DRIVER" value="array"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="DB_DRIVER" value="sqlite"/>
</php>
然后,我们需要修改config/database.php
文件中的以下行:
'default' => 'mysql',
我们修改前面的行以匹配以下行:
'default' => env('DB_DRIVER', 'mysql'),
现在,我们将设置 PHPUnit 在内存中的sqlite
数据库上运行我们的迁移。
在tests
目录中,有两个类:一个TestCase
类,继承了LaravelTestCase
类,和一个ExampleTest
类,继承了TestCase
类。
我们需要向TestCase
添加两个方法来执行迁移,运行 seeder,然后将数据库恢复到其原始状态:
<?php
class TestCase extends Illuminate\Foundation\Testing\TestCase {
public function setUp()
{
parent::setUp();
Artisan::call('migrate');
Artisan::call('db:seed');
}
/**
* Creates the application.
*
* @return \Illuminate\Foundation\Application
*/
public function createApplication()
{
$app = require __DIR__.'/../bootstrap/app.php';
$app->make('Illuminate\Contracts\Console\Kernel')->bootstrap();
return $app;
}
public function tearDown()
{
Artisan::call('migrate:rollback');
}
}
现在,我们将创建一个 PHPUnit 测试来验证数据是否正确保存在数据库中。我们需要将tests/ExampleTest.php
修改为以下代码:
<?php
class ExampleTest extends TestCase {
/**
* A basic functional test example.
*
* @return void
*/
public function testReserveRoomExample()
{
$reservationRepository = new \MyCompany\Accommodation\ReservationRepository(
new \MyCompany\Accommodation\Reservation());
$reservationValidator = new \MyCompany\Accommodation\ReservationValidator();
$start_date = '2015-10-01';
$end_date = '2015-10-10';
$rooms = \MyCompany\Accommodation\Room::take(2)->lists('id')->toArray();
if ($reservationValidator->validate($start_date,$end_date,$rooms)) {
$reservation = $reservationRepository->create(['date_start'=>$start_date,'date_end'=>$end_date,'rooms'=>$rooms,'reservation_number'=>'0001']);
}
$this->assertInstanceOf('\MyCompany\Accommodation\Reservation',$reservation);
$this->assertEquals('2015-10-01',$reservation->date_start);
$this->assertEquals(2,count($reservation->rooms));
}
运行 PHPUnit
要启动 PHPUnit,只需输入以下命令:
**$ phpunit**
测试将会运行。由于Reservation
类的create
方法返回一个预订,我们可以使用 PHPUnit 的assertInstanceOf
方法来确定数据库中是否创建了预订。我们可以添加任何其他断言来确保保存的值正是我们想要的。例如,我们可以断言开始日期等于'2015-10-01'
,room
数组的大小等于two
。与testBasicExample()
方法一起,我们可以确保对"/"
的GET
请求返回200
。PHPUnit 的结果将如下所示:
运行 PHPUnit
请注意,有两个点表示测试。OK表示没有失败,我们再次被告知有两个测试和四个断言;一个是在示例中的断言,另外三个是我们添加到testReserveRoomExample
测试中的。如果我们测试了三个房间而不是两个,PHPUnit 将产生以下输出:
**$ phpunit**
**PHPUnit 4.5.0 by Sebastian Bergmann and contributors.**
**Configuration read from /var/www/laravel.example/phpunit.xml**
**.**
**F**
**Time: 1.59 seconds, Memory: 10.75Mb**
**There was 1 failure:**
**1) ExampleTest::testReserveRoomExample**
**Failed asserting that 2 matches expected 3.**
**/var/www/laravel.example/tests/ExampleTest.php:24**
**FAILURES!**
**Tests: 2, Assertions: 4, Failures: 1.**
请注意,我们有一个F
表示失败,而不是第二个点,而不是OK
,我们被告知有1
个失败。然后 PHPUnit 列出了哪些测试失败,并很好地告诉我们我故意修改为不正确的行。
$this->assertEquals(3,count($reservationResult->rooms));
前面的行确实是不正确的:
**Failed asserting that 2 matches expected 3.**
请记住,2
是($reservationResult->rooms)
的计数值。
使用 Behat 进行功能测试
虽然 phpspec 遵循 BDD 的规范,并且在隔离中很有用于规范和设计,但它的补充工具 Behat 用于集成和功能测试。由于 phpspec 建议对所有内容进行模拟,数据库查询实际上不会被执行,因为数据库在该方法的上下文之外。Behat 是一个在某个功能上执行行为测试的好工具。虽然 phpspec 已经包含在 Laravel 5 的依赖项中,但 Behat 将作为外部模块安装。
应该运行以下命令来安装并使 Behat 与 Laravel 5 一起工作:
**$ composer require behat/behat behat/mink behat/mink-extension laracasts/behat-laravel-extension --dev**
运行 composer update 后,Behat 的功能将添加到 Laravel 中。接下来,应在 Laravel 项目的根目录中添加一个behat.yaml
文件,以指定要使用哪些扩展。
接下来,运行以下命令:
**$ behat --init**
这将创建一个features
目录,里面有一个bootstrap
目录。还将创建一个FeaturesContext
类。bootstrap
中的所有内容都将在每次运行behat
时运行。这对于自动运行迁移和填充是有用的。
features/bootstrap/FeaturesContext.php
文件如下:
<?php
use Behat\Behat\Context\Context;
use Behat\Behat\Context\SnippetAcceptingContext;
use Behat\Gherkin\Node\PyStringNode;
use Behat\Gherkin\Node\TableNode;
/**
* Defines application features from the specific context.
*/
class FeatureContext implements Context, SnippetAcceptingContext
{
/**
* Initializes context.
*
* Every scenario gets its own context instance.
* You can also pass arbitrary arguments to the
* context constructor through behat.yml.
*/
public function __construct()
{
}
}
接下来,FeatureContext
类需要扩展MinkContext
类,因此类定义行需要修改如下:
class FeatureContext implements Context, SnippetAcceptingContext
接下来,将在类中添加prepare
和cleanup
方法以执行迁移。我们将添加@BeforeSuite
和@AfterSuite
注释,告诉 Behat 在每个套件之前执行迁移和种子,并在每个套件之后回滚以将数据库恢复到其原始状态。将在文档块中使用注释将在第六章中讨论,使用注释驯服复杂性。我们的类现在结构如下:
<?php
use Behat\Behat\Context\Context;
use Behat\Behat\Context\SnippetAcceptingContext;
use Behat\Gherkin\Node\PyStringNode;
use Behat\Gherkin\Node\TableNode;
/**
* Defines application features from the specific context.
*/
class FeatureContext implements Context, SnippetAcceptingContext
{
/**
* Initializes context.
*
* Every scenario gets its own context instance.
* You can also pass arbitrary arguments to the
* context constructor through behat.yml.
*/
public function __construct()
{
}
/**
* @BeforeSuite
*/
public static function prepare(SuiteEvent $event)
{
Artisan::call('migrate');
Artisan::call('db:seed');
}
/**
* @AfterSuite
*/
public function cleanup(ScenarioEvent $event)
{
Artisan::call('migrate:rollback');
}
}
现在,需要创建一个功能文件。在 room 目录中创建reservation.feature
:
Feature: Reserve Room
In order to verify the reservation system
As an accommodation reservation user
I need to be able to create a reservation in the system
Scenario: Reserve a Room
When I create a reservation
Then I should have one reservation
当运行behat
如下时:
**$ behat**
产生以下输出:
**Feature: Reserve Room**
**In order to verify the reservation system**
**As an accommodation reservation user**
**I need to be able to create a reservation in the system**
**Scenario: List 2 files in a directory # features/reservation.feature:5**
**When I create a reservation**
**Then I should have one reservation**
**1 scenario (1 undefined)**
**2 steps (2 undefined)**
**0m0.10s (7.48Mb)**
**--- FeatureContext has missing steps. Define them with these snippets:**
**/****
*** @When I create a reservation**
***/**
**public function iCreateAReservation()**
**{**
**throw new PendingException();**
**}**
**/****
*** @Then I should have one reservation**
***/**
**public function iShouldHaveOneReservation()**
**{**
**throw new PendingException();**
**}**
Behat,就像 phpspec 一样,熟练地生成输出,向您显示需要创建的方法。请注意,此处使用驼峰命名法而不是蛇形命名法。此代码应复制到FeatureContext
类中。请注意,默认情况下会抛出异常。
在这里,将调用 RESTful API,因此需要将 guzzle HTTP 包添加到项目中:
**$ composer require guzzlehttp/guzzle**
接下来,向类添加一个属性来保存guzzle
对象。我们将向 RESTful 资源控制器添加一个POST
请求来创建预订,并期望获得 201 代码。请注意,返回代码是一个字符串,需要转换为整数。接下来,执行get
以返回所有预订。
应该只创建一个预订,因为迁移和种子每次运行时都会运行:
<?php
use Behat\Behat\Context\Context;
use Behat\Behat\Context\SnippetAcceptingContext;
use Behat\Gherkin\Node\PyStringNode;
use Behat\Gherkin\Node\TableNode;
use Behat\MinkExtension\Context\MinkContext;
use Behat\Testwork\Hook\Scope\BeforeSuiteScope;
use Behat\Testwork\Hook\Scope\AfterSuiteScope;
use GuzzleHttp\Client;
/**
* Defines application features from the specific context.
*/
class FeatureContext extends MinkContext implements Context, SnippetAcceptingContext
{
/**
* Initializes context.
*
* Every scenario gets its own context instance.
* You can also pass arbitrary arguments to the
* context constructor through behat.yml.
*/
protected $httpClient;
public function __construct()
{
$this->httpClient = new Client();
}
/**
* @BeforeSuite
*/
public static function prepare(BeforeSuiteScope $scope)
{
Artisan::call('migrate');
Artisan::call('db:seed');
}
/**
* @When I create a reservation
*/
public function iCreateAReservation()
{
$request = $this->httpClient->post('http://laravel.example/reservations',['body'=> ['start_date'=>'2015-04-01','end_date'=>'2015-04-04','rooms[]'=>'100']]);
if ((int)$request->getStatusCode()!==201)
{
throw new Exception('A successfully created status code must be returned');
}
}
/**
* @Then I should have one reservation
*/
public function iShouldHaveOneReservation()
{
$request = $this->httpClient->get('http://laravel.example/reservations');
$arr = json_decode($request->getBody());
if (count($arr)!==1)
{
throw new Exception('there must be exactly one reservation');
}
}
/**
* @AfterSuite
*/
public static function cleanup(AfterSuiteScope $scope)
{
Artisan::call('migrate:rollback');
}
}
/**
* @When I create a reservation
*/
public function iCreateAReservation()
{
$request = $this->httpClient->post('http://laravel.example/reservations',['body'=> ['start_date'=>'2015-04-01','end_date'=>'2015-04-04','rooms[]'=>'100']]);
if ((int)$request->getStatusCode()!==201)
{
throw new Exception('A successfully created status code must be returned');
}
}
现在,使用命令行中的 artisan 来创建ReservationController
:
**$ php artisan make:controller ReservationsController**
以下是预订控制器的内容:
<?php namespace MyCompany\Http\Controllers;
use MyCompany\Http\Requests;
use MyCompany\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
use MyCompany\Accommodation\ReservationRepository;
use MyCompany\Accommodation\ReservationValidator;
use MyCompany\Accommodation\Reservation;
class ReservationsController extends Controller {
/**
* Display a listing of the resource.
*
* @return Response
*/
public function index()
{
return Reservation::all();
}
/**
* Store a newly created resource in storage.
*
* @return Response
*/
public function store()
{
$reservationRepository = new ReservationRepository(new Reservation());
$reservationValidator = new ReservationValidator();
if ($reservationValidator->validate(\Input::get('start_date'),
\Input::get('end_date'),\Input::get('rooms')))
{
$reservationRepository->create(['date_start'=>\Input::get('start_date'),'date_end'=>\Input::get('end_date'),'rooms'=>\Input::get('rooms')]);
return response('', '201');
}
}
}
最后,将ReservationController
添加到routes.php
文件中,该文件位于app/Http/routes.php
中:
**Route::resource('reservations','ReservationController');**
现在,当运行behat
时,结果如下:
**Feature: Reserve Room**
**In order to verify the reservation system**
**As an accommodation reservation user**
**I need to be able to create a reservation in the system**
**Scenario: Reserve a Room**
**When I create a reservation # FeatureContext::iCreateAReservation()**
**Then I should have one reservation # FeatureContext::iShouldHaveOneReservation()**
**1 scenario (1 passed)**
**2 steps (2 passed)**
总结
配置 Laravel 以从现有模式创建迁移文件也是非全新项目的一个有用框架。通过在测试环境中运行迁移和种子,每个测试都可以从数据库的完全干净版本中受益,并且可以通过初始数据最小地验证软件的执行是否符合需要。当需要将遗留代码移植到 Laravel 时,PHPUnit 可以用于测试任何现有功能。Behat 提供了一种基于行为的替代方案,可以熟练地执行端到端测试。
我们使用 phpspec 在一个独立的环境中设计了我们的类,只专注于业务规则和客户端的请求,同时模拟诸如实际实体(如房间)之类的事物。然后,我们通过使用功能测试工具 PHPUnit 验证了实际查询是否正确执行并保存在数据库中。最后,我们使用 Behat 执行端到端测试。
在下一章中,我们将看到 RESTful API 的创建,基本的 CRUD 操作(创建,读取,更新和删除),并讨论一些最佳实践。
第三章:构建服务、命令和事件
在前两章中,我们建立了我们的住宿预订系统的基本结构。我们设计了我们的类,创建了我们的数据库模式,并学会了如何测试它们。现在我们需要将业务需求转化为代码。
在本章中,我们将涵盖以下主题:
-
命令
-
事件
-
命令处理程序
-
事件处理程序
-
排队的事件处理程序
-
排队的命令
-
控制台命令
-
命令调度程序
请求路由
如前所述,Laravel 5 采用了命令总线模式。Laravel 4 将命令视为从命令行执行的内容,而在 Laravel 5 中,命令可以在任何上下文中使用,从而实现代码的优秀重用。
以下是 Laravel 4 的 HTTP 请求流程示例:
以下是 Laravel 5 的 HTTP 请求流程示例:
第一张图片说明了 Laravel 4 的请求流程。通过 HTTP 的请求由路由处理,然后发送到控制器,通常情况下,我们可以与存储库或模型的目录进行交互。在 Laravel 5 中,这仍然是可能的;然而,正如第二张图片所示,我们可以看到添加额外的块、层或模块的能力使我们能够将请求的生命周期分离成单独的部分。Laravel 4 允许我们将处理请求的所有代码放在控制器内,而在 Laravel 5 中,我们可以自由地做同样的事情,尽管现在我们也能够轻松地将请求分离成各种部分。其中一些概念源自领域驱动设计(DDD)。
在控制器内,使用数据传输对象(DTO)范例实例化命令。然后,命令被发送到命令总线,在那里由处理程序类处理,该类有两个方法:__construct()
和handle()
。在处理程序内部,我们触发或实例化一个事件。同样,事件也由事件处理程序方法处理,该方法有两个方法:__construct()
和handle()
。
目录结构非常清晰,如下所示:
**/app/Commands**
**/app/Events/**
**/app/Handlers/**
**/app/Handlers/Commands**
**/app/Handlers/Events**
**/app/HTTP/Controllers**
这相当直观;命令和事件分别在它们各自的目录中,而每个处理程序都有自己的目录。
注意
Laravel 5.1 已将app/Commands
目录的名称更改为app/Jobs
,以确保程序员不会混淆命令总线和控制台命令的概念。
用户故事
命令组件的想法可以很容易地从用户故事或用户为实现目标而需要的任务中得出。最简单的例子是搜索一个房间:
As a hotel website user,
I want to search for a room
so that I can select from a list of results.
源自敏捷方法论的用户故事保证编写的代码与业务需求紧密匹配。它们通常遵循“作为…我想要…以便…”的模式。这定义了角色
、意图
和利益
。它帮助我们计划如何将每个任务转换为代码。在我们的例子中,用户故事可以转化为任务。
作为酒店网站用户,我会创建以下任务列表:
-
作为酒店网站用户,我希望搜索房间,以便我可以从结果列表中选择一个房间。
-
作为酒店网站用户,我希望预订一个房间,以便我可以住在酒店里。
-
作为酒店网站用户,我希望收到包含预订详情的电子邮件,以便我可以拥有预订的副本。
-
作为酒店网站用户,我希望在等候名单上,以便我可以在有房间可用时预订一个房间。
-
作为酒店网站用户,我希望收到房间的可用性通知,以便我可以预订房间。
用户故事转换为代码
搜索房间的第一个任务很可能是来自用户或外部服务的 RESTful 调用,因此这个任务会暴露给我们的控制器,从而暴露给我们的 RESTful API。
第二个任务,预订房间,是由用户或其他服务发起的类似操作。这个任务可能需要用户登录。
第三个任务可能取决于第二个任务。这个任务需要与另一个过程进行交互,向用户发送包含预订详情的确认电子邮件。我们也可以这样写:作为酒店网站,我想发送一封带有预订详情的电子邮件,以便他或她可以拥有预订的副本。
第四个任务,加入等待列表,可能是在发出预订房间请求后执行的命令;如果另一个用户同时预订了房间。它很可能是从应用程序本身而不是用户那里调用的,因为用户对实时住宿库存没有了解。这可以帮助我们处理竞争条件。此外,我们应该假设当网站用户决定预订哪个房间时,该房间没有锁定机制来保证可用性。我们也可以这样写:作为酒店网站,我想将用户放在等待列表中,以便在房间可用时通知他们。
对于第五个任务,当用户被放在等待列表上时,用户也可以在房间可用时收到通知。此操作检查房间的可用性,然后检查等待列表上的任何用户。用户故事可以重写如下:作为酒店网站,我想通知等待列表用户房间的可用性,以便他或她可以预订房间。如果房间变得可用,等待列表上的第一个用户将通过电子邮件收到可用性通知。这个命令将经常执行,就像是一个定时任务。幸运的是,Laravel 5 有一种新的机制,允许命令以给定的频率执行。
很明显,如果用户故事必须以使用网站作为行动者(“作为酒店网站...”)或网站用户作为行动者(“作为酒店网站用户...”)来编写,命令是有用的,并且可以从 RESTful API(用户端)或 Laravel 应用程序内部启动。
由于我们的第一个任务很可能涉及外部服务,我们将创建一个路由和一个控制器来处理请求。
控制器
第一步涉及创建一个路由,第二步涉及创建一个控制器。
搜索房间
首先,在routes.php
文件中创建一个路由,并将其映射到controller
方法,如下所示:
Route::get('search', 'RoomController@search');
请求参数,如开始/结束日期和位置详情将如下所示:
{
"start_date": "2015-07-10"
"end_date": "2015-07-17"
"city": "London"
"country": "England"
}
搜索参数将以 JSON 编码的对象形式发送。它们将发送如下:
http://websiteurl.com/search?query={%22start_date%22:%222015-07-10%22,%22end_date%22:%222015-07-17%22,%22city%22:%22London%22,%22country%22:%22England%22}
现在,让我们在我们的room
控制器中添加一个search
方法,以处理以对象形式输入的 JSON 请求,如下所示:
/**
* Search for a room in an accommodation
*/
public function search()
{
json_decode(\Request::input('query'));
}
请求外观处理输入变量查询,然后将其 JSON 结构解码为对象。
在第四章中,创建 RESTful API,我们将完成search
方法的代码,但现在,我们将简单地创建我们的 RESTful API 系统的这一部分的架构。
控制器转命令
对于第二个任务,预订房间,我们将创建一个命令,因为我们很可能需要后续操作,我们将通过发布者订阅者模式启用。发布者订阅者模式用于表示发送消息的发布者和监听这些消息的订阅者。
将以下路由添加到routes.php
中:
**Route::post('reserve-room', 'RoomController@store');**
我们将 post 映射到 room 控制器的store
方法;这将创建预订。记住我们创建了这样的命令:
**$ php artisan make:commandReserveRoomCommand -–handler**
我们的ReserveRoomCommand
类如下所示:
<?php namespace MyCompany\Commands;
use MyCompany\Commands\Command;
use MyCompany\User;
class ReserveRoomCommand extends Command {
public $user;
public $rooms;
public $start_date;
public $end_date;
/**
* Create a new command instance.
*
* @return void
*/
public function __construct(User $user, $start_date, $end_date, $rooms)
{
$this->rooms = $rooms;
$this->user = $user;
$this->start_date = $start_date;
$this->end_date = $end_date;
}
}
我们需要将以下属性添加到构造函数中:
public $user;
public $rooms;
public $start_date;
public $end_date;
此外,将以下赋值添加到构造函数中:
$this->rooms = $rooms;
$this->user = $user;
$this->start_date = $start_date;
$this->end_date = $end_date;
这使我们能够传递值。
命令转事件
现在让我们创建一个事件。使用artisan
创建一个事件RoomWasReserved
,当房间被创建时触发:
**$ phpartisan make:eventRoomWasReserved**
RoomWasReserved
事件类看起来像以下代码片段:
<?php namespace MyCompany\Events;
use MyCompany\Accommodation\Reservation;
use MyCompany\Events\Event;
use MyCompany\User;
use Illuminate\Queue\SerializesModels;
class RoomWasReserved extends Event {
use SerializesModels;
private $user;
private $reservation;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct(User $user, Reservation $reservation)
{
$this->user = $user;
$this->reservation = $reservation;
}
}
我们将告诉它使用MyCompany\Accommodation\Reservation
和MyCompany\User
实体,以便我们可以将它们传递给构造函数。在构造函数内部,我们将它们分配给event
对象内的实体。
现在,让我们从命令处理程序内部触发事件。Laravel 为您提供了一个简单的event()
方法作为一个方便/辅助方法,它将触发一个事件。我们将实例化的预订和user
注入RoomWasReserved
事件如下:
**event(new RoomWasReserved($user, $reservation));**
ReserveRoomCommandHandler
类
我们的ReserveRoomCommandHandler
类现在实例化一个新的预订,使用createNew
工厂方法来注入依赖项,最后,触发RoomWasReserved
事件如下:
<?phpnamespace MyCompany\Handlers\Commands;
use MyCompany\Commands\ReserveRoomCommand;
use Illuminate\Queue\InteractsWithQueue;
class ReserveRoomCommandHandler {
/**
* Create the command handler.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Handle the command.
*
* @paramReserveRoomCommand $command
* @return void
*/
public function handle(ReserveRoomCommand $command)
{
$reservationValidator = new \MyCompany\Accommodation\ReservationValidator();
if ($reservationValidator->validate($command->start_date,$command->end_date,$command->rooms)) {
$reservation =
$reservationRepository->create(
['date_start'=>$command->$command→start_date,
'date_end'=>$command->end_date,
'rooms'=>$command->'rooms']);
}
$reservation = new
event(new RoomWasReserved($command->user,$reservation));
}
}
事件到处理程序
现在,我们需要创建事件处理程序。正如您所期望的那样,Artisan 提供了一个方便的方法来做到这一点,尽管语法有点不同。这一次,奇怪的是,make这个词没有出现在短语中:
**$ php artisan handler:eventRoomReservedEmail --event=RoomWasReserved**
**<?php namespace MyCompany\Handlers\Events;**
**use MyCompany\Events\RoomWasReserved;**
**use Illuminate\Queue\InteractsWithQueue;**
**use Illuminate\Contracts\Queue\ShouldBeQueued;**
**class RoomReservedEmail {**
**/****
*** Create the event handler.**
*** @return void**
***/**
**public function __construct()**
**{**
**}**
**public function handle(RoomWasReserved $event)**
**{**
**//TODO: send email to $event->user**
**//TODO: with details about $event->reservation;**
**}**
**}**
现在我们需要将事件连接到其监听器。我们将编辑app/Providers/EventServiceProvider.php
文件如下:
protected $listen = [
'MyCompany\Events\RoomWasReserved' => [
'MyCompany\Handlers\Events\RoomReservedEmail',
],
];
如前面的代码片段所示,我们将向$listen
数组添加键值对。如所示,需要完整路径作为键,事件名称和处理程序数组。在这种情况下,我们只有一个处理程序。
排队的事件处理程序
如果我们不希望事件立即处理,而是放入队列中,我们可以在创建命令中添加-queued
如下:
**$ php artisan handler:eventRoomReservedEmail --event=RoomWasReserved --queued**
**<?php namespace MyCompany\Handlers\Events;**
**use MyCompany\Events\RoomWasReserved;**
**use Illuminate\Queue\InteractsWithQueue;**
**use Illuminate\Contracts\Queue\ShouldBeQueued;**
**class RoomReservedEvent implements ShouldBeQueued {**
**use InteractsWithQueue;**
**public function __construct()**
**{**
**//**
**}**
**use Illuminate\Contracts\Queue\ShouldBeQueued;**
这个接口告诉 Laravel 事件处理程序应该被排队,而不是同步执行:
use Illuminate\Queue\InteractsWithQueue;
这个 trait 允许我们与队列交互,以便执行任务,比如删除任务。
等待列表命令
对于第四个任务,被放置在等待列表中,我们需要创建另一个命令,该命令将从预订控制器内部调用。再次使用 Artisan,我们可以轻松地创建命令及其相应的事件如下:
**$ php artisan make:commandPlaceOnWaitingListCommand**
**$ php artisan make:eventPlacedOnWaitinglist**
现在,在我们的预订控制器中,我们将添加roomAvailability
的检查,然后按以下方式分派PlaceOnWaitinglist
命令:
public function store()
{
…
…
if ($roomAvailable) {
$this->dispatch(
new ReserveRoomCommand( $start_date, $end_date, $rooms)
);
} else {
$this->dispatch(
new PlaceOnWaitingListCommand($start_date, $end_date, $rooms)
);
}
…
排队的命令
通过在create
命令中添加queued
,我们可以轻松地将命令加入队列:
**$ php artisan make:commandReserveRoomCommand -–handler --queued**
这将使用可用的任何队列系统,比如 beanstalkd,并不会立即运行命令。相反,它将被放置在队列中,并稍后运行。我们需要为Command
类添加一个接口:
**Illuminate\Contracts\Queue\ShouldBeQueued**
在这种情况下,ReserveRoomCommand
类将如下所示:
<?php namespace MyCompany\Commands;
use MyCompany\Commands\Command;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldBeQueued;
class MyCommand extends Command implements ShouldBeQueued {
use InteractsWithQueue, SerializesModels;
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
//
}
}
在这里,我们可以看到InteractsWithQueue
和ShouldBeQueued
类已经被包含,ReserveRoomCommand
类扩展了命令并实现了ShouldBeQueued
类。另一个有趣的特性是SerializesModels
。这将序列化传递的任何模型,以便稍后使用。
控制台命令
对于第五个任务,让我们创建一个console
命令,这个命令将经常被执行:
**$ php artisan make:consoleManageWaitinglist**
这将创建一个可以从 Artisan 命令行工具执行的命令。如果您使用过 Laravel 4,您可能对这种类型的命令很熟悉。这些命令存储在Console/Commands/
目录中。
为了让 Laravel 知道这一点,我们需要将它添加到app/Console/Kernel.php
中的$commands
数组中:
protected $commands = [
'MyCompany\Console\Commands\Inspire',
'MyCompany\Console\Commands\ManageWaitinglist',
];
内容如下:
<?php namespace MyCompany\Console\Commands;
use Illuminate\Console\Command;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputArgument;
class ManageWaitinglist extends Command {
/**
* The console command name.
*
* @var string
*/
protected $name = 'command:name';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description.';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function fire()
{
//
}
/**
* Get the console command arguments.
*
* @return array
*/
protected function getArguments()
{
return [
['example', InputArgument::REQUIRED, 'An example argument.'],
];
}
/**
* Get the console command options.
*
* @return array
*/
protected function getOptions()
{
return [
['example', null, InputOption::VALUE_OPTIONAL, 'An example option.', null],
];
}
}
$name
属性是从 Artisan 调用的名称。例如,如果我们设置如下:
protected $name = 'manage:waitinglist';
然后,通过运行以下命令,我们可以管理等待列表:
**$ php artisan manage:waitinglist**
getArguments()
和getOptions()
方法是具有相同签名的类似方法,但用途不同。
getArguments()
方法指定了必须用于启动命令的参数数组。getOptions()
方法用-
指定,并且可以是optional
、repeated
,并且使用VALUE_NONE
选项,它们可以简单地用作标志。
我们将在fire()
方法中编写命令的主要代码。如果我们想要从该命令中调度一个命令,我们将在类中添加DispatchesCommands
trait,如下所示:
**use DispatchesCommands;**
**<?php namespace MyCompany\Console\Commands;**
**use Illuminate\Console\Command;**
**use Illuminate\Foundation\Bus\DispatchesCommands;**
**use Symfony\Component\Console\Input\InputOption;**
**use Symfony\Component\Console\Input\InputArgument;**
**class ManageWaitinglist extends Command {**
**use DispatchesCommands;**
**/****
*** The console command name.**
*** @var string**
***/**
**protected $name = 'manage:waitinglist';**
**/****
*** The console command description.**
*** @var string**
***/**
**protected $description = 'Manage the accommodation waiting list.';**
**/****
*** Create a new command instance.**
*****
*** @return void**
***/**
**public function __construct()**
**{**
**parent::__construct();**
**}**
**/****
*** Execute the console command.**
*** @return mixed**
***/**
**public function fire()**
**{**
**// TODO: write business logic to manage waiting list**
**if ($roomIsAvailableFor($user)) {**
**$this->dispatch(new ReserveRoomCommand());**
**}**
**}**
**/****
*** Get the console command arguments.**
*** @return array**
***/**
**protected function getArguments()**
**{**
**return [];**
**}**
**/****
*** Get the console command options.**
*** @return array**
***/**
**protected function getOptions()**
**{**
**return [];**
**}**
**}**
命令调度程序
现在,我们将安排此命令每 10 分钟运行一次。传统上,这是通过创建一个 cron 作业来执行 Laravel 控制台命令来完成的。现在,Laravel 5 提供了一个新的机制来做到这一点——命令调度程序。
新的artisan
命令的运行方式如下:
**$ php artisan schedule:run**
通过简单地将此命令添加到 cron 中,Laravel 将自动运行Kernel.php
文件中的所有命令。
命令需要添加到Schedule
函数中,如下所示:
protected function schedule(Schedule $schedule)
{
$schedule->command('inspire')
->hourly();
$schedule->command('manage:waitinglist')
->everyFiveMinutes();
}
inspire
命令是 Laravel 提供的一个示例命令,用于演示功能。我们将简单地添加我们的命令。这将每 5 分钟调用manage:waitinglist
命令——比这更简单的方式都没有了。
现在我们需要修改crontab
文件以使 Artisan 运行调度程序。
crontab
是一个包含在特定时间运行的命令的文件。要修改此文件,请键入以下命令:
**$ sudo crontab -e**
我们将使用vi
或分配的编辑器来修改cron
表。添加以下行将告诉cron
每分钟运行调度程序:
*** * * * * php /path/to/artisan schedule:run 1>> /dev/null 2>&1**
总结
Laravel 在短短两年内发生了变化,从 CodeIgniter 的模型-视图-控制器范式转变为采用现代领域驱动设计的命令总线和发布者-订阅者事件监听器模式。是否使用这些模式将取决于所需的每个层之间的分离程度。当然,即使使用自处理命令也是开始创建完全独立的代码块的一种方式,这将促使代码进入一个单独的处理程序类,进一步实现关注点分离原则。通过减少控制器内的代码量,命令变得更加重要。
我们甚至还没有为每个用户故事编写与数据库交互的代码,我们只是对数据库进行了种子和测试,但结构开始变得非常设计良好;每个类都有一个非常有意义的名称,并且组织成一个有用的目录结构。
在下一章中,我们将填写有关 RESTful 控制器如何接受来自另一个系统或网站前端的输入,以及模型属性如何返回给用户以创建界面的详细信息。
第四章:创建 RESTful API
如果有一个单一的核心功能可以展示 Laravel 的优越性,那就是快速轻松地创建 RESTful API 的能力。随着 Laravel 5 的到来,添加了几个新功能;然而,通过 Artisan 命令行工具创建应用程序模型和控制器的能力仍然是最有用的功能。
这个功能最初鼓励了我和其他许多人放弃诸如 CodeIgniter 之类的框架,因为在 Laravel 4 测试版时,它并没有原生具有相同的集成功能。Laravel 提供了基本的 CRUD 方法:创建,读取,更新,删除,并且还列出了所有内容。
通过 HTTP 到达 Laravel URL 的请求是通过它们的动词管理的,随后是routes.php
文件,该文件位于app/Http/routes.php
。请求处理有两种方式。一种方式是直接通过闭包处理请求,代码完全在routes
文件中。另一种方式是将请求路由到控制器,其中将执行一个方法。
此外,使用的基本范式是约定优于配置,其中方法名称已准备好处理各种请求,而无需太多额外的努力。
Laravel 中的 RESTful API
由 RESTful API 处理的 RESTful API 请求列表如下:
HTTP VERB | Function | URL | |
---|---|---|---|
1 | GET |
列出所有住宿 | /accommodations |
2 | GET |
显示(读取)单个住宿 | /accommodations/{id} |
3 | POST |
创建新的住宿 | /accommodations |
4 | PUT |
完全修改(更新)住宿 | /accommodations/{id} |
5 | PATCH |
部分修改(更新)住宿 | /accommodations/{id} |
6 | DELETE |
删除住宿 | /accommodations/{id} |
大多数 RESTful API 最佳实践建议使用模型名称的复数形式。Laravel 的文档使用单数格式。大多数实践都同意一致的复数命名,即/accommodations/{id}
指的是单个住宿,/accommodations
指的是多个住宿,都使用复数形式,而不是混合的,但语法上正确的/accommodation/{id}
(单数形式)和/accommodations
(复数形式)。
基本 CRUD
为简单起见,我已经对每一行进行了编号。第一和第二项代表了 CRUD 的“读取”部分。
第一项是对模型名称的复数形式进行的GET
调用,相当简单;它显示所有项目。有时,这被称为“列表”,以区别于单个记录的“读取”。因此,添加一个“列表”将扩展首字母缩写为 CRUDL。它们可以进行分页或需要授权。
第二项,也是GET
调用,将模型的 ID 添加到 URL 的末尾,显示具有相应 ID 的单个模型。这也可能需要身份验证,但不需要分页。
第三项代表了 CRUD 的“创建”部分。它使用POST
动词来创建一个新的模型。请注意,URL 格式与第一项相同;这展示了动词在区分操作中的重要性。
第四、第五和第六项使用了一些浏览器不支持的新的HTTP
动词。无论这些动词是否受支持,JavaScript 库和框架(如 jQuery)都会以 Laravel 可以正确处理的方式发送动词。
第四项是 CRUD 的“更新”部分,使用PUT
动词更新模型。请注意,它与第二项具有相同的 URL 格式,因为它需要知道要更新哪个模型。它也是幂等的,这意味着整个模型必须被更新。
第五项类似于第四项;它更新模型,但使用PATCH
动词。这用于指示模型将被部分修改,这意味着一个或多个模型的属性必须被更改。
第六项删除一个单个模型,因此需要模型的 ID,使用不言自明的 DELETE
动词。
额外功能
Laravel 添加了两个通常不是标准 RESTful API 的额外方法。在模型 URL 上使用 GET
方法,添加 create
用于显示创建模型的表单。在带有 ID 的模型 URL 上使用 GET
方法,添加 edit
用于显示创建模型的表单。这两个功能对于提供将加载表单的 URL 非常有用,尽管这种类型的使用不是标准的 RESTful:
HTTP VERB | Function | URL | |
---|---|---|---|
GET |
这显示一个住宿创建表单 | /accommodations/create |
|
GET |
这显示一个住宿修改/更新表单 | /accommodations/{id}/edit |
控制器创建
要为住宿创建一个控制器,使用以下 Artisan 命令:
**$ php artisan make:controller AccommodationsController**
**<?php namespace MyCompany\Http\Controllers;**
**use MyCompany\Http\Requests;**
**use MyCompany\Http\Controllers\Controller;**
**use Illuminate\Http\Request;**
**class AccommodationController extends Controller {**
**/****
*** Display a listing of the resource.**
*** @return Response**
***/**
**public function index()**
**{**
**}**
**/****
*** Show the form for creating a new resource.**
*** @return Response**
***/**
**public function create()**
**{**
**}**
**/****
*** Store a newly created resource in storage.**
*** @return Response**
***/**
**public function store()**
**{**
**}**
**/****
*** Display the specified resource.**
*** @param int $id**
*** @return Response**
***/**
**public function show($id)**
**{**
**}**
**/****
*** Show the form for editing the specified resource.**
*** @param int $id**
*** @return Response**
***/**
**public function edit($id)**
**{**
**}**
**/****
*** Update the specified resource in storage.**
*****
*** @param int $id**
*** @return Response**
***/**
**public function update($id)**
**{**
**}**
**/****
*** Remove the specified resource from storage.**
*** @param int $id**
*** @return Response**
***/**
**public function destroy($id)**
**{**
**}**
**}**
通过示例进行 CRUD(L)
我们之前看过这个控制器,但这里有一些示例。RESTful 调用的最简单示例将如下所示。
cRudl – 读取
创建一个 GET
调用到 http://www.hotelwebsite.com/accommmodations/1
,其中 1
将是房间的 ID:
/**
* Display the specified resource.
*
* @param int $id
* @return Response
*/
public function show($id)
{
return \MyCompany\Accommodation::findOrFail($id);
}
这将返回一个单个模型作为 JSON 编码对象:
{
"id": 1,
"name": "Hotel On The Hill","description":"Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
"location_id": 1,
"created_at": "2015-02-08 20:13:10",
"updated_at": "2015-02-08 20:13:10",
"deleted_at": null
}
crudL – 列表
创建一个 GET 调用到 http://www.hotelwebsite.com/accommmodations
。
这与前面的代码类似,但略有不同:
/** Display a listing of the resource.
* @return Response
*/
public function index()
{
return Accommodation::all();
}
这将返回所有模型,自动编码为 JSON 对象;没有其他要求。已添加格式,以便 JSON 结果更易读,但基本上整个模型都会返回:
[{
"id": 1,
"name": "Hotel On The Hill","description":"Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
"location_id": 1,
"created_at": "2015-02-08 20:13:10",
"updated_at": "2015-02-08 20:13:10",
"deleted_at": null
}
{ "id": 2,
"name": "Patterson Place",
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
"location_id": 2,
"created_at": "2015-02-08 20:15:02",
"updated_at": "2015-02-08 20:15:02",
"deleted_at": null
},
{
"id": 3,
"name": "Neat and Tidy Hotel",
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
"location_id": 3,
"created_at": "2015-02-08 20:17:34",
"updated_at": "2015-02-08 20:17:34",
"deleted_at": null
}
]
提示
deleted_at
字段是软删除或回收站机制。对于未删除的情况,它要么是 null
,要么是已删除的 date
/time
时间戳。
分页
要添加分页,只需用 paginate()
替换 all()
:
public function index()
{
return Accommodation::paginate();
}
现在结果看起来像这样。Eloquent 集合数组现在移动到 date
属性内:
{"total":15,
"per_page":15,
"current_page":1,
"last_page":1,
"next_page_url":null,
"prev_page_url":null,
"from":1,
"to":15,
"data":[{"id":9,
"name":"Lovely Hotel",
"description":"Lovely Hotel Greater Pittsburgh",
….
Crudl – 创建
创建一个 POST
调用到 http://www.hotelwebsite.com/accommmodations
。
要创建一个新模型,将发送一个 POST
调用到 /accommodations
。前端将发送一个 JSON 如下:
{
"name": "Lovely Hotel",
"description": "Lovely Hotel Greater Pittsburgh",
"location_id":1
}
store
函数可能看起来像这样:
public function store()
{
$input = \Input::json();
$accommodation = new Accommodation;
$accommodation->name = $input->get('name');
$accommodation->description = $input->get('description');
$accommodation->location_id = $input->get('location_id');
$accommodation->save();
return response($accommodation, 201)
;
}
提示
201
是 created
的 HTTP 状态码(HTTP/1.1 201 created
)。
在这个例子中,我们将模型作为 JSON 编码对象返回。对象将包括插入的 ID:
{
"name":"Lovely Hotel",
"description":"Lovely Hotel Greater Pittsburgh",
"location_id":1,
"updated_at":"2015-03-13 20:48:19",
"created_at":"2015-03-13 20:48:19",
"id":26
}
crUdl – 更新
创建一个 PUT
调用到 http://www.hotelwebsite.com/accommmodations/1
,其中 1
是要更新的 ID:
/**
* Update the specified resource in storage.
*
* @param int $id
* @return Response
*/
public function update($id)
{
$input = \Input::json();
$accommodation = \MyCompany\Accommodation::findOrFail($id);
$accommodation->name = $input->get('name');
$accommodation->description = $input->get('description');
$accommodation->location_id = $input->get('location_id');
$accommodation->save();
return response($accommodation, 200)
->header('Content-Type', 'application/json');
}
要更新现有模型,代码与之前使用的完全相同,只是使用以下行来查找现有模型:
$accommodation = Accommodation::find($id);
PUT
动词将发送到 /accommodations/{id}
,其中 id
将是住宿表的数字 ID。
cruDl – 删除
要删除一个模型,创建一个 DELETE
调用到 http://www.hotelwebsite.com/accommmodation/1
,其中 1
是要删除的 ID:
/**
* Remove the specified resource from storage.
*
* @param int $id
* @return Response
*/
public function destroy($id)
{
$accommodation = Accommodation::find($id);
$accommodation->delete();
return response('Deleted.', 200)
;
}
提示
关于删除模型的适当状态码似乎存在一些分歧。
模型绑定
现在,我们可以使用一种称为模型绑定的技术来进一步简化代码:
public function boot(Router $router)
{
parent::boot($router);
$router->model('accommodations', '\MyCompany\Accommodation');
}
在 app/Providers/RouteServiceProvider.php
中,添加接受路由作为第一个参数并将要绑定的模型作为第二个参数的 $router->model()
方法。
重新访问读取
现在,我们的 show
控制器方法看起来像这样:
public function show(Accommodation $accommodation)
{
return $accommodation;
}
当调用 /accommodations/1
时,例如,与该 ID 对应的模型将被注入到方法中,允许我们替换查找方法。
重新访问列表
同样,对于 list
方法,我们按照类型提示的模型注入如下:
public function index(Accommodation $accommodation)
{
return $accommodation;
}
更新重新访问
同样,update
方法现在看起来像这样:
public function update(Accommodation $accommodation)
{
$input = \Input::json();
$accommodation->name = $input->get('name');
$accommodation->description = $input->get('description');
$accommodation->location_id = $input->get('location_id');
$accommodation->save();
return response($accommodation, 200)
->header('Content-Type', 'application/json');
}
删除重新访问
此外,destroy
方法看起来像这样:
public function destroy(Accommodation $accommodation)
{
$accommodation->delete();
return response('Deleted.', 200)
->header('Content-Type', 'text/html');
}
超越 CRUD
如果软件应用的一个要求是能够搜索住宿,那么我们可以很容易地添加一个搜索函数。搜索函数将使用name
字符串查找住宿。一种方法是将路由添加到routes.php
文件中。这将把GET
调用映射到AccommodationsController
中包含的新的search()
函数:
Route::get('search', 'AccommodationsController@search');
Route::resource('accommodations', 'AccommodationsController');
提示
在这种情况下,GET
方法比POST
方法更可取,因为它可以被收藏并稍后调用。
现在,我们将编写我们的搜索函数:
public function search(Request $request, Accommodation $accommodation)
{
return $accommodation
->where('name',
'like',
'%'.$request->get('name').'%')
->get();
}
这里有几种机制:
-
包含来自
GET
请求的变量的Request
对象被类型提示,然后注入到搜索函数中 -
Accommodation
模型被类型提示,然后注入到search
函数中 -
在 Eloquent 模型
$accommodation
上调用了where()
方法 -
从
request
对象中使用name
参数 -
使用
get()
方法来执行实际的 SQL 查询
提示
请注意,查询构建器和 Eloquent 方法中的一些返回查询构建器的实例,而其他方法执行查询并返回结果。where()
方法返回查询构建器的实例,而get()
方法执行查询。
- 返回的 Eloquent 集合会自动编码为 JSON
因此,GET
请求如下:
http://www.hotelwebsite.com/search-accommodation?name=Lovely
生成的 JSON 看起来会像这样:
[{"id":3,
"name":"Lovely Hotel",
"description":"Lovely Hotel Greater Pittsburgh",
"location_id":1,
"created_at":"2015-03-13 22:00:23",
"updated_at":"2015-03-13 22:00:23",
"deleted_at":null},
{"id":4,
"name":"Lovely Hotel",
"description":"Lovely Hotel Greater Philadelphia",
"location_id":2,
"created_at":"2015-03-11 21:43:31",
"updated_at":"2015-03-11 21:43:31",
"deleted_at":null}]
嵌套控制器
嵌套控制器是 Laravel 5 中的一个新功能,用于处理涉及关系的所有 RESTful 操作。例如,我们可以利用这个功能来处理住宿和客房之间的关系。
住宿和客房之间的关系如下:
-
一个住宿可以有一个或多个房间(一对多)
-
一个房间只属于一个住宿(一对一)
现在,我们将编写代码,使得我们的模型能够熟练处理 Laravel 的一对一和一对多关系。
Accommodation 有许多 rooms
首先,我们将添加所需的代码到代表accommodation
模型的Accomodation.php
文件中:
class Accommodation extends Model {
public function rooms(){
return $this->hasMany('\MyCompany\Accommodation\Room');
}
}
rooms()
方法创建了一种从住宿模型内部访问关系的简单方法。关系说明了“住宿有许多房间”。hasMany
函数,当位于Accommodation
类内部时,没有额外的参数,期望Room
模型的表中存在一个名为accommodation_id
的列,这在这种情况下是rooms
。
Room 属于 accommodation
现在,我们将添加Room.php
文件所需的代码,该文件代表Room
模型:
class Room extends Model
{
public function accommodation(){
return $this->belongsTo('\MyCompany\Accommodation');
}
}
这段代码说明了“一个房间属于一个住宿”。Room
类中的belongsTo方法,没有额外的参数,期望room
模型的表中存在一个字段;在这种情况下,名为accommodation_id
的rooms
。
提示
如果应用程序数据库中的表遵循了活动记录约定,那么大多数 Eloquent 关系功能将自动运行。所有参数都可以很容易地配置。
创建嵌套控制器的命令如下:
**$php artisan make:controller AccommodationsRoomsController**
然后,以下行将被添加到app/Http/routes.php
文件中:
Route::resource('accommodations.rooms', 'AccommodationsRoomsController');
要显示创建的路由,应执行以下命令:
**$php artisan route:list**
以下表列出了 HTTP 动词及其功能:
HTTP 动词 | 功能 | URL | |
---|---|---|---|
1 | GET |
这显示了住宿和客房的关系 | /accommodations/{accommodations}/rooms |
2 | GET |
这显示了住宿和客房的关系 | /accommodations/{accommodations}/rooms/{rooms} |
3 | POST |
这创建了一个新的住宿和客房的关系 | /accommodations/{accommodations}/rooms |
4 | PUT |
这完全修改(更新)了住宿和客房的关系 | /accommodations/{accommodations}/rooms/{rooms} |
5 | PATCH |
部分修改(更新)住宿和房间关系 | /accommodations/{accommodations}/rooms/{rooms} |
6 | DELETE |
删除住宿和房间关系 | /accommodations/{accommodations}/rooms/{rooms} |
雄辩的关系
一个很好的机制用于直接在控制器内部说明雄辩关系,通过使用嵌套关系来执行,其中两个模型首先通过路由连接,然后通过它们的控制器方法的参数通过模型依赖注入连接。
嵌套更新
让我们调查update
/modify PUT
嵌套控制器命令。URL 看起来像这样:http://www.hotelwebsite.com/accommodations/21/rooms/13
。
这里,21
将是住宿的 ID,13
将是房间的 ID。参数是类型提示的模型。这使我们可以轻松地更新关系,如下所示:
public function update(Accommodation $accommodation, Room $room)
{
$room->accommodation()->associate($accommodation);
$room->save();
}
嵌套创建
同样,可以通过POST
请求将嵌套的create
操作执行到http://www.hotelwebsite.com/accommodations/21/rooms
。POST
请求的 body 是一个 JSON 格式的对象:
{"roomNumber":"123"}
请注意,由于我们正在创建房间,因此不需要房间 ID:
public function store(Accommodation $accommodation)
{
$input = \Input::json();
$room = new Room();
$room->room_number = $input->get('roomNumber');
$room->save();
$accommodation->rooms()->save($room);
}
雄辩模型转换
模型以 JSON 格式返回,就像它们在数据库中表示的那样。通常,模型属性,其性质为布尔值,分别用0
和1
表示true
和false
。在这种情况下,更方便的是返回一个真正的true
和false
给 RESTful 调用的返回对象。
在 Laravel 4 中,这是使用访问器完成的。如果值是$status
,则方法将定义如下:
public function getStatusAttribute($value){
//do conversion;
}
在 Laravel 5 中,由于有了一个称为模型转换的新功能,这个过程变得更加容易。要应用这种技术,只需将一个受保护的键和一个名为$casts
的值数组添加到模型中,如下所示:
class Room extends Model
{
protected $casts = ['room_number'=>'integer','status'=>'boolean'];
public function accommodation(){
return $this->belongsTo('\MyCompany\Accommodation');
}
}
在这个例子中,room_number
是一个字符串,但我们想返回一个整数。状态是一个小整数,但我们想返回一个布尔值。在模型中对这两个值进行转换将以以下方式修改结果 JSON:
{"id":1,
"room_number": "101",
"status": 1,
"created_at":"2015-03-14 09:25:59",
"updated_at":"2015-03-14 19:03:03",
"deleted_at":null,
"accommodation_id":2}
前面的代码现在将改变如下:
{"id":1,
"room_number": 101,
"status": true,
"created_at":"2015-03-14 09:25:59",
"updated_at":"2015-03-14 19:03:03",
"deleted_at":null,
"accommodation_id":2}
路由缓存
Laravel 5 有一个新的机制用于缓存路由,因为routes.php
文件很容易变得非常庞大,并且会迅速减慢请求过程。要启用缓存机制,输入以下artisan
命令:
**$ php artisan route:cache**
这将在/storage/framework/routes.php
中创建另一个routes.php
文件。如果该文件存在,则会使用它,而不是位于app/Http/routes.php
中的routes.php
文件。文件的结构如下:
<?php
/*
|--------------------------------------------------------------------------
| Load The Cached Routes
|
…
*/
app('router')->setRoutes(
unserialize(base64_decode('TzozNDoiSWxsdW1pbmF0ZVxSb3V0aW5nXFJvdXRlQ29sbGVjdGlvbiI6NDp7czo5OiIAKgByb3V0ZXMiO2E6Njp7czozOiJHRVQiO2E6M
…
... VyQGluZGV4IjtzOjk6Im5hbWVzcGFjZSI7czoyNjoiTXlDb21wYWbXBhbnlcSHR0cFxDb250cm9sbGVyc1xIb3RlbENvbnRyb2xsZXJAZGVzdHJveSI7cjo4Mzg7fX0='))
);
请注意,这里使用了一个有趣的技术。路由被序列化,然后进行 base64 编码。显然,要读取路由,使用相反的方法,base64_decode()
,然后unserialize()
。
如果routes.php
缓存文件存在,则每次对routes.php
文件进行更改时,都必须执行路由缓存artisan
命令。这将清除文件,然后重新创建它。如果以后决定不再使用这种机制,则可以使用以下artisan
命令来消除该文件:
**$ php artisan route:clear**
Laravel 对于构建几种完全不同类型的应用程序非常有用。在构建传统的 Web 应用程序时,控制器和视图之间通常有着紧密的集成。当构建可以在智能手机上使用的应用程序时,它也非常有用。在这种情况下,前端将使用另一种编程语言和/或框架为智能手机的操作系统创建。在这种情况下,可能只会使用控制器和模型。无论哪种情况,拥有一个良好文档化的 RESTful API 是现代软件设计的重要组成部分。
嵌套控制器帮助开发人员立即阅读代码——这是一种理解特定控制器处理“嵌套”或一个类与另一个相关联的概念的简单方法。
在控制器中对模型和对象进行类型提示也提高了可读性,同时减少了执行对象的基本操作所需的代码量。
此外,雄辩的模型转换为模型的属性提供了一种简单的方式,无需依赖外部包或繁琐的访问器函数,就像在 Laravel 4 中那样。
现在我们很清楚为什么 Laravel 正在成为许多开发人员的选择。学习并重复本章中所述的一些步骤将允许在一个小时内为一个中小型程序创建一个 RESTful API。
总结
RESTful API 为将来扩展程序提供了一种简单的方式,也与公司内部可能需要与应用程序通信的第三方程序和软件集成。RESTful API 是程序内部的最前端外壳,并提供了外部世界与应用程序本身之间的桥梁。程序的内部部分将是所有业务逻辑和数据库连接所在的地方,因此从根本上说,控制器只是连接路由和应用程序的工作。
Laravel 遵循 RESTful 最佳实践,因此文档化 API 对其他开发人员和第三方集成商来说应该足够容易理解。Laravel 5 为框架引入了一些功能,使代码更易读。
在未来的章节中,将讨论中间件。中间件在路由和控制器之间添加了各种“中间”层。中间件可以提供诸如身份验证之类的功能。中间件将丰富、保护并帮助将路由组织成逻辑和功能组。
我们还将讨论 DocBlock 注释。虽然 PHP 本身不支持注释,但可以通过 Laravel 社区包启用。然后,在控制器和控制器函数的 DocBlock 中,每个控制器的路由将自动创建,而无需实际修改app/Http/routes.php
文件。这是 Laravel 轻松适应的另一个伟大的社区概念,就像 phpspec 和 Behat 一样。
第五章:使用表单生成器
在本章中,您将学习如何使用 Laravel 的表单生成器。表单生成器将被演示以便于构建以下元素:
-
表单(打开和关闭)
-
标签
-
输入(文本,HTML5 密码,HTML5 电子邮件等)
-
复选框
-
提交
-
锚标签(href 链接)
最后,我们将看到如何使用表单生成器为住宿预订软件表单创建月份、日期和年份选择元素的示例,以及如何创建一个宏来减少代码重复。
历史
Laravel 4 中的表单生成器包称为 HTML。这是用来帮助您创建 HTML 的,特别是那些还必须执行 Web 设计师职责但更喜欢使用 Laravel 门面和辅助方法的开发人员。例如,以下是 Laravel 门面select()
方法的示例,其中语言的选项,例如英式和美式英语,在此示例中作为数组参数传递:
Form::select('language', ['en-us' => 'English (US)','en-gb' => 'English (UK)']);
这可以作为标准 HTML 的替代方案,标准 HTML 需要更多重复的代码,如下面的代码所示:
<select name="language">
<option value="en-us">English (US)</option>
<option value="en-gb">English (UK)</option>
</select>
由于框架不断发展,它们需要适应满足大多数用户的需求。此外,尽可能地,它们应该继续变得更加高效。在某些情况下,这意味着重写或重构框架的部分,添加功能,甚至删除它们。
尽管可能看起来奇怪,但删除功能有几个有效的原因。以下是删除包的原因列表:
-
减轻框架核心开发人员需要维护的包和功能的负担和数量。
-
减少下载和自动加载的包数量。
-
删除一个不必要的功能。
-
HTML 包已经从 Laravel 5 的核心中移除,现在是一个外部包。在这种情况下,任何之前的原因都可以被引用为移除这个包的原因。
-
HTML 有助于开发人员构建表单,如果前端开发人员也是后端或全栈开发人员,并且喜欢 Laravel 的做事方式,可以使用。然而,在其他情况下,Web 应用的 HTML 界面可以使用 JavaScript 框架或库来构建,例如 AngularJS 或 Backbone.js。在这种情况下,Laravel 表单包就不是必需的。另外,如前所述,Laravel 可以用来创建一个仅仅是 RESTful API 的应用程序。在这种情况下,将 HTML 包包含在框架核心中就不是必要的,因此仍然是辅助的。
在这种特殊情况下,某些 Laravel 包被移除以简化整体体验,并朝着更基于组件的方法迈进,这与 Symfony 中使用的方法类似。
安装 HTML 包
如果您希望在 Laravel 5 中使用 HTML 包,安装它是一个简单的过程。Laravel 社区的一群开发人员成立了一个名为 Laravel collective 的存储库,用于维护已从 Laravel 中移除的包。要安装 HTML 包,只需使用composer
命令将包添加到应用程序中,如下所示:
**$ composer require laravelcollective/html**
注意
请注意,illuminate/HTML
包已被弃用。
这将安装 HTML 包,并且composer.json
将显示您添加到require
部分的包如下:
"require": {
"laravel/framework": "5.0.*",
"laravelcollective/html": "~5.0",
},
此时,包已安装。
现在,我们需要将HTMLServiceProvider
添加到config/app.php
文件中的提供者列表中:
'providers' => [
...
'Collective\Html\HtmlServiceProvider',
...
],
最后,需要将Form
和Html
别名添加到config/app.php
文件中,如下所示:
'aliases' => [
...
'Form' => 'Collective\Html\FormFacade',
'Html' => 'Collective\Html\HtmlFacade',
...
],
使用 Laravel 构建网页
Laravel 构建 Web 内容的方法是灵活的。可以使用尽可能多或尽可能少的 Laravel 来创建 HTML。Laravel 使用filename.blade.php
约定来说明文件应该由 blade 解析器解析,实际上将文件转换为普通的 PHP。Blade 的名称受到了.NET 的剃刀模板引擎的启发,因此对于曾经使用过它的人来说可能会很熟悉。Laravel 5 在/resources/views/
目录中提供了一个表单的工作演示。当请求/home
路由并且用户当前未登录时,将显示此视图。显然,这个表单并不是使用 Laravel 的表单方法创建的。
路由在routes
文件中定义如下:
Route::get('home', 'HomeController@index');
将讨论此路由如何使用中间件来检查如何执行用户身份验证,详见第七章,“使用中间件过滤请求”。
主模板
这是以下的app
(或master
)模板:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Laravel</title>
<link href="/css/app.css" rel="stylesheet">
<!-- Fonts -->
<link href='//fonts.googleapis.com/css?family=Roboto:400,300' rel='stylesheet' type='text/css'>
<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
</head>
<body>
<nav class="navbarnavbar-default">
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1">
<span class="sr-only">Toggle Navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">Laravel</a>
</div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="navnavbar-nav">
<li><a href="/">Home</a></li>
</ul>
<ul class="navnavbar-navnavbar-right">
@if (Auth::guest())
<li><a href="{{ route('auth.login') }}">Login</a></li>
<li><a href="/auth/register">Register</a></li>
@else
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">{{ Auth::user()->name }} <span class="caret"></span></a>
<ul class="dropdown-menu" role="menu">
<li><a href="/auth/logout">Logout</a></li>
</ul>
</li>
@endif
</ul>
</div>
</div>
</nav>
@yield('content')
<!-- Scripts -->
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.1/js/bootstrap.min.js"></script>
</body>
</html>
Laravel 5 主模板是一个具有以下特点的标准 HTML5 模板:
-
如果浏览器旧于 Internet Explorer 9:
-
使用 HTML5 Shim 来自 CDN
-
使用 Respond.js JavaScript 代码来自 CDN 以适应媒体查询和 CSS3 特性
-
使用
@if (Auth::guest())
,如果用户未经过身份验证,则显示登录表单;否则,显示注销选项。 -
Twitter bootstrap 3.x 包含在 CDN 中
-
jQuery2.x 包含在 CDN 中
-
任何扩展此模板的模板都可以覆盖内容部分
示例页面
以下截图显示了登录页面:
登录页面的源代码如下:
@extends('app')
@section('content')
<div class="container-fluid">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div class="panel panel-default">
<div class="panel-heading">Login</div>
<div class="panel-body">
@if (count($errors) > 0)
<div class="alert alert-danger">
<strong>Whoops!</strong> There were some problems with your input.<br><br>
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<form class="form-horizontal" role="form" method="POST" action="/auth/login">
<input type="hidden" name="_token" value="{{ csrf_token() }}">
<div class="form-group">
<label class="col-md-4 control-label">E-Mail Address</label>
<div class="col-md-6">
<input type="email" class="form-control" name="email" value="{{ old('email') }}">
</div>
</div>
<div class="form-group">
<label class="col-md-4 control-label">Password</label>
<div class="col-md-6">
<input type="password" class="form-control" name="password">
</div>
</div>
<div class="form-group">
<div class="col-md-6 col-md-offset-4">
<div class="checkbox">
<label>
<input type="checkbox" name="remember"> Remember Me
</label>
</div>
</div>
</div>
<div class="form-group">
<div class="col-md-6 col-md-offset-4">
<button type="submit" lass="btn btn-primary" style="margin-right: 15px;">
Login
</button>
<a href="/password/email">Forgot Your Password?</a>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection
从静态 HTML 到静态方法
此登录页面以以下内容开始:
@extends('app')
显然,它使用面向对象的范例来说明将呈现app.blade.php
模板。以下行覆盖了内容:
@section('content')
在这个练习中,将使用表单构建器而不是静态 HTML。
表单标签
我们将把静态的form
标签转换为FormBuilder
方法。HTML 如下:
<form class="form-horizontal" role="form" method="POST" action="/auth/login">
我们将使用的外观方法如下:
Form::open();
在FormBuilder.php
类中,$reserved
属性定义如下:
protected $reserved = ['method', 'url', 'route', 'action', 'files'];
我们需要传递给open()
方法的属性是 class、role、method 和 action。由于 method 和 action 是保留字,因此需要以以下方式传递参数:
Laravel 表单外观方法数组元素 | HTML 表单标签属性 |
---|---|
方法 | 方法 |
url | action |
role | role |
class | class |
因此,方法调用如下:
{!!
Form::open(['class'=>'form-horizontal',
'role =>'form',
'method'=>'POST',
'url'=>'/auth/login'])
!!}
{!! !!}
标签用于开始和结束表单构建器方法的解析。form
方法POST
首先放置在 HTML 表单标签的属性列表中。
提示
action
属性实际上需要是一个url
。如果使用action
参数,则它指的是控制器动作。在这种情况下,url
参数会生成form
标签的action
属性。
其他属性将传递给数组并添加到属性列表中。生成的 HTML 将如下所示:
<form method="POST" action="http://laravel.example/auth/login" accept-charset="UTF-8" class="form-horizontal" role="form">
<input name="_token" type="hidden" value="wUY2hFSEWCzKHFfhywHvFbq9TXymUDiRUFreJD4h">
CRSF 令牌会自动添加,因为form
方法是POST
。
文本输入字段
要转换输入字段,使用外观。输入字段的 HTML 如下:
<input type="email" class="form-control" name="email" value="{{ old('email') }}">
使用外观转换前面的输入字段如下:
{!! Form::input('email','email',old('email'),['class'=>'form-control' ]) !!}
同样,文本字段变为:
{!! Form::input('password','password',null,['class'=>'form-control']) !!}
输入字段具有相同的签名。当然,这可以重构如下:
<?php $inputAttributes = ['class'=>'form-control'] ?>
{!! Form::input('email','email',old('email'),$inputAttributes ) !!}
...
{!! Form::input('password','password',null,$inputAttributes ) !!}
标签标签
label
标签如下:
<label class="col-md-4 control-label">E-Mail Address</label>
<label class="col-md-4 control-label">Password</label>
要转换label
标签(E-Mail Address
和Password
),我们首先创建一个数组来保存属性,然后将此数组传递给标签,如下所示:
$labelAttributes = ['class'=>'col-md-4 control-label'];
以下是表单标签代码:
{!! Form::label('email', 'E-Mail Address', $labelAttributes) !!}
{!! Form::label('password', 'Password', $labelAttributes) !!}
复选框
要将复选框转换为外观,我们将转换为:
<input type="checkbox" name="remember"> Remember Me
前面的代码转换为以下代码:
{!! Form::checkbox('remember','') !!} Remember Me
提示
请记住,如果字符串中没有变量或其他特殊字符(如换行符),则应该用单引号发送 PHP 参数,而生成的 HTML 将使用双引号。
提交按钮
最后,提交按钮将被转换如下:
<button type="submit" class="btn btn-primary" style="margin-right: 15px;">
Login
</button>
转换后的前一行代码如下:
{!!
Form::submit('Login',
['class'=>'btn btn-primary',
'style'=>'margin-right: 15px;'])
!!}
提示
请注意,数组参数提供了一种简单的方式来提供任何所需的属性,甚至那些不在标准 HTML 表单元素列表中的属性。
带有链接的锚标签
为了转换链接,使用了一个辅助方法。考虑以下代码行:
<a href="/password/email">Forgot Your Password?</a>
转换后的前一行代码如下:
{!! link_to('/password/email', $title = 'Forgot Your Password?', $attributes = array(), $secure = null) !!}
注意
link_to_route()
方法可用于链接到一个路由。有关类似的辅助函数,请访问laravelcollective.com/docs/5.0/html
。
关闭表单
为了结束表单,我们将把传统的 HTML 表单标签</form>
转换为 Laravel 的{!! Form::close() !!}
表单方法。
结果表单
将所有内容放在一起后,页面现在看起来是这样的:
@extends('app')
@section('content')
<div class="container-fluid">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div class="panel panel-default">
<div class="panel-heading">Login</div>
<div class="panel-body">
@if (count($errors) > 0)
<div class="alert alert-danger">
<strong>Whoops!</strong> There were some problems with your input.<br><br>
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<?php $inputAttributes = ['class'=>'form-control'];
$labelAttributes = ['class'=>'col-md-4 control-label']; ?>
{!! Form::open(['class'=>'form-horizontal','role'=>'form','method'=>'POST','url'=>'/auth/login']) !!}
<div class="form-group">
{!! Form::label('email', 'E-Mail Address',$labelAttributes) !!}
<div class="col-md-6">
{!! Form::input('email','email',old('email'), $inputAttributes) !!}
</div>
</div>
<div class="form-group">
{!! Form::label('password', 'Password',$labelAttributes) !!}
<div class="col-md-6">
{!! Form::input('password','password',null,$inputAttributes) !!}
</div>
</div>
<div class="form-group">
<div class="col-md-6 col-md-offset-4">
<div class="checkbox">
<label>
{!! Form::checkbox('remember','') !!} Remember Me
</label>
</div>
</div>
</div>
<div class="form-group">
<div class="col-md-6 col-md-offset-4">
{!! Form::submit('Login',['class'=>'btn btn-primary', 'style'=>'margin-right: 15px;']) !!}
{!! link_to('/password/email', $title = 'Forgot Your Password?', $attributes = array(), $secure = null); !!}
</div>
</div>
{!! Form::close() !!}
</div>
</div>
</div>
</div>
</div>
@endsection
我们的例子
如果我们想要创建一个预订住宿的表单,我们可以轻松地从我们的控制器中调用一个路由:
/**
* Show the form for creating a new resource.
*
* @return Response
*/
public function create()
{
return view('auth/reserve');
}
现在我们需要创建一个位于resources/views/auth/reserve.blade.php
的新视图。
在这个视图中,我们可以创建一个表单来预订住宿,用户可以选择开始日期,其中包括月份和年份的开始日期,以及结束日期,也包括月份和年份的开始日期:
表单将如前所述开始,通过 POST 到reserve-room
。然后,表单标签将放置在选择输入字段旁边。最后,日期、月份和年份选择表单元素将被创建如下:
{!! Form::open(['class'=>'form-horizontal',
'role'=>'form',
'method'=>'POST',
'url'=>'reserve-room']) !!}
{!! Form::label(null, 'Start Date',$labelAttributes) !!}
{!! Form::selectMonth('month',date('m')) !!}
{!! Form::selectRange('date',1,31,date('d')) !!}
{!! Form::selectRange('year',date('Y'),date('Y')+3) !!}
{!! Form::label(null, 'End Date',$labelAttributes) !!}
{!! Form::selectMonth('month',date('m')) !!}
{!! Form::selectRange('date',1,31,date('d')) !!}
{!! Form::selectRange('year',date('Y'),date('Y')+3,date('Y')) !!}
{!! Form::submit('Reserve',
['class'=>'btn btn-primary',
'style'=>'margin-right: 15px;']) !!}
{!! Form::close() !!}
月份选择
首先,在selectMonth
方法中,第一个参数是输入属性的名称,而第二个属性是默认值。这里,PHP 日期方法被用来提取当前月份的数字部分——在这种情况下是三月:
**{!! Form::selectMonth('month',date('m')) !!}**
格式化后的输出如下:
<select name="month">
<option value="1">January</option>
<option value="2">February</option>
<option value="3" selected="selected">March</option>
<option value="4">April</option>
<option value="5">May</option>
<option value="6">June</option>
<option value="7">July</option>
<option value="8">August</option>
<option value="9">September</option>
<option value="10">October</option>
<option value="11">November</option>
<option value="12">December</option>
</select>
日期选择
类似的技术也适用于选择日期,但是使用selectRange
方法,将月份中的日期范围传递给该方法。同样,PHP 日期函数被用来将当前日期作为第四个参数传递给该方法:
{!! Form::selectRange('date',1,31,date('d')) !!}
这里是格式化后的输出:
<select name="date">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
...
<option value="28">28</option>
<option value="29">29</option>
<option value="30" selected="selected">30</option>
<option value="31">31</option>
</select>
应该选择的日期是 30,因为今天是 2015 年 3 月 30 日。
提示
对于没有 31 天的月份,通常会使用 JavaScript 方法根据月份和/或年份修改天数。
年份选择
用于日期范围的相同技术也适用于年份的选择;再次使用selectRange
方法。年份范围被传递给该方法。PHP 日期函数被用来将当前年份作为第四个参数传递给该方法:
{!! Form::selectRange('year',date('Y'),date('Y')+3,date('Y')) !!}
这里是格式化后的输出:
<select name="year">
<option value="2015" selected="selected">2015</option>
<option value="2016">2016</option>
<option value="2017">2017</option>
<option value="2018">2018</option>
</select>
这里,选择的当前年份是 2015 年。
表单宏
我们有相同的代码,用于生成我们的月份、日期和年份选择表单块两次:一次用于开始日期,一次用于结束日期。为了重构代码,我们可以应用 DRY(不要重复自己)原则并创建一个表单宏。这将允许我们避免两次调用表单元素创建方法,如下所示:
<?php
Form::macro('monthDayYear',function($suffix='')
{
echo Form::selectMonth(($suffix!=='')?'month-'.$suffix:'month',date('m'));
echo Form::selectRange(($suffix!=='')?'date-'.$suffix:'date',1,31,date('d'));
echo Form::selectRange(($suffix!=='')?'year-'.$suffix:'year',date('Y'),date('Y')+3,date('Y'));
});
?>
这里,月份、日期和年份生成代码被放入一个宏中,该宏位于 PHP 标签内,并且需要添加echo
来打印结果。给这个宏方法取名为monthDayYear
。调用我们的宏两次:每个标签后调用一次;每次通过$suffix
变量添加不同的后缀。现在,我们的表单代码看起来是这样的:
<?php
Form::macro('monthDayYear',function($suffix='')
{
echo Form::selectMonth(($suffix!=='')?'month-'.$suffix:'month',date('m'));
echo Form::selectRange(($suffix!=='')?'date-'.$suffix:'date',1,31,date('d'));
echo Form::selectRange(($suffix!=='')?'year-'.$suffix:'year',date('Y'),date('Y')+3,date('Y'));
});
?>
{!! Form::open(['class'=>'form-horizontal',
'role'=>'form',
'method'=>'POST',
'url'=>'/reserve-room']) !!}
{!! Form::label(null, 'Start Date',$labelAttributes) !!}
{!! Form::monthDayYear('-start') !!}
{!! Form::label(null, 'End Date',$labelAttributes) !!}
{!! Form::monthDayYear('-end') !!}
{!! Form::submit('Reserve',['class'=>'btn btn-primary',
'style'=>'margin-right: 15px;']) !!}
{!! Form::close() !!}
结论
在 Laravel 5 中选择包含 HTML 表单生成包可以减轻创建大量 HTML 表单的负担。这种方法允许开发人员使用方法,创建可重用的宏,并使用熟悉的 Laravel 方法来构建前端。一旦学会了基本方法,就可以很容易地复制和粘贴以前创建的表单元素,然后更改它们的元素名称和/或发送给它们的数组。
根据项目的大小,这种方法可能是正确的选择,也可能不是。对于非常小的应用程序,需要编写的代码量的差异并不明显,尽管,如selectMonth
和selectRange
方法所示,所需的代码量是 drastc 的。
这种技术与宏的使用结合起来,可以轻松减少复制重复的发生。此外,前端设计的一个主要问题是各种元素的类的内容可能需要在整个应用程序中进行更改。这意味着需要执行大量的查找和替换操作,需要对 HTML 进行更改,例如更改类属性。通过创建包含类等属性的数组,可以通过修改这些元素使用的数组来执行对整个表单的更改。
然而,在一个更大的项目中,表单的部分可能在整个应用程序中重复,明智地使用宏可以轻松减少需要编写的代码量。不仅如此,宏还可以将代码与多个文件中需要更改的更改隔离开来。在要选择月份、日期和年份的示例中,这在一个大型应用程序中可能会被使用多达 20 次。对所需的 HTML 块进行的任何更改可以简单地通过修改这个宏来反映在使用它的所有元素中。
最终,是否使用此包的选择将由开发人员和设计人员决定。由于想要使用替代前端设计工具的设计人员可能不喜欢也可能不熟练地使用包中的方法,因此可能不想使用它。
总结
在本章中,概述了 HTML Laravel composer 包的历史和安装。解释了主模板的构建,然后通过示例展示了表单组件,如各种表单输入类型。
最后,解释了在书中示例软件中使用的房间预订表单的构建,以及“不要重复自己”的表单宏创建技术。
在下一章中,我们将看一种使用注释来减少应用程序控制器创建路由所需时间的方法。
第六章:使用注解驯服复杂性
在上一章中,您学习了如何创建一个涉及从互联网接收请求、将其路由到控制器并处理的 RESTful API。在本章中,您将学习如何在 DocBlock 中使用注解,这是一种需要更少代码的路由执行方式,可以更快、更有组织地进行团队协作编程。
注解将被用于:
-
路由 HTTP 请求,如 GET、POST 和 PUT
-
将控制器转换为完全启用的 CRUDL 资源
-
监听从命令触发的事件
-
向控制器添加中间件以限制或过滤请求
注解是编程中使用的重要机制。注解是增强其他数据的元数据。由于这可能看起来有点混乱,所以我们需要首先了解元数据的含义。元数据是一个包含两部分的词:
-
meta:这是一个希腊词,意思是超越或包含。
-
data:这是一个拉丁词,意思是信息片段。
因此,元数据用于增强或扩展某物的含义。
其他编程语言中的注解
接下来,我们将讨论在计算机编程中使用的注解。我们将从 Java、C#和 PHP 中看几个例子,然后最后,看一下注解在 Laravel 中的使用。
Java 中的注解
注解首次在 Java 版本 1.1 中提出,并在版本 1.2 中添加。以下是一个用于覆盖动物的speak
方法的注解示例:
Java 1.2
/**
* @author Jon Doe <jon@doe.com>
* @version 1.6 (current version number)
* @since 2010-03-31 (version package...)
*/
public void speak() {
}
public class Animal {
public void speak() {
}
}
public class Cat extends Animal {
@Override
public void speak() {
System.out.println("Meow.");
}
}
请注意,@
符号用于向编译器发出此注解@Override
很重要的信号。
C#中的注解
在 C#中,注解称为属性,使用方括号而不是更常用的@
符号:
[AttributeUsageAttribute(AttributeTargets.Property|AttributeTargets.Field, AllowMultiple = false, Inherited = true)]
public sealed class AssociationAttribute : Attribute
PHP 中的注解
其他 PHP 框架也使用注解。Symfony 广泛使用注解。在Doctrine中,这是 Symfony 的 ORM,类似于 Laravel 的 Eloquent,使用注解来定义关系。Symfony 还使用注解进行路由。Zend Framework(ZF)也使用注解。测试工具 Behat 和 PHPUnit 都使用注解。在 Behat 的以下示例中,使用注解指示应在测试套件之前执行此方法:
/**
* @BeforeSuite
*/
public static function prepare(SuiteEvent $event)
{
// prepare system for test suite
// before it runs
}
DocBlock 注解
在前面的 Behat 示例中展示的注解使用示例相当有趣,因为它将注解放在了 DocBlock 内部。DocBlock 以斜杠和两个星号开头:
/**
它包含n行以星号开头。
DocBlock 以单个星号和斜杠结束:
*/
这种语法告诉解析器,除了普通注释之外,DocBlock 中还有一些有用的东西。
Laravel 中的 DocBlock 注解
当 Laravel 5 正在开发时,最初添加了通过 DocBlock 注解支持路由和事件监听器。它的语法类似于 Symfony 和 Zend。
Symfony
Symfony 的语法如下:
/**
* @Route("/accommodations/search")
* @Method({"GET"})
*/
public function searchAction($id)
{
Zend
Zend 的语法如下:
/**
* @Route(route="/accommodations/search")
*/
public function searchAction()
{
Laravel
Laravel 的语法如下:
/**
* @Get("/hotels/search")
*/
public function search()
{
但是,DocBlock 注解试图解决什么类型的问题呢?
Doc-annotations 的一个用途是将它们添加到控制器中,从而将路由和中间件的控制移交给控制器。这将使控制器更具可移植性,甚至是与框架无关的,因为routes.php
文件的作用会减少,甚至完全不存在。如下例所示,routes.php
文件可能会变得非常庞大,这将导致复杂性甚至使文件难以管理:
Route::patch('hotel/{hid}/room/{rid}','AccommodationsController@editRoom');
Route::post('hotel/{hid}/room/{rid}','AccommodationsController@reserve');
Route::get('hotel/stats,HotelController@Stats');
Route::resource('country', 'CountryController');
Route::resource(city', 'CityController');
Route::resource('state', 'StateController');
Route::resource('amenity', 'AmenitiyController');
Route::resource('country', 'CountryController');
Route::resource(city', 'CityController');
Route::resource('country', 'CountryController');
Route::resource('city', 'CityController');
Route::resource('horse', 'HorseController');
Route::resource('cow', 'CowController');
Route::resource('zebra', 'ZebraController');
Route::get('dragon/{id}', 'DragonController@show');
Route::resource('giraffe', 'GiraffeController');
Route::resource('zebrafish', 'ZebrafishController');
DocBlock 注解的想法是驯服这种复杂性,因为路由将被移动到控制器中。
在 Laravel 5.0 发布之前不久,由于社区的不满,该功能被移除。此外,由于一些开发人员可能不想使用这种方法,将此包从 Laravel 的核心中移出并打包是合适的。安装该包的方法类似于添加 HTML 包的方式。这个包也得到了 Laravel Collective 的支持。通过输入以下 composer 命令很容易添加注释:
**$ composer require laravelcollective/annotations**
这将安装注释包,而composer.json
将显示包添加到 require 部分,如下所示:
"require": {
"laravel/framework": "5.0.*",
"laravelcollective/annotations": "~5.0",
},
下一步将是创建一个名为AnnotationsServiceProvider.php
的文件,并添加以下代码:
<?php namespace App\Providers;
use Collective\Annotations\AnnotationsServiceProvider as ServiceProvider;
class AnnotationsServiceProvider extends ServiceProvider {
/**
* The classes to scan for event annotations.
*
* @var array
*/
protected $scanEvents = [];
/**
* The classes to scan for route annotations.
*
* @var array
*/
protected $scanRoutes = [];
/**
* The classes to scan for model annotations.
*
* @var array
*/
protected $scanModels = [];
/**
* Determines if we will auto-scan in the local environment.
*
* @var bool
*/
protected $scanWhenLocal = false;
/**
* Determines whether or not to automatically scan the controllers
* directory (App\Http\Controllers) for routes
*
* @var bool
*/
protected $scanControllers = false;
/**
* Determines whether or not to automatically scan all namespaced
* classes for event, route, and model annotations.
*
* @var bool
*/
protected $scanEverything = false;
}
接下来,AnnotationsServiceProvider.php
文件将需要添加到config/app.php
文件中。需要添加命名空间的类应添加到 providers 数组中,如下所示:
'providers' => [
// ...
'App\Providers\AnnotationsServiceProvider'
];
使用 DocBlock 注释的资源控制器
现在,为了说明 Laravel 的 DocBlock 注释的使用,我们将检查以下步骤。
首先,我们将像往常一样创建住宿控制器:
**$ php artisan make:controller AccommodationsController**
接下来,我们将将住宿控制器添加到注释服务提供程序要扫描的路由列表中:
protected $scanRoutes = [
'App\Http\Controllers\HomeController',
'App\Http\Controllers\AccommodationsController'
];
现在,我们将向控制器添加 DocBlock 注释。在这种情况下,我们将指示解析器将此控制器用作住宿路由的资源控制器。要添加的代码如下:
/**
* @Resource("/accommodations")
*/
由于整个控制器将被转换为资源,因此 DocBlock 注释应该在类定义之前插入。AccommodationsController
类现在应该如下所示:
<?php namespace MyCompany\Http\Controllers;
use Illuminate\Support\Facades\Response;
use MyCompany\Http\Requests;
use MyCompany\Http\Controllers\Controller;
use MyCompany\Accommodation;
use Illuminate\Http\Request;
/**
* @Resource("/accommodations")
*/
class AccommodationsController extends Controller {
/**
* Display a listing of the resource.
*
* @return Response
*/
public function index(Accommodation $accommodation)
{
return $accommodation->paginate();
}
注意
请注意,这里需要双引号:
@Resource("/accommodations")
以下语法,使用单引号,将不正确并且不起作用:
@Resource('/accommodations')
单方法路由
如果我们只想为单个方法添加一个路由,比如“搜索住宿”,那么一个注解将被添加到单个方法的上方;然而,这一次是在类的内部。为了处理 GET HTTP 请求动词,代码将如下所示:
/**
* Search for an accommodation
* @Get("/search-accommodation")
*/
类将如下所示:
<?php namespace MyCompany\Http\Controllers;
use Illuminate\Support\Facades\Response;
use MyCompany\Http\Requests;
use MyCompany\Http\Controllers\Controller;
use MyCompany\Accommodation;
use Illuminate\Http\Request;
class AccommodationsController extends Controller {
/**
* Search for an accommodation
* @Get("/search-accommodation")
*/
public function index(Accommodation $accommodation)
{
return $accommodation->paginate();
}
扫描路由
接下来的步骤非常重要。Laravel 应用程序必须处理注释。为此,Artisan 用于扫描路由。
以下命令用于扫描路由。输出将显示Routes scanned!
,如下所示:
**$ php artisan route:scan**
**Routes scanned!**
此扫描的结果将在storage/framework
目录中产生一个名为routes.scanned.php
的文件。
以下代码将写入storage/framework/routes.scanned.php
文件:
$router->get('search-accommodation', [
'uses' => 'MyCompany\Http\Controllers\AccommodationsController@search',
'as' => NULL,
'middleware' => [],
'where' => [],
'domain' => NULL,
]);
注意
请注意,storage/framework/routes.scanned.php
文件不需要放入源代码控制中,因为它是生成的。
自动扫描
如果开发人员在构建控制器时必须执行 Artisan 路由扫描命令,那么这样做可能变得乏味。为了方便开发人员,在开发模式下,有一种方法可以让 Laravel 自动扫描scanRoutes
数组中的控制器。
在AnnotationsServiceProvider.php
文件中,将scanWhenLocal
属性设置为true
。
对于$scanControllers
和$scanEverything
也是如此;这两个布尔标志允许框架自动扫描App\Http\Controllers
目录和任何有命名空间的类。
必须记住,这应该只在开发和开发机器上使用,因为它会给请求周期增加不必要的开销。将属性设置为true
的示例如下所示:
<?php namespace App\Providers;
use Collective\Annotations\AnnotationsServiceProvider as ServiceProvider;
class AnnotationsServiceProvider extends ServiceProvider {
/**
* The classes to scan for event annotations.
*
* @var array
*/
protected $scanEvents = [];
…
/**
* Determines if we will auto-scan in the local environment.
*
* @var bool
*/
protected $scanWhenLocal = true;
/**
* Determines whether or not to automatically scan the controllers
* directory (App\Http\Controllers) for routes
*
* @var bool
*/
protected $scanControllers = true;
/**
* Determines whether or not to automatically scan all namespaced
* classes for event, route, and model annotations.
*
* @var bool
*/
protected $scanEverything = true;
}
启用这些选项将减慢框架的速度,但允许在开发阶段灵活性。
额外的注释
要将 ID 传递给路由,就像在显示单个住宿时一样,代码将如下所示:
/**
* Display the specified resource.
* @Get("/accommodation/{id}")
*/
这个 DocBlock 注释将被放置在类内部的函数上方,这与之前的例子类似。
要将 ID 限制为一个或多个数字,可以使用@Where
注释如下:
@Where({"id": "\d+"})
如下所示,两个注释被合并在一起:
/**
* Display the specified resource.
* @Get("/accommodation/{id}")
* @Where({"id": "\d+"})
*/
要向示例添加中间件,限制请求仅限于经过身份验证的用户,可以使用@Middleware
注释:
/**
* Display the specified resource.
* @Get("/accommodation/{id}")
* @Where({"id": "\d+"})
* @Middleware("auth")
*/
HTTP 动词
以下是可以使用注释的各种 HTTP 动词的列表,它们与 RESTful 标准相对应:
-
@Delete
:此动词删除一个资源。 -
@Get
:此动词显示一个资源或多个资源。 -
@Options
:此动词显示选项列表。 -
@Patch
:此动词修改资源的属性。 -
@Post
:此动词创建一个新资源。 -
@Put
:此动词修改资源。
其他注释
还有其他注释也可以在控制器中使用。这些注释如下:
-
@Any
:对任何 HTTP 请求做出响应。 -
@Controller
:为资源创建一个控制器。 -
@Middleware
:这为资源添加中间件。 -
@Route
:这使得路由可用。 -
@Where
:根据特定条件限制请求。 -
@Resource
:这使得资源可用。
在 Laravel 5 中使用注释
让我们回顾一下在 Laravel 中实现的路径,如下所示:
-
HTTP 请求被路由到控制器
-
命令是在控制器内部实例化的
-
事件被触发
-
事件被处理
Laravel 的现代基于命令的发布-订阅路径。
使用注释,这个过程可以变得更加简单。首先,将创建一个预订控制器:
$ php artisan make:controller ReservationsController
为了创建一个路由,允许用户创建一个新的预订,将使用 POST HTTP 动词。@Post
注释将监听附加到/bookRoom
网址的具有POST
方法的请求。这将代替通常在routes.php
文件中找到的路由:
<?php namespace MyCompany\Http\Controllers;
use ...
class ReservationsController extends Controller {
/**
* @Post("/bookRoom")
*/
public function reserve()
{
}
如果我们想要将请求限制为有效的 URL,则域参数将请求限制为特定的 URL。此外,auth 中间件要求对希望预订房间的任何请求进行身份验证:
<?php namespace App\Http\Controllers;
use …
/**
* @Controller(domain="booking.hotelwebsite.com")
*/
class ReservationsController extends Controller {
/**
* @Post("/bookRoom")
* @Middleware("auth")
*/
public function reserve()
{
接下来,应该创建ReserveRoom
命令。这个命令将在控制器内实例化:
**$ php artisan make:command ReserveRoom**
ReserveRoom 命令的内容如下:
<?php namespace MyCompany\Commands;
use MyCompany\Commands\Command;
use MyCompany\User;
use MyCompany\Accommodation\Room;
use MyCompany\Events\RoomWasReserved;
use Illuminate\Contracts\Bus\SelfHandling;
class ReserveRoomCommand extends Command implements SelfHandling {
public function __construct()
{
}
/**
* Execute the command.
*/
public function handle()
{
}
}
接下来,我们需要在预订控制器内部实例化ReserveRoom
命令:
<?php namespace MyCompany\Http\Controllers;
use MyCompany\Accommodation\Reservation;
use MyCompany\Commands\PlaceOnWaitingListCommand;
use MyCompany\Commands\ReserveRoomCommand;
use MyCompany\Events\RoomWasReserved;
use MyCompany\Http\Requests;
use MyCompany\Http\Controllers\Controller;
use MyCompany\User;
use MyCompany\Accommodation\Room;
use Illuminate\Http\Request;
class ReservationsController extends Controller {
/**
* @Post("/bookRoom")
* @Middleware("auth")
*/
public function reserve()
{
$this->dispatch(
new ReserveRoom(\Auth::user(),$start_date,$end_date,$rooms)
);
}
现在我们将创建RoomWasReserved
事件:
**$ php artisan make:event RoomWasReserved**
要从ReserveRoom
处理程序中实例化RoomWasReserved
事件,我们可以利用event()
辅助方法。在这个例子中,命令是自处理的,因此这样做很简单:
<?php namespace App\Commands;
use App\Commands\Command;
use Illuminate\Contracts\Bus\SelfHandling;
class ReserveRoom extends Command implements SelfHandling {
public function __construct(User $user, $start_date, $end_date, $rooms)
{
}
public function handle()
{
$reservation = Reservation::createNew();
event(new RoomWasReserved($reservation));
}
}
由于用户需要收到房间预订电子邮件的详细信息,下一步是为RoomWasReserved
事件创建一个电子邮件发送处理程序。为此,再次使用artisan
来创建处理程序:
**$ php artisan handler:event RoomReservedEmail –event=RoomWasReserved**
RoomWasReserved
事件的SendEmail
处理程序的方法只是构造函数和处理程序。发送电子邮件的工作将在处理程序方法内执行。@Hears
注释被添加到其 DocBlock 中以完成这个过程:
<?php namespace MyCompany\Handlers\Events;
use MyCompany\Events\RoomWasReserved;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldBeQueued;
class RoomReservedEmail {
public function __construct()
{
}
/**
* Handle the event.
* @Hears("\App\Events\RoomWasReserved")
* @param RoomWasReserved $event
*/
public function handle(RoomWasReserved $event)
{
//TODO: send email to $event->user
}
}
只需将RoomReservedEmail
添加到scanEvents
数组中,以允许扫描该事件,如下所示:
protected $scanEvents = [
'App\Handlers\Events\RoomReservedEmail'
];
最后一步是导入。Artisan 用于扫描事件的注释并写入输出文件:
**$ php artisan event:scan**
**Events scanned!**
这是storage/framework/events.scanned.php
文件的输出,显示了事件监听器:
<?php $events->listen(array(0 => 'App\\Events\\RoomWasReserved',
), App\Handlers\Events\RoomReservedEmail@handle');
在存储目录中扫描的注释文件的最终视图如下。请注意它们是并列的:
**storage/framework/events.scanned.php**
**storage/framework/routes.scanned.php**
提示
Laravel 使用artisan
来缓存路由,但不用来扫描事件,因此以下命令会生成一个缓存文件:
**$ php artisan route:cache**
**Route cache cleared!**
**Routes cached successfully!**
在运行route:cache
之前必须先运行route:scan
命令,因此按照这个顺序执行这两个命令非常重要:
$ php artisan route:scan
Routes scanned!
$ php artisan route:cache
Route cache cleared!
Routes cached successfully!
此命令写入到:storage/framework/routes.php
。
<?php
app('router')->setRoutes(
unserialize(base64_decode('TzozNDoiSWxsdW1pbmF0ZVxSb3V0aW5nXFd…'))
);
两个文件都会被创建,但只有编译后的routes.php
文件在再次运行php artisan route:scan
之前才会被使用。
优势
在 Laravel 中使用 DocBlock 注解进行路由有几个主要优势:
-
每个控制器保持独立。控制器不与单独的路由“绑定”,这使得共享控制器,并将其从一个项目移动到另一个项目变得更容易。对于只有少数控制器的简单项目来说,
routes.php
文件可能被视为不必要。 -
开发人员无需担心
routes.php
。与其他开发人员合作时,路由文件需要保持同步。通过 DocBlock 注解方法,routes.php
文件被缓存,不放在源代码控制下;每个开发人员可以专注于自己的控制器。 -
路由注解将路由与控制器保持在一起。当控制器和路由分开时,当新程序员第一次阅读代码时,可能不会立即清楚每个控制器方法附加到哪些路由上。通过直接将路由放在函数上方的 DocBlock 中,这一点立即变得明显。
-
熟悉并习惯在 Symfony 和 Zend 等框架中使用注解的开发人员可能会发现在 Laravel 中使用注解是开发软件应用的一种非常方便的方式。此外,将 Laravel 作为首次 PHP 体验的 Java 和 C#开发人员会发现注解非常方便。
结论
是否在软件中使用注解的决定取决于开发人员。从 Laravel 核心中移除它的决定,以及 HTML 表单包,表明该框架变得越来越灵活,只有一组最小的包作为默认。这使得在 Laravel 5.1 发布长期支持(LTS)版本时,核心开发人员可以更加稳定和减少维护工作。
由于注解包是 Laravel Collective 的一部分,该团队将负责管理此包的支持,这保证了该功能的实用性将通过对存储库的贡献得到扩展和扩展。
此外,该包可以扩展以包括一个模板,该模板会自动创建与控制器同名的路由注解。这将在创建控制器和路由的过程中节省另一个步骤,这是软件开发过程中最重要但又单调的任务之一。
总结
在本章中,我们了解了注解的用法,它们在编程中的一般用法,它们在其他框架中的用法,以及它们如何被引入到 Laravel 注解 composer 包中。我们学会了如何通过使用注解来加快开发过程,以及如何自动扫描注解。在下一章中,我们将学习中间件,这是一种在路由和应用程序之间使用的机制。
第七章:使用中间件过滤请求
在本章中,将详细讨论中间件,并提供来自住宿软件的示例。中间件是帮助将软件应用程序分隔成不同层的重要机制。为了说明这一原则,中间件在应用程序的最内部提供了保护层,可以将其视为内核。
在 Laravel 4 中,中间件被称为过滤器。这些过滤器用于路由中执行在控制器之前的操作,如身份验证,用户将根据特定标准进行过滤。此外,过滤器也可以在控制器之后执行。
在 Laravel 5 中,中间件的概念已经存在,但在 Laravel 4 中并不突出,现在已经被引入到实际请求工作流中,并可以以各种方式使用。可以将其视为俄罗斯套娃,其中每个套娃代表应用程序中的一层 - 拥有正确凭据将允许我们深入应用程序。
HTTP 内核
位于app/Http/Kernel.php
的文件是管理程序内核配置的文件。基本结构如下:
<?php namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel {
/**
* The application's global HTTP middleware stack.
*
* @var array
*/
protected $middleware = [
'Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode',
'Illuminate\Cookie\Middleware\EncryptCookies',
'Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse',
'Illuminate\Session\Middleware\StartSession',
'Illuminate\View\Middleware\ShareErrorsFromSession',
'Illuminate\Foundation\Http\Middleware\VerifyCsrfToken',
];
/**
* The application's route middleware.
*
* @var array
*/
protected $routeMiddleware = [
'auth' => 'App\Http\Middleware\Authenticate',
'auth.basic' => 'Illuminate\Auth\Middleware\AuthenticateWithBasicAuth',
'guest' => 'App\Http\Middleware\RedirectIfAuthenticated',
];
}
$middleware
数组是中间件类及其命名空间的列表,并在每个请求时执行。$routeMiddleware
数组是一个键值数组,作为别名列表,可与路由一起使用以过滤请求。
基本中间件结构
路由中间件类实现了Middleware
接口:
<?php namespace Illuminate\Contracts\Routing;
use Closure;
interface Middleware {
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next);
}
在实现此基类的任何类中,必须有一个接受$request
和Closure
的handle
方法。
中间件的基本结构如下:
<?php namespace Illuminate\Foundation\Http\Middleware;
use Closure;
use Illuminate\Contracts\Routing\Middleware;
use Illuminate\Contracts\Foundation\Application;
use Symfony\Component\HttpKernel\Exception\HttpException;
class CheckForMaintenanceMode implements Middleware {
/**
* The application implementation.
*
* @var \Illuminate\Contracts\Foundation\Application
*/
protected $app;
/**
* Create a new filter instance.
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @return void
*/
public function __construct(Application $app)
{
$this->app = $app;
}
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
if ($this->app->isDownForMaintenance())
{
throw new HttpException(503);
}
return $next($request);
}
}
在这里,CheckForMaintenanceMode
中间件确实如其名称所示:handle
方法检查应用程序是否处于应用模式。调用应用程序的isDownForMaintenance
方法,如果返回true
,则会返回 503 HTTP 异常并停止方法的执行。否则,将带有$request
参数的$next
闭包返回给调用类。
提示
诸如CheckForMaintenanceMode
之类的中间件可以从$middleware
数组中移除,并移入$routeMiddleware
数组中,以便不需要在每个请求时执行,而只在从特定路由所需时执行。
路由中间件揭秘
在 Laravel 5 中存在两个基于路由的中间件类,位于app/Http/Middleware/
中。其中一个类名为Authenticate
。它提供基本身份验证并使用合同。
关于路由,中间件位于路由和控制器之间:
默认中间件 - Authenticate 类
一个名为Authenticate.php
的类有以下代码:
<?php namespace MyCompany\Http\Middleware;
use Closure;
use Illuminate\Contracts\Auth\Guard;
class Authenticate {
/**
* The Guard implementation.
*
* @var Guard
*/
protected $auth;
/**
* Create a new filter instance.
*
* @param Guard $auth
* @return void
*/
public function __construct(Guard $auth)
{
$this->auth = $auth;
}
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
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);
}
}
首先要注意的是Illuminate\Contracts\Auth\Guard
,它处理检查用户是否已登录的逻辑。它被注入到构造函数中。
合同
请注意,合同的概念是使用接口提供非具体类以将实际类与调用类分离的新方法。这提供了一个良好的分离层,并允许在需要时轻松切换底层类,同时保持方法的参数和返回类型。
处理
handle
类是真正工作的地方。$request
对象与$next
闭包一起传入。接下来发生的事情非常简单但重要。代码询问当前用户是否是访客,即未经身份验证或登录。如果用户未登录,则该方法将不允许用户访问下一步。如果请求是通过 Ajax 到达的,则会向浏览器返回 401 消息。
如果请求不是通过 Ajax 请求到达的,代码会假定请求是通过标准页面请求到达的,并且用户被重定向到 auth/login 页面,允许用户登录应用程序。否则,如果用户已经认证(guest()
不等于true
),则将$next
闭包与$request
对象作为参数返回给软件应用程序。总之,只有在用户未经认证时才会停止应用程序的执行;否则,执行将继续。
要记住的重要一点是,在这种情况下,$request
对象被返回给软件。
自定义中间件 - 记录
使用 Artisan 创建自定义中间件很简单。artisan
命令如下:
**$ php artisan make:middleware LogMiddleware**
我们的LogMiddleware
类需要添加到Http/Kernel.php
文件中的$middleware
数组中,如下所示:
protected $middleware = [
'Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode',
'Illuminate\Cookie\Middleware\EncryptCookies',
'Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse',
'Illuminate\Session\Middleware\StartSession',
'Illuminate\View\Middleware\ShareErrorsFromSession',
'MyCompany\Http\Middleware\LogMiddleware'
];
LogMiddleware
类是给中间件类的名称,用于记录使用网站的用户。该类只有一个方法,即handle
。与认证中间件一样,它接受$request
对象以及$next
闭包:
<?php namespace MyCompany\Http\Middleware;
use Closure;
class LogMiddleware {
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
return $next($request);
}
}
在这种情况下,我们只想简单地记录用户 ID 以及执行某个操作的日期和时间。将$request
对象分配给$response
对象,然后返回$response
对象而不是$next
。代码如下:
public function handle($request, Closure $next)
{
$response = $next($request);
Log::create(['user_id'=>\Auth::user()->id,'created_at'=>date("Y-
m-d H:i:s")]);
return $response;
}
记录模型
使用以下命令创建Log
模型:
**$php artisan make:model Log**
使用受保护的$table
属性将Log
模型设置为使用名为log
而不是logs
的表。接下来,通过将公共$timestamps
属性设置为false
,设置模型不使用时间戳。最后,通过将受保护的$fillable
属性设置为要填充的字段数组,允许使用create
函数同时填充user_id
和created_at
字段。在进行上述修改后,该类将如下所示:
<?php namespace MyCompany;
use Illuminate\Database\Eloquent\Model;
class Log extends Model {
protected $table = 'log';
public $timestamps = false;
protected $fillable = ['user_id','created_at'];
}
我们还可以将Log
模型创建为多态模型,使其可以在多个上下文中使用,通过将以下代码添加到Log
模型中:
public function loggable()
{
return $this->morphTo();
}
提示
有关此更多信息,请参阅 Laravel 文档。
记录模型迁移
需要调整database/migrations/[date_time]_create_logs_table.php
迁移,以使用log
表而不是logs
。还需要创建两个字段:user_id
,一个无符号的小整数,以及created_at
,一个将模仿 Laravel 时间戳格式的datetime
字段。代码如下:
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateLogsTable extends Migration {
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('log', function(Blueprint $table)
{
$table->smallInteger('user_id')->unsigned();
$table->dateTime('created_at');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('log');
}
}
可终止中间件
除了在请求到达或响应到达后执行操作之外,甚至可以在响应发送到浏览器后执行操作。该类添加了terminate
方法并实现了TerminableMiddleware
:
use Illuminate\Contracts\Routing\TerminableMiddleware;
class StartSession implements TerminableMiddleware {
public function handle($request, $next)
{
return $next($request);
}
public function terminate($request, $response)
{
// Store the session data...
}
}
作为可终止的记录
我们可以在terminate
函数中轻松地执行用户记录,因为记录可能是生命周期中的最后一个动作。代码如下:
<?php namespace MyCompany\Http\Middleware;
use Closure;
use Illuminate\Contracts\Routing\TerminableMiddleware;
use MyCompany\Log;
class LogMiddleware implements TerminableMiddleware {
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
return $next($request);
}
/**
* Terminate the request.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Http\Response $response
*/
public function terminate($request, $response)
{
Log::create(['user_id'=>\Auth::user()- >id,'created_at'=>date("Y-m-d H:i:s")]);
}
}
代码已放置到terminate
方法中,因此它位于请求-响应路径之外,使得代码保持清晰。
使用中间件
如果我们希望用户在执行某个操作之前必须经过身份验证,我们可以将数组作为第二个参数传递,middleware
作为键强制路由在AccommodationsController
的search
方法上调用auth
中间件:
Route::get('search-accommodation',
['middleware' => 'auth','AccommodationsController@search']);
在这种情况下,如果用户未经认证,将被重定向到登录页面。
路由组
路由可以分组以共享相同的中间件。例如,如果我们想保护应用程序中的所有路由,我们可以创建一个路由组,并只传入键值对middleware
和auth
。代码如下:
Route::group(['middleware' => 'auth'], function()
{
Route::resource('accommodations', 'AccommodationsController');
Route::resource('accommodations.amenities', 'AccommodationsAmenitiesController');
Route::resource('accommodations.rooms', 'AccommodationsRoomsController');
Route::resource('accommodations.locations', 'AccommodationsLocationsController');
Route::resource('amenities', 'AmenitiesController');
Route::resource('rooms', 'RoomsController');
Route::resource('locations', 'LocationsController');
})
这将保护路由组内的每个路由的每个方法。
路由组中的多个中间件
如果希望进一步保护非经过身份验证的用户,可以创建一个白名单,只允许特定范围的 IP 地址访问应用程序。
以下命令将创建所需的中间件:
$ php artisan make:middleware WhitelistMiddleware
WhitelistMiddleware
类如下所示:
<?php namespace MyCompany\Http\Middleware;
use Closure;
class WhitelistMiddleware {
private $whitelist = ['192.2.3.211'];
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
if (in_array($request->getClientIp(),$this->whitelist)) {
return $next($request);
} else {
return response('Unauthorized.', 401);
}
}
}
在这里,创建了一个私有的$whitelist
数组,其中包含设置在公司内的 IP 地址列表。 然后,将请求的远程端口与数组中的值进行比较,并通过返回$next
闭包来允许其继续。 否则,将返回未经授权的响应。
现在,需要将whitelist
中间件与auth
中间件结合使用。 要在路由组内使用whitelist
中间件,需要为中间件创建别名,并将其插入到app/Http/Kernel.php
文件的$routeMiddleware
数组中。 代码如下:
protected $routeMiddleware = [
'auth' => 'MyCompany\Http\Middleware\Authenticate',
'auth.basic' => 'Illuminate\Auth\Middleware\AuthenticateWithBasicAuth',
'guest' => 'MyCompany\Http\Middleware\RedirectIfAuthenticated',
'log' => 'MyCompany\Http\Middleware\LogMiddleware',
'whitelist' => 'MyCompany\Http\Middleware\WhitelistMiddleware'
];
接下来,要将其添加到此路由组的中间件列表中,需要用数组替换字符串auth
,其中包含auth
和whitelist
。 代码如下:
Route::group(['middleware' => ['auth','whitelist']], function()
{
Route::resource('accommodations', 'AccommodationsController');
Route::resource('accommodations.amenities',
'AccommodationsAmenitiesController');
Route::resource('accommodations.rooms', 'AccommodationsRoomsController');
Route::resource('accommodations.locations', 'AccommodationsLocationsController');
Route::resource('amenities', 'AmenitiesController');
Route::resource('rooms', 'RoomsController');
Route::resource('locations', 'LocationsController');
});
现在,即使用户已登录,也将无法访问受保护的内容,除非 IP 地址在白名单中。
此外,如果只想要对某些路由进行白名单操作,可以嵌套路由组如下:
Route::group(['middleware' => 'auth', function()
{
Route::resource('accommodations', 'AccommodationsController');
Route::resource('accommodations.amenities',
'AccommodationsAmenitiesController');
Route::resource('accommodations.rooms', 'AccommodationsRoomsController');
Route::resource('accommodations.locations', 'AccommodationsLocationsController');
Route::resource('amenities', 'AmenitiesController');
Route::group(['middleware' => 'whitelist'], function()
{
Route::resource('rooms', 'RoomsController');
});
Route::resource('locations', 'LocationsController');
});
这将要求对RoomsController
进行身份验证(auth
)和白名单操作,而路由组内的所有其他控制器将仅需要身份验证。
中间件排除和包含
如果希望仅对某些路由执行身份验证或白名单操作,则应向控制器添加构造方法,并且可以使用类的middleware
方法如下所示:
<?php namespace MyCompany\Http\Controllers;
use MyCompany\Http\Requests;
use MyCompany\Http\Controllers\Controller;
use Illuminate\Http\Request;
use MyCompany\Accommodation\Room;
class RoomsController extends Controller {
public function __construct()
{
$this->middleware('auth',['except' => ['index','show']);
}
第一个参数是Kernel.php
文件中$routeMiddleware
数组的键。 第二个参数是键值数组。 选项要么是except
,要么是only
。 except
选项显然是排除,而only
选项是包含。 在上面的示例中,auth
中间件将应用于除index
或show
方法之外的所有方法,这两个方法是两种读取方法(它们不修改数据)。 相反,如果log
中间件应用于index
和show
,则将使用以下构造方法:
public function __construct()
{
$this->middleware('log',['only' => ['index','show']);
}
如预期的那样,两种方法都如下所示,并且还添加了whitelist
中间件:
public function __construct()
{
$this->middleware('whitelist',['except' => ['index','show']);
$this->middleware('auth',['except' => ['index','show']);
$this->middleware('log',['only' => ['index','show']);
}
此代码将要求对所有非读取操作进行身份验证和白名单 IP 地址,同时记录对index
和show
的任何请求。
结论
中间件可以巧妙地过滤请求并保护应用程序或 RESTful API 免受不必要的请求。 它还可以执行日志记录并重定向任何符合特定条件的请求。
中间件还可以为现有应用程序提供附加功能。 例如,Laravel 提供了EncryptCookies
和AddQueuedCookiesToResponse
中间件来处理 cookies,而StartSession
和ShareErrorsFromSession
处理会话。
AddQueuedCookiesToResponse
中的代码不会过滤请求,而是向其添加内容:
public function handle($request, Closure $next)
{
$response = $next($request);
foreach ($this->cookies->getQueuedCookies() as $cookie)
{
$response->headers->setCookie($cookie);
}
return $response;
}
总结
在本章中,我们看了中间件,这是一个对每个请求执行的任何功能或附加到某些路由的有用机制。 这是一种灵活的机制,并允许程序员编码到接口,因为任何实现Middleware
接口的中间件类都必须包括handle
方法。 通过这种结构不仅鼓励,而且要求遵循良好的开发原则。
在下一章中,我们将讨论 Eloquent ORM。
第八章:使用 Eloquent ORM 查询数据库
在之前的章节中,您学习了如何构建应用程序的基本组件。在本章中,将介绍 Eloquent ORM,这是使 Laravel 如此受欢迎的最佳功能之一。
在本章中,我们将涵盖以下主题:
-
基本查询语句
-
一对一,一对多和多对多关系
-
多态关系
-
急切加载
ORM,或对象关系映射,在最简单的意义上解释,将表转换为类,将其列转换为属性,并将其行转换为该类的实例。它在开发人员和数据库之间创建了一个抽象层,并允许更容易的编程,因为它使用熟悉的面向对象范式。
我们假设有一个带有以下结构的帖子表:
id | contents | author_id |
---|
为了说明这个例子,以下将是帖子表的表示:
<?php
namespace MyBlog;
class Post {
}
要添加id
,contents
和author_id
属性,我们将在类中添加以下代码:
class Post {
private $id;
private $contents;
private $author_id;
public function getId()
{
return $this->id;
}
public function setId($id)
{
$this->id = $id;
}
public function getContents()
{
return $this->contents;
}
public function setContents($contents)
{
$this->contents = $contents;
}
public function getAuthorId()
{
return $this->author_id;
}
public function setAuthorId($author_id)
{
$this->author_id = $author_id;
}
}
这给我们一个关于如何用类表示表的概述:Post
类表示一个具有posts集合的实体。
如果遵循了活动记录模式,那么 Eloquent 可以自动管理所有类名、键名和它们的相关关系。Eloquent 的强大之处在于它能够让程序员使用面向对象的方法来管理类之间的关系。
基本操作
现在我们将讨论一些基本操作。使用 Eloquent 有几乎无数种方式,当然每个开发人员都会以最适合其项目的方式使用 Eloquent。以下技术是更复杂查询的基本构建块。
查找一个
最基本的操作之一是执行以下查询:
select from rooms where id=1;
这是通过使用find()
方法实现的。
使用find
方法调用Room
外观,该方法接受 ID 作为参数:
MyCompany\Accommodation\Room::find($id);
由于 Eloquent 基于流畅的查询构建器,任何流畅的方法都可以混合和匹配。一些流畅的方法是可链接的,而其他方法执行查询。
find()
方法实际上执行查询,因此它总是需要在表达式的末尾。
如果未找到模型的 ID,则不返回任何内容。要强制ModelNotFoundException
,然后可以捕获它以执行其他操作,例如记录日志,添加OrFail
如下:
MyCompany\Accommodation\Room::findOrFail($id);
where 方法
要查询除 ID 以外的属性(列),请使用以下命令:
select from accommodations where name='Lovely Hotel';
使用where
方法后跟get()
方法:
MyCompany\Accommodation::where('name','Lovely Hotel')->get();
like
比较器可以如下使用:
MyCompany\Accommodation::where('name','like','%Lovely%')->get();
链接函数
多个 where 方法可以链接如下:
MyCompany\Accommodation::where('name','Lovely Hotel')- >where('city','like','%Pittsburgh%')->get();
上述命令产生以下查询:
select * from accommodations where name ='Lovely Hotel' and description like '%Pittsburgh%'
请注意,如果where
比较器是=
(相等),则不需要第二个参数(比较器),并且比较的第二部分传递到函数中。还要注意,在两个where
方法之间添加了and
操作。要实现or
操作,必须对代码进行以下更改:
MyCompany\Accommodation::where('name','Lovely Hotel')- >orWhere('description','like','%Pittsburgh%')->get();
请注意,or
被添加到where
创建orWhere()
。
查找所有
要找到所有房间,使用all()
方法代替find
。请注意,此方法实际上执行查询:
MyCompany\Accommodation\Room::all();
为了限制房间的数量,使用take
方法代替find
。由于take
是可链接的,需要使用get
来执行查询:
MyCompany\Accommodation\Room::take(10)->get();
要实现分页,可以使用以下查询:
MyCompany\Accommodation\Room::paginate();
默认情况下,上述查询将返回一个 JSON 对象,如下所示:
{"total":15, "per_page":15,
"current_page":1, "last_page":1,
"next_page_url":null, "prev_page_url":null,
"from":1, "to":15,
"data":
{"id":9,"name":"LovelyHotel","description":"Lovely Hotel Greater Pittsburgh","location_id":1,"created_at":null,"updated_at": "2015-03-13 22:00:23","deleted_at":null,"franchise_id":1},{"id":12, "name":"Grand Hotel","description":"Grand Hotel Greater Cleveland","location_id":2,"created_at":"2015-02- 0820:09:35","updated_at":"2015-02- 0820:09:35","deleted_at":null,"franchise_id":1}
...
属性,如total
,per_page
,current_page
和last_page
,用于为开发人员提供一种简单的实现分页的方法,而数据数组则返回在名为data
的数组中。
优雅的关系
诸如一对一、一对多(或多对一)和多对多之类的关系对于数据库程序员来说是熟悉的。Laravel 的 Eloquent 已经将这些概念带入了面向对象的环境中。此外,Eloquent 还有更强大的工具,比如多态关系,其中实体可以与多个其他实体相关联。在接下来的示例中,我们将看到住宿、房间和便利设施之间的关系。
![Eloquent 关系
一对一
第一个关系是一对一。在我们的示例软件中,我们可以使用我们住宿中的房间的例子。一个房间可能只(至少很容易)属于一个住宿,所以房间属于住宿。在Room
Eloquent 模型中,以下代码告诉 Eloquent 房间属于accommodation
函数:
class Room extends Eloquent {
public function accommodation()
{
return $this->belongsTo('MyCompany\Accommodation');
}
}
有时,数据库表不遵循活动记录模式,特别是如果程序员继承了遗留数据库。如果数据库使用了一个名为bedroom
而不是rooms
的表,那么类将添加一个属性来指示表名:
class Room extends Eloquent {
protected $table = 'bedroom';
}
当执行以下路由代码时,accommodation
对象将以 JSON 对象的形式返回:
Route::get('test-relation',function(){
$room = MyCompany\Accommodation\Room::find(1);
return $room->accommodation;
});
响应将如下:
{"id":9,"name":"LovelyHotel","description":"Lovely Hotel Greater Pittsburgh","location_id":1,"created_at":null,"updated_at": "2015-03-13 22:00:23","deleted_at":null}
提示
一个常见的错误是使用以下命令:
return $room->accommodation();
在这种情况下,程序员期望返回模型。这将返回实际的belongsTo
关系,在 RESTful API 的上下文中,将会抛出错误:
Object of class Illuminate\Database\Eloquent\Relations\BelongsTo could not be converted to string
这是因为 Laravel 可以将 JSON 对象转换为字符串,但不能转换为关系。
运行的 SQL 如下:
select * from rooms where rooms.id = '1' limit 1
select * from accommodations where accommodations.id = '9' limit 1
Eloquent 倾向于使用多个简单的查询,而不是进行更大的连接。
首先找到房间。然后,添加limit 1
,因为find
只用于查找单个实体或行。一旦找到accommodation_id
,下一个查询将找到具有相应 ID 的住宿并返回对象。如果遵循了活动记录模式,Eloquent 生成的 SQL 非常易读。
一对多
第二个关系是一对多。在我们的示例软件中,我们可以使用住宿有许多房间的例子。因为房间可能属于一个住宿,那么住宿有许多房间。在Accommodation
Eloquent 模型中,以下代码告诉 Eloquent 住宿有许多房间。
class Accommodation {
public function rooms(){
return $this->hasMany('\MyCompany\Accommodation\Room');
}
}
在类似的路由中,运行以下代码。这次,将以 JSON 格式的对象数组返回一组rooms
对象:
Route::get('test-relation',function(){
$accommodation = MyCompany\Accommodation::find(9);
return $accommodation->rooms;
});
响应将是以下数组:
[{"id":1,"room_number":0,"created_at":null,"updated_at":null, "deleted_at":null,"accommodation_id":9},{"id":3,"room_number": 12,"created_at":"2015-03-14 08:52:25","updated_at":"2015-03-14 08:52:25","deleted_at":null,"accommodation_id":9},{"id":6, "room_number":12,"created_at":"2015-03-14 09:03:36","updated_at":"2015-03-14 09:03:36","deleted_at":null,"accommodation_id":9},{"id": 14,"room_number":12,"created_at":"2015-03-14 09:26:36","updated_at":"2015-03- 1409:26:36","deleted_at":null,"accommodation_id":9}]
运行的 SQL 如下:
select * from accommodations where accommodations.id = ? limit 1
select * from rooms where rooms.accommodation_id = '9' and rooms.accommodation_id is not null
与之前一样,找到住宿。第二个查询将找到属于该住宿的房间。添加了一个检查以确认accommodation_id
不为空。
多对多
在我们的示例软件应用程序中,便利设施和房间之间的关系是多对多的。每个房间可以有许多便利设施,比如互联网接入和按摩浴缸,每个便利设施都在许多房间之间共享:住宿中的每个房间都可以并且应该有互联网接入!以下代码使用belongsToMany
关系,使便利设施可以属于许多房间:
class Amenity {
public function rooms(){
return $this- >belongsToMany('\MyCompany\Accommodation\Room');
}
}
告诉我们每个房间都有某个便利设施的测试路由写成如下:
Route::get('test-relation',function(){
$amenity = MyCompany\Accommodation\Amenity::find(3);
return $amenity->rooms;
});
返回一个房间列表:
[{"id":1,"room_number":0,"created_at":2015-03-14 08:10:45,"updated_at":null,"deleted_at":null, "accommodation_id":9},{"id":5,"room_number":12, "created_at":"2015-03-14 09:00:38","updated_at":"2015-03-14", 09:00:38","deleted_at":null,"accommodation_id":12},
...]
执行的 SQL 如下:
select * from amenities where amenities.id = ? limit 1
select rooms.*, amenity_room.amenity_id as pivot_amenity_id, amenity_room.room_id as pivot_room_id from rooms inner join amenity_room on rooms.id = amenity_room.room_id where amenity_room.amenity_id = 3
我们回忆一下belongToMany
关系,它返回具有特定便利设施的房间:
class Amenity {
public function rooms(){
return $this- >belongsToMany('\MyCompany\Accommodation\Room');
}
}
Eloquent 巧妙地给了我们相应的belongsToMany
关系,以确定特定房间有哪些便利设施。语法完全相同:
class Room {
public function amenities(){
return $this- >belongsToMany('\MyCompany\Accommodation\Amenity');
}
}
测试路由几乎相同,只是用rooms
替换amenities
:
Route::get('test-relation',function(){
$room = MyCompany\Accommodation\Room::find(1);
return $room->amenities;
});
结果是 ID 为 1 的房间的便利设施列表:
[{"id":1,"name":"Wifi","description":"Wireless Internet Access","created_at":"2015-03-1409:00:38","updated_at":"2015-03-14 09:00:38","deleted_at":null},{"id":2,"name": "Jacuzzi","description":"Hot tub","created_at":"2015-03-14 09:00:38","updated_at":null,"deleted_at":null},{"id":3,"name": "Safe","description":"Safe deposit box for protecting valuables","created_at":"2015-03-1409:00:38","updated_at": "2015-03-1409:00:38","deleted_at":null}]
使用的查询如下:
select * from rooms where rooms.id = 1 limit 1
select amenities.*, amenity_room.room_id as pivot_room_id, amenity_room.amenity_id as pivot_amenity_id from amenities inner join amenity_room on amenities.id = amenity_room.amenity_id where amenity_room.room_id = '1'
查询,用room_id
替换amenity_id
,用rooms
替换amenities
,显然是并行的。
有许多通过
Eloquent 的一个很棒的特性是“has-many-through”。如果软件的需求发生变化,并且我们被要求将一些住宿分组到特许经营店中,该怎么办?如果应用程序用户想要搜索一个房间,那么属于该特许经营店的任何住宿中的任何房间都可以被找到。将添加一个特许经营店表,并在住宿表中添加一个可空列,名为 franchise_id
。这将可选地允许住宿属于特许经营店。房间已经通过 accommodation_id
列属于住宿。
一个房间通过其 accommodation_id
键属于一个 住宿
,而一个住宿通过其 franchise_id
键属于一个特许经营店。
Eloquent 允许我们通过使用 hasManyThrough
来检索与特许经营店相关联的房间:
<?php namespace MyCompany;
use Illuminate\Database\Eloquent\Model;
class Franchise extends Model {
public function rooms()
{
return $this- >hasManyThrough('\MyCompany\Accommodation\Room', '\MyCompany\Accommodation');
}
}
hasManyThrough
关系将目标或“拥有”作为其第一个参数(在本例中是房间),将“通过”作为第二个参数(在本例中是住宿)。
作为短语陈述的逻辑是:这个特许经营店通过其住宿拥有许多房间。
使用先前的测试路由,代码编写如下:
Route::get('test-relation',function(){
$franchise = MyCompany\Franchise::find(1);
return $franchise->rooms;
});
返回的房间是一个数组,正如预期的那样:
[{"id":1,"room_number":0,"created_at":null,"updated_at":null,"deleted_at":null,"accommodation_id":9,"franchise_id":1}, {"id":3,"room_number":12,"created_at":"2015-03-14 08:52:25","updated_at":"2015-03-14 08:52:25","deleted_at":null,"accommodation_id":9, "franchise_id":1},{"id":6,"room_number":12,"created_at":"2015-03-14 09:03:36","updated_at":"2015-03-14 09:03:36","deleted_at":null,"accommodation_id":9, "franchise_id":1},
]
执行的查询如下:
select * from franchises where franchises.id = ? limit 1
select rooms.*, accommodations.franchise_id from rooms inner join accommodations on accommodations.id = rooms.accommodation_id where accommodations.franchise_id = 1
多态关系
Eloquent 的一个很棒的特性是拥有一个关系是多态的实体的可能性。这个词的两个部分,poly 和 morphic,来自希腊语。由于 poly 意味着 许多,morphic 意味着 形状,我们现在可以很容易地想象一个关系有多种形式。
设施关系
在我们的示例软件中,一个设施是与房间相关联的东西,比如按摩浴缸。某些设施,比如有盖停车场或机场班车服务,也可能与住宿本身相关。我们可以为此创建两个中间表,一个叫做 amenity_room
,另一个叫做 accommodation_amenity
。另一种很好的方法是将两者合并成一个表,并使用一个字段来区分两种类型或关系。
为了做到这一点,我们需要一个字段来区分 设施和房间 和 设施和房间,我们可以称之为关系类型。Laravel 的 Eloquent 能够自动处理这一点。
Eloquent 使用后缀 -able
来实现这一点。在我们的示例中,我们将创建一个具有以下字段的表:
-
id
-
name
-
description
-
amenitiable_id
-
amenitiable_type
前三个字段是熟悉的,但添加了两个新字段。其中一个将包含住宿或房间的 ID。
设施表结构
例如,给定 ID 为 5 的房间,amenitiable_id
将是 5
,而 amenitiable_type
将是 Room
。给定 ID 为 5 的住宿,amenitiable_id
将是 5
,而 amenitiable_type
将是 Accommodation
:
id | name | description | amenitiable_id | amenitiable_type |
---|---|---|---|---|
1 | 无线网络 | 网络连接 | 5 | 房间 |
2 | 有盖停车场 | 车库停车 | 5 | 住宿 |
3 | 海景 | 房间内海景 | 5 | 房间 |
设施模型
在代码方面,Amenity
模型现在将包含一个 "amenitiable" 函数:
<?php
namespace MyCompany\Accommodation;
use Illuminate\Database\Eloquent\Model;
class Amenity extends Model
{
public function rooms(){
return $this->belongsToMany('\MyCompany\Accommodation\Room');
}
public function amenitiable()
{
return $this->morphTo();
}
住宿模型
住宿
模型将更改 amenities
方法,使用 morphMany
而不是 hasMany
:
<?php namespace MyCompany;
use Illuminate\Database\Eloquent\Model;
class Accommodation extends Model {
public function rooms(){
return $this->hasMany('\MyCompany\Accommodation\Room');
}
public function amenities()
{
return $this- >morphMany('\MyCompany\Accommodation\Amenity', 'amenitiable');
}
}
房间模型
Room
模型将包含相同的 morphMany
方法:
<?php
namespace MyCompany\Accommodation;
use Illuminate\Database\Eloquent\Model;
class Room extends Model
{
protected $casts = ['room_number'=>'integer'];
public function accommodation(){
return $this->belongsTo('\MyCompany\Accommodation');
}
public function amenities() {
return $this- >morphMany('\MyCompany\Accommodation\Amenity', 'amenitiable');
}
}
现在,当要求为房间或住宿请求设施时,Eloquent 将自动区分它们:
$accommodation->amenities();
$room->amenities();
这些函数中的每一个都返回了房间和住宿的正确类型的设施。
多对多多态关系
然而,一些设施可能在房间和住宿之间共享。在这种情况下,使用多对多多态关系。现在中间表添加了几个字段:
amenity_id | amenitiable_id | amenitiable_type |
---|---|---|
1 | 5 | 房间 |
1 | 5 | 住宿 |
2 | 5 | 房间 |
2 | 5 | 住宿 |
正如所示,ID 为 5 的房间和 ID 为 5 的住宿都有 ID 为 1 和 2 的设施。
具有关系
如果我们想选择与特许经营连锁店关联的所有住宿,使用has()
方法,其中关系作为参数传递:
MyCompany\Accommodation::has('franchise')->get();
我们将得到以下 JSON 数组:
[{"id":9,"name":"LovelyHotel","description":"Lovely Hotel Greater Pittsburgh","location_id":1,"created_at":null,"updated_at": "2015-03-13 22:00:23","deleted_at":null,"franchise_id":1}, {"id":12,"name": "Grand Hotel","description":"Grand Hotel Greater Cleveland","location_id":2,"created_at": "2015-02-0820:09:35","updated_at": "2015-02-0820:09:35","deleted_at":null,"franchise_id":1}]
请注意,franchise_id
的值为 1,这意味着住宿与特许经营连锁店相关联。可选地,可以在has
中添加where
,创建一个whereHas
函数。代码如下:
MyCompany\Accommodation::whereHas('franchise',
function($query){
$query->where('description','like','%Pittsburgh%');
})->get();
请注意,whereHas
将闭包作为其第二个参数。
这将仅返回描述中包含匹兹堡
的住宿,因此返回的数组将只包含这样的结果:
[{"id":9,"name":"LovelyHotel","description":"Lovely Hotel Greater Pittsburgh","location_id":1,"created_at":null,"updated_at": "2015-03-13 22:00:23","deleted_at":null,"franchise_id":1}]
贪婪加载
Eloquent 提供的另一个很棒的机制是贪婪加载。如果我们想要返回所有的特许经营连锁店以及它们的所有住宿,我们只需要在我们的Franchise
模型中添加一个accommodations
函数,如下所示:
public function accommodations()
{
return $this->hasMany('\MyCompany\Accommodation');
}
然后,通过向语句添加with
子句,为每个特许经营连锁店返回住宿:
MyCompany\Franchise::with('accommodations')->get();
我们还可以列出与每个住宿相关的房间,如下所示:
MyCompany\Franchise::with('accommodations','rooms')->get();
如果我们想要返回嵌套在住宿数组中的房间,则应使用以下语法:
MyCompany\Franchise::with('accommodations','accommodations.rooms') ->get();
我们将得到以下输出:
[{"id":1,"accommodations":
[
{"id":9,
"name":"Lovely Hotel",
"description":"Lovely Hotel Greater Pittsburgh",
"location_id":1,
"created_at":null,
"updated_at":"2015-03-13 22:00:23",
"deleted_at":null,
"franchise_id":1,
"rooms":[{"id":1,"room_number":0,"created_at":null,"updated_at": null,"deleted_at":null,"accommodation_id":9},
]},
{"id":12,"name":"GrandHotel","description":"Grand Hotel Greater Cleveland","location_id":2,"created_at":"2015-02-08…
在这个例子中,rooms
包含在accommodation
中。
结论
Laravel 的 ORM 非常强大。事实上,有太多类型的操作无法在一本书中列出。最简单的查询可以用几个按键完成。
Laravel 的 Eloquent 命令被转换为流畅的命令,因此如果需要更复杂的操作,可以使用流畅的语法。如果需要执行非常复杂的查询,甚至可以使用DB::raw()
函数。这将允许在查询构建器中使用精确的字符串。以下是一个例子:
$users = DB::table('accommodation')
->select(DB::raw('count(*) as number_of_hotels'))->get();
这将只返回酒店的数量:
[{"number_of_hotels":15}]
学习设计软件,从领域开始,然后考虑该领域涉及的实体,将有助于开发人员以面向对象的方式思考。拥有实体列表会导致表的创建,因此实际的模式创建将在最后执行。这种方法可能需要一些时间来适应。理解 Eloquent 关系对于能够生成表达性、可读性的查询数据库语句至关重要,同时隐藏复杂性。
Eloquent 极其有用的另一个原因是在遗留数据库的情况下。如果 ORM 应用在表名不符合标准、键名不相同或列名不易理解的情况下,Eloquent 提供了开发人员工具,实际上帮助使表名和字段名同质化,并通过提供属性的 getter 和 setter 来执行关系。
例如,如果字段名为fname1
和fname2
,我们可以在我们的模型中使用一个获取属性函数,语法是get
后跟应用中要使用的所需名称和属性。因此,在fname1
的情况下,函数将被添加如下:
public function getUsernameAttribute($value)
{
return $this->attributes['fname1'];
}
这些函数是 Eloquent 的真正卖点。在本章中,您学会了如何通过使用实体模型在数据库中查找数据,通过添加where
、关系、强大的约定(如多态关系)以及辅助工具(如分页)来限制结果。
摘要
在本章中,详细演示了 Eloquent ORM。Eloquent 是一个面向对象的包装器,用于实际发生在数据库和代码之间的事情。由于 Fluent 查询构建器很容易访问,因此熟悉查询的编写方式非常重要。这将有助于调试,并且还涵盖了 Eloquent 不足的复杂情况。在本章中,讨论了大部分 Eloquent 的概念。然而,还有许多其他可用的方法,因此鼓励进一步阅读。
在下一章中,除了其他主题,您将学习如何扩展数据库以在更大规模上表现更好。
第九章:扩展 Laravel
任何编程语言中构建的框架的特点是使用各种组件。正如我们在前几章中看到的,框架为软件开发人员提供了许多不同的预构建工具,以完成诸如身份验证、数据库交互和 RESTful API 创建等任务。
然而,就框架而言,可扩展性问题总是信息技术领域任何经理最担心的问题。与使用现有代码的任何库一样,总会有一定程度的开销,一定程度的膨胀,总会有比实际需要的更多的东西。
可扩展性问题
框架无法轻松扩展的原因有很多。让我们来看一下问题的简要列表:
-
一个问题是不必要的代码和与实际构建的应用程序无直接关系的包。例如,并非每个项目都需要身份验证,数据库驱动程序也不一定是 MySQL。框架核心的包必须监控兼容性问题。
-
设计模式、观点和学习曲线经常阻碍新团队成员快速熟悉。随着项目的扩大,日常开发需求也需要增长,软件开发团队必须不断招募那些对框架已经有一定了解或至少了解其基本概念的成员。
-
框架安全问题需要持续监控框架社区的网站或存储库,以收集有关所需的紧急安全更新的信息。甚至底层的 Web 服务器和操作系统本身也需要监控。在撰写本文时,Laravel 5.1 即将发布,它将需要 PHP 5.5,因为 PHP 5.4 将在 2015 年晚些时候宣布终止生命周期。
-
诸如 Eloquent 之类的 ORM 总是会增加一些开销,因为代码首先需要从 Eloquent 转换为流畅的查询构建器,然后再转换为 PDO 代码。显然,使用面向对象的方法来查询数据库是明智的选择,但它是有成本的。
走向企业
尽管可能会遇到一些障碍,Laravel 在未来的企业中仍将是一个强大的选择。PHP 7 将会非常快,而 Zend Framework 3 等框架已经宣布了他们在 PHP 7 优化方面的路线图。此外,通过使用FastCGI 进程管理器(FPM)、NGINX Web 服务器,并允许 PHP 的缓存机制正常工作,应用程序的可扩展性将继续在企业空间中得到更多的认可,因为它的复兴持续进行,新的开发人员也在为其核心做出贡献。
在本章中,您将学习如何让 Laravel 在企业环境中表现更好,其中可扩展性问题至关重要。首先,将讨论路由器缓存。然后,您将了解许多工具、技术,甚至是正在开发的以可扩展性为重点的新微框架。具体来说,我们将讨论从 Laravel 派生的官方微框架Lumen。最后,您将学习如何通过一种称为读和写的技术有效地使用数据库。
在代码库的大小方面,与 Zend 或 Symfony 相比,Laravel 的代码库是最小的之一,尽管它确实使用了一些 Symfony 组件。如前几章所述,不同的包被移除以减轻占用空间,这是从 Symfony 的基于组件的思想中得到的启示。例如,默认情况下不再包括 HTML、SSH 和注释包。
路由缓存
路由缓存有助于加快速度。在 Laravel 5 中,引入了一种缓存机制来加快执行速度。
这里显示了一个示例routes.php
:
Route::post('reserve-room', 'ReservationController@store');
Route::controllers([
'auth' => 'Auth\AuthController',
'password' => 'Auth\PasswordController',
]);
Route::post('/bookRoom','ReservationsController@reserve', ['middleware' => 'auth', 'domain'=>'booking.hotelwebsite.com']);
Route::resource('rooms', 'RoomsController');
Route::group(['middleware' => ['auth','whitelist']], function()
{
Route::resource('accommodations', 'AccommodationsController');
Route::resource('accommodations.amenities', 'AccommodationsAmenitiesController');
Route::resource('accommodations.rooms', 'AccommodationsRoomsController');
Route::resource('accommodations.locations', 'AccommodationsLocationsController');
Route::resource('amenities', 'AmenitiesController');
Route::resource('locations', 'LocationsController');
});
通过运行以下命令,Laravel 将缓存路由:
**$ php artisan route:cache**
然后,将它们放入以下目录中:
**/vendor/routes.php**
这是结果文件的一小部分:
<?php
/*
| Load The Cached Routes
|--------------------------------------------------------------------------
|
| Here we will decode and unserialize the RouteCollection instance that
| holds all of the route information for an application. This allows
| us to instantaneously load the entire route map into the router.
|
*/
app('router')->setRoutes(
unserialize(base64_decode('TzozNDoiSWxsdW1pbmF0ZVxSb3V0aW5nXF JvdXRlQ29sbGVjdGlvbiI6NDp7czo5OiIAKgByb3V0ZXMiO2E6Njp7czozOiJH RVQiO2E6NTA6e3M6MToiLyI7TzoyNDoiSWxsdW1pbmF0ZVxSb3V0aW5nXFJvdX RlIjo3OntzOjY6IgAqAHVyaSI7czoxOiIvIjtzOjEwOiIAKgBtZXRob2RzIjth OjI6e2k6MDtzOjM6IkdFVCI7aToxO3M6NDoiSEVBRCI7fX
...
Db250cm9sbGVyc1xBbWVuaXRpZXNDb250cm9sbGVyQHVwZGF0ZSI7cjoxNDQx O3M6NTQ6Ik15Q29tcGFueVxIyb2xsZXJzXEhvdGVsQ29udHJvbGxlckBkZXN0c m95IjtyOjE2MzI7fX0='))
);
如 DocBlock 所述,路由被编码为 base64,然后进行序列化:
unserialize(base64_decode( … ));
这执行一些预编译。如果我们对文件的内容进行 base64 解码,我们将获得序列化的数据。以下代码是文件的一部分:
O:34:"Illuminate\Routing\RouteCollection":4:{s:9:"*routes"; a:6:{s:3:"GET";a:50:{s:1:"/";O:24:"Illuminate\Routing\Route": 7:{s:6:"*uri";s:1:"/";s:10:"*methods";a:2:{i:0;s:3:"GET";i:1; s:4:"HEAD";}s:9:"*action";a:5:{s:4:"uses";s:50:"MyCompany \Http\Controllers\WelcomeController@index";s:10:"controller"; s:50:"MyCompany\Http\Controllers\WelcomeController@index"; s:9:"namespace";s:26:"MyCompany\Http\Controllers";s:6:"prefix"; N;s:5:"where";a:0:{}}s:11:"*defaults";a:0:{}s:9:"*wheres"; a:0:{}s:13:"*parameters";N;s:17:"*parameterNames";N; }s:4:"home";O:24:"Illumin…
"MyCompany\Http\Controllers\HotelController@destroy";r:1632;}}
如果/vendor/routes.php
文件存在,则使用它,而不是位于/app/Http/routes.php
的routes.php
文件。如果在某个时候不再希望使用路由缓存文件,则使用以下artisan
命令:
**$ php artisan route:clear**
这个命令将删除缓存的routes
文件,Laravel 将重新开始使用/app/Http/routes.php
文件。
提示
需要注意的是,如果在routes.php
文件中使用了任何闭包,缓存将失败。以下是路由中闭包的一个示例:
Route::get('room/{$id}', function(){
return Room::find($id);
});
出于任何原因,在routes.php
文件中使用闭包是不可取的。为了能够使用路由缓存,将闭包中使用的代码移到控制器中。
Illuminate 路由
所有这些工作都加快了请求生命周期中的一个重要部分,即路由。在 Laravel 中,路由类位于illuminate/routing
命名空间中:
<?php namespace Illuminate\Routing;
use Closure;
use LogicException;
use ReflectionFunction;
use Illuminate\Http\Request;
use Illuminate\Container\Container;
use Illuminate\Routing\Matching\UriValidator;
use Illuminate\Routing\Matching\HostValidator;
use Illuminate\Routing\Matching\MethodValidator;
use Illuminate\Routing\Matching\SchemeValidator;
use Symfony\Component\Routing\Route as SymfonyRoute;
use Illuminate\Http\Exception\HttpResponseException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
检查use
操作符,可以清楚地看出路由机制由许多类组成。最重要的一行是:
use Symfony\Component\Routing\Route as SymfonyRoute;
Laravel 使用 Symfony 的路由类。然而,Nikita Popov 编写了一个新的路由软件包。FastRoute
是一个快速的请求路由器,比其他路由软件包更快,并解决了现有路由软件包的一些问题。这个组件是 Lumen 微框架的主要优势之一。
Lumen
从苏打营销的角度来看,Lumen 可以被认为是 Laravel Light或 Laravel Zero。除了使用FastRoute
路由软件包外,许多软件包已从 Lumen 中删除,使其变得最小化并减少其占用空间。
Laravel 和 Lumen 之间的比较
在下表中列出了 Laravel 和 Lumen 中的软件包,并进行了比较。运行以下命令时,将安装这些软件包:
$ composer update –-no-dev
前面的命令是在开发完成并且应用程序准备好部署到服务器时使用的。在这个阶段,诸如 PHPUnit 和 PHPSpec 之类的工具显然被排除在外。
软件包名称对齐,以说明这些软件包在 Laravel 和 Lumen 中的位置:
Laravel 软件包 | Lumen 软件包 |
---|---|
- | nikic/fast-route |
illuminate/cache |
- |
illuminate/config |
illuminate/config |
illuminate/console |
illuminate/console |
illuminate/container |
illuminate/container |
illuminate/contracts |
illuminate/contracts |
illuminate/cookie |
illuminate/cookie |
illuminate/database |
illuminate/database |
illuminate/encryption |
illuminate/encryption |
illuminate/events |
illuminate/events |
illuminate/exception |
- |
illuminate/filesystem |
illuminate/filesystem |
illuminate/foundation |
- |
illuminate/hashing |
illuminate/hashing |
illuminate/http |
illuminate/http |
illuminate/log |
- |
illuminate/mail |
- |
illuminate/pagination |
illuminate/pagination |
illuminate/pipeline |
- |
illuminate/queue |
illuminate/queue |
illuminate/redis |
- |
illuminate/routing |
- |
illuminate/session |
illuminate/session |
illuminate/support |
illuminate/support |
illuminate/translation |
illuminate/translation |
illuminate/validation |
illuminate/validation |
illuminate/view |
illuminate/view |
jeremeamia/superclosure |
- |
league/flysystem |
- |
monolog/monolog |
monolog/monolog |
mtdowling/cron-expression |
mtdowling/cron-expression |
nesbot/carbon |
- |
psy/psysh |
- |
swiftmailer/swiftmailer |
- |
symfony/console |
- |
symfony/css-selector |
- |
symfony/debug |
- |
symfony/dom-crawler |
- |
symfony/finder |
- |
symfony/http-foundation |
symfony/http-foundation |
symfony/http-kernel |
symfony/http-kernel |
symfony/process |
- |
symfony/routing |
- |
symfony/security-core |
symfony/security-core |
symfony/var-dumper |
symfony/var-dumper |
vlucas/phpdotenv |
- |
classpreloader/classpreloader |
- |
danielstjules/stringy |
- |
doctrine/inflector |
- |
ext-mbstring |
- |
ext-mcrypt |
- |
在撰写本文时,使用非开发配置在 Laravel 5.0 中安装了 51 个包(显示在左列)。将此包数量与 Lumen 中安装的包数量进行比较(显示在右列)-只有 24 个。
前述的nikic/fast-route
包是 Lumen 拥有而 Laravel 没有的唯一包。symfony/routing
包是 Laravel 中的补充包。
精简应用程序开发
我们将使用一个示例,一个简单的面向公众的 RESTful API。这个 RESTful API 以 JSON 格式向任何用户显示一系列住宿的名称和地址,通过GET
:
-
如果不需要使用密码,则不需要
ext/mcrypt
。 -
如果不需要进行日期计算,则不需要
nesbot/carbon
。由于没有 HTML 界面,因此不需要涉及测试应用程序的 HTML 的以下库,symfony/css-selector
和symfony/dom-crawler
。 -
如果不需要向用户发送电子邮件,则不需要
illuminate/mail
或swiftmailer/swiftmailer
。 -
如果不需要与文件系统进行特殊交互,则不需要
league/flysystem
。 -
如果不是从命令行运行的命令,则不需要
symfony/console
。 -
如果不需要 Redis,则可以不使用
illuminate/redis
。 -
如果不需要不同环境的特定配置值,则不需要
vlucas/phpdotenv
。
提示
vlucas/phpdotenv
包是composer.json
文件中的一个建议包。
很明显,删除某些包的决定是经过慎重考虑的,以便根据最简单的应用程序需要简化 Lumen。
读/写
Laravel 还有另一个帮助其在企业中提高性能的机制:读/写。这与数据库性能有关,但功能如此易于设置,以至于任何应用程序都可以利用其有用性。
关于 MySQL,原始的 MyISAM 数据库引擎在插入、更新和删除期间需要锁定整个表。这在修改数据的大型操作期间造成了严重瓶颈,而选择查询等待访问这些表。随着 InnoDB 的引入,UPDATE
、INSERT
和DELETE
SQL 语句只需要在行级别上锁定。这对性能产生了巨大影响,因为选择可以从表的各个部分读取,而其他操作正在进行。
MariaDB,一个 MySQL 分支,声称比传统的 MySQL 性能更快。将数据库引擎替换为 TokuDB 将提供更高的性能,特别是在大数据环境中。
加速数据库性能的另一种机制是使用主/从配置。在下图中,所有操作都在单个表上执行。插入和更新将锁定单行,选择语句将按分配执行。
传统数据库表操作
主表
主/从配置使用允许SELECT
、UPDATE
和DELETE
语句的主表。这些语句修改表或向其写入。也可能有多个主表。每个主表都保持持续同步:对任何表所做的更改需要通知主表。
从表
从数据库表是主数据库表的从属。它依赖于主数据库表进行更改。SQL 客户端只能从中执行读操作(SELECT
)。可能还有多个从数据库依赖于一个或多个主数据库表。主数据库表将其所有更改通知给所有从数据库。以下图表显示了主/从设置的基本架构:
主从(读/写设置)
这种持续的同步会给数据库结构增加一些开销;然而,它提供了重要的优势:
由于从数据库表只能执行SELECT
语句,而主数据库表可以执行INSERT
、UPDATE
和DELETE
语句,因此从数据库表可以自由接受许多SELECT
语句,而无需等待涉及相同行的任何操作完成。
一个例子是货币汇率或股票价格表。这个表将实时不断地更新最新值,甚至可能每秒更新多次。显然,一个允许许多用户访问这些信息的网站可能会有成千上万的访问者。此外,用于显示这些数据的网页可能会为每个用户不断发出多个请求。
当有UPDATE
语句需要同时访问相同数据时,执行许多SELECT
语句会稍微慢一些。
通过使用主/从配置,SELECT
语句将仅在从数据库表上执行。这个表只以极其优化的方式接收已更改的数据。
在纯 PHP 中使用诸如mysqli
之类的库,可以配置两个数据库连接:
$master=mysqli_connect('127.0.0.1:3306','dbuser','dbpassword','mydatabase');
$slave=mysqli_connect('127.0.0.1:3307','dbuser','dbpassword','mydatabase');
在这个简化的例子中,从数据库设置在同一台服务器上。在实际应用中,它很可能会设置在另一台服务器上,以利用独立的硬件。
然后,所有涉及写语句的 SQL 语句将在从数据库上执行,读将在主数据库上执行。
这将增加一些编程工作量,因为每个 SQL 语句都需要传入不同的连接:
$result= mysqli_real_query($master,"UPDATE exchanges set rate='1.345' where exchange_id=2");
$result= mysqli_query($slave,"SELECT rate from exchanges where exchange_id=2");
在上面的代码示例中,应该记住哪些 SQL 语句应该用于主数据库,哪些 SQL 语句应该用于从数据库。
配置读/写
如前所述,用 Eloquent 编写的代码会转换为流畅的查询构建器代码。然后,该代码将转换为 PDO,这是各种数据库驱动程序的标准封装。
Laravel 通过其读/写配置提供了管理主/从配置的能力。这使程序员能够编写 Eloquent 和流畅的查询构建器代码,而不必担心查询是在主数据库表还是从数据库表上执行。此外,一个最初没有主/从配置的软件项目,后来需要扩展到主/从设置,只需要改变数据库配置的一个方面。数据库配置文件位于config/database.php
。
作为connections
数组的一个元素,将创建一个带有键mysql
的条目,其配置如下:
'connections' =>
'mysql' => [
'read' => [
'host' => '192.168.1.1',
'password' => 'slave-Passw0rd',
],
'write' => [
'host' => '196.168.1.2',
'username' => 'dbhostusername'
],
'driver' => 'mysql',
'database' => 'database',
'username' => 'dbusername',
'password' => 's0methingSecure',
'charset' => 'utf8',
'collation' => 'utf8_unicode_ci',
'prefix' => '',
],
读和写分别代表从和主。由于参数级联,如果用户名、密码和数据库名称相同,则只需要列出主机名的 IP 地址。但是,任何值都可以被覆盖。在这个例子中,读取的密码与主数据库不同,写入的用户名与从数据库不同。
创建主/从数据库配置
要设置主/从数据库,请从命令行执行以下步骤。
- 第一步是确定 MySQL 服务器绑定到哪个地址。为此,请找到包含 bind-address 参数的 MySQL 配置文件的行:
**bind-address = 127.0.0.1**
此 IP 地址将设置为主服务器使用的 IP 地址。
-
接下来,取消注释包含
server-id
的 MySQL 配置文件中的行,该文件很可能位于/etc/my.cn
或/etc/mysql/mysql.conf.d/mysqld.cnf
。 -
Unix 的
sed
命令可以轻松执行此操作:
**$ sed -i s/#server-id/server-id/g /etc/mysql/my.cnf**
提示
/etc/mysql/my.cnf
字符串需要替换为正确的文件名。
- 取消注释包含
server-id
的 MySQL 配置文件中的行:
**$ sed -i s/#log_bin/log_bin/g /etc/mysql/my.cnf**
提示
同样,/etc/mysql/my.cnf
字符串需要替换为正确的文件名。
- 现在,需要重新启动 MySQL。您可以使用以下命令执行此操作:
**$ sudo service mysql restart**
- 以下占位符应替换为实际值:
**MYSQLUSER**
**MYSQLPASSWORD**
**MASTERDATABASE**
**MASTERDATABASEUSER**
**MASTERDATABASEPASSWORD**
**SLAVEDATABASE**
**SLAVEDATABASEUSER**
**SLAVEDATABASEPASSWORD**
设置主服务器
设置主服务器的步骤如下:
- 授予从数据库用户权限:
**$ echo "GRANT REPLICATION SLAVE ON *.* TO 'DATABASEUSER'@'%' IDENTIFIED BY 'DATABASESLAVEPASSWORD';" | mysql -u MYSQLUSER -p"MYSQLPASSWORD"**
- 接下来,必须使用以下命令刷新权限:
**$ echo "FLUSH PRIVILEGES;" | mysql -u MYSQLUSER -p"MYSQLPASSWORD"**
- 接下来,使用以下命令切换到主数据库:
**$ echo "USE MASTERDATABASE;" | mysql -u MYSQLUSER -p"DATABASEPASSWORD"**
- 接下来,使用以下命令刷新表:
**$ echo "FLUSH TABLES WITH READ LOCK;" | mysql -u MYSQLUSER -p"MYSQLPASSWORD"**
- 使用以下命令显示主数据库状态:
**$ echo "SHOW MASTER STATUS;" | mysql -u MYSQLUSER -p"MYSQLPASSWORD"**
注意输出中的位置和文件名:
POSITION
FILENAME
- 使用以下命令转储主数据库:
**$ mysqldump -u root -p"MYSQLPASSWORD" --opt "MASTERDATABASE" > dumpfile.sql**
- 使用以下命令解锁表:
**$ echo "UNLOCK TABLES;" | mysql -u MYSQLUSER -p"MYSQLPASSWORD"**
设置从服务器
设置从服务器的步骤如下:
- 在从服务器上,使用以下命令创建从数据库:
**$ echo "CREATE DATABASE SLAVEDATABASE;" | mysql -u MYSQLUSER -p"MYSQLPASSWORD"**
- 使用以下命令导入从主数据库创建的转储文件:
**$ mysql -u MYSQLUSER -p"MYSQLPASSWORD" "MASTERDATABASE" < dumpfile.sql**
- 现在,MySQL 配置文件使用 server-id 2:
server-id = 2
- 在 MySQL 配置文件中,应取消注释两行,如下所示:
**#log_bin = /var/log/mysql/mysql-bin.log**
**expire_logs_days = 10**
**max_binlog_size = 100M**
**#binlog_do_db = include_database_name**
- 您将得到以下结果:
log_bin = /var/log/mysql/mysql-bin.log
expire_logs_days = 10
max_binlog_size = 100M
binlog_do_db = include_database_name
- 此外,需要在
binglog_do_db
下面添加以下行:
relay-log = /var/log/mysql/mysql-relay-bin.log
- 现在,需要使用以下命令重新启动 MySQL:
**$ sudo service mysql restart**
- 最后,设置主密码。主日志文件和位置将设置为步骤 5 中记录的文件名和位置。运行以下命令:
MASTER_PASSWORD='password', MASTER_LOG_FILE='FILENAME', MASTER_LOG_POS= POSITION;
总结
在本章中,您学会了如何通过路由缓存加快路由速度。您还学会了如何完全用 Lumen 替换 Laravel,这是完全源自 Laravel 的微框架。最后,我们讨论了 Laravel 如何使用读写配置充分利用主从配置。
Symfony 2.7 于 2015 年 5 月发布。这是一个长期支持版本。该版本将得到 36 个月的支持。在那之后不久,Taylor Otwell 决定创建 Laravel 的第一个 LTS 版本。这是 Laravel 牢固地定位在企业空间的第一个迹象。与 Symfony 和 Zend 的情况不同,Laravel 背后还没有正式的公司。然而,有一个庞大的社区包和服务生态系统,比如由 Jeffrey Way 运营的 Laracasts,他与 Taylor 密切合作提供官方培训视频。
此外,Taylor Otwell 还运行一个名为 Envoyer 的服务,该服务消除了 Laravel 部署的所有初始障碍,并为 Laravel 以及其他类型的现代 PHP 项目提供零停机部署。
随着 Laravel 5.1 LTS 的到来,Laravel 将会发生许多新的令人兴奋的事情。使用许多社区包的决定使 Taylor 和他的社区能够专注于框架的最重要方面,而无需重新发明轮子并维护许多冗余的包。此外,Laravel Collective 维护了已被弃用的包,即使最终从 Laravel 中删除的包也将继续得到多年的支持。
除了方便的服务,比如 Envoyer,下一章还将介绍一个最近出现的优秀自动化工具:Elixir。
第十章:使用 Elixir 构建、编译和测试
本章将涵盖以下主题:
-
安装 Node.js,Gulp 和 Elixir
-
运行 Elixir
-
使用 Elixir 合并 CSS 和 JavaScript 文件
-
设置通知
-
使用 Elixir 运行测试
-
扩展 Elixir
自动化 Laravel
在整本书中,已经构建了示例应用程序的许多部分。我们讨论了创建应用程序涉及的步骤。然而,关于帮助搭建、样板模板和为 CRUD 应用程序构建 RESTful API 的工具还有更多信息可用。直到最近,关于自动化开发过程和部署过程的一些部分并没有太多的资料。
在 PHP 领域,近年来出现了一个新的领域,即持续集成和构建工具的概念。持续集成和持续交付的流行使开发团队能够不断发布许多小的改进,每天多次发布他们的应用程序。在本章中,您将了解到 Laravel 具有一套新的工具集,可以使团队快速轻松地部署他们的软件版本,并自动构建和组合软件的许多组件。
持续集成和持续交付在开发过程中引起了相当大的变革,大大改变了软件构建的方式。然而,不久之前,标准的部署过程只涉及将代码放在服务器上。大多数早期采用 PHP 的人只是需要添加功能,比如论坛或联系我们表单的网页设计师。由于他们大多不是程序员,因此网页设计和图形设计中使用的大多数实践也被用于 PHP 部署。这些实践通常涉及使用诸如 FileZilla 之类的应用程序,将文件从左侧面板(用户的计算机)拖放到右侧(服务器的目录)。对于更有经验的人来说,使用终端仿真器(如 PuTTY)执行当时晦涩的 UNIX 命令。
使用不安全的文件传输端口 21,并且所有内容都未经压缩,只是简单地复制到服务器上。通常,所有文件都会被覆盖,而且部署大型网站的过程通常需要将近一个小时,因为有很多图片和文件。
最终,源代码控制系统变得普遍。在最近几年,SVN 和 Git 已成为大多数软件项目的行业标准。这些工具允许直接从代码仓库部署。
最近,composer 的到来为简单地将整个软件包包含到软件应用程序中添加功能创造了一种简单的方式。开发人员只需向配置文件添加一行代码即可轻松实现!
自动化开发和部署过程可能涉及许多步骤,以下是其中一些。
部署
以下是部署过程的一些功能:
-
复制与生产环境相关的某些配置设置
-
处理或编译使用快捷语法或预处理器编写的任何层叠样式表(CSS)或 JavaScript 文件
-
将各种资产(源代码或图像)复制到镜像、集群服务器或内容交付网络中
-
修改某些文件或目录的读/写/执行权限和/或所有权
-
将多个文件合并为一个文件,以减少执行多个 HTTP 调用所需的开销
-
减少文件中的无用空格和注释(缩小和/或混淆)以减小文件大小
-
将服务器上的现有文件与本地环境中的文件进行比较,以确定是否覆盖它们
-
对源代码进行标记和/或版本控制,以便可能进行代码回滚
开发或部署
以下是开发或部署过程的一些功能:
-
验证代码是否通过了编写的所有单元、功能和验收测试,以确保其质量
-
运行执行各种操作的脚本
-
执行任何迁移、种子播种或对数据库表的其他修改
-
从托管的源代码控制系统(如 GitHub)获取源代码控制
很明显,现代开发非常复杂。软件开发的更加困难的方面是在开发过程中不断重新创建生产或最终环境。
朝着自动化的方向
诸如文件监视器之类的工具可以在每次文件被修改时运行脚本或执行操作。此外,诸如 PHPStorm 之类的 IDE 将识别文件扩展名,并提供监视文件更改并允许开发人员执行某些操作的选项。虽然这种方法是可以接受的,但它并不是非常便携,每个开发人员都必须创建和共享一个包含 IDE 或文本编辑器中各种监视器的配置文件。这会产生依赖性,依赖于整个团队的一个单一 IDE。
此外,还可以创建其他方法,例如 Bash-shell 脚本,以在特定时间间隔运行。但是,使用这些脚本需要 UNIX-shell 编码知识。正如先前所示,像 artisan 这样的工具有助于自动化许多手动任务。但是,大多数默认的 artisan 命令是设计为手动执行的。
幸运的是,出现了两个使用 Node.js JavaScript 平台的工具:Grunt和gulp。Grunt 和 gulp 都取得了相当大的成功,但 gulp 最近变得更加流行。然而,对于可能不熟悉 JavaScript 语法的 PHP 开发人员来说,学习如何快速编写 gulp 任务并不容易。
考虑以下示例代码,摘自 gulp 的文档:
gulp.task('scripts', ['clean'], function() {
// Minify and copy all JavaScript (except vendor scripts)
// with sourcemaps all the way down
return gulp.src(paths.scripts)
.pipe(sourcemaps.init())
.pipe(coffee())
.pipe(uglify())
.pipe(concat('all.min.js'))
.pipe(sourcemaps.write())
.pipe(gulp.dest('build/js'));
});
从 Gulp 到 Elixir
幸运的是,Laravel 社区一直秉承着前瞻性思维,专注于减少复杂性。一个名为Elixir的官方社区工具已经出现,以便于使用 gulp。Gulp 是建立在 Node.js 之上的,而 Elixir 是建立在 gulp 之上的,创建了一个包装器:
注意
Laravel Elixir 不应与同名的动态功能语言混淆。另一个 Elixir 使用 Erlang 虚拟机,而 Laravel Elixir 使用 gulp 和 Node.js
入门
第一步是在开发计算机上安装 Node.js(如果尚未安装)。
注意
可以在以下网址找到说明:
安装 Node.js
对于像 Ubuntu 这样的基于 Debian 的操作系统,安装 Node.js 可能就像使用apt
软件包管理器一样简单。从命令行使用以下命令:
**$ sudo apt-get install -y nodejs**
请参考 Node.js 网站(nodejs.org
)上的正确操作系统的安装说明。
安装 Node.js 包管理器
下一步涉及安装 gulp,Elixir 将使用它来运行其任务。对于这一步,需要Node.js 包管理器(npm)。如果尚未安装npm
,则应使用apt
软件包安装程序。以下命令将用于安装npm
:
**$ sudo apt-get install npm**
npm 使用一个json
文件来管理项目的依赖关系:package.json
。该文件位于 Laravel 项目目录的根目录中,格式如下:
{
"devDependencies": {
"gulp": "³.8.8",
"laravel-elixir": "*"
}
}
安装 gulp 和 Laravel Elixir 作为依赖项。
安装 Gulp
以下命令用于安装gulp
:
**$ sud onpm install --global gulp**
安装 Elixir
一旦安装了 Node.js、npm 和 gulp,下一步是安装 Laravel Elixir。通过运行npm
install 而不带任何参数,npm
将读取其配置文件并安装 Laravel Elixir:
**$ npm install**
运行 Elixir
默认情况下,Laravel 包含一个gulpfile.js
文件,该文件由 gulp 用于运行其任务。该文件包含一个require
方法,用于包含运行任务所需的一切:
var elixir = require('laravel-elixir');
/*
|----------------------------------------------------------------
| Elixir Asset Management
|----------------------------------------------------------------
|
| Elixir provides a clean, fluent API for defining some basic gulp tasks
| for your Laravel application. By default, we are compiling the Sass
| file for our application, as well as publishing vendor resources.
|
*/
elixir(function(mix) {
mix.less('app.less');
});
第一个混合示例显示为:app.less
。要运行 gulp,只需在命令行中输入gulp
,如下所示:
**$ gulp**
输出如下所示:
**[21:23:38] Using gulpfile /var/www/laravel.example/gulpfile.js**
**[21:23:38] Starting 'default'...**
**[21:23:38] Starting 'less'...**
**[21:23:38] Running Less: resources/assets/less/app.less**
**[21:23:41] Finished 'default' after 2.35 s**
**[21:23:43] gulp-notify: [Laravel Elixir] Less Compiled!**
**[21:23:43] Finished 'less' after 4.27 s**
第一行表示已加载 gulp 文件。接下来的行显示每个任务的运行情况。less
任务处理层叠样式表预处理器Less
。
设置通知
如果您的开发环境是 Vagrant Box,则安装vagrant-notify
将允许 Laravel Elixir 直接与主机交互,并在操作系统中直接显示本机消息。要安装它,应从主机操作系统运行以下命令:
**$ vagrant plugin install vagrant-notify**
以下是通知的截图,显示 PHPUnit 测试失败了:
安装说明取决于每个操作系统。
注意
有关更多信息,请访问github.com/fgrehm/vagrant-notify
。
使用 Elixir 合并 CSS 和 JavaScript 文件
可能,部署过程中最重要的一步是合并和缩小 CSS 和 JavaScript 文件。缩小和合并五个 JavaScript 文件和三个 CSS 文件意味着不再有八个 HTTP 请求,而只有一个。此外,通过去除空格、换行符、注释和其他技术(例如缩短变量名)来缩小文件大小,文件大小将减少到原始大小的一小部分。尽管有这些优势,仍然有许多网站继续使用未缩小和未合并的 CSS 和 JavaScript 文件。
Elixir 提供了一种简单的方法来轻松合并和缩小文件。以下代码说明了这个示例:
elixir(function(mix) {
mix.scripts().styles();
});
scripts()
和styles()
两种方法将所有 JavaScript 和 CSS 文件合并为单个文件,分别为all.js
和all.css
。默认情况下,这两个函数期望文件位于/resources/assets/js
和/resources/assets/css
。
当 gulp 命令完成时,输出将如下所示:
**[00:36:20] Using gulpfile /var/www/laravel.example/gulpfile.js**
**[00:36:20] Starting 'default'...**
**[00:36:20] Starting 'scripts'...**
**[00:36:20] Merging: resources/assets/js/**/*.js**
**[00:36:20] Finished 'default' after 246 ms**
**[00:36:20] Finished 'scripts' after 280 ms**
**[00:36:20] Starting 'styles'...**
**[00:36:20] Merging: resources/assets/css/**/*.css**
**[00:36:21] Finished 'styles' after 191 ms**
请注意输出方便地说明了扫描了哪些目录。内容被合并,但没有被缩小。这是因为在开发过程中,在缩小文件上进行调试太困难。如果只有某个文件需要合并,则可以将文件名作为第一个参数传递给函数:
mix.scripts('app.js');
如果要合并多个文件,则可以将文件名数组作为第一个参数传递给函数:
mix.scripts(['app.js','lib.js']);
在生产环境中,希望有缩小的文件。要让 Elixir 缩小 CSS 和 JavaScript,只需在 gulp 命令中添加--production
选项,如下所示:
**$ gulp --production**
这将产生所需的缩小输出。默认输出目录位于:
/public/js
/public/css
使用 Laravel Elixir 编译
Laravel Elixir 非常擅长执行通常需要学习脚本语言的例行任务。以下各节将演示 Elixir 可以执行的各种编译类型。
编译 Sass 和 Less
层叠样式表预处理器Less
和Sass
出现是为了增强 CSS 的功能。例如,它不包含任何变量。Less
和Sass
允许前端开发人员利用变量和其他熟悉的语法特性。以下代码是标准 CSS 的示例。DOM 元素p
和li
(分别表示段落和列表项),以及具有post
类的任何元素将具有font-family
Arial,sans-serif 作为回退,并且颜色为黑色:
p, li, .post {
font-family: Arial, sans-serif;
color: #000;
}
接下来,使用Sass
CSS 预处理器,将字体族和文本颜色替换为两个变量:$text-font
和$text-color
。这样在需要更改时可以轻松维护。而且,这些变量可以共享。代码如下:
$text-font: Arial, sans-serif;
$text-color: #000;
p, li, .post {
font: 100% $text-font;
color: $text-color;
}
h2 {
font: 2em $text-font;
color: $text-color;
}
Less
预处理器使用@
而不是$
;因此,它的语法看起来更像是注释而不是php
变量:
@text-font: Arial, sans-serif;
@text-color: #000;
p, li, .post {
font: 100% @text-font;
color: @text-color;
}
h2 {
font: 2em @text-font;
color: @text-color;
}
还需要执行一个额外的步骤,因为它不会被浏览器引擎解释。增加的步骤是将Less
或Sass
代码编译成真正的 CSS。这在开发阶段会增加额外的时间;因此,Elixir 通过自动化流程来帮助。
在之前的 Laravel Elixir 示例中,less
函数只接受文件名app.less
作为其唯一参数。现在,示例应该更清晰一些。此外,less
可以接受一个将被编译的参数数组。
less
方法在/resources/assets/less
中搜索,默认情况下输出将放在public/css/
中:
elixir(function(mix) {
mix.less([
'style.less',
'style-rtl.less'
]);
});
编译 CoffeeScript
CoffeeScript 是一种编译成 JavaScript 的编程语言。与 Less 和 Sass 一样,它的目标是简化或扩展它所编译的语言的功能。在 CoffeeScript 的情况下,它通过减少按键次数来简化 Javascript。在下面的 JavaScript 代码中,创建了两个变量——一个数组和一个对象:
var available, list, room;
room = 14;
available = true;
list = [101,102,311,421];
room = {
id: 1,
number: 102,
status: "available"
}
在下面的 CoffeeScript 代码中,语法非常相似,但不需要分号,也不需要var
来创建变量。此外,缩进用于定义对象的属性。代码如下:
room = 14
available = true
list = [101,102,311,421]
room =
id: 1
number: 102
status: "available"
在这个 CoffeeScript 示例中,字符较少;然而,对于程序员来说,减少按键次数可以帮助提高速度和效率。要将 coffee 编译器添加到 Elixir 中,只需使用coffee
函数,如下面的代码所示:
elixir(function(mix) {
mix.coffee([
'app.coffee'
]);
});
编译器命令摘要
下表显示了预处理器、语言、函数以及每个函数期望源文件的位置。右侧的最后一列显示了结果合并文件的目录和/或名称。
processor | Language | function | Source directory | Default Output Location |
---|---|---|---|---|
Less | CSS | less() |
/resources/assets/less/file(s).less |
/public/css/file(s).css |
Sass | CSS | sass() |
/resources/assets/sass/file(s).scss |
/public/css/file(s).css |
N/A | CSS | styles() |
/resources/assets/css/ |
/public/css/all.css |
N/A | JavaScript | scripts() |
/resources/assets/js/ |
/public/js/all.js |
CoffeeScript | JavaScript | coffee() |
/resources/assets/coffee/ |
/public/js/app.js |
使用不同的名称保存
可选地,每个方法都可以接受第二个参数,该参数将覆盖默认位置。要使用不同的目录(在本例中是一个名为app
的目录),只需将该目录作为第二个参数添加:
mix.scripts(null,'public/app/js').styles(null,'public/app/css');
在这个例子中,文件将保存在public/app/js
和public/app/css
。
把所有东西放在一起
最后,让我们把所有东西放在一起得出一个有趣的结论。由于 CoffeeScript 脚本和less
和sass
文件不是合并而是直接复制到目标中,我们首先将 CoffeeScript、less
和sass
文件保存到 Elixir 期望 JavaScript 和 CSS 文件的目录中。然后,我们指示 Elixir 将所有 JavaScript 和 CSS 文件合并和压缩成两个合并和压缩的文件。代码如下:
elixir(function(mix) {
mix.coffee(null,'resources/assets/js')
.sass(null,'resources/assets/css')
.less(null,'resources/assets/css')
.scripts()
.styles();
});
提示
非常重要的一点是,Elixir 会覆盖文件而不验证文件是否存在,因此需要为每个文件选择一个唯一的名称。命令完成后,all.js
和all.css
将合并和压缩在public/js
和public/css
目录中。
使用 Elixir 运行测试
除了编译和发送通知之外,Elixir 还可以用于自动化测试的启动。接下来的部分将讨论 Elixir 如何用于 PHPSpec 和 PHPUnit。
PHPSpec
第一步是运行 PHPSpec 测试以自动化代码测试。通过将phpSpec()
添加到我们的gulpfile.js
中,PHPSpec 测试将运行:
elixir(function(mix) {
mix.less('app.less').phpSpec();
});
以下截图显示了输出。PHPSpec 输出被保留,因此测试输出非常有用:
当 PHPSpec 测试失败时,结果很容易阅读:
Laravel Elixir 输出的截图
在这个例子中,phpspec 在it creates a reservation test一行遇到了错误,如前面的截图所示。
PHPUnit
同样,我们可以通过将phpUnit
添加到任务列表中来将 PHPUnit 添加到我们的测试套件中,如下所示:
elixir(function(mix) {
mix.less('app.less').phpSpec().phpUnit();
});
创建自定义任务
Elixir 使我们能够创建自定义任务来几乎做任何事情。我们可以编写一个扫描控制器注释的自定义任务的一个例子。所有自定义任务都需要gulp
和laravel-elixir
。重要的是要记住所使用的编程语言是 JavaScript,因此语法可能或可能不熟悉,但很容易快速学习。如果命令将从命令行界面执行,那么我们还将导入 gulp-shell。代码如下:
var gulp = require('gulp');
var elixir = require('laravel-elixir');
var shell = require('gulp-shell');
/*
|----------------------------------------------------------------
| Route Annotation Scanner
|----------------------------------------------------------------
|
| We'll run route:scan Artisan to scan for changed files.
| Output is written to storage/framework/routes.scanned.php
|
*/
elixir.extend('routeScanning', function() {
gulp.task('routeScanning', function() {
return gulp.src('').
pipe(shell('php artisan route:scan'));
});
return this.queueTask('routeScanning');
});
在这段代码中,我们首先扩展 Elixir 并给方法一个名称,例如routeScanning
。然后,定义了一个 gulp 任务,task
方法的第一个参数是命令的名称。第二个命令是包含将被执行和返回的代码的闭包。
最后,通过将命令的名称传递给queueTask
方法,将任务排队执行。
将此脚本添加到我们的链中,如下所示:
elixir(function(mix) {
mix.routeScanning();
});
输出将如下所示:
**$ gulp**
**[23:24:19] Using gulpfile /var/www/laravel.example/gulpfile.js**
**[23:24:19] Starting 'default'...**
**[23:24:19] Starting 'routeScanning'...**
**[23:24:19] Finished 'default' after 12 ms**
**[23:24:20] Finished 'routeScanning' after 1 s**
由于pipe
函数允许命令链接,很容易添加一个通知,以警报通知系统,如下所示:
var gulp = require('gulp');
var elixir = require('laravel-elixir');
var shell = require('gulp-shell');
var Notification = require('./commands/Notification');
elixir.extend('routeScanning', function() {
gulp.task('routeScanning', function() {
return gulp.src('').
pipe(shell('php artisan route:scan')).
pipe(new Notification().message('Annotations scanned.'));
});
return this.queueTask('routeScanning');
});
在这里,Notification
类被引入,并创建了一个新的通知,以将消息Annotations scanned.
发送到通知系统。
运行代码会产生以下输出。请注意,已添加了gulp-notify
:
**$ gulp**
**[23:46:59] Using gulpfile /var/www/laravel.example/gulpfile.js**
**[23:46:59] Starting 'default'...**
**[23:46:59] Starting 'routeScanning'...**
**[23:46:59] Finished 'default' after 38 ms**
**PHP Warning: Module 'xdebug' already loaded in Unknown on line 0**
**Routes scanned!**
**[23:47:00] gulp-notify: [Laravel Elixir] Annotations scanned**
**[23:47:00] Finished 'routeScanning' after 1.36 s**
设置文件监视器
显然,每次我们想要编译层叠样式表或扫描注释时运行 gulp 是很繁琐的。幸运的是,Elixir 内置了一个监视机制。要调用它,只需运行以下命令:
**$ gulp watch**
这将允许将任务自动运行到gulpfile.js
链中的任何任务在发生某些更改时。启用此功能的必要代码在注释任务中如下:
this.registerWatcher("routeScanning", "app/Http/Controllers/**/*.php");
上面的代码注册了一个监视器。第一个参数是routeScanning
任务。第二个命令是将被监视以进行修改的目录模式。
由于我们知道路由注释将在控制器内部,我们可以设置路径仅在app/Http/Controllers/
目录内查找。正则表达式样式语法将匹配位于控制器下的任何一个目录中具有php
扩展名的文件。
现在,每当修改与模式匹配的文件时,routeScanning
任务以及任何其他监视匹配相同模式的文件的任务都将被执行。
额外的 Laravel Elixir 任务
npm 网站提供了超过 75 个任务,涉及测试、JavaScript、CSS 等。npm
网站位于npmjs.com
。
npm 网站的截图包含了许多有用的 Laravel Elixir 任务
总结
在本章中,您了解了 Elixir 不断增长的任务列表如何帮助全栈开发人员以及开发团队。一些任务与前端开发相关,例如编译、合并和压缩 CSS 和 JavaScript。其他任务与后端开发相关,例如行为驱动开发。将这些任务集成到日常开发工作流程中,将使整个团队能够理解在持续集成服务器中执行的步骤,其中 Elixir 将执行其任务,例如测试和编译,以准备将文件从开发转换为生产。
由于 Elixir 是建立在 gulp 之上的,随着 gulp 和 Elixir 社区的持续增长和新的贡献者继续为 Elixir 做出贡献,Elixir 的未来将继续丰富。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异
· 三行代码完成国际化适配,妙~啊~