精通-Laravel(全)

精通 Laravel(全)

原文:zh.annas-archive.org/md5/d10bf45da1cebf8f2b06a9600172079d

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

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 特性,使它成为任何希望构建强大应用的开发者的绝佳选择。

使用 phpspec 正确设计

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目录变得更加精简,只留下了应用程序中最基本的部分。诸如configdatabasestoragetests等目录已经从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 的真正力量在于它如何使用表达自然语言的语法。让我们来看看之前给我们关于预订的业务规则:

  • 预订的开始日期必顶在结束日期之前

  • 预订不能超过十五天

  • 预订不能包括超过四个房间

运行以下命令:

**# 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 以itits开始规范。phpspec 使用蛇形命名法以提高可读性,而start_date_must_be_less_than_the_end_date则是规范的精确副本。这不是很棒吗?

当传入$start_date$end_dateroom时,它们会自动被模拟。不需要其他任何东西。我们将创建一个有效的$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类。

有两种方法:updown,分别在使用migrate命令和rollback命令时使用。Schema::create()方法以表名作为第一个参数调用,并以函数回调作为第二个参数,接受Blueprint对象的实例作为参数。

创建表

$table对象有一些方法,执行任务,如创建索引,设置自增字段,指定应创建的字段类型,并将字段名称作为参数传递。

第一个命令用于创建自增字段id,这将是表的主键。然后,创建字符串字段,如nameemailpassword。请注意,unique方法链接到email字段的create语句,说明email字段将用作登录名/用户 ID,这是大多数现代 Web 应用程序的常见做法。rememberToken用于允许用户在每个会话中保持身份验证。此令牌在每次登录和注销时重置,保护用户免受潜在的恶意劫持尝试。

Laravel 迁移魔法

Laravel 迁移还能够创建时间戳字段,用于自动存储每个模型的创建和更新信息。

$table->timestamps();

以下代码告诉迁移自动在表中创建两列,即 created_atupdated_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**

迁移已启动并创建了表。现在出现了迁移表,如下截图所示:

$table->timestamps();

users 表的结构如下截图所示:

$table->timestamps();

要回滚迁移,请运行以下命令:

**$ 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 generatorXethron 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。使用索引,我们可以加快查找时间并访问表中数据的缓存版本。

软删除和时间戳属性

关于列表表的softDeletestimestamps,这取决于。如果表不是很大,跟踪更新、插入或删除不会太有害;但是,如果列表包含国家,其中更改不经常发生且非常小,最好省略softDeletestimestamps。因此,整个表可能适合内存,并且速度非常快。要省略时间戳,需要添加以下代码行:

public $timestamps = false;

创建种子

要创建我们的数据库 seeder,我们将修改扩展SeederDatabaseSeeder类。文件的名称是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

接下来,将在类中添加preparecleanup方法以执行迁移。我们将添加@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.

源自敏捷方法论的用户故事保证编写的代码与业务需求紧密匹配。它们通常遵循“作为…我想要…以便…”的模式。这定义了角色意图利益。它帮助我们计划如何将每个任务转换为代码。在我们的例子中,用户故事可以转化为任务。

作为酒店网站用户,我会创建以下任务列表:

  1. 作为酒店网站用户,我希望搜索房间,以便我可以从结果列表中选择一个房间。

  2. 作为酒店网站用户,我希望预订一个房间,以便我可以住在酒店里。

  3. 作为酒店网站用户,我希望收到包含预订详情的电子邮件,以便我可以拥有预订的副本。

  4. 作为酒店网站用户,我希望在等候名单上,以便我可以在有房间可用时预订一个房间。

  5. 作为酒店网站用户,我希望收到房间的可用性通知,以便我可以预订房间。

用户故事转换为代码

搜索房间的第一个任务很可能是来自用户或外部服务的 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\ReservationMyCompany\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()
	{
		//
	}

}

在这里,我们可以看到InteractsWithQueueShouldBeQueued类已经被包含,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()方法用-指定,并且可以是optionalrepeated,并且使用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)
;
}

提示

201created 的 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_idrooms

提示

如果应用程序数据库中的表遵循了活动记录约定,那么大多数 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/roomsPOST请求的 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 格式返回,就像它们在数据库中表示的那样。通常,模型属性,其性质为布尔值,分别用01表示truefalse。在这种情况下,更方便的是返回一个真正的truefalse给 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',
  ...
  ],

最后,需要将FormHtml别名添加到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 AddressPassword),我们首先创建一个数组来保存属性,然后将此数组传递给标签,如下所示:

$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 方法来构建前端。一旦学会了基本方法,就可以很容易地复制和粘贴以前创建的表单元素,然后更改它们的元素名称和/或发送给它们的数组。

根据项目的大小,这种方法可能是正确的选择,也可能不是。对于非常小的应用程序,需要编写的代码量的差异并不明显,尽管,如selectMonthselectRange方法所示,所需的代码量是 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 FrameworkZF)也使用注解。测试工具 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 5 中使用注释

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);

}

在实现此基类的任何类中,必须有一个接受$requestClosurehandle方法。

中间件的基本结构如下:

<?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_idcreated_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作为键强制路由在AccommodationsControllersearch方法上调用auth中间件:

Route::get('search-accommodation',
  ['middleware' => 'auth','AccommodationsController@search']);

在这种情况下,如果用户未经认证,将被重定向到登录页面。

路由组

路由可以分组以共享相同的中间件。例如,如果我们想保护应用程序中的所有路由,我们可以创建一个路由组,并只传入键值对middlewareauth。代码如下:

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,其中包含authwhitelist。 代码如下:

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,要么是onlyexcept选项显然是排除,而only选项是包含。 在上面的示例中,auth中间件将应用于除indexshow方法之外的所有方法,这两个方法是两种读取方法(它们不修改数据)。 相反,如果log中间件应用于indexshow,则将使用以下构造方法:

  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 地址,同时记录对indexshow的任何请求。

结论

中间件可以巧妙地过滤请求并保护应用程序或 RESTful API 免受不必要的请求。 它还可以执行日志记录并重定向任何符合特定条件的请求。

中间件还可以为现有应用程序提供附加功能。 例如,Laravel 提供了EncryptCookiesAddQueuedCookiesToResponse中间件来处理 cookies,而StartSessionShareErrorsFromSession处理会话。

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 {
}

要添加idcontentsauthor_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}
...

属性,如totalper_pagecurrent_pagelast_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 的一个很棒的特性是拥有一个关系是多态的实体的可能性。这个词的两个部分,polymorphic,来自希腊语。由于 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 来执行关系。

例如,如果字段名为fname1fname2,我们可以在我们的模型中使用一个获取属性函数,语法是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.phproutes.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-selectorsymfony/dom-crawler

  • 如果不需要向用户发送电子邮件,则不需要illuminate/mailswiftmailer/swiftmailer

  • 如果不需要与文件系统进行特殊交互,则不需要league/flysystem

  • 如果不是从命令行运行的命令,则不需要symfony/console

  • 如果不需要 Redis,则可以不使用illuminate/redis

  • 如果不需要不同环境的特定配置值,则不需要vlucas/phpdotenv

提示

vlucas/phpdotenv包是composer.json文件中的一个建议包。

很明显,删除某些包的决定是经过慎重考虑的,以便根据最简单的应用程序需要简化 Lumen。

读/写

Laravel 还有另一个帮助其在企业中提高性能的机制:读/写。这与数据库性能有关,但功能如此易于设置,以至于任何应用程序都可以利用其有用性。

关于 MySQL,原始的 MyISAM 数据库引擎在插入、更新和删除期间需要锁定整个表。这在修改数据的大型操作期间造成了严重瓶颈,而选择查询等待访问这些表。随着 InnoDB 的引入,UPDATEINSERTDELETE SQL 语句只需要在行级别上锁定。这对性能产生了巨大影响,因为选择可以从表的各个部分读取,而其他操作正在进行。

MariaDB,一个 MySQL 分支,声称比传统的 MySQL 性能更快。将数据库引擎替换为 TokuDB 将提供更高的性能,特别是在大数据环境中。

加速数据库性能的另一种机制是使用主/从配置。在下图中,所有操作都在单个表上执行。插入和更新将锁定单行,选择语句将按分配执行。

读/写

传统数据库表操作

主表

主/从配置使用允许SELECTUPDATEDELETE语句的主表。这些语句修改表或向其写入。也可能有多个主表。每个主表都保持持续同步:对任何表所做的更改需要通知主表。

从表

从数据库表是主数据库表的从属。它依赖于主数据库表进行更改。SQL 客户端只能从中执行读操作(SELECT)。可能还有多个从数据库依赖于一个或多个主数据库表。主数据库表将其所有更改通知给所有从数据库。以下图表显示了主/从设置的基本架构:

从数据库表

主从(读/写设置)

这种持续的同步会给数据库结构增加一些开销;然而,它提供了重要的优势:

由于从数据库表只能执行SELECT语句,而主数据库表可以执行INSERTUPDATEDELETE语句,因此从数据库表可以自由接受许多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 地址。但是,任何值都可以被覆盖。在这个例子中,读取的密码与主数据库不同,写入的用户名与从数据库不同。

创建主/从数据库配置

要设置主/从数据库,请从命令行执行以下步骤。

  1. 第一步是确定 MySQL 服务器绑定到哪个地址。为此,请找到包含 bind-address 参数的 MySQL 配置文件的行:
**bind-address            = 127.0.0.1**

此 IP 地址将设置为主服务器使用的 IP 地址。

  1. 接下来,取消注释包含server-id的 MySQL 配置文件中的行,该文件很可能位于/etc/my.cn/etc/mysql/mysql.conf.d/mysqld.cnf

  2. Unix 的sed命令可以轻松执行此操作:

**$ sed -i s/#server-id/server-id/g  /etc/mysql/my.cnf**

提示

/etc/mysql/my.cnf字符串需要替换为正确的文件名。

  1. 取消注释包含server-id的 MySQL 配置文件中的行:
**$ sed -i s/#log_bin/log_bin/g  /etc/mysql/my.cnf**

提示

同样,/etc/mysql/my.cnf字符串需要替换为正确的文件名。

  1. 现在,需要重新启动 MySQL。您可以使用以下命令执行此操作:
**$ sudo service mysql restart**

  1. 以下占位符应替换为实际值:
**MYSQLUSER**
**MYSQLPASSWORD**
**MASTERDATABASE**
**MASTERDATABASEUSER**
**MASTERDATABASEPASSWORD**
**SLAVEDATABASE**
**SLAVEDATABASEUSER**
**SLAVEDATABASEPASSWORD**

设置主服务器

设置主服务器的步骤如下:

  1. 授予从数据库用户权限:
**$ echo  "GRANT REPLICATION SLAVE ON *.* TO 'DATABASEUSER'@'%' IDENTIFIED BY 'DATABASESLAVEPASSWORD';" | mysql -u MYSQLUSER -p"MYSQLPASSWORD"** 

  1. 接下来,必须使用以下命令刷新权限:
**$ echo  "FLUSH PRIVILEGES;" | mysql -u MYSQLUSER -p"MYSQLPASSWORD"** 

  1. 接下来,使用以下命令切换到主数据库:
**$ echo  "USE MASTERDATABASE;" | mysql -u MYSQLUSER -p"DATABASEPASSWORD"** 

  1. 接下来,使用以下命令刷新表:
**$ echo  "FLUSH TABLES WITH READ LOCK;" | mysql -u MYSQLUSER -p"MYSQLPASSWORD"** 

  1. 使用以下命令显示主数据库状态:
**$ echo  "SHOW MASTER STATUS;" | mysql -u MYSQLUSER -p"MYSQLPASSWORD"** 

注意输出中的位置和文件名:

POSITION
FILENAME
  1. 使用以下命令转储主数据库:
**$ mysqldump -u root -p"MYSQLPASSWORD"  --opt "MASTERDATABASE" > dumpfile.sql**

  1. 使用以下命令解锁表:
**$ echo  "UNLOCK TABLES;" | mysql -u MYSQLUSER -p"MYSQLPASSWORD"** 

设置从服务器

设置从服务器的步骤如下:

  1. 在从服务器上,使用以下命令创建从数据库:
**$ echo  "CREATE DATABASE SLAVEDATABASE;" | mysql -u MYSQLUSER -p"MYSQLPASSWORD"** 

  1. 使用以下命令导入从主数据库创建的转储文件:
**$ mysql -u MYSQLUSER -p"MYSQLPASSWORD"  "MASTERDATABASE" < dumpfile.sql**

  1. 现在,MySQL 配置文件使用 server-id 2:
server-id            = 2
  1. 在 MySQL 配置文件中,应取消注释两行,如下所示:
**#log_bin			= /var/log/mysql/mysql-bin.log**
**expire_logs_days	= 10**
**max_binlog_size   = 100M**
**#binlog_do_db		= include_database_name**

  1. 您将得到以下结果:
log_bin			= /var/log/mysql/mysql-bin.log
expire_logs_days	= 10
max_binlog_size    = 100M
binlog_do_db		= include_database_name
  1. 此外,需要在binglog_do_db下面添加以下行:
relay-log                = /var/log/mysql/mysql-relay-bin.log
  1. 现在,需要使用以下命令重新启动 MySQL:
**$ sudo service mysql restart**

  1. 最后,设置主密码。主日志文件和位置将设置为步骤 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 平台的工具:Gruntgulp。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 之上的,创建了一个包装器:

从 Gulp 到 Elixir

注意

Laravel Elixir 不应与同名的动态功能语言混淆。另一个 Elixir 使用 Erlang 虚拟机,而 Laravel Elixir 使用 gulp 和 Node.js

入门

第一步是在开发计算机上安装 Node.js(如果尚未安装)。

注意

可以在以下网址找到说明:

nodejs.org

安装 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.jsall.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

层叠样式表预处理器LessSass出现是为了增强 CSS 的功能。例如,它不包含任何变量。LessSass允许前端开发人员利用变量和其他熟悉的语法特性。以下代码是标准 CSS 的示例。DOM 元素pli(分别表示段落和列表项),以及具有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;
}

还需要执行一个额外的步骤,因为它不会被浏览器引擎解释。增加的步骤是将LessSass代码编译成真正的 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/jspublic/app/css

把所有东西放在一起

最后,让我们把所有东西放在一起得出一个有趣的结论。由于 CoffeeScript 脚本和lesssass文件不是合并而是直接复制到目标中,我们首先将 CoffeeScript、lesssass文件保存到 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.jsall.css将合并和压缩在public/jspublic/css目录中。

使用 Elixir 运行测试

除了编译和发送通知之外,Elixir 还可以用于自动化测试的启动。接下来的部分将讨论 Elixir 如何用于 PHPSpec 和 PHPUnit。

PHPSpec

第一步是运行 PHPSpec 测试以自动化代码测试。通过将phpSpec()添加到我们的gulpfile.js中,PHPSpec 测试将运行:

elixir(function(mix) {
    mix.less('app.less').phpSpec();
});

以下截图显示了输出。PHPSpec 输出被保留,因此测试输出非常有用:

PHPSpec

当 PHPSpec 测试失败时,结果很容易阅读:

PHPSpec

Laravel Elixir 输出的截图

在这个例子中,phpspec 在it creates a reservation test一行遇到了错误,如前面的截图所示。

PHPUnit

同样,我们可以通过将phpUnit添加到任务列表中来将 PHPUnit 添加到我们的测试套件中,如下所示:

elixir(function(mix) {
    mix.less('app.less').phpSpec().phpUnit();
});

创建自定义任务

Elixir 使我们能够创建自定义任务来几乎做任何事情。我们可以编写一个扫描控制器注释的自定义任务的一个例子。所有自定义任务都需要gulplaravel-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

额外的 Laravel Elixir 任务

npm 网站的截图包含了许多有用的 Laravel Elixir 任务

总结

在本章中,您了解了 Elixir 不断增长的任务列表如何帮助全栈开发人员以及开发团队。一些任务与前端开发相关,例如编译、合并和压缩 CSS 和 JavaScript。其他任务与后端开发相关,例如行为驱动开发。将这些任务集成到日常开发工作流程中,将使整个团队能够理解在持续集成服务器中执行的步骤,其中 Elixir 将执行其任务,例如测试和编译,以准备将文件从开发转换为生产。

由于 Elixir 是建立在 gulp 之上的,随着 gulp 和 Elixir 社区的持续增长和新的贡献者继续为 Elixir 做出贡献,Elixir 的未来将继续丰富。

posted @ 2024-05-05 12:11  绝不原创的飞龙  阅读(29)  评论(0编辑  收藏  举报