我如何开始使用Laravel的故事很普遍:我写了多年的PHP,但我已经准备放弃,追求Rails和其他现代Web框架的力量。Rails特别是有一个充满活力的社区,一个完美的结合了倾向性默认和灵活性,以及Ruby-Gems的力量来利用预打包的常用代码。
有些东西阻止我跳船,当我发现Laravel时我为此感到高兴。它提供了我在Rails中被吸引的一切,但它不仅仅是Rails的克隆;这是一个创新的框架,具有令人难以置信的文档、一个友好的社区,并且明显受到许多语言和框架的影响。
这不是关于Laravel的第一本书,也不会是最后一本。我不打算让这本书覆盖每一行代码或每一个实现模式。我不希望这本书在新版本的Laravel发布时就过时。相反,它的主要目的是为开发人员提供一个高层次的概述和具体示例,以便他们在任何Laravel代码库中工作,并使用每一个Laravel的功能和子系统。与其复制文档,我想帮助你理解Laravel背后的基本概念。
Laravel是一个功能强大且灵活的PHP框架。它有一个充满活力的社区和广泛的工具生态系统,因此它在吸引力和影响力上都在增长。这本书是为那些已经知道如何制作网站和应用程序的开发人员准备的,他们希望学习如何在Laravel中做到更好。
我认为你会发现这本书在高层次介绍和具体应用之间有一个舒适的平衡,在最后,你应该能够从头开始在Laravel中编写一个完整的应用程序。而且,如果我做得好的话,你会对尝试感到兴奋。
本书假定读者具备基本的面向对象编程实践知识,了解PHP(或至少C语系语言的一般语法),以及模型-视图-控制器(MVC)模式和模板化的基本概念。如果你以前没有制作过网站,可能会感到有些吃力。但只要你有一些编程经验,在阅读本书之前不需要对Laravel有任何了解——我们将覆盖你需要了解的一切,从最简单的“Hello,world!”开始。
Laravel可以在任何操作系统上运行,但本书中有些bash(shell)命令在Linux/macOS上运行起来最简单。Windows用户可能会在使用这些命令和现代PHP开发时遇到一些困难,但如果按照获取Homestead(Linux虚拟机)运行的说明进行操作,你将能够在那里运行所有命令。
本书的每个部分都可以单独阅读,但对于框架新手来说,我尝试将章节结构化,从头开始阅读到结尾是非常合理的。
如适用,每个章节都会以两个部分结束:“测试”和“TL;DR”。如果你不熟悉,“TL;DR”意思是“太长了,不想读”。这些最后的部分将展示如何为每个章节涵盖的特性编写测试,并提供所覆盖内容的高级概述。
本书是为Laravel10编写的。
本书使用以下排版约定:
斜体
指示新术语、网址、电子邮件地址、文件名和文件扩展名。
固定宽度
用于程序列表,以及段落内引用程序元素,如变量或函数名、数据库、数据类型、环境变量、语句和关键字。
固定宽度粗体
显示用户应按字面意思键入的命令或其他文本。
固定宽度斜体
显示应由用户提供值或由上下文确定的代码文本。
{大括号中的斜体}
显示应由用户提供值或由上下文确定的文件名或文件路径。
这个元素表示提示或建议。
这个元素表示一般注意事项。
这个元素表示警告或注意事项。
在这个项目中,我收到了许多人的支持,但我甚至不知道从哪里开始表达我的感激之情。
我的伴侣,Imani,在每一个胜利中都与我共庆,鼓励我,坐在我旁边,她打开笔记本电脑,狂热地打字,我们一起推动,以满足我们的截止日期。我的儿子Malachi和女儿Mia在整个过程中非常宽容和理解。自Tighten团队成立以来,我的整个团队一直给予我支持和鼓励。我的朋友Trent和Tevin努力创造了艺术和艺术家的空间,我很荣幸能成为他们小家庭的一部分。
我有一系列的研究助理:WilburPowery、BrittanyJonesDumas、ReekaMaharaj和AnaLisboa。在我现在繁忙的生活中,没有他们的帮助,我不可能写出第二版和第三版。
Laravel社区中有如此多的人值得感谢,以至于我甚至无法列举所有。因此,对于所有为此付出了大量爱、奉献、关心和精湛技艺的人,谢谢你们。感谢你们帮助这个社区成为一个令人难以置信的地方;感谢你们在育儿、离婚、疫情、抑郁等方面鼓励我的许多人。你们都是了不起的。
TaylorOtwell为创建Laravel而受到感谢和荣誉——因此创造了如此多的工作岗位,帮助了众多开发者更加热爱我们的生活。他因专注于开发者的幸福感而受到赞赏,以及他为理解开发者并建立积极鼓舞的社区而付出的努力。但我也想感谢他成为一位友善、鼓舞和富有挑战性的朋友。Taylor,你真是一位了不起的领导者。
感谢所有我的技术审阅者!对于第一版:KeithDamiani,MichaelDyrynda,AdamFairholm和MylesHyson。对于第二版:TatePearanda,AndySwick,MohamedSaid和SamanthaGeitz。对于第三版:AnthonyClark,BenHolmen,JakeBathman和TonyMessias。
当然,也感谢我的家人和朋友们,无论是直接还是间接地支持我度过这一过程——我的父母和兄弟姐妹,我在芝加哥、盖恩斯维尔、迪凯特和亚特兰大的社区,其他企业主和作家,其他会议演讲者,其他父母,以及我有幸遇见和交往的大量了不起的人类。
在动态网络的早期阶段,编写Web应用程序看起来与今天大不相同。当时的开发人员不仅负责编写我们应用程序独特的业务逻辑代码,还负责编写那些在各个站点中如此常见的组件——用户认证、输入验证、数据库访问、模板化等等。
如今,程序员们可以轻松访问数十种应用程序开发框架和数千个组件和库。程序员们常说,当你学会一个框架时,可能已经有三个更新(据称更好)的框架出现,试图取代它。
“因为它存在”可能是攀登山峰的有效理由,但选择使用特定框架——或者根本使用框架——有更好的理由。值得问的问题是,为什么要使用框架?更具体地说,为什么选择Laravel?
像Laravel、Symfony、Lumen和Slim这样的框架,会将一系列第三方组件与自定义框架“粘合剂”(如配置文件、服务提供者、预定义的目录结构和应用程序引导)打包在一起。因此,使用框架的好处不仅在于有人已经为您做出了关于单独组件的决定,还包括这些组件如何组合在一起的决策。
假设您开始一个新的Web应用程序,没有框架的帮助。您应该从哪里开始呢?嗯,它可能需要路由HTTP请求,所以现在您需要评估所有可用的HTTP请求和响应库并选择一个。然后,您将不得不选择一个路由器。哦,您可能还需要设置某种形式的路由配置文件。它应该使用什么语法?应该放在哪里?控制器呢?它们应该放在哪里,如何加载?嗯,您可能需要一个依赖注入容器来解析控制器及其依赖关系。但是选哪一个呢?
框架通过提供慎重考虑的答案来解决“我们应该在这里使用哪个组件?”的问题,并确保所选组件能够很好地协同工作。此外,框架提供的约定减少了新项目的开发者需要理解的代码量——例如,如果你理解一个Laravel项目中的路由工作原理,那么你就理解了所有Laravel项目中它是如何工作的。
当有人建议为每个新项目定制框架时,他们实际上是在主张能够控制应用程序基础中包含和排除什么。这意味着最好的框架不仅会为你提供一个坚实的基础,还会让你有自由进行定制。正如我将在本书的其余部分中展示的那样,这是使Laravel如此特别的部分。
能够回答“为什么选择Laravel?”的一个重要部分是了解Laravel的历史——以及它之前的发展。在Laravel兴起之前,PHP和其他Web开发领域有各种框架和其他运动。
DavidHeinemeierHansson于2004年发布了RubyonRails的第一个版本,自那以后,很难找到一种Web应用框架不受Rails影响的情况。
Rails推广了MVC、RESTfulJSONAPI、约定优于配置、ActiveRecord等许多工具和惯例,对Web开发者处理他们的应用程序的方式产生了深远影响——特别是快速应用程序开发方面。
大多数开发者很清楚,Rails和类似的Web应用框架是未来的趋势,包括那些明显模仿Rails的PHP框架迅速涌现。
CakePHP是2005年的第一款,很快又有Symfony、CodeIgniter、ZendFramework和Kohana(CodeIgniter的一个分支)。Yii在2008年出现,Aura和Slim则在2010年。2011年推出的FuelPHP和Laravel既不是CodeIgniter的分支,而是提出的替代方案。
这些框架中有些更像Rails,专注于数据库对象关系映射(ORM)、MVC结构和其他旨在快速开发的工具。另一些像Symfony和Zend则更专注于企业设计模式和电子商务。
CakePHP和CodeIgniter是最早公开承认从Rails获得灵感的两个早期PHP框架。CodeIgniter迅速走红,并且到2010年,可以说是独立PHP框架中最受欢迎的。
CodeIgniter简单易用,拥有出色的文档和强大的社区。但它的现代技术和模式使用进展缓慢;随着框架世界的发展和PHP工具的进步,CodeIgniter在技术进步和开箱即用功能方面开始落后。与许多其他框架不同,CodeIgniter由一家公司管理,它在适应PHP5.3的新功能(如命名空间以及后来的GitHub和Composer)方面进展缓慢。正是在2010年,Laravel的创始人TaylorOtwell对CodeIgniter不满意到足以自己动手写框架。
Laravel1的第一个beta版于2011年6月发布,完全从头开始编写。它具有自定义ORM(Eloquent)、基于闭包的路由(受RubySinatra启发)、用于扩展的模块系统以及表单、验证、认证等助手。
早期的Laravel开发进展迅速,Laravel2和3分别于2011年11月和2012年2月发布。它们引入了控制器、单元测试、命令行工具、控制反转(IoC)容器、Eloquent关系和迁移。
到了Laravel4,Taylor从头重新编写了整个框架。此时的Composer,PHP现在普遍使用的包管理器,显示出成为行业标准的迹象,Taylor看到了将框架重写为Composer分发和捆绑的组件集合的价值。
Taylor开发了一组名为Illuminate的组件,并于2013年5月发布了全新结构的Laravel4。现在,Laravel不再将大部分代码捆绑为下载包,而是通过Composer从Symfony(另一个将其组件释放供他人使用的框架)和Illuminate组件中拉取大部分组件。
Laravel4还引入了队列、邮件组件、门面和数据库种子。因为Laravel现在依赖于Symfony组件,因此宣布Laravel将会(并非完全一样,但很快)效仿Symfony的六个月发布计划。
Laravel4.3原定于2014年11月发布,但随着开发的进展,其变化的重要性变得明显,因此Laravel5于2015年2月发布。
Laravel5采用了全新的目录结构,删除了表单和HTML助手,引入了契约接口,大量新的视图,Socialite用于社交媒体认证,Elixir用于资产编译,Scheduler简化了cron任务,dotenv简化了环境管理,还有全新的REPL(read–evaluate–printloop)。从那时起,它在功能和成熟度上都有所增长,但没有像以前版本那样有重大变化。
在2019年9月,引入了Laravel6,并带来了两个主要变化:首先,移除了Laravel提供的字符串和数组全局助手(改用Facade);其次,转向语义化版本控制(SemVer)进行版本编号。这种变化的实际影响意味着,对于5之后的所有Laravel版本,无论是主要版本(6、7等)还是次要版本(6.1、6.2等),发布频率都大大增加了。
那么,是什么让Laravel与众不同?为什么在任何时候都值得拥有超过一个PHP框架?他们毕竟都使用Symfony的组件,不是吗?让我们稍微谈谈是什么让Laravel如此“tick”。
您只需阅读Laravel的市场材料和README就可以开始看到它的价值。Taylor使用像“Illuminate”和“Spark”这样与光有关的词语。然后还有这些:“Artisans”。“优雅”。还有这些:“一股清新的空气”。“新的开始”。最后还有:“快速”。“光速”。
Laravel的核心是装备和使开发者能力。其目标是提供清晰、简单和优美的代码和功能,帮助开发者快速学习、启动、开发和编写简单、清晰且持久的代码。
仅仅说你想让开发者开心是一回事。实现它是另一回事,它要求你质疑框架中最有可能让开发者不开心的因素,以及最有可能让他们开心的因素。Laravel试图通过多种方式使开发者的生活更轻松。
首先,Laravel是一个快速应用开发框架。这意味着它专注于浅显易懂的学习曲线,并尽量减少从开始新应用到发布的步骤。Laravel提供了构建Web应用程序中最常见任务的所有组件,从数据库交互到身份验证、队列、电子邮件到缓存,都由Laravel提供简化。但Laravel的组件不仅仅在其自身上很棒;它们在整个框架中提供了一致的API和可预测的结构。这意味着,当你在Laravel中尝试新事物时,你很可能会说,“...它就这样运行了。”
Laravel与其他PHP框架不同的一个有趣之处在于,它的创作者和社区更多地受到Ruby和Rails以及函数式编程语言的影响和启发,而不是Java。在现代PHP中,有一种强烈的潮流倾向于冗长和复杂,接纳PHP更类似于Java的方面。但是Laravel往往站在另一边,拥抱富有表现力、动态和简单的编码实践和语言特性。
从Laravel的最初开始,我就有这样一个想法,即所有人都希望感觉自己是某个群体的一部分。想要归属并被其他志同道合的人接受,是人类的自然本能。因此,通过在web框架中注入个性并积极参与社区,这种感觉可以在社区中蔓延。
TaylorOtwell,产品与支持采访
Taylor从Laravel早期就明白,一个成功的开源项目需要两样东西:良好的文档和一个友好的社区。而这两点现在已成为Laravel的标志性特征。
它看起来非常类似于控制器,正如你在示例1-2中所看到的(如果你想立即测试,请首先运行phpartisanmake:controllerWelcomeController创建控制器)。
所以,为什么选择Laravel?
因为Laravel能帮助你将想法变为现实,无需编写多余的代码,采用现代编码标准,支持活跃的社区,并拥有强大的工具生态系统。
还有因为你,亲爱的开发者,值得快乐。
PHP之所以成功的一部分原因是几乎找不到不能运行PHP的网络服务器。然而,现代PHP工具对于过去的要求更为严格。为了更好地为Laravel开发,最好保持代码在本地和远程服务器环境的一致性,幸运的是,Laravel生态系统为此提供了一些工具。
本章中涵盖的所有内容在Windows机器上都是可能的,但你需要几十页的定制说明和注意事项。我将这些说明和注意事项留给真正的Windows用户,因此本书的例子将专注于Unix/Linux/macOS开发者。
无论你选择通过在本地机器上安装PHP和其他工具来为你的网站提供服务,还是通过Vagrant或Docker在虚拟机中提供开发环境,或者依赖像MAMP/WAMP/XAMPP这样的工具,你的开发环境都需要安装以下所有内容才能为Laravel站点提供服务:
对于许多项目,使用更简单的工具集来托管你的开发环境可能已经足够了。如果你的系统上已经安装了MAMP或WAMP或XAMPP,那很可能能够运行Laravel。
然而,如果你希望在你的开发环境中拥有更多的功能(每个项目的不同本地域名、像MySQL这样的依赖管理等),你将需要一个比PHP内置服务器更强大的工具。
Laravel提供了五种本地开发工具:Artisanserve、Sail、Valet、Herd和Homestead。我们会简要介绍每一种。如果你不确定该使用哪一种,我个人推荐Mac用户使用Valet,其他人使用Sail。
Sail是开始本地Laravel开发的最简单方法,无论你使用的是什么操作系统,它都是相同的。它带有一个PHPWeb服务器、数据库以及许多其他方便的功能,使得运行单个Laravel安装变得非常容易,这对于项目中的每个开发人员都是一致的,不论项目的依赖项或开发人员的工作环境如何。
为什么我不使用Sail?它使用Docker来完成上述任务,而macOS上的Docker速度刚好足够慢,我更喜欢Valet。但如果你是Laravel的新手,特别是如果你不使用Mac,Sail就是最简单的开始构建Laravel应用程序的方式。
如果你是macOS用户(也有非官方的Windows和Linux版本),LaravelValet可以轻松地为你的每一个本地Laravel应用程序(以及大多数其他静态和基于PHP的应用程序)提供服务在不同的本地域名上。
你需要使用Homebrew安装一些工具,文档将引导你完成这些步骤,但从初始安装到提供服务你的应用程序,步骤非常少。
Valet可以轻松为Laravel应用提供服务;我们可以使用valetpark将给定文件夹中的所有子文件夹作为{foldername}.test提供服务,使用valetlink只服务一个单独的文件夹,使用valetopen打开浏览器显示Valet服务的域名,使用valetsecure以HTTPS方式提供Valet网站,使用valetshare打开一个ngrok或Expose隧道,这样你可以与他人共享你的站点。
Herd是一个原生的macOS应用程序,它将Valet及其所有依赖项捆绑在一个单独的安装程序中。虽然Herd不像ValetCLI那样可定制,但它省去了使用Homebrew、Docker或任何其他依赖管理器的必要,并且允许你通过一个漂亮的图形界面与Valet的核心功能进行交互。
Homestead是另一个你可能想用来设置本地开发环境的工具。它是一个配置工具,基于Vagrant(一个管理虚拟机的工具),提供了一个预配置的虚拟机镜像,完美地设置了Laravel开发环境,并且反映了许多Laravel网站运行的最常见生产环境。
创建新的Laravel项目有两种方式,都可以通过命令行运行。第一种是全局安装Laravel安装工具(使用Composer);第二种是使用Composer的create-project功能。
如果你已经全局安装了Composer,安装Laravel安装工具就像运行以下命令一样简单:
composerglobalrequire"laravel/installer"一旦安装了Laravel安装工具,启动一个新的Laravel项目就很简单。只需从命令行运行此命令:
laravelnewprojectName这将在当前目录下创建一个名为*{projectName}*的新子目录,并在其中安装一个裸的Laravel项目。
Composer还提供了一个称为create-project的功能,用于使用特定骨架创建新项目。要使用此工具创建新的Laravel项目,请发出以下命令:
composercreate-projectlaravel/laravelprojectName就像安装工具一样,这将在当前目录下创建一个名为*{projectName}*的子目录,其中包含一个Laravel安装的骨架,准备好供您开发。
如果你计划使用LaravelSail工作,可以同时安装Laravel应用程序并开始其Sail安装过程。确保你的计算机上已安装了Docker,然后使用以下命令,将*example-app*替换为你的应用程序名称:
安装过程完成后,切换到新目录并启动Sail:
当你打开一个包含骨架Laravel应用程序的目录时,你会看到以下文件和目录:
app/bootstrap/config/database/public/resources/routes/storage/tests/vendor/.editorconfig.env.env.example.gitattributes.gitignoreartisancomposer.jsoncomposer.lockpackage.jsonphpunit.xmlreadme.mdvite.config.js让我们一一详细介绍它们,以便熟悉。
根目录默认包含以下文件夹:
app
实际应用程序的大部分内容将存放在这里。模型、控制器、命令和PHP领域代码都在这里。
bootstrap
包含Laravel框架每次运行时使用的文件。
config
所有配置文件所在位置。
database
数据库迁移、种子和工厂所在位置。
public
当服务器为网站提供服务时指向的目录。这包含index.php,它是启动引导过程并适当路由所有请求的前端控制器。也是任何公开文件(如图像、样式表、脚本或下载文件)的位置。
资源
其他脚本所需文件的位置。视图,以及(可选)源CSS和源JavaScript文件存放在这里。
路由
所有路由定义的位置,包括HTTP路由和“控制台路由”或Artisan命令。
storage
缓存、日志和编译的系统文件存放位置。
测试
单元测试和集成测试的位置。
vendor
Composer安装其依赖项的位置。它被Git忽略(标记为从版本控制系统中排除),因为Composer预期在任何远程服务器上作为部署过程的一部分运行。
根目录还包含以下文件:
.editorconfig
给你的IDE/文本编辑器关于Laravel编码标准的指令(例如缩进大小、字符集以及是否修剪尾随空白)。
.env和.env.example
指定环境变量(在每个环境中预期不同的变量,因此不提交到版本控制)。.env.example是一个模板,每个环境都应复制它以创建自己的*.env*文件,该文件被Git忽略。
.gitignore和.gitattributes
Git配置文件。
artisan
允许您从命令行运行Artisan命令(参见第八章)。
composer.json和composer.lock
Composer的配置文件;composer.json可由用户编辑,composer.lock则不可。这些文件共享一些关于项目的基本信息,并定义其PHP依赖项。
package.json
类似于composer.json,但用于前端资产和构建系统的依赖项;它指示NPM拉取哪些基于JavaScript的依赖项。
phpunit.xml
PHPUnit的配置文件,Laravel默认使用该工具进行测试。
readme.md
一份关于Laravel的基本介绍的Markdown文件。如果使用Laravel安装程序,您将看不到此文件。
vite.config.js
(可选的)Vite的配置文件。此文件指示您的构建系统如何编译和处理前端资产。
您的Laravel应用程序的核心设置——数据库连接设置、队列和邮件设置等——存储在config文件夹中的文件中。这些文件中的每一个都返回一个PHP数组,并且数组中的每个值可以通过由文件名和所有后代键组成的配置键来访问,这些键由点(.)分隔。
因此,如果您在config/services.php创建以下内容的文件:
任何应该对每个环境都不同的配置变量(因此不应提交到源代码控制)将存储在你的.env文件中。假设你想为每个环境使用不同的BugsnagAPI密钥。你可以设置配置文件从.env中获取它:
#In.envBUGSNAG_API_KEY=oinfp9813410942#In.env.exampleBUGSNAG_API_KEY=你的.env文件已经包含了框架需要的许多环境特定变量,比如你将使用的邮件驱动程序以及基本的数据库设置。
Laravel中的某些功能,包括一些缓存和优化功能,如果你在配置文件之外的任何地方使用env()调用,则这些功能将不可用。
最佳方法是引入环境变量是设置为任何你想要的环境特定项目的配置项。让这些配置项读取环境变量,然后在你的应用程序的任何地方引用配置变量即可。
//config/services.phpreturn['bugsnag'=>['key'=>env('BUGSNAG_API_KEY'),],];//Incontroller,orwhatever$bugsnag=newBugsnag(config('services.bugsnag.key'));.env文件让我们快速查看一下.env文件的默认内容。确切的键名会根据你使用的Laravel版本而有所不同,但可以查看示例2-1来了解它们的样子。
APP_KEY
一个随机生成的字符串,用于加密数据。如果这个值为空,你可能会遇到“未指定应用加密密钥”的错误。在这种情况下,只需运行phpartisankey:generate,Laravel将为你生成一个密钥。
APP_DEBUG
一个布尔值,确定你的应用实例的用户是否应该看到调试错误——适用于本地和暂存环境,但对生产环境不适用。
其余的非认证设置(BROADCAST_DRIVER、QUEUE_CONNECTION等)都给定了默认值,尽可能少依赖外部服务,这对刚开始使用时非常合适。
当你启动你的第一个Laravel应用时,大多数项目你可能唯一想要更改的是数据库配置设置。我使用LaravelValet,所以我将DB_DATABASE更改为我的项目名称,将DB_USERNAME更改为root,将DB_PASSWORD更改为空字符串:
DB_DATABASE=myProjectDB_USERNAME=rootDB_PASSWORD=然后,我在我最喜欢的MySQL客户端中创建一个与我的项目同名的数据库,然后就可以开始了。
现在,你已经使用裸的Laravel安装运行起来了。运行gitinit,使用gitadd.和gitcommit提交裸文件,然后你就可以开始编码了。就是这样!如果你使用Valet,你可以运行以下命令,立即在浏览器中看到你的站点上线:
laravelnewmyProjectcdmyProjectgitinitgitadd.gitcommit-m"Initialcommit"我将所有的站点都放在~/Sites文件夹中,这是我设置为主要Valet目录的地方,所以在这种情况下,我可以立即在浏览器中访问myProject.test,而无需额外工作。我可以编辑*.env*并将其指向特定的数据库,在我的MySQL应用程序中添加该数据库,然后就可以开始编码了。
此后的每一章,“测试”部分都会展示如何为涵盖的功能编写测试。由于本章不涉及可测试的功能,让我们快速讨论一下测试。(要了解更多关于在Laravel中编写和运行测试的内容,请转到第十二章。)
Laravel默认带有PHPUnit作为依赖项,并配置为在tests目录中任何以Test.php结尾的文件中运行测试(例如tests/UserTest.php)。
所以,编写测试的最简单方法是在名为tests的目录中创建一个以Test.php结尾的文件。而运行它们的最简单方法是从命令行(在项目根目录中)运行./vendor/bin/phpunit。
此外,一些测试部分将使用测试语法和功能,如果您是第一次阅读本书,可能会对其中的代码感到困惑。如果测试部分的代码令人困惑,只需跳过它,并在阅读测试章节后再回头查看。
由于Laravel是一个PHP框架,因此在本地运行它非常简单。Laravel还提供了三种工具来管理您的本地开发环境:Sail,一个Docker设置;Valet,一个更简单的基于macOS的工具;以及Homestead,一个预配置的Vagrant设置。Laravel依赖于Composer,并且默认情况下带有一系列反映其约定和与其他开源工具关系的文件和文件夹。
任何Web应用程序框架的基本功能是接收用户请求并传递响应,通常通过HTTP(S)完成。这意味着定义应用程序路由是学习Web框架时首先要解决的最重要的项目;没有路由,您几乎无法与最终用户进行交互。
在本章中,我们将探讨Laravel中的路由;您将看到如何定义它们,如何将它们指向应执行的代码,并如何使用Laravel的路由工具处理各种各样的路由需求。
我们在本章中讨论的大部分内容涉及如何组织Model–View–Controller(MVC)应用程序的结构,我们将查看许多示例使用类REST的路由名称和动词,因此让我们快速看一下两者。
在MVC中,您有三个主要概念:
模型
表示一个单独的数据库表(或来自该表的记录)—想象“公司”或“狗”。
视图
控制器
类似于交通警察,从浏览器接收HTTP请求,从数据库和其他存储机制获取正确的数据,验证用户输入,并最终向用户发送响应。
在图3-1中,您可以看到,最终用户将首先通过其浏览器发送HTTP请求与控制器进行交互。控制器响应该请求后,可能会向模型(数据库)写入数据和/或从模型中拉取数据。然后,控制器很可能会向视图发送数据,然后将视图返回给最终用户在其浏览器中显示。
我们将涵盖一些不符合这种相对简单的应用架构方式的Laravel用例,所以不要陷入MVC,但这将至少让您准备好在我们讨论视图和控制器时接近本章的其余部分。
最常见的HTTP动词是GET和POST,其次是PUT和DELETE。还有HEAD,OPTIONS和PATCH,以及两个在正常网页开发中几乎不使用的其他动词,TRACE和CONNECT。
这里是一个快速概述:
GET
请求资源(或资源列表)。
HEAD
请求GET响应的仅包含头信息的版本。
POST
创建资源。
PUT
覆盖资源。
PATCH
修改资源。
DELETE
删除资源。
OPTIONS
询问服务器此URL允许哪些动词。
Table3-1显示了资源控制器上可用的操作(更多详情见“资源控制器”)。每个动作期望你使用特定的URL模式和特定的动词调用,因此你可以了解每个动词的用途。
表3-1.Laravel资源控制器的方法
我们将在“REST-LikeJSONAPIs基础”中详细讨论REST,但简要介绍一下,它是一种构建API的架构风格。在本书中讨论REST时,主要指一些特征,例如:
这是更复杂的内容,但通常情况下,本书中使用的“RESTful”将意味着“基于这些基于URL结构的模式,因此我们可以像GET/tasks/14/edit这样进行可预测的调用”。这很重要(即使不构建API)因为Laravel的路由结构是基于类似REST的结构,正如你可以在Table3-1中看到的。
基于REST的API主要遵循相同的结构,除了它们没有create路由或edit路由,因为API只表示动作,而不是为动作准备页面。
定义路由的最简单方法是将路径(例如/)与闭包匹配,如Example3-1中所示。
//routes/web.phpRoute::get('/',function(){return'Hello,World!';});现在你已经定义了如果有人访问/(你域名的根),Laravel的路由器应该运行在那里定义的闭包并返回结果。注意我们是return我们的内容,而不是echo或print它。
你可能会想:“为什么我返回‘Hello,World!’而不是回显它?”
许多简单的网站完全可以在web路由文件中定义。通过一些简单的GET路由结合一些模板,如示例3-2所示,您可以轻松地提供经典网站服务。
Route::get('/',function(){returnview('welcome');});Route::get('about',function(){returnview('about');});Route::get('products',function(){returnview('products');});Route::get('services',function(){returnview('services');});静态调用如果您有PHP开发经验,您可能会惊讶地看到在Route类上进行静态调用。这实际上不是静态方法本身,而是使用Laravel的门面进行的服务定位,我们将在第11章中介绍。
如果您喜欢避免使用门面,您可以通过以下方式完成相同的定义:
$router->get('/',function(){return'Hello,World!';});路由动词你可能已经注意到,我们在路由定义中一直在使用Route::get()。这意味着我们告诉Laravel只有当HTTP请求使用GET动作时才匹配这些路由。但是如果是表单的POST,或者可能是一些JavaScript发送的PUT或DELETE请求呢?在路由定义中调用的方法还有几个其他选项,如示例3-3所示。
另一个常见选项是在闭包的位置以字符串形式传递控制器名称和方法,如示例3-4所示。
useApp\Http\Controllers\WelcomeController;Route::get('/',[WelcomeController::class,'index']);这告诉Laravel将请求传递给该路径的index()方法,该方法位于App\Http\Controllers\WelcomeController控制器中。此方法将接收相同的参数并像您可能替代放置在其中的闭包一样对待它。
如果你定义的路由有参数——URL结构中的可变段落——那么在路由中定义它们并传递给闭包非常简单(见示例3-5)。
Route::get('users/{id}/friends',function($id){//});您还可以通过在参数名后面加上问号()使路由参数变为可选,如示例3-6所示。在这种情况下,您还应为路由的对应变量提供默认值。
Route::get('users/{id}',function($id='fallbackId'){//});你还可以使用正则表达式(regexes)来定义一个路由只有在参数满足特定要求时才匹配,就像在示例3-7中一样。
Route::get('users/{id}',function($id){//})->where('id','[0-9]+');Route::get('users/{username}',function($username){//})->where('username','[A-Za-z]+');Route::get('posts/{id}/{slug}',function($id,$slug){//})->where(['id'=>'[0-9]+','slug'=>'[A-Za-z]+']);正如你可能猜到的那样,如果访问的路径匹配了路由字符串但正则表达式不匹配参数,它将不会被匹配。由于路由从上到下匹配,users/abc会跳过示例3-7的第一个闭包,但它将会被第二个闭包匹配,因此会被路由到那里。另一方面,posts/abc/123不会匹配任何闭包,因此会返回404(未找到)错误。
Laravel还提供了方便的方法来匹配常见的正则表达式模式,正如你在示例3-8中看到的那样。
Route::get('users/{id}/friends/{friendname}',function($id,$friendname){//})->whereNumber('id')->whereAlpha('friendname');Route::get('users/{name}',function($name){//})->whereAlphaNumeric('name');Route::get('users/{id}',function($id){//})->whereUuid('id');Route::get('users/{id}',function($id){//})->whereUlid('id');Route::get('friends/types/{type}',function($type){//})->whereIn('type',['acquaintance','bestie','frenemy']);路由名称在应用程序中最简单的引用这些路由的方式只是使用它们的路径。如果需要的话,在视图中有一个url()全局辅助函数来简化链接;例如,查看示例3-9。这个辅助函数会在你的路由前加上你的站点的完整域名。
在我们的例子中,我们将这个路由命名为members.show;*resourcePlural*.*action*是Laravel中用于路由和视图名称的常见约定。
一般来说,我建议使用路由名称而不是路径来引用你的路由,因此建议使用route()辅助函数而不是url()辅助函数。有时会显得有些笨拙,比如当你使用多个子域名时,但它提供了极大的灵活性,以便稍后更改应用程序的路由结构而不会受到重大惩罚。
通常,一组路由共享特定的特征—特定的身份验证要求、路径前缀或者控制器命名空间。在每个路由上重复定义这些共享特征不仅显得乏味,而且还可能使路由文件的结构混乱,并且模糊了应用程序的一些结构。
路由组允许您通过将多个路由分组在一起并一次性应用任何共享的配置设置来减少这种重复。此外,路由组对未来开发人员(以及您自己)是视觉线索,表明这些路由是一组的。
要将两个或多个路由分组在一起,您可以通过路由组定义周围的路由定义,如Example3-11中所示。实际上,您正在将一个闭包传递给组定义,并在该闭包中定义分组的路由。
Route::group(function(){Route::get('hello',function(){return'Hello';});Route::get('world',function(){return'World';});});默认情况下,路由组实际上并不执行任何操作。在Example3-11中使用组与在路由中使用代码注释分离的效果没有区别。
路由组最常见的用途之一是对一组路由应用中间件。您将在Chapter10中学习更多关于中间件的内容,但它们主要用于Laravel中对用户进行身份验证和限制访客用户访问站点某些部分。
Route::middleware('auth')->group(function(){Route::get('dashboard',function(){returnview('dashboard');});Route::get('account',function(){returnview('account');});});通常,将中间件附加到控制器中的路由比在路由定义时更清晰、更直接。您可以通过在控制器的构造函数中调用middleware()方法来实现这一点。传递给middleware()方法的字符串是中间件的名称,您还可以可选地链式调用修饰方法(only()和except())来定义哪些方法将接收该中间件:
classDashboardControllerextendsController{publicfunction__construct(){$this->middleware('auth');$this->middleware('admin-auth')->only('editUsers');$this->middleware('team-member')->except('editUsers');}}注意,如果您经常进行“only”和“except”自定义,这通常是需要为异常路由新建一个控制器的标志。
如果一组路由共享其路径的一部分—例如,如果您的站点的仪表板以/dashboard为前缀—您可以使用路由组来简化此结构(参见Example3-13)。
Route::prefix('dashboard')->group(function(){Route::get('/',function(){//Handlesthepath/dashboard});Route::get('users',function(){//Handlesthepath/dashboard/users});});注意,每个带前缀的组还有一个表示前缀根的/路由—在Example3-13中即为/dashboard。
子域路由与路由前缀相同,但其范围限定为子域而不是路由前缀。这有两个主要用途。首先,您可能希望为应用程序的不同部分(或完全不同的应用程序)提供不同的子域。示例3-14展示了如何实现这一点。
Route::domain('api.myapp.com')->group(function(){Route::get('/',function(){//});});其次,您可能希望将子域的一部分设置为参数,如示例3-15所示。这在多租户情况下最常见(想想Slack或Harvest,每个公司都有自己的子域,如tighten.slack.co)。
Route::domain('{account}.myapp.com')->group(function(){Route::get('/',function($account){//});Route::get('users/{id}',function($account,$id){//});});注意,组的任何参数都作为第一个参数传递给组内路由的方法。
路由名称通常会反映路径元素的继承链,因此users/comments/5将由名为users.comments.show的路由提供服务。在这种情况下,通常在所有属于users.comments资源下的路由周围使用路由组。
就像我们可以为URL段添加前缀一样,我们也可以为路由名称添加前缀字符串。使用路由组名称前缀,我们可以定义该组内的每个路由名称都应该以给定的字符串前缀"users."开头,然后是"comments."(参见示例3-16)。
Route::name('users.')->prefix('users')->group(function(){Route::name('comments.')->prefix('comments')->group(function(){Route::get('{id}',function(){//...})->name('show');//Routenamed'users.comments.show'Route::destroy('{id}',function(){})->name('destroy');});});路由组控制器当您对由同一控制器提供服务的路由进行分组时,例如我们显示、编辑和删除用户时,可以使用路由组的controller()方法,如示例3-17所示,避免为每个路由定义完整的元组。
useApp\Http\Controllers\UserController;Route::controller(UserController::class)->group(function(){Route::get('/','index');Route::get('{id}','show');});回退路由在Laravel中,您可以定义一个“回退路由”(需要在路由文件末尾定义),以捕获所有未匹配的请求:
Route::fallback(function(){//});签名路由许多应用程序定期发送关于一次性操作的通知(如重置密码、接受邀请等),并提供简单的链接执行这些操作。让我们想象发送一封电子邮件,确认收件人愿意加入邮件列表。
有三种方式发送该链接:
实现最后一种选项的一种简单方法是使用称为signedURLs的功能,它使得为发送验证链接的人员构建签名身份验证系统变得简单。这些链接由正常路由链接组成,附加一个“签名”,证明自链接发送以来未更改该URL(因此没有人修改了URL以访问他人的信息)。
要构建一个签名URL来访问给定路由,该路由必须有一个名称:
现在您已经生成了到您的签名路由的链接,您需要保护免受任何未签名的访问。最简单的选择是应用signed中间件:
Route::get('invitations/{invitation}/{answer}',InvitationController::class)->name('invitations')->middleware('signed');如果您愿意,您可以手动验证使用Request对象上的hasValidSignature()方法,而不是使用signed中间件:
classInvitationController{publicfunction__invoke(Invitation$invitation,$answer,Request$request){if(!$request->hasValidSignature()){abort(403);}//}}视图在我们之前查看的一些路由闭包中,我们看到类似returnview('account')的代码。这里发生了什么?
在MVC模式中(见图3-1),视图(或模板)是描述特定输出应该如何看起来的文件。您可能有输出JSON、XML或电子邮件的视图,但在Web框架中,最常见的视图输出是HTML。
在Laravel中,您可以使用两种视图格式:纯PHP和Blade模板(参见第四章)。区别在于文件名:about.php将使用PHP引擎呈现,而about.blade.php将使用Blade引擎呈现。
一旦您使用view()辅助函数“加载”视图,您可以选择简单地返回它(如示例3-18),如果视图不依赖于控制器中的任何变量,这将运行良好。
Route::get('/',function(){returnview('home');});此代码查找resources/views/home.blade.php或resources/views/home.php中的视图,并加载其内容并解析任何内联PHP或控制结构,直到只剩下视图的输出。一旦返回它,它将传递给响应堆栈的其余部分,并最终返回给用户。
但是如果需要传入变量怎么办?看一下示例3-19。
Route::get('tasks',function(){returnview('tasks.index')->with('tasks',Task::all());});此闭包加载resources/views/tasks/index.blade.php或resources/views/tasks/index.php视图,并传递一个名为tasks的单一变量,其中包含Task::all()方法的结果。Task::all()是您将在第五章学习的Eloquent数据库查询。
因为路由只返回视图而不传递自定义数据非常常见,所以Laravel允许您将路由定义为“视图”路由,甚至不传递闭包或控制器/方法引用,正如您可以在示例3-20中看到的那样。
可以与每个模板或仅某些模板共享某些变量,如以下代码所示:
view()->share('variableName','variableValue');想了解更多,请参阅“视图组合器和服务注入”。
或许把应用程序所有的逻辑都塞进控制器中是很诱人的,但是将控制器视为路由HTTP请求在应用程序中导航的交通警察会更好。由于请求可以通过其他方式进入应用程序——如定时任务、Artisan命令行调用、队列作业等——因此不要依赖控制器处理太多行为是明智的。这意味着控制器的主要工作是捕获HTTP请求的意图并将其传递给应用程序的其他部分。
所以,让我们创建一个控制器。一个简单的方法是使用Artisan命令,在命令行中运行以下命令:
phpartisanmake:controllerTaskControllerArtisan和Artisan生成器Laravel捆绑了一个称为Artisan的命令行工具。Artisan可用于运行迁移,手动创建用户和其他数据库记录,并执行许多其他一次性手动任务。
在make命名空间下,Artisan提供了生成各种系统文件骨架文件的工具。这就是我们能够运行phpartisanmake:controller的原因。
要了解更多关于这个和其他Artisan功能的信息,请参阅第八章。
这将在app/Http/Controllers目录下创建一个名为TaskController.php的新文件,并显示示例3-21中的内容。
控制器方法最常见的用法之一将是像示例3-24那样,它提供了与我们在示例3-19中路由闭包相同的功能。
//TaskController.php...publicfunctionindex(){returnview('tasks.index')->with('tasks',Task::all());}这个控制器方法加载resources/views/tasks/index.blade.php或resources/views/tasks/index.php视图,并传递一个名为tasks的单个变量,其中包含Task::all()Eloquent方法的结果。
如果你想要创建一个资源控制器,并为所有基本的资源路由(如create()和update())生成自动生成的方法,你可以在使用phpartisanmake:controller时传递--resource标志:
phpartisanmake:controllerTaskController--resource获取用户输入控制器方法中执行的第二个最常见操作是从用户获取输入并对其进行操作。这引入了一些新概念,所以让我们看一些示例代码,并逐步了解新的内容。
首先,让我们绑定我们的路由;参见示例3-25。
//routes/web.phpRoute::get('tasks/create',[TaskController::class,'create']);Route::post('tasks',[TaskController::class,'store']);注意我们正在绑定tasks/create的GET动作(显示创建新任务的表单)和tasks的POST动作(我们创建新任务时将POST到的地方)。我们可以假设我们控制器中的create()方法只是显示一个表单,所以让我们看看示例3-26中的store()方法。
//TaskController.php...publicfunctionstore(){Task::create(request()->only(['title','description']));returnredirect('tasks');}这个示例使用了Eloquent模型和redirect()功能,稍后我们会详细讲解它们,但现在让我们快速看看我们如何在这里获取我们的数据。
我们使用request()助手来表示HTTP请求(稍后详细介绍),并使用它的only()方法来提取用户提交的title和description字段。
然后,我们将这些数据传递给我们的Task模型的create()方法,该方法使用传入的标题设置一个新的Task实例,并将传入的描述设置为description。最后,我们重定向回显示所有任务的页面。
这里有几层抽象在起作用,我们稍后会详细介绍,但需要知道来自only()方法的数据来自于Request对象上所有常用方法使用的同一组数据池,包括all()和get()。每个方法提取的数据集代表了所有用户提供的数据,无论是来自查询参数还是POST值。所以,我们的用户在“添加任务”页面上填写了两个字段:“标题”和“描述”。
简单解释一下,request()->only()接受一个输入名称的关联数组并返回它们:
request()->only(['title','description']);//returns:['title'=>'Whatevertitletheusertypedonthepreviouspage','description'=>'Whateverdescriptiontheusertypedonthepreviouspage',]而Task::create()接受一个关联数组并根据此数组创建一个新任务:
Task::create(['title'=>'Buymilk','description'=>'Remembertochecktheexpirationdatethistime,Norbert!',]);结合它们一起,只需用户提供的“标题”和“描述”字段就可以创建一个任务。
Laravel的外观和全局助手为Laravel代码库中最有用的类提供了一个简单的接口。你可以获取关于当前请求和用户输入、会话、缓存等的信息。
但是如果你更喜欢注入你的依赖项,或者想使用一个没有外观或助手的服务,你需要找到一些方法将这些类的实例引入到你的控制器中。
这是我们第一次接触Laravel的服务容器。如果现在这个概念还不熟悉,可以把它想象成是一点点Laravel的魔法;或者,如果你想更深入地了解它的实际运作方式,可以跳到第十一章。
所有控制器方法(包括构造函数)都是通过Laravel的容器解析出来的,这意味着任何你在容器中能够解析的类型提示将被自动注入。
PHP中的类型提示意味着在方法签名中的变量前放置类或接口的名称:
publicfunction__construct(Logger$logger){}这个类型提示告诉PHP,传递到方法中的任何东西必须是Logger类型,这可以是接口或类。
作为一个好的示例,如果你更喜欢使用Request对象的实例而不是使用全局助手,只需在你的方法参数中进行类型提示Illuminate\Http\Request,就像在示例3-27中一样。
//TaskController.php...publicfunctionstore(\Illuminate\Http\Request$request){Task::create($request->only(['title','description']));returnredirect('tasks');}所以,你定义了一个必须传递到store()方法中的参数。由于你进行了类型提示,并且由于Laravel知道如何解析该类名,你将在方法中直接得到Request对象,而不需要额外工作。没有显式绑定,没有其他任何东西——它只是作为$request变量存在。
而且,通过比较示例3-26和3-27,可以看出request()助手和Request对象的行为完全一样。
有时候,在编写控制器时,为控制器中的方法命名可能是最困难的部分。幸运的是,Laravel对传统的REST/CRUD控制器(在Laravel中称为资源控制器)有一些约定,此外,它还提供了一个默认生成器和一个便捷的路由定义,允许你一次性绑定整个资源控制器。
要查看Laravel期望资源控制器的方法,请从命令行生成一个新的控制器:
phpartisanmake:controllerMySampleResourceController--resource现在打开app/Http/Controllers/MySampleResourceController.php。你会看到它已经预先填充了许多方法。让我们逐一看看每个方法代表什么。我们将以一个Task为例。
还记得前面的表格吗?Table3-1显示了每个默认生成的方法的HTTP动词、URL、控制器方法名以及名称。
所以,我们已经看到这些是在Laravel中使用的传统路由名称,也看到可以轻松生成一个带有每个默认路由方法的资源控制器。幸运的是,如果你不想手动为每个控制器方法生成路由,也不必如此。有一个名为资源控制器绑定的技巧,看看Example3-28。
//routes/web.phpRoute::resource('tasks',TaskController::class);这将自动将资源中列出的所有路由绑定到指定控制器上相应的方法名。它还会适当地命名这些路由;例如,tasks资源控制器上的index()方法将被命名为tasks.index。
如果你发现自己想知道当前应用程序有哪些可用路由,请使用工具来解决这个问题:从命令行运行phpartisanroute:list,你将得到所有可用路由的列表。我更喜欢phpartisanroute:list--exclude-vendor,这样我就不会看到所有我依赖项注册的怪异路由(参见图3-2)。
当你使用RESTfulAPI时,资源的潜在操作列表与HTML资源控制器不同。例如,你可以向API发送POST请求来创建资源,但在API中你不能真正“显示创建表单”。
要生成一个API资源控制器,它的结构与资源控制器相同,但不包括create和edit操作,请在创建控制器时传递--api标志:
phpartisanmake:controllerMySampleResourceController--api要绑定一个API资源控制器,请使用apiResource()方法,而不是resource()方法,如Example3-29所示。
//routes/web.phpRoute::apiResource('tasks',TaskController::class);单一操作控制器在你的应用程序中会有时候需要一个控制器只服务一个路由。你可能会想知道如何为该路由命名控制器方法。幸运的是,你可以将单个路由指向单个控制器,而无需担心命名该方法。
正如你可能已经知道的那样,__invoke()方法是PHP的一个魔术方法,允许你“调用”类的实例,像调用函数一样调用它。这是Laravel的单操作控制器使用的工具,允许你将路由指向单个控制器,正如你可以在示例3-30中看到的那样。
//\App\Http\Controllers\UpdateUserAvatar.phppublicfunction__invoke(User$user){//Updatetheuser'savatarimage}//routes/web.phpRoute::post('users/{user}/update-avatar',UpdateUserAvatar::class);路由模型绑定最常见的路由模式之一是任何控制器方法的第一行尝试查找具有给定ID的资源,例如示例3-31。
Route::get('conferences/{id}',function($id){$conference=Conference::findOrFail($id);});Laravel提供了一个简化此模式的功能称为路由模型绑定。这允许你定义一个特定的参数名称(例如{conference}),表示路由解析器应该查找具有该ID的Eloquent数据库记录,然后将其作为参数传递给闭包或控制器方法而不是仅仅传递ID。
有两种类型的路由模型绑定:隐式和自定义(或显式)。
使用路由模型绑定的最简单方法是将路由参数命名为该模型独有的名称(例如,将其命名为$conference而不是$id),然后在闭包/控制器方法中对该参数进行类型提示并在那里使用相同的变量名。展示起来比描述容易,因此请查看示例3-32。
Route::get('conferences/{conference}',function(Conference$conference){returnview('conferences.show')->with('conference',$conference);});因为路由参数({conference})与方法参数($conference)相同,并且方法参数是用Conference模型进行类型提示的(Conference$conference),Laravel将其视为路由模型绑定。每次访问此路由时,应用程序将假定传入URL中的任何内容代替{conference}都是一个ID,应该用于查找Conference,然后将生成的模型实例传递给你的闭包或控制器方法。
每当通过URL段查找Eloquent模型(通常是因为路由模型绑定),Eloquent将使用其主键(ID)进行查找。
要更改你的Eloquent模型在所有路由中用于URL查找的列,请在模型中添加一个名为getRouteKeyName()的方法:
publicfunctiongetRouteKeyName(){return'slug';}现在,像conferences/{conference}这样的URL将期望从slug列获取条目而不是ID,并且将根据其进行查找。
在Laravel中,你还可以通过在路由定义中追加冒号和列名来在特定路由上更改路由键:
useApp\Models\Conference;useApp\Models\Organizer;Route::get('organizers/{organizer}/conferences/{conference:slug}',function(Organizer$organizer,Conference$conference){return$conference;});自定义路由模型绑定要手动配置路由模型绑定,请在App\Providers\RouteServiceProvider的boot()方法中添加类似Example3-33的行。
publicfunctionboot(){//PerformthebindingRoute::model('event',Conference::class);}现在你指定了当路由定义中有一个名为{event}的参数时(例如Example3-34中演示的),路由解析器将返回一个带有该URL参数ID的Conference类的实例。
要缓存你的路由文件,你需要使用所有控制器、重定向、视图和资源路由(不使用路由闭包)。如果你的应用程序不使用任何路由闭包,你可以运行phpartisanroute:cache,Laravel将序列化你的routes/*文件的结果。如果想删除缓存,请运行phpartisanroute:clear。
这里的问题是:Laravel现在将匹配路由与缓存文件而不是实际的routes/*文件。你可以对路由文件进行无限更改,但在再次运行route:cache之前,这些更改不会生效。这意味着每次更改都需要重新缓存,这可能会带来很多混乱的潜力。
我建议的替代方案是:由于Git默认会忽略路由缓存文件,因此考虑仅在生产服务器上使用路由缓存,并在每次部署新代码时运行phpartisanroute:cache命令(无论是通过Git的后处理挂钩、Forge部署命令还是其他部署系统的一部分)。这样,你不会在本地开发时遇到混乱的问题,但远程环境仍然可以从路由缓存中受益。
有时你需要手动定义表单应发送的HTTP动词。HTML表单只允许GET或POST,所以如果你想使用其他动词,就需要自行指定。
正如我们已经看到的,你可以使用Route::get()、Route::post()、Route::any()或Route::match()来定义路由匹配的动词。你还可以使用Route::patch()、Route::put()和Route::delete()进行匹配。
但是如何通过Web浏览器发送除了GET之外的请求呢?首先,HTML表单中的method属性决定了它的HTTP动词:如果你的表单的method是"GET",它将通过查询参数和GET方法提交;如果表单的method是"POST",它将通过postbody和POST方法提交。
JavaScript框架使得发送其他请求变得容易,比如DELETE和PATCH。但是如果你发现自己需要在Laravel中提交除了GET或POST之外的动词的HTML表单,你需要使用表单方法伪造(formmethodspoofing),这意味着在HTML表单中伪造HTTP方法。
要告诉Laravel当前提交的表单应被视为非POST方法的请求,需添加一个名为_method的隐藏变量,并赋值为"PUT","PATCH"或"DELETE",Laravel将会根据该动词匹配和路由该表单提交。
示例3-35中的表单,因为它传递了方法"DELETE"给Laravel,将匹配使用Route::delete()定义的路由,但不会匹配使用Route::post()定义的路由。
在Laravel中,默认情况下,除了“只读”路由(使用GET、HEAD或OPTIONS)之外的所有路由都受到跨站请求伪造(CSRF)攻击的保护,要求在每个请求中传递一个名为_token的输入。此token在每个会话开始时生成,并且每个非“只读”路由会将提交的_token与会话token进行比较。
避免CSRF攻击的最佳方法是默认保护所有入站路由——POST、DELETE等——通过一个token,Laravel已经内置支持。
你有两种方法可以解决CSRF错误。第一种,也是首选的方法,是在每个提交中添加_token输入。在HTML表单中,有一种简单的方法,如示例3-36中所示。
//InjQuery:$.ajaxSetup({headers:{'X-CSRF-TOKEN':$('meta[name="csrf-token"]').attr('content')}});//WithAxios:itautomaticallyretrievesitfromacookie.Nothingtodo!Laravel将在每个请求中检查X-CSRF-TOKEN(以及Axios和其他JavaScript框架如Angular使用的X-XSRF-TOKEN),并且传递有效的令牌将标记CSRF保护为满足状态。
到目前为止,我们明确讨论过从控制器方法或路由定义中返回的仅仅是视图。但是还有一些其他结构可以返回,以便向浏览器提供行为指令。
首先,让我们来讨论重定向。你在其他示例中已经看到了一些这样的示例。生成重定向有两种常见的方法;我们将在这里使用redirect()全局辅助方法,但你可能更喜欢门面。两者都会创建一个Illuminate\Http\RedirectResponse实例,对其执行一些便捷方法,然后返回它。你也可以手动执行这些操作,但你将需要做更多的工作。看看示例3-38,你可以看到几种返回重定向的方法。
//UsingtheglobalhelpertogeneratearedirectresponseRoute::get('redirect-with-helper',function(){returnredirect()->to('login');});//UsingtheglobalhelpershortcutRoute::get('redirect-with-helper-shortcut',function(){returnredirect('login');});//UsingthefacadetogeneratearedirectresponseRoute::get('redirect-with-facade',function(){returnRedirect::to('login');});//UsingtheRoute::redirectshortcutRoute::redirect('redirect-by-route','login');请注意,redirect()辅助方法暴露了与Redirect门面相同的方法,但它还有一个快捷方式;如果你直接将参数传递给辅助方法,而不是在其后链接方法,那么这是to()重定向方法的快捷方式。
还要注意,Route::redirect()路由辅助方法的(可选的)第三个参数可以是重定向的状态码(例如,302)。
用于重定向的to()方法的方法签名如下:
Route::get('redirect',function(){returnredirect()->to('home');//Orsame,usingtheshortcut:returnredirect('home');});redirect()->route()route()方法与to()方法相同,但不是指向特定路径,而是指向特定路由名称(参见示例3-40)。
Route::get('redirect',function(){returnredirect()->route('conferences.index');});请注意,由于某些路由名称需要参数,其参数顺序有点不同。route()方法有一个可选的第二个参数用于路由参数:
functionroute($to=null,$parameters=[],$status=302,$headers=[])因此,使用它可能看起来有点像示例3-41。
Route::get('redirect',function(){returnto_route('conferences.show',['conference'=>99,];});使用to_route()辅助方法你可以使用to_route()辅助方法作为redirect()->route()方法的别名。它们的签名都是一样的:
Route::get('redirect',function(){returnto_route('conferences.show',['conference'=>99]);});redirect()->back()由于Laravel会话实现的一些内置便利性,您的应用程序始终知道用户之前访问的页面是什么。这就开启了redirect()->back()重定向的机会,它简单地将用户重定向到他们来自的页面。这也有一个全局快捷方式:back()。
重定向服务提供了其他一些较少使用但仍可用的方法:
refresh()
重定向到用户当前正在访问的同一页面。
away()
允许重定向到外部URL,而不进行默认URL验证。
secure()
像to()一样,secure参数设置为"true"。
action()
允许您以两种方式之一链接到控制器和方法:作为字符串(redirect()->action('MyController@myMethod'))或作为元组(redirect()\->action([MyController::class,'myMethod']))。
guest()
intended()
也在认证系统内部使用;成功认证后,它会获取guest()方法存储的“预期”URL,并将用户重定向到那里。
虽然它的结构与您在redirect()上调用的其他方法类似,但with()不同之处在于它不定义您要重定向到哪里,而是定义您要传递的数据。当您将用户重定向到不同页面时,通常希望将某些数据传递给他们。您可以手动将数据闪存到会话中,但Laravel提供了一些便利方法来帮助您完成这些操作。
最常见的是,您可以使用with()传递键和值的数组或单个键和值,就像示例3-42中的一样。这会将您的with()数据保存到会话中,只用于下一次页面加载。
Route::get('redirect-with-key-value',function(){returnredirect('dashboard')->with('error',true);});Route::get('redirect-with-array',function(){returnredirect('dashboard')->with(['error'=>true,'message'=>'Whoops!']);});在重定向上链接方法与许多其他门面一样,Redirect门面的大多数调用可以接受流畅的方法链,就像示例3-42中的with()调用一样。您将在“什么是流畅接口?”中了解更多信息。
您还可以使用withInput(),如示例3-43中所示,以闪存用户的表单输入重定向;这在验证错误的情况下最常见,您希望将用户发送回他们刚刚来自的表单。
Route::get('form',function(){returnview('form');});Route::post('form',function(){returnredirect('form')->withInput()->with(['error'=>true,'message'=>'Whoops!']);});获取通过withInput()传递的闪存输入的最简单方法是使用old()辅助函数,它可以用于获取所有旧输入(old())或只是特定键的值,如下例所示,如果没有旧值,则第二个参数作为默认值。你通常会在视图中看到这一点,这使得这段HTML可以在此表单的“创建”和“编辑”视图中通用:
Route::post('form',function(Illuminate\Http\Request$request){$validator=Validator::make($request->all(),$this->validationRules);if($validator->fails()){returnback()->withErrors($validator)->withInput();}});withErrors()会自动与它重定向到的页面的视图共享一个$errors变量,以便你可以按照自己的意愿处理。
不喜欢示例3-44的外观?有一个简单而强大的工具,可以帮助你轻松清理代码。在“请求对象上的validate()”中详细了解更多。
除了返回视图和重定向之外,退出路由的最常见方式是中止。有几种全局可用的方法(abort()、abort_if()和abort_unless()),可以选择使用HTTP状态码、消息和头数组作为参数。
如示例3-45所示,abort_if()和abort_unless()接受一个首要参数,该参数根据其真实性进行评估,并根据结果执行中止。
Route::post('something-you-cant-do',function(Illuminate\Http\Request$request){abort(403,'Youcannotdothat!');abort_unless($request->has('magicToken'),403);abort_if($request->user()->isBanned,403);});自定义响应对于我们返回的几种其他选项,让我们先了解一下最常见的视图、重定向和中止响应之后的响应。与重定向一样,你可以在response()辅助函数或Response外观上运行这些方法。
如果你想手动创建HTTP响应,只需将你的数据传递给response()->make()的第一个参数:例如returnresponse()->make(*Hello,World!*)。再次提醒,第二个参数是HTTP状态代码,第三个是你的头部。
要手动创建JSON编码的HTTP响应,请将你的可JSON化内容(数组、集合或其他内容)传递给json()方法:例如returnresponse()->json(User::all())。它与make()类似,只是json_encode了你的内容并设置了适当的头。
要发送文件供最终用户下载,请将download()传递给SplFileInfo实例或字符串文件名,第二个可选参数是下载文件名:例如,returnresponse()->download('file501751.pdf','myFile.pdf'),这将发送名为file501751.pdf的文件,并在发送时重命名为myFile.pdf。
要在浏览器中显示相同的文件(如果是PDF或浏览器可以处理的图像或其他内容),请改用response()->file(),它接受与response->download()相同的参数。
如果您希望将外部服务的某些内容作为下载可用,而无需直接将其写入服务器磁盘,则可以使用response()->streamDownload()来流式下载。该方法期望的参数包括一个回调函数(回显一个字符串)、一个文件名,以及可选的头部数组;参见示例3-46。
returnresponse()->streamDownload(function(){echoDocumentService::file('myFile')->getContent();},'myFile.pdf');测试在其他一些社区中,单元测试控制器方法的想法很常见,但在Laravel(以及大多数PHP社区)中,通常依赖应用程序测试来测试路由的功能。
例如,要验证POST路由是否正常工作,我们可以编写类似于示例3-47的测试。
//tests/Feature/AssignmentTest.phppublicfunctiontest_post_creates_new_assignment(){$this->post('/assignments',['title'=>'Mygreatassignment',]);$this->assertDatabaseHas('assignments',['title'=>'Mygreatassignment',]);}我们直接调用了控制器方法吗?没有。但我们确保了该路由的目标——接收POST并将其重要信息保存到数据库中——得到了实现。
您还可以使用类似的语法访问一个路由,并验证页面上是否显示了某些文本,或者点击某些按钮是否执行了某些操作(参见示例3-48)。
//AssignmentTest.phppublicfunctiontest_list_page_shows_all_assignments(){$assignment=Assignment::create(['title'=>'Mygreatassignment',]);$this->get('/assignments')->assertSee('Mygreatassignment');}简而言之Laravel的路由定义在routes/web.php和routes/api.php中。您可以为每个路由定义预期的路径,哪些段是静态的,哪些是参数,哪些HTTP动词可以访问路由,以及如何解析它。您还可以将中间件附加到路由上,对它们进行分组,并为它们命名。
路由闭包或控制器方法返回的内容决定了Laravel如何响应用户。如果是字符串或视图,它会呈现给用户;如果是其他类型的数据,它会转换为JSON并呈现给用户;如果是重定向,它会强制进行重定向。