Laravel 启动指南(一)
原文:
zh.annas-archive.org/md5/d0c72cd35a2ef551cf4f36bed0d4e4e2译者:飞龙
前言
我如何开始使用 Laravel 的故事很普遍:我写了多年的 PHP,但我已经准备放弃,追求 Rails 和其他现代 Web 框架的力量。Rails 特别是有一个充满活力的社区,一个完美的结合了倾向性默认和灵活性,以及 Ruby-Gems 的力量来利用预打包的常用代码。
有些东西阻止我跳船,当我发现 Laravel 时我为此感到高兴。它提供了我在 Rails 中被吸引的一切,但它不仅仅是 Rails 的克隆;这是一个创新的框架,具有令人难以置信的文档、一个友好的社区,并且明显受到许多语言和框架的影响。
从那天起,我能够通过博客、播客和在会议上演讲来分享我学习 Laravel 的旅程;我为工作和副业项目编写了数十个 Laravel 应用程序;我在网上和面对面见过成千上万的 Laravel 开发人员。我有很多开发工具,但当我坐在命令行前,输入laravel new *项目名称*时,我是最开心的。
这本书讲的是什么
这不是关于 Laravel 的第一本书,也不会是最后一本。我不打算让这本书覆盖每一行代码或每一个实现模式。我不希望这本书在新版本的 Laravel 发布时就过时。相反,它的主要目的是为开发人员提供一个高层次的概述和具体示例,以便他们在任何 Laravel 代码库中工作,并使用每一个 Laravel 的功能和子系统。与其复制文档,我想帮助你理解 Laravel 背后的基本概念。
Laravel 是一个功能强大且灵活的 PHP 框架。它有一个充满活力的社区和广泛的工具生态系统,因此它在吸引力和影响力上都在增长。这本书是为那些已经知道如何制作网站和应用程序的开发人员准备的,他们希望学习如何在 Laravel 中做到更好。
Laravel 的文档详尽而出色。如果你发现我没有深入讨论你喜欢的任何特定主题,我鼓励你访问在线文档,深入了解该特定主题。
我认为你会发现这本书在高层次介绍和具体应用之间有一个舒适的平衡,在最后,你应该能够从头开始在 Laravel 中编写一个完整的应用程序。而且,如果我做得好的话,你会对尝试感到兴奋。
这本书适合谁
本书假定读者具备基本的面向对象编程实践知识,了解 PHP(或至少 C 语系语言的一般语法),以及模型-视图-控制器(MVC)模式和模板化的基本概念。如果你以前没有制作过网站,可能会感到有些吃力。但只要你有一些编程经验,在阅读本书之前不需要对 Laravel 有任何了解——我们将覆盖你需要了解的一切,从最简单的“Hello, world!”开始。
Laravel 可以在任何操作系统上运行,但本书中有些 bash(shell)命令在 Linux/macOS 上运行起来最简单。Windows 用户可能会在使用这些命令和现代 PHP 开发时遇到一些困难,但如果按照获取 Homestead(Linux 虚拟机)运行的说明进行操作,你将能够在那里运行所有命令。
本书结构
本书的结构按照我设想的时间顺序进行排列:如果你正在使用 Laravel 构建你的第一个 Web 应用程序,早期章节涵盖了你开始所需的基本组件,而后续章节涵盖了较少基础或更深奥的特性。
本书的每个部分都可以单独阅读,但对于框架新手来说,我尝试将章节结构化,从头开始阅读到结尾是非常合理的。
如适用,每个章节都会以两个部分结束:“测试”和“TL;DR”。如果你不熟悉,“TL;DR”意思是“太长了,不想读”。这些最后的部分将展示如何为每个章节涵盖的特性编写测试,并提供所覆盖内容的高级概述。
本书是为 Laravel 10 编写的。
关于第三版
Laravel: Up & Running的第一版于 2016 年 12 月发布,涵盖了 Laravel 5.1 至 5.3 版本。第二版于 2019 年 4 月发布,增加了对 5.4 至 5.8 以及 Laravel Dusk 和 Horizon 的覆盖,并添加了第十八章关于社区资源和其他非核心 Laravel 包,这些在前 17 章中没有涵盖。第三版将本书更新至 Laravel 10,并添加了 Breeze、Jetstream、Fortify、Vite 等新内容。
本书中使用的约定
本书使用以下排版约定:
斜体
指示新术语、网址、电子邮件地址、文件名和文件扩展名。
固定宽度
用于程序列表,以及段落内引用程序元素,如变量或函数名、数据库、数据类型、环境变量、语句和关键字。
固定宽度粗体
显示用户应按字面意思键入的命令或其他文本。
固定宽度斜体
显示应由用户提供值或由上下文确定的代码文本。
{大括号中的斜体}
显示应由用户提供值或由上下文确定的文件名或文件路径。
小贴士
这个元素表示提示或建议。
注意
这个元素表示一般注意事项。
警告
这个元素表示警告或注意事项。
O’Reilly 在线学习
注意
超过 40 年来,O’Reilly Media一直为公司提供技术和商业培训、知识和见解,帮助它们取得成功。
我们独特的专家和创新者网络通过图书、文章和我们的在线学习平台分享他们的知识和专长。O’Reilly 的在线学习平台为您提供按需访问的实时培训课程、深入学习路径、交互式编码环境以及来自 O’Reilly 和其他 200 多家出版商的大量文本和视频。有关更多信息,请访问https://oreilly.com。
如何联系我们
请将关于本书的评论和问题发送至出版商:
-
O’Reilly Media, Inc.
-
1005 Gravenstein Highway North
-
CA 95472,Sebastopol
-
800-889-8969(美国或加拿大境内)
-
707-829-7019(国际或本地)
-
707-829-0104(传真)
我们为本书设有网页,列出勘误、示例和任何额外信息。您可以访问此页面:https://oreil.ly/laravel-up-and-running-3e。
有关我们的图书和课程的新闻和信息,请访问https://oreilly.com。
在 LinkedIn 上找到我们:https://linkedin.com/company/oreilly-media
在 Twitter 上关注我们:https://twitter.com/oreillymedia
观看我们的 YouTube 频道:https://www.youtube.com/oreillymedia
致谢
在这个项目中,我收到了许多人的支持,但我甚至不知道从哪里开始表达我的感激之情。
我的伴侣,Imani,在每一个胜利中都与我共庆,鼓励我,坐在我旁边,她打开笔记本电脑,狂热地打字,我们一起推动,以满足我们的截止日期。我的儿子 Malachi 和女儿 Mia 在整个过程中非常宽容和理解。自 Tighten 团队成立以来,我的整个团队一直给予我支持和鼓励。我的朋友 Trent 和 Tevin 努力创造了艺术和艺术家的空间,我很荣幸能成为他们小家庭的一部分。
我有一系列的研究助理:Wilbur Powery、Brittany Jones Dumas、Reeka Maharaj 和 Ana Lisboa。在我现在繁忙的生活中,没有他们的帮助,我不可能写出第二版和第三版。
Laravel 社区中有如此多的人值得感谢,以至于我甚至无法列举所有。因此,对于所有为此付出了大量爱、奉献、关心和精湛技艺的人,谢谢你们。感谢你们帮助这个社区成为一个令人难以置信的地方;感谢你们在育儿、离婚、疫情、抑郁等方面鼓励我的许多人。你们都是了不起的。
Taylor Otwell 为创建 Laravel 而受到感谢和荣誉——因此创造了如此多的工作岗位,帮助了众多开发者更加热爱我们的生活。他因专注于开发者的幸福感而受到赞赏,以及他为理解开发者并建立积极鼓舞的社区而付出的努力。但我也想感谢他成为一位友善、鼓舞和富有挑战性的朋友。Taylor,你真是一位了不起的领导者。
感谢所有我的技术审阅者!对于第一版:Keith Damiani, Michael Dyrynda, Adam Fairholm 和 Myles Hyson。对于第二版:Tate Peñaranda, Andy Swick, Mohamed Said 和 Samantha Geitz。对于第三版:Anthony Clark, Ben Holmen, Jake Bathman 和 Tony Messias。
当然,也感谢我的家人和朋友们,无论是直接还是间接地支持我度过这一过程——我的父母和兄弟姐妹,我在芝加哥、盖恩斯维尔、迪凯特和亚特兰大的社区,其他企业主和作家,其他会议演讲者,其他父母,以及我有幸遇见和交往的大量了不起的人类。
第一章:为什么选择 Laravel?
在动态网络的早期阶段,编写 Web 应用程序看起来与今天大不相同。当时的开发人员不仅负责编写我们应用程序独特的业务逻辑代码,还负责编写那些在各个站点中如此常见的组件——用户认证、输入验证、数据库访问、模板化等等。
如今,程序员们可以轻松访问数十种应用程序开发框架和数千个组件和库。程序员们常说,当你学会一个框架时,可能已经有三个更新(据称更好)的框架出现,试图取代它。
“因为它存在”可能是攀登山峰的有效理由,但选择使用特定框架——或者根本使用框架——有更好的理由。值得问的问题是,为什么要使用框架?更具体地说,为什么选择 Laravel?
为什么使用框架?
显然,使用 PHP 开发者可用的各个组件或包是有益的。通过包,其他人负责开发和维护一个有明确定义作用的隔离代码片段,理论上,这个人对这个单一组件的理解应该比你有时间去深入了解得更多。
像 Laravel、Symfony、Lumen 和 Slim 这样的框架,会将一系列第三方组件与自定义框架“粘合剂”(如配置文件、服务提供者、预定义的目录结构和应用程序引导)打包在一起。因此,使用框架的好处不仅在于有人已经为您做出了关于单独组件的决定,还包括这些组件如何组合在一起的决策。
“我只是自己构建它”
假设您开始一个新的 Web 应用程序,没有框架的帮助。您应该从哪里开始呢?嗯,它可能需要路由 HTTP 请求,所以现在您需要评估所有可用的 HTTP 请求和响应库并选择一个。然后,您将不得不选择一个路由器。哦,您可能还需要设置某种形式的路由配置文件。它应该使用什么语法?应该放在哪里?控制器呢?它们应该放在哪里,如何加载?嗯,您可能需要一个依赖注入容器来解析控制器及其依赖关系。但是选哪一个呢?
此外,如果您确实花时间回答所有这些问题并成功创建您的应用程序,那么对下一个开发者的影响是什么?当您有四个这样的基于定制框架的应用程序,或者十二个时,您需要记住每个应用程序中控制器的位置或路由语法是什么?
一致性与灵活性
框架通过提供慎重考虑的答案来解决“我们应该在这里使用哪个组件?”的问题,并确保所选组件能够很好地协同工作。此外,框架提供的约定减少了新项目的开发者需要理解的代码量——例如,如果你理解一个 Laravel 项目中的路由工作原理,那么你就理解了所有 Laravel 项目中它是如何工作的。
当有人建议为每个新项目定制框架时,他们实际上是在主张能够控制应用程序基础中包含和排除什么。这意味着最好的框架不仅会为你提供一个坚实的基础,还会让你有自由进行定制。正如我将在本书的其余部分中展示的那样,这是使 Laravel 如此特别的部分。
Web 和 PHP 框架的简史
能够回答“为什么选择 Laravel?”的一个重要部分是了解 Laravel 的历史——以及它之前的发展。在 Laravel 兴起之前,PHP 和其他 Web 开发领域有各种框架和其他运动。
Ruby on Rails
David Heinemeier Hansson 于 2004 年发布了 Ruby on Rails 的第一个版本,自那以后,很难找到一种 Web 应用框架不受 Rails 影响的情况。
Rails 推广了 MVC、RESTful JSON API、约定优于配置、ActiveRecord 等许多工具和惯例,对 Web 开发者处理他们的应用程序的方式产生了深远影响——特别是快速应用程序开发方面。
PHP 框架的激增
大多数开发者很清楚,Rails 和类似的 Web 应用框架是未来的趋势,包括那些明显模仿 Rails 的 PHP 框架迅速涌现。
CakePHP 是 2005 年的第一款,很快又有 Symfony、CodeIgniter、Zend Framework 和 Kohana(CodeIgniter 的一个分支)。Yii 在 2008 年出现,Aura 和 Slim 则在 2010 年。2011 年推出的 FuelPHP 和 Laravel 既不是 CodeIgniter 的分支,而是提出的替代方案。
这些框架中有些更像 Rails,专注于数据库对象关系映射(ORM)、MVC 结构和其他旨在快速开发的工具。另一些像 Symfony 和 Zend 则更专注于企业设计模式和电子商务。
CodeIgniter 的优劣
CakePHP 和 CodeIgniter 是最早公开承认从 Rails 获得灵感的两个早期 PHP 框架。CodeIgniter 迅速走红,并且到 2010 年,可以说是独立 PHP 框架中最受欢迎的。
CodeIgniter 简单易用,拥有出色的文档和强大的社区。但它的现代技术和模式使用进展缓慢;随着框架世界的发展和 PHP 工具的进步,CodeIgniter 在技术进步和开箱即用功能方面开始落后。与许多其他框架不同,CodeIgniter 由一家公司管理,它在适应 PHP 5.3 的新功能(如命名空间以及后来的 GitHub 和 Composer)方面进展缓慢。正是在 2010 年,Laravel 的创始人 Taylor Otwell 对 CodeIgniter 不满意到足以自己动手写框架。
Laravel 1、2 和 3
Laravel 1 的第一个 beta 版于 2011 年 6 月发布,完全从头开始编写。它具有自定义 ORM(Eloquent)、基于闭包的路由(受 Ruby Sinatra 启发)、用于扩展的模块系统以及表单、验证、认证等助手。
早期的 Laravel 开发进展迅速,Laravel 2 和 3 分别于 2011 年 11 月和 2012 年 2 月发布。它们引入了控制器、单元测试、命令行工具、控制反转(IoC)容器、Eloquent 关系和迁移。
Laravel 4
到了 Laravel 4,Taylor 从头重新编写了整个框架。此时的 Composer,PHP 现在普遍使用的包管理器,显示出成为行业标准的迹象,Taylor 看到了将框架重写为 Composer 分发和捆绑的组件集合的价值。
Taylor 开发了一组名为Illuminate的组件,并于 2013 年 5 月发布了全新结构的 Laravel 4。现在,Laravel 不再将大部分代码捆绑为下载包,而是通过 Composer 从 Symfony(另一个将其组件释放供他人使用的框架)和 Illuminate 组件中拉取大部分组件。
Laravel 4 还引入了队列、邮件组件、门面和数据库种子。因为 Laravel 现在依赖于 Symfony 组件,因此宣布 Laravel 将会(并非完全一样,但很快)效仿 Symfony 的六个月发布计划。
Laravel 5
Laravel 4.3 原定于 2014 年 11 月发布,但随着开发的进展,其变化的重要性变得明显,因此 Laravel 5 于 2015 年 2 月发布。
Laravel 5 采用了全新的目录结构,删除了表单和 HTML 助手,引入了契约接口,大量新的视图,Socialite 用于社交媒体认证,Elixir 用于资产编译,Scheduler 简化了 cron 任务,dotenv 简化了环境管理,还有全新的 REPL(read–evaluate–print loop)。从那时起,它在功能和成熟度上都有所增长,但没有像以前版本那样有重大变化。
Laravel 6
在 2019 年 9 月,引入了 Laravel 6,并带来了两个主要变化:首先,移除了 Laravel 提供的字符串和数组全局助手(改用 Facade);其次,转向语义化版本控制(SemVer)进行版本编号。这种变化的实际影响意味着,对于 5 之后的所有 Laravel 版本,无论是主要版本(6、7 等)还是次要版本(6.1、6.2 等),发布频率都大大增加了。
Laravel 在新 SemVer 世界(6+)的版本
从版本 6 开始,由于新的 SemVer 发布计划,Laravel 的发布不再像过去那样具有里程碑意义。因此,未来的发布将更多地关注时间流逝,而不是非常具体的全新大功能。
Laravel 有什么特别之处?
那么,是什么让 Laravel 与众不同?为什么在任何时候都值得拥有超过一个 PHP 框架?他们毕竟都使用 Symfony 的组件,不是吗?让我们稍微谈谈是什么让 Laravel 如此“tick”。
Laravel 的哲学
您只需阅读 Laravel 的市场材料和 README 就可以开始看到它的价值。Taylor 使用像“Illuminate”和“Spark”这样与光有关的词语。然后还有这些:“Artisans”。“优雅”。还有这些:“一股清新的空气”。“新的开始”。最后还有:“快速”。“光速”。
框架最强烈传达的两个价值观是提高开发速度和开发者的幸福感。Taylor 将“Artisan”语言描述为故意与更实用主义的价值观对立。你可以在他 2011 年在 StackExchange 上的提问中看到这种思维方式的起源,他说:“有时我花费了大量时间(几个小时)来苦恼地让代码‘看起来漂亮’”,仅仅是为了改善看代码本身的体验。他经常谈论简化和加快开发者将想法变成现实的过程的价值,摒弃创建优秀产品的不必要障碍。
Laravel 的核心是装备和使开发者能力。其目标是提供清晰、简单和优美的代码和功能,帮助开发者快速学习、启动、开发和编写简单、清晰且持久的代码。
Laravel 文档明确表达了面向开发者的概念。"开心的开发者编写最佳代码" 已经写入文档。"从下载到部署的开发者幸福" 曾是一段时间内的非官方口号。当然,任何工具或框架都会声称希望开发者开心。但将开发者幸福作为首要关注点,而不是次要,对 Laravel 的风格和决策进展产生了巨大影响。其他框架可能将架构纯净性作为首要目标,或者与企业开发团队的目标和价值兼容,但 Laravel 的主要关注点是为个体开发者服务。这并不意味着你不能在 Laravel 中编写架构纯净或企业级的应用程序,但这不会以损害代码库的可读性和理解性为代价。
Laravel 如何实现开发者的幸福感
仅仅说你想让开发者开心是一回事。实现它是另一回事,它要求你质疑框架中最有可能让开发者不开心的因素,以及最有可能让他们开心的因素。Laravel 试图通过多种方式使开发者的生活更轻松。
首先,Laravel 是一个快速应用开发框架。这意味着它专注于浅显易懂的学习曲线,并尽量减少从开始新应用到发布的步骤。Laravel 提供了构建 Web 应用程序中最常见任务的所有组件,从数据库交互到身份验证、队列、电子邮件到缓存,都由 Laravel 提供简化。但 Laravel 的组件不仅仅在其自身上很棒;它们在整个框架中提供了一致的 API 和可预测的结构。这意味着,当你在 Laravel 中尝试新事物时,你很可能会说,“...它就这样运行了。”
这并不仅限于框架本身。Laravel 提供了一个完整的工具生态系统,用于构建和发布应用程序。你有 Sail 和 Valet 和 Homestead 用于本地开发,Forge 用于服务器管理,Envoyer 和 Vapor 用于高级部署。还有一套附加包:Cashier 用于付款和订阅,Echo 用于 WebSockets,Scout 用于搜索,Sanctum 和 Passport 用于 API 认证,Dusk 用于前端测试,Socialite 用于社交登录,Horizon 用于监控队列,Nova 用于构建管理面板,以及 Spark 用于启动你的 SaaS。Laravel 努力减少开发者工作中的重复性工作,以便他们可以做一些独特的事情。
其次,Laravel 注重“约定优于配置”——这意味着如果你愿意使用 Laravel 的默认设置,你将比其他需要你声明所有设置的框架少做很多工作,即使你使用推荐的配置。使用 Laravel 构建的项目所需时间比大多数其他 PHP 框架构建的项目要少。
Laravel 也深入关注简洁性。如果你愿意,你可以在 Laravel 中使用依赖注入、模拟和数据映射器模式、仓储模式和命令查询职责分离等各种复杂的架构模式。但是,而其他框架可能建议在每个项目中使用这些工具和结构,Laravel 及其文档和社区更倾向于从最简单的实现开始——这里用一个全局函数,那里用一个外观模式,再在那里用 ActiveRecord。这使开发人员能够创建最简单的应用程序来满足他们的需求,而不会限制其在复杂环境中的有用性。
Laravel 与其他 PHP 框架不同的一个有趣之处在于,它的创作者和社区更多地受到 Ruby 和 Rails 以及函数式编程语言的影响和启发,而不是 Java。在现代 PHP 中,有一种强烈的潮流倾向于冗长和复杂,接纳 PHP 更类似于 Java 的方面。但是 Laravel 往往站在另一边,拥抱富有表现力、动态和简单的编码实践和语言特性。
Laravel 社区
如果这本书是你第一次接触 Laravel 社区,你有特别的期待。Laravel 的一个显著特点之一是其欢迎、教育氛围,这一点促成了它的成长和成功。从 Jeffrey Way 的Laracasts 视频教程到Laravel News,再到 Slack、IRC 和 Discord 频道,从 Twitter 的朋友到博客作者、播客到 Laracon 大会,Laravel 拥有一个充满活力和丰富多样的社区,其中既有从一开始就参与的人,也有刚刚开始的新人。“这并非偶然:
从 Laravel 的最初开始,我就有这样一个想法,即所有人都希望感觉自己是某个群体的一部分。想要归属并被其他志同道合的人接受,是人类的自然本能。因此,通过在 web 框架中注入个性并积极参与社区,这种感觉可以在社区中蔓延。
Taylor Otwell,产品与支持采访
Taylor 从 Laravel 早期就明白,一个成功的开源项目需要两样东西:良好的文档和一个友好的社区。而这两点现在已成为 Laravel 的标志性特征。
如何运作
直到现在,我在这里分享的一切都是完全抽象的。你可能会问,关于代码呢?让我们深入一个简单的应用程序(示例 1-1)来看看日常使用 Laravel 到底是怎样的体验。
示例 1-1. 在 routes/web.php 中的“Hello, World”
<?php
Route::get('/', function () {
return 'Hello, World!';
});
在 Laravel 应用中,你可以采取的最简单的操作就是定义一个路由并在访问该路由时返回结果。如果在你的机器上初始化一个全新的 Laravel 应用程序,在 示例 1-1 中定义路由,然后从 public 目录提供站点服务,你将拥有一个完全运作的“Hello, World”示例(见 图 1-1)。
图 1-1. 使用 Laravel 返回“Hello, World!”
它看起来非常类似于控制器,正如你在 示例 1-2 中所看到的(如果你想立即测试,请首先运行 php artisan make:controller WelcomeController 创建控制器)。
示例 1-2. 使用控制器打印“Hello, World”
// File: routes/web.php
<?php
use App\Http\Controllers\WelcomeController;
Route::get('/', [WelcomeController::class, 'index']);
// File: app/Http/Controllers/WelcomeController.php
<?php
namespace App\Http\Controllers;
class WelcomeController extends Controller
{
public function index()
{
return 'Hello, World!';
}
}
如果你将问候语存储在数据库中,它看起来也会非常相似(参见 示例 1-3)。
示例 1-3. 多问候语“Hello, World”并访问数据库
// File: routes/web.php
<?php
use App\Greeting;
Route::get('create-greeting', function () {
$greeting = new Greeting;
$greeting->body = 'Hello, World!';
$greeting->save();
});
Route::get('first-greeting', function () {
return Greeting::first()->body;
});
// File: app/Models/Greeting.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Greeting extends Model
{
use HasFactory;
}
// File: database/migrations/2023_03_12_192110_create_greetings_table.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('greetings', function (Blueprint $table) {
$table->id();
$table->string('body');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('greetings');
}
};
示例 1-3 可能会有些令人不知所措,如果是这样,请跳过。在后面的章节中你将了解到这里发生的一切,但你已经可以看到,仅仅几行代码就可以设置数据库迁移和模型,并提取记录。就是这么简单。
为什么选择 Laravel?
所以,为什么选择 Laravel?
因为 Laravel 能帮助你将想法变为现实,无需编写多余的代码,采用现代编码标准,支持活跃的社区,并拥有强大的工具生态系统。
还有因为你,亲爱的开发者,值得快乐。
第二章:设置 Laravel 开发环境
PHP 之所以成功的一部分原因是几乎找不到不能运行 PHP 的网络服务器。然而,现代 PHP 工具对于过去的要求更为严格。为了更好地为 Laravel 开发,最好保持代码在本地和远程服务器环境的一致性,幸运的是,Laravel 生态系统为此提供了一些工具。
系统要求
本章中涵盖的所有内容在 Windows 机器上都是可能的,但你需要几十页的定制说明和注意事项。我将这些说明和注意事项留给真正的 Windows 用户,因此本书的例子将专注于 Unix/Linux/macOS 开发者。
无论你选择通过在本地机器上安装 PHP 和其他工具来为你的网站提供服务,还是通过 Vagrant 或 Docker 在虚拟机中提供开发环境,或者依赖像 MAMP/WAMP/XAMPP 这样的工具,你的开发环境都需要安装以下所有内容才能为 Laravel 站点提供服务:
-
PHP >= 8.1
-
OpenSSL PHP 扩展
-
PDO PHP 扩展
-
Mbstring PHP 扩展
-
Tokenizer PHP 扩展
-
XML PHP 扩展
-
Ctype PHP 扩展
-
JSON PHP 扩展
-
BCMath PHP 扩展
Composer
无论你用什么机器开发,都需要全局安装Composer。Composer是现代大多数 PHP 开发的基础工具。它是 PHP 的依赖管理器,类似于 Node 的 NPM(Node Package Manager)或 Ruby 的 RubyGems。但与 NPM 一样,Composer 也是我们测试、本地脚本加载、安装脚本等许多基础工具的基础。安装 Laravel、更新 Laravel 和引入外部依赖都需要 Composer。
本地开发环境
对于许多项目,使用更简单的工具集来托管你的开发环境可能已经足够了。如果你的系统上已经安装了 MAMP 或 WAMP 或 XAMPP,那很可能能够运行 Laravel。
你也可以通过 PHP 的内置 Web 服务器来运行 Laravel。在你的 Laravel 站点根目录下运行 php -S localhost:8000 -t public,PHP 的内置 Web 服务器将在*http://localhost:8000/*上为你提供站点服务。
然而,如果你希望在你的开发环境中拥有更多的功能(每个项目的不同本地域名、像 MySQL 这样的依赖管理等),你将需要一个比 PHP 内置服务器更强大的工具。
Laravel 提供了五种本地开发工具:Artisan serve、Sail、Valet、Herd 和 Homestead。我们会简要介绍每一种。如果你不确定该使用哪一种,我个人推荐 Mac 用户使用 Valet,其他人使用 Sail。
Artisan Serve
如果你在设置 Laravel 应用程序后运行 php artisan serve,它将在 http://localhost:8000 上提供服务,就像我们之前使用 PHP 的内置 Web 服务器设置的那样。在这里你并没有得到其他免费的东西,因此它唯一有意义的好处是更容易记住。
Laravel Sail
Sail 是开始本地 Laravel 开发的最简单方法,无论你使用的是什么操作系统,它都是相同的。它带有一个 PHP Web 服务器、数据库以及许多其他方便的功能,使得运行单个 Laravel 安装变得非常容易,这对于项目中的每个开发人员都是一致的,不论项目的依赖项或开发人员的工作环境如何。
为什么我不使用 Sail?它使用 Docker 来完成上述任务,而 macOS 上的 Docker 速度刚好足够慢,我更喜欢 Valet。但如果你是 Laravel 的新手,特别是如果你不使用 Mac,Sail 就是最简单的开始构建 Laravel 应用程序的方式。
Laravel Valet
如果你是 macOS 用户(也有非官方的 Windows 和 Linux 版本),Laravel Valet 可以轻松地为你的每一个本地 Laravel 应用程序(以及大多数其他静态和基于 PHP 的应用程序)提供服务在不同的本地域名上。
你需要使用 Homebrew 安装一些工具,文档将引导你完成这些步骤,但从初始安装到提供服务你的应用程序,步骤非常少。
安装 Valet——请查阅 Valet 文档 获取最新的安装说明——并将其指向一个或多个存放站点的目录。我从我的 ~/Sites 目录运行了 valet park,这是我放置所有正在开发中的应用程序的地方。现在,你只需在目录名后加上 .test,就可以在浏览器中访问它了。
Valet 可以轻松为 Laravel 应用提供服务;我们可以使用 valet park 将给定文件夹中的所有子文件夹作为 {foldername}.test 提供服务,使用 valet link 只服务一个单独的文件夹,使用 valet open 打开浏览器显示 Valet 服务的域名,使用 valet secure 以 HTTPS 方式提供 Valet 网站,使用 valet share 打开一个 ngrok 或 Expose 隧道,这样你可以与他人共享你的站点。
Laravel Herd
Herd 是一个原生的 macOS 应用程序,它将 Valet 及其所有依赖项捆绑在一个单独的安装程序中。虽然 Herd 不像 Valet CLI 那样可定制,但它省去了使用 Homebrew、Docker 或任何其他依赖管理器的必要,并且允许你通过一个漂亮的图形界面与 Valet 的核心功能进行交互。
Laravel Homestead
Homestead 是另一个你可能想用来设置本地开发环境的工具。它是一个配置工具,基于 Vagrant(一个管理虚拟机的工具),提供了一个预配置的虚拟机镜像,完美地设置了 Laravel 开发环境,并且反映了许多 Laravel 网站运行的最常见生产环境。
Homestead 文档非常全面,并且始终保持更新,所以如果你想了解它的工作原理和设置方法,我建议你直接查阅这些文档。
创建一个新的 Laravel 项目
创建新的 Laravel 项目有两种方式,都可以通过命令行运行。第一种是全局安装 Laravel 安装工具(使用 Composer);第二种是使用 Composer 的create-project功能。
你可以在安装文档页面上详细了解这两种选项,但我建议使用 Laravel 安装工具。
使用 Laravel 安装工具安装 Laravel
如果你已经全局安装了 Composer,安装 Laravel 安装工具就像运行以下命令一样简单:
composer global require "laravel/installer"
一旦安装了 Laravel 安装工具,启动一个新的 Laravel 项目就很简单。只需从命令行运行此命令:
laravel new projectName
这将在当前目录下创建一个名为*{projectName}*的新子目录,并在其中安装一个裸的 Laravel 项目。
使用 Composer 的 create-project 功能安装 Laravel
Composer 还提供了一个称为create-project的功能,用于使用特定骨架创建新项目。要使用此工具创建新的 Laravel 项目,请发出以下命令:
composer create-project laravel/laravel projectName
就像安装工具一样,这将在当前目录下创建一个名为*{projectName}*的子目录,其中包含一个 Laravel 安装的骨架,准备好供您开发。
使用 Sail 安装 Laravel
如果你计划使用 Laravel Sail 工作,可以同时安装 Laravel 应用程序并开始其 Sail 安装过程。确保你的计算机上已安装了 Docker,然后使用以下命令,将*example-app*替换为你的应用程序名称:
curl -s "https://laravel.build/example-app" | bash
这将把 Laravel 安装到当前文件夹下的*example-app*文件夹中,然后开始 Sail 安装过程。
安装过程完成后,切换到新目录并启动 Sail:
cd example-app
./vendor/bin/sail up
注意
第一次运行sail up时,它会比其他安装过程花费更长时间,因为它需要构建初始 Docker 镜像。
Laravel 的目录结构
当你打开一个包含骨架 Laravel 应用程序的目录时,你会看到以下文件和目录:
app/
bootstrap/
config/
database/
public/
resources/
routes/
storage/
tests/
vendor/
.editorconfig
.env
.env.example
.gitattributes
.gitignore
artisan
composer.json
composer.lock
package.json
phpunit.xml
readme.md
vite.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 创建以下内容的文件:
// config/services.php
<?php
return [
'sparkpost' => [
'secret' => 'abcdefg',
],
];
您可以使用 config('services.sparkpost.secret') 访问该配置变量。
任何应该对每个环境都不同的配置变量(因此不应提交到源代码控制)将存储在你的 .env 文件中。假设你想为每个环境使用不同的 Bugsnag API 密钥。你可以设置配置文件从 .env 中获取它:
// config/services.php
<?php
return [
'bugsnag' => [
'api_key' => env('BUGSNAG_API_KEY'),
],
];
这个 env() 辅助函数从你的 .env 文件中获取相同键的值。所以现在,将该键添加到你的 .env(这个环境的设置)和 .env.example(所有环境的模板)文件中:
# In .env
BUGSNAG_API_KEY=oinfp9813410942
# In .env.example
BUGSNAG_API_KEY=
你的 .env 文件已经包含了框架需要的许多环境特定变量,比如你将使用的邮件驱动程序以及基本的数据库设置。
在配置文件之外使用 env()
Laravel 中的某些功能,包括一些缓存和优化功能,如果你在配置文件之外的任何地方使用 env() 调用,则这些功能将不可用。
最佳方法是引入环境变量是设置为任何你想要的环境特定项目的配置项。让这些配置项读取环境变量,然后在你的应用程序的任何地方引用配置变量即可。
// config/services.php
return [
'bugsnag' => [
'key' => env('BUGSNAG_API_KEY'),
],
];
// In controller, or whatever
$bugsnag = new Bugsnag(config('services.bugsnag.key'));
.env 文件
让我们快速查看一下 .env 文件的默认内容。确切的键名会根据你使用的 Laravel 版本而有所不同,但可以查看 示例 2-1 来了解它们的样子。
示例 2-1. Laravel 中的默认环境变量
APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
LOG_CHANNEL=stack
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=
BROADCAST_DRIVER=log
CACHE_DRIVER=file
FILESYSTEM_DISK=local
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120
MEMCACHED_HOST=127.0.0.1
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=smtp
MAIL_HOST=mailpit
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_HOST=
PUSHER_PORT=443
PUSHER_SCHEME=https
PUSHER_APP_CLUSTER=mt1
VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
VITE_PUSHER_HOST="${PUSHER_HOST}"
VITE_PUSHER_PORT="${PUSHER_PORT}"
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
我不会详细讨论所有这些,因为其中有相当多只是各种服务的认证信息组(Pusher、Redis、DB、Mail)。但是,以下是你应该知道的两个重要环境变量:
APP_KEY
一个随机生成的字符串,用于加密数据。如果这个值为空,你可能会遇到“未指定应用加密密钥”的错误。在这种情况下,只需运行 php artisan key:generate,Laravel 将为你生成一个密钥。
APP_DEBUG
一个布尔值,确定你的应用实例的用户是否应该看到调试错误——适用于本地和暂存环境,但对生产环境不适用。
其余的非认证设置(BROADCAST_DRIVER、QUEUE_CONNECTION 等)都给定了默认值,尽可能少依赖外部服务,这对刚开始使用时非常合适。
当你启动你的第一个 Laravel 应用时,大多数项目你可能唯一想要更改的是数据库配置设置。我使用 Laravel Valet,所以我将 DB_DATABASE 更改为我的项目名称,将 DB_USERNAME 更改为 root,将 DB_PASSWORD 更改为空字符串:
DB_DATABASE=myProject
DB_USERNAME=root
DB_PASSWORD=
然后,我在我最喜欢的 MySQL 客户端中创建一个与我的项目同名的数据库,然后就可以开始了。
运行与启动
现在,你已经使用裸的 Laravel 安装运行起来了。运行 git init,使用 git add . 和 git commit 提交裸文件,然后你就可以开始编码了。就是这样!如果你使用 Valet,你可以运行以下命令,立即在浏览器中看到你的站点上线:
laravel new myProject && cd myProject && valet open
每次我启动一个新项目时,我都会执行以下步骤:
laravel new myProject
cd myProject
git init
git add .
git commit -m "Initial commit"
我将所有的站点都放在~/Sites文件夹中,这是我设置为主要 Valet 目录的地方,所以在这种情况下,我可以立即在浏览器中访问myProject.test,而无需额外工作。我可以编辑*.env*并将其指向特定的数据库,在我的 MySQL 应用程序中添加该数据库,然后就可以开始编码了。
测试
此后的每一章,“测试”部分都会展示如何为涵盖的功能编写测试。由于本章不涉及可测试的功能,让我们快速讨论一下测试。 (要了解更多关于在 Laravel 中编写和运行测试的内容,请转到第十二章。)
Laravel 默认带有 PHPUnit 作为依赖项,并配置为在tests目录中任何以Test.php结尾的文件中运行测试(例如tests/UserTest.php)。
所以,编写测试的最简单方法是在名为tests的目录中创建一个以Test.php结尾的文件。而运行它们的最简单方法是从命令行(在项目根目录中)运行./vendor/bin/phpunit。
如果任何测试需要访问数据库,请确保从托管数据库的机器上运行您的测试—如果您在 Vagrant 中托管数据库,请确保通过ssh连接到您的 Vagrant 盒子来运行测试。同样,您可以在第十二章中了解更多相关内容。
此外,一些测试部分将使用测试语法和功能,如果您是第一次阅读本书,可能会对其中的代码感到困惑。如果测试部分的代码令人困惑,只需跳过它,并在阅读测试章节后再回头查看。
TL;DR
由于 Laravel 是一个 PHP 框架,因此在本地运行它非常简单。Laravel 还提供了三种工具来管理您的本地开发环境:Sail,一个 Docker 设置;Valet,一个更简单的基于 macOS 的工具;以及 Homestead,一个预配置的 Vagrant 设置。Laravel 依赖于 Composer,并且默认情况下带有一系列反映其约定和与其他开源工具关系的文件和文件夹。
第三章:路由和控制器
任何 Web 应用程序框架的基本功能是接收用户请求并传递响应,通常通过 HTTP(S)完成。这意味着定义应用程序路由是学习 Web 框架时首先要解决的最重要的项目;没有路由,您几乎无法与最终用户进行交互。
在本章中,我们将探讨 Laravel 中的路由;您将看到如何定义它们,如何将它们指向应执行的代码,并如何使用 Laravel 的路由工具处理各种各样的路由需求。
MVC、HTTP 动词和 REST 的快速介绍
我们在本章中讨论的大部分内容涉及如何组织 Model–View–Controller(MVC)应用程序的结构,我们将查看许多示例使用类 REST 的路由名称和动词,因此让我们快速看一下两者。
什么是 MVC?
在 MVC 中,您有三个主要概念:
模型
表示一个单独的数据库表(或来自该表的记录)—想象“公司”或“狗”。
视图
表示将数据输出到最终用户的模板—想象“带有给定 HTML、CSS 和 JavaScript 集的登录页面模板”。
控制器
类似于交通警察,从浏览器接收 HTTP 请求,从数据库和其他存储机制获取正确的数据,验证用户输入,并最终向用户发送响应。
在图 3-1 中,您可以看到,最终用户将首先通过其浏览器发送 HTTP 请求与控制器进行交互。控制器响应该请求后,可能会向模型(数据库)写入数据和/或从模型中拉取数据。然后,控制器很可能会向视图发送数据,然后将视图返回给最终用户在其浏览器中显示。
图 3-1. MVC 的基本示意图
我们将涵盖一些不符合这种相对简单的应用架构方式的 Laravel 用例,所以不要陷入 MVC,但这将至少让您准备好在我们讨论视图和控制器时接近本章的其余部分。
HTTP 动词
最常见的 HTTP 动词是GET和POST,其次是PUT和DELETE。还有HEAD,OPTIONS和PATCH,以及两个在正常网页开发中几乎不使用的其他动词,TRACE和CONNECT。
这里是一个快速概述:
GET
请求资源(或资源列表)。
HEAD
请求GET响应的仅包含头信息的版本。
POST
创建资源。
PUT
覆盖资源。
PATCH
修改资源。
DELETE
删除资源。
OPTIONS
询问服务器此 URL 允许哪些动词。
Table 3-1 显示了资源控制器上可用的操作(更多详情见 “资源控制器”)。每个动作期望你使用特定的 URL 模式和特定的动词调用,因此你可以了解每个动词的用途。
表 3-1. Laravel 资源控制器的方法
| 动词 | URL | 控制器方法 | 名称 | 描述 |
|---|---|---|---|---|
GET | tasks | index() | tasks.index | 显示所有任务 |
GET | tasks/create | create() | tasks.create | 显示创建任务表单 |
POST | tasks | store() | tasks.store | 接受创建任务表单的表单提交 |
GET | tasks/{task} | show() | tasks.show | 显示一个任务 |
GET | tasks/{task}/edit | edit() | tasks.edit | 编辑一个任务 |
PUT/PATCH | tasks/{task} | update() | tasks.update | 接受编辑任务表单的表单提交 |
DELETE | tasks/{task} | destroy() | tasks.destroy | 删除一个任务 |
什么是 REST?
我们将在 “REST-Like JSON APIs 基础” 中详细讨论 REST,但简要介绍一下,它是一种构建 API 的架构风格。在本书中讨论 REST 时,主要指一些特征,例如:
-
每次围绕一个主要资源结构化(例如,
tasks) -
由使用 HTTP 动词与可预测的 URL 结构进行交互组成(如 Table 3-1 中所见)
-
返回 JSON 数据并经常被请求为 JSON
这是更复杂的内容,但通常情况下,本书中使用的 “RESTful” 将意味着 “基于这些基于 URL 结构的模式,因此我们可以像 GET /tasks/14/edit 这样进行可预测的调用”。这很重要(即使不构建 API)因为 Laravel 的路由结构是基于类似 REST 的结构,正如你可以在 Table 3-1 中看到的。
基于 REST 的 API 主要遵循相同的结构,除了它们没有 create 路由或 edit 路由,因为 API 只表示动作,而不是为动作准备页面。
路由定义
在 Laravel 应用中,你将在 routes/web.php 中定义你的 web 路由,而在 routes/api.php 中定义 API 路由。Web 路由 是终端用户访问的路由;API 路由 是你的 API 的路由(如果有的话)。目前,我们主要关注 routes/web.php 中的路由。
定义路由的最简单方法是将路径(例如 /)与闭包匹配,如 Example 3-1 中所示。
Example 3-1. 基本路由定义
// routes/web.php
Route::get('/', function () {
return 'Hello, World!';
});
现在你已经定义了如果有人访问 /(你域名的根),Laravel 的路由器应该运行在那里定义的闭包并返回结果。注意我们是 return 我们的内容,而不是 echo 或 print 它。
中间件的快速介绍
你可能会想:“为什么我返回 ‘Hello, World!’ 而不是回显它?”
有很多答案,但最简单的答案是 Laravel 的请求和响应周期周围有很多包装,包括一种称为中间件的东西。当路由闭包或控制器方法完成时,现在还不是将输出发送到浏览器的时间;返回内容允许它继续通过响应堆栈和中间件流动,然后再返回给用户。
许多简单的网站完全可以在 web 路由文件中定义。通过一些简单的GET路由结合一些模板,如示例 3-2 所示,您可以轻松地提供经典网站服务。
示例 3-2. 示例网站
Route::get('/', function () {
return view('welcome');
});
Route::get('about', function () {
return view('about');
});
Route::get('products', function () {
return view('products');
});
Route::get('services', function () {
return view('services');
});
静态调用
如果您有 PHP 开发经验,您可能会惊讶地看到在Route类上进行静态调用。这实际上不是静态方法本身,而是使用 Laravel 的门面进行的服务定位,我们将在第 11 章中介绍。
如果您喜欢避免使用门面,您可以通过以下方式完成相同的定义:
$router->get('/', function () {
return 'Hello, World!';
});
路由动词
你可能已经注意到,我们在路由定义中一直在使用Route::get()。这意味着我们告诉 Laravel 只有当 HTTP 请求使用GET动作时才匹配这些路由。但是如果是表单的POST,或者可能是一些 JavaScript 发送的PUT或DELETE请求呢?在路由定义中调用的方法还有几个其他选项,如示例 3-3 所示。
示例 3-3. 路由动词
Route::get('/', function () {
return 'Hello, World!';
});
Route::post('/', function () {
// Handle someone sending a POST request to this route
});
Route::put('/', function () {
// Handle someone sending a PUT request to this route
});
Route::delete('/', function () {
// Handle someone sending a DELETE request to this route
});
Route::any('/', function () {
// Handle any verb request to this route
});
Route::match(['get', 'post'], '/', function () {
// Handle GET or POST requests to this route
});
路由处理
正如您可能已经猜到的那样,将闭包传递给路由定义并不是教它如何解析路由的唯一方法。闭包快速简单,但是随着应用程序的规模变大,将所有路由逻辑放在一个文件中变得越来越笨拙。此外,使用路由闭包的应用程序无法利用 Laravel 的路由缓存(稍后详述),这可以减少每个请求高达数百毫秒的响应时间。
另一个常见选项是在闭包的位置以字符串形式传递控制器名称和方法,如示例 3-4 所示。
示例 3-4. 调用控制器方法的路由
use App\Http\Controllers\WelcomeController;
Route::get('/', [WelcomeController::class, 'index']);
这告诉 Laravel 将请求传递给该路径的index()方法,该方法位于App\Http\Controllers\WelcomeController控制器中。此方法将接收相同的参数并像您可能替代放置在其中的闭包一样对待它。
路由参数
如果你定义的路由有参数——URL 结构中的可变段落——那么在路由中定义它们并传递给闭包非常简单(见示例 3-5)。
示例 3-5. 路由参数
Route::get('users/{id}/friends', function ($id) {
//
});
您还可以通过在参数名后面加上问号(?)使路由参数变为可选,如示例 3-6 所示。在这种情况下,您还应为路由的对应变量提供默认值。
示例 3-6. 可选路由参数
Route::get('users/{id?}', function ($id = 'fallbackId') {
//
});
你还可以使用正则表达式(regexes)来定义一个路由只有在参数满足特定要求时才匹配,就像在 示例 3-7 中一样。
示例 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 中看到的那样。
示例 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。这个辅助函数会在你的路由前加上你的站点的完整域名。
示例 3-9. url()辅助函数
<a href="<?php echo url('/'); ?>">
// Outputs <a href="http://myapp.com/">
然而,Laravel 也允许你为每个路由命名,这样你就可以在不显式引用 URL 的情况下引用它们。这很有帮助,因为这意味着你可以给复杂的路由起一个简单的昵称,同时通过名称链接它们意味着如果路径变化,你不必重写前端链接(参见 示例 3-10)。
示例 3-10. 定义路由名称
// Defining a route with name() in routes/web.php:
Route::get('members/{id}', [\App\Http\Controller\MemberController::class, 'show'])
->name('members.show');
// Linking the route in a view using the route() helper:
<a href="<?php echo route('members.show', ['id' => 14]); ?>">
这个例子展示了一些新概念。首先,我们使用流畅的路由定义通过在get()方法后链接name()方法来添加名称。这种方法允许我们为路由命名,为其提供一个简短的别名,以便在其他地方更容易引用。
在我们的例子中,我们将这个路由命名为members.show;*resourcePlural*.*action*是 Laravel 中用于路由和视图名称的常见约定。
本例还介绍了route()辅助函数。就像url()一样,它旨在在视图中使用,简化链接到命名路由。如果路由没有参数,你可以简单地传递路由名称(route('members.index')),会得到一个路由字符串("http://myapp.com/members")。如果有参数,就像我们在 示例 3-10 中所做的那样,将它们作为数组传递到第二个参数中。
一般来说,我建议使用路由名称而不是路径来引用你的路由,因此建议使用route()辅助函数而不是url()辅助函数。有时会显得有些笨拙,比如当你使用多个子域名时,但它提供了极大的灵活性,以便稍后更改应用程序的路由结构而不会受到重大惩罚。
路由组
通常,一组路由共享特定的特征—特定的身份验证要求、路径前缀或者控制器命名空间。在每个路由上重复定义这些共享特征不仅显得乏味,而且还可能使路由文件的结构混乱,并且模糊了应用程序的一些结构。
路由组 允许您通过将多个路由分组在一起并一次性应用任何共享的配置设置来减少这种重复。此外,路由组对未来开发人员(以及您自己)是视觉线索,表明这些路由是一组的。
要将两个或多个路由分组在一起,您可以通过路由组定义周围的路由定义,如 Example 3-11 中所示。实际上,您正在将一个闭包传递给组定义,并在该闭包中定义分组的路由。
Example 3-11. 定义一个路由组
Route::group(function () {
Route::get('hello', function () {
return 'Hello';
});
Route::get('world', function () {
return 'World';
});
});
默认情况下,路由组实际上并不执行任何操作。在 Example 3-11 中使用组与在路由中使用代码注释分离的效果没有区别。
中间件
路由组最常见的用途之一是对一组路由应用中间件。您将在 Chapter 10 中学习更多关于中间件的内容,但它们主要用于 Laravel 中对用户进行身份验证和限制访客用户访问站点某些部分。
在 Example 3-12 中,我们围绕 dashboard 和 account 视图创建了一个路由组,并将 auth 中间件应用于两者。在此示例中,这意味着用户必须登录应用程序才能查看仪表板或帐户页面。
Example 3-12. 限制一组路由仅供已登录用户使用
Route::middleware('auth')->group(function() {
Route::get('dashboard', function () {
return view('dashboard');
});
Route::get('account', function () {
return view('account');
});
});
通常,将中间件附加到控制器中的路由比在路由定义时更清晰、更直接。您可以通过在控制器的构造函数中调用 middleware() 方法来实现这一点。传递给 middleware() 方法的字符串是中间件的名称,您还可以可选地链式调用修饰方法 (only() 和 except()) 来定义哪些方法将接收该中间件:
class DashboardController extends Controller
{
public function __construct()
{
$this->middleware('auth');
$this->middleware('admin-auth')
->only('editUsers');
$this->middleware('team-member')
->except('editUsers');
}
}
注意,如果您经常进行“only”和“except”自定义,这通常是需要为异常路由新建一个控制器的标志。
路径前缀
如果一组路由共享其路径的一部分—例如,如果您的站点的仪表板以 /dashboard 为前缀—您可以使用路由组来简化此结构(参见 Example 3-13)。
Example 3-13. 为一组路由添加前缀
Route::prefix('dashboard')->group(function () {
Route::get('/', function () {
// Handles the path /dashboard
});
Route::get('users', function () {
// Handles the path /dashboard/users
});
});
注意,每个带前缀的组还有一个表示前缀根的 / 路由—在 Example 3-13 中即为 /dashboard。
子域路由
子域路由与路由前缀相同,但其范围限定为子域而不是路由前缀。这有两个主要用途。首先,您可能希望为应用程序的不同部分(或完全不同的应用程序)提供不同的子域。示例 3-14 展示了如何实现这一点。
示例 3-14. 子域路由
Route::domain('api.myapp.com')->group(function () {
Route::get('/', function () {
//
});
});
其次,您可能希望将子域的一部分设置为参数,如示例 3-15 所示。这在多租户情况下最常见(想想 Slack 或 Harvest,每个公司都有自己的子域,如tighten.slack.co)。
示例 3-15. 参数化子域路由
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)。
示例 3-16. 路由组名称前缀
Route::name('users.')->prefix('users')->group(function () {
Route::name('comments.')->prefix('comments')->group(function () {
Route::get('{id}', function () {
// ...
})->name('show'); // Route named 'users.comments.show'
Route::destroy('{id}', function () {})->name('destroy');
});
});
路由组控制器
当您对由同一控制器提供服务的路由进行分组时,例如我们显示、编辑和删除用户时,可以使用路由组的controller()方法,如示例 3-17 所示,避免为每个路由定义完整的元组。
示例 3-17. 路由组控制器
use App\Http\Controllers\UserController;
Route::controller(UserController::class)->group(function () {
Route::get('/', 'index');
Route::get('{id}', 'show');
});
回退路由
在 Laravel 中,您可以定义一个“回退路由”(需要在路由文件末尾定义),以捕获所有未匹配的请求:
Route::fallback(function () {
//
});
签名路由
许多应用程序定期发送关于一次性操作的通知(如重置密码、接受邀请等),并提供简单的链接执行这些操作。让我们想象发送一封电子邮件,确认收件人愿意加入邮件列表。
有三种方式发送该链接:
-
将该 URL 公开,并希望没有其他人发现批准 URL 或修改自己的批准 URL 以批准其他人。
-
将操作放在身份验证后,链接到操作,并要求用户在尚未登录的情况下登录(在这种情况下,许多邮件列表接收者可能不会是具有帐户的用户,因此可能不可能登录)。
-
“签名”链接,以唯一证明用户从您的电子邮件收到了链接,而无需登录——类似于*myapp.com/invitations…
实现最后一种选项的一种简单方法是使用称为 signed URLs 的功能,它使得为发送验证链接的人员构建签名身份验证系统变得简单。这些链接由正常路由链接组成,附加一个“签名”,证明自链接发送以来未更改该 URL(因此没有人修改了 URL 以访问他人的信息)。
签名路由
要构建一个签名 URL 来访问给定路由,该路由必须有一个名称:
Route::get('invitations/{invitation}/{answer}', InvitationController::class)
->name('invitations');
要生成到此路由的普通链接,您可以使用我们已经介绍的 route() 辅助函数,但您也可以使用 URL 外观执行相同的操作:URL::route('invitations', ['invitation' => 12345, 'answer' => 'yes'])。要生成带有 signed 的链接到此路由,只需使用 signedRoute() 方法。如果您想生成带有过期时间的签名路由,请使用 temporarySignedRoute():
// Generate a normal link
URL::route('invitations', ['invitation' => 12345, 'answer' => 'yes']);
// Generate a signed link
URL::signedRoute('invitations', ['invitation' => 12345, 'answer' => 'yes']);
// Generate an expiring (temporary) signed link
URL::temporarySignedRoute(
'invitations',
now()->addHours(4),
['invitation' => 12345, 'answer' => 'yes']
);
使用 now() 辅助函数
Laravel 提供了 now() 辅助函数,相当于 Carbon::now();它返回一个代表当前时间的 Carbon 对象。
Carbon 是 Laravel 自带的日期时间库。
修改路由以允许签名链接
现在您已经生成了到您的签名路由的链接,您需要保护免受任何未签名的访问。最简单的选择是应用 signed 中间件:
Route::get('invitations/{invitation}/{answer}', InvitationController::class)
->name('invitations')
->middleware('signed');
如果您愿意,您可以手动验证使用 Request 对象上的 hasValidSignature() 方法,而不是使用 signed 中间件:
class InvitationController
{
public function __invoke(Invitation $invitation, $answer, Request $request)
{
if (! $request->hasValidSignature()) {
abort(403);
}
//
}
}
视图
在我们之前查看的一些路由闭包中,我们看到类似 return view('account') 的代码。这里发生了什么?
在 MVC 模式中(见图 3-1),视图(或模板)是描述特定输出应该如何看起来的文件。您可能有输出 JSON、XML 或电子邮件的视图,但在 Web 框架中,最常见的视图输出是 HTML。
在 Laravel 中,您可以使用两种视图格式:纯 PHP 和 Blade 模板(参见第四章)。区别在于文件名:about.php 将使用 PHP 引擎呈现,而 about.blade.php 将使用 Blade 引擎呈现。
加载视图的三种方式
有三种方法可以返回视图。目前只需要关注 view(),但如果你看到 View::make(),它是一样的,或者你可以注入 Illuminate\View\ViewFactory 如果你更喜欢。
一旦您使用 view() 辅助函数“加载”视图,您可以选择简单地返回它(如示例 3-18),如果视图不依赖于控制器中的任何变量,这将运行良好。
示例 3-18. 简单的 view() 使用方法
Route::get('/', function () {
return view('home');
});
此代码查找 resources/views/home.blade.php 或 resources/views/home.php 中的视图,并加载其内容并解析任何内联 PHP 或控制结构,直到只剩下视图的输出。一旦返回它,它将传递给响应堆栈的其余部分,并最终返回给用户。
但是如果需要传入变量怎么办?看一下示例 3-19。
示例 3-19. 将变量传递给视图
Route::get('tasks', function () {
return view('tasks.index')
->with('tasks', Task::all());
});
此闭包加载 resources/views/tasks/index.blade.php 或 resources/views/tasks/index.php 视图,并传递一个名为 tasks 的单一变量,其中包含 Task::all() 方法的结果。 Task::all() 是您将在第五章学习的 Eloquent 数据库查询。
使用 Route::view() 直接返回简单路由
因为路由只返回视图而不传递自定义数据非常常见,所以 Laravel 允许您将路由定义为“视图”路由,甚至不传递闭包或控制器/方法引用,正如您可以在示例 3-20 中看到的那样。
示例 3-20. Route::view()
// Returns resources/views/welcome.blade.php
Route::view('/', 'welcome');
// Passing simple data to Route::view()
Route::view('/', 'welcome', ['User' => 'Michael']);
使用视图组合器与每个视图共享变量
有时反复传递相同变量可能会变得很麻烦。可能有一个您希望每个网站视图或某个视图类或某个包含的子视图都可以访问的变量,例如与任务相关的所有视图或页眉部分。
可以与每个模板或仅某些模板共享某些变量,如以下代码所示:
view()->share('variableName', 'variableValue');
想了解更多,请参阅“视图组合器和服务注入”。
控制器
我已经多次提到控制器,但直到现在,大多数示例都展示了路由闭包。在 MVC 模式中,控制器本质上是组织一个或多个路由逻辑的类,集中在一个地方。控制器倾向于将类似的路由组合在一起,特别是如果您的应用程序结构类似于传统的 CRUD 格式;在这种情况下,控制器可能处理与特定资源相关的所有操作。
什么是 CRUD?
CRUD 是 创建、读取、更新、删除 的缩写,这是 Web 应用程序对资源提供的四种主要操作。例如,您可以创建新的博客文章,可以阅读该文章,可以更新它,或者可以删除它。
或许把应用程序所有的逻辑都塞进控制器中是很诱人的,但是将控制器视为路由 HTTP 请求在应用程序中导航的交通警察会更好。由于请求可以通过其他方式进入应用程序——如定时任务、Artisan 命令行调用、队列作业等——因此不要依赖控制器处理太多行为是明智的。这意味着控制器的主要工作是捕获 HTTP 请求的意图并将其传递给应用程序的其他部分。
所以,让我们创建一个控制器。一个简单的方法是使用 Artisan 命令,在命令行中运行以下命令:
php artisan make:controller TaskController
Artisan 和 Artisan 生成器
Laravel 捆绑了一个称为 Artisan 的命令行工具。 Artisan 可用于运行迁移,手动创建用户和其他数据库记录,并执行许多其他一次性手动任务。
在make命名空间下,Artisan 提供了生成各种系统文件骨架文件的工具。这就是我们能够运行php artisan make:controller的原因。
要了解更多关于这个和其他 Artisan 功能的信息,请参阅第八章。
这将在app/Http/Controllers目录下创建一个名为TaskController.php的新文件,并显示示例 3-21 中的内容。
示例 3-21. 默认生成的控制器
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class TaskController extends Controller
{
//
}
修改这个文件,如示例 3-22 所示,创建一个名为index()的新公共方法。我们将在那里返回一些文本。
示例 3-22. 简单的控制器示例
<?php
namespace App\Http\Controllers;
class TaskController extends Controller
{
public function index()
{
return 'Hello, World!';
}
}
然后,就像我们之前学到的那样,我们将把一个路由链接到它,如示例 3-23 所示。
示例 3-23. 简单控制器的路由
// routes/web.php
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\TaskController;
Route::get('/', [TaskController::class, 'index']);
就这样。访问/路由,你将看到“Hello, World!”这几个字。
控制器方法最常见的用法之一将是像示例 3-24 那样,它提供了与我们在示例 3-19 中路由闭包相同的功能。
示例 3-24. 常见的控制器方法示例
// TaskController.php
...
public function index()
{
return view('tasks.index')
->with('tasks', Task::all());
}
这个控制器方法加载resources/views/tasks/index.blade.php或resources/views/tasks/index.php视图,并传递一个名为tasks的单个变量,其中包含Task::all() Eloquent 方法的结果。
生成资源控制器
如果你想要创建一个资源控制器,并为所有基本的资源路由(如create()和update())生成自动生成的方法,你可以在使用php artisan make:controller时传递--resource标志:
php artisan make:controller TaskController --resource
获取用户输入
控制器方法中执行的第二个最常见操作是从用户获取输入并对其进行操作。这引入了一些新概念,所以让我们看一些示例代码,并逐步了解新的内容。
首先,让我们绑定我们的路由;参见示例 3-25。
示例 3-25. 绑定基本表单操作
// routes/web.php
Route::get('tasks/create', [TaskController::class, 'create']);
Route::post('tasks', [TaskController::class, 'store']);
注意我们正在绑定tasks/create的GET动作(显示创建新任务的表单)和tasks的POST动作(我们创建新任务时将POST到的地方)。我们可以假设我们控制器中的create()方法只是显示一个表单,所以让我们看看示例 3-26 中的store()方法。
示例 3-26. 常见的表单输入控制器方法
// TaskController.php
...
public function store()
{
Task::create(request()->only(['title', 'description']));
return redirect('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' => 'Whatever title the user typed on the previous page',
'description' => 'Whatever description the user typed on the previous page',
]
而 Task::create() 接受一个关联数组并根据此数组创建一个新任务:
Task::create([
'title' => 'Buy milk',
'description' => 'Remember to check the expiration date this time, Norbert!',
]);
结合它们一起,只需用户提供的“标题”和“描述”字段就可以创建一个任务。
将依赖项注入到控制器中
Laravel 的外观和全局助手为 Laravel 代码库中最有用的类提供了一个简单的接口。你可以获取关于当前请求和用户输入、会话、缓存等的信息。
但是如果你更喜欢注入你的依赖项,或者想使用一个没有外观或助手的服务,你需要找到一些方法将这些类的实例引入到你的控制器中。
这是我们第一次接触 Laravel 的服务容器。如果现在这个概念还不熟悉,可以把它想象成是一点点 Laravel 的魔法;或者,如果你想更深入地了解它的实际运作方式,可以跳到第十一章。
所有控制器方法(包括构造函数)都是通过 Laravel 的容器解析出来的,这意味着任何你在容器中能够解析的类型提示将被自动注入。
PHP 中的类型提示
PHP 中的类型提示 意味着在方法签名中的变量前放置类或接口的名称:
public function __construct(Logger $logger) {}
这个类型提示告诉 PHP,传递到方法中的任何东西 必须 是 Logger 类型,这可以是接口或类。
作为一个好的示例,如果你更喜欢使用 Request 对象的实例而不是使用全局助手,只需在你的方法参数中进行类型提示 Illuminate\Http\Request,就像在示例 3-27 中一样。
示例 3-27. 通过类型提示进行控制器方法注入
// TaskController.php
...
public function store(\Illuminate\Http\Request $request)
{
Task::create($request->only(['title', 'description']));
return redirect('tasks');
}
所以,你定义了一个必须传递到 store() 方法中的参数。由于你进行了类型提示,并且由于 Laravel 知道如何解析该类名,你将在方法中直接得到 Request 对象,而不需要额外工作。没有显式绑定,没有其他任何东西——它只是作为 $request 变量存在。
而且,通过比较示例 3-26 和 3-27,可以看出 request() 助手和 Request 对象的行为完全一样。
资源控制器
有时候,在编写控制器时,为控制器中的方法命名可能是最困难的部分。幸运的是,Laravel 对传统的 REST/CRUD 控制器(在 Laravel 中称为 资源控制器)有一些约定,此外,它还提供了一个默认生成器和一个便捷的路由定义,允许你一次性绑定整个资源控制器。
要查看 Laravel 期望资源控制器的方法,请从命令行生成一个新的控制器:
php artisan make:controller MySampleResourceController --resource
现在打开 app/Http/Controllers/MySampleResourceController.php。你会看到它已经预先填充了许多方法。让我们逐一看看每个方法代表什么。我们将以一个Task为例。
Laravel 资源控制器的方法
还记得前面的表格吗?Table 3-1 显示了每个默认生成的方法的 HTTP 动词、URL、控制器方法名以及名称。
绑定资源控制器
所以,我们已经看到这些是在 Laravel 中使用的传统路由名称,也看到可以轻松生成一个带有每个默认路由方法的资源控制器。幸运的是,如果你不想手动为每个控制器方法生成路由,也不必如此。有一个名为 资源控制器绑定 的技巧,看看 Example 3-28。
示例 3-28. 资源控制器绑定
// routes/web.php
Route::resource('tasks', TaskController::class);
这将自动将资源中列出的所有路由绑定到指定控制器上相应的方法名。它还会适当地命名这些路由;例如,tasks资源控制器上的index()方法将被命名为tasks.index。
artisan route:list
如果你发现自己想知道当前应用程序有哪些可用路由,请使用工具来解决这个问题:从命令行运行 php artisan route:list,你将得到所有可用路由的列表。我更喜欢 php artisan route:list --exclude-vendor,这样我就不会看到所有我依赖项注册的怪异路由(参见图 3-2)。
图 3-2. artisan route:list
API 资源控制器
当你使用 RESTful API 时,资源的潜在操作列表与 HTML 资源控制器不同。例如,你可以向 API 发送 POST 请求来创建资源,但在 API 中你不能真正“显示创建表单”。
要生成一个 API 资源控制器,它的结构与资源控制器相同,但不包括 create 和 edit 操作,请在创建控制器时传递 --api 标志:
php artisan make:controller MySampleResourceController --api
要绑定一个 API 资源控制器,请使用 apiResource() 方法,而不是 resource() 方法,如 Example 3-29 所示。
示例 3-29. API 资源控制器绑定
// routes/web.php
Route::apiResource('tasks', TaskController::class);
单一操作控制器
在你的应用程序中会有时候需要一个控制器只服务一个路由。你可能会想知道如何为该路由命名控制器方法。幸运的是,你可以将单个路由指向单个控制器,而无需担心命名该方法。
正如你可能已经知道的那样,__invoke() 方法是 PHP 的一个魔术方法,允许你“调用”类的实例,像调用函数一样调用它。这是 Laravel 的 单操作控制器 使用的工具,允许你将路由指向单个控制器,正如你可以在 示例 3-30 中看到的那样。
示例 3-30. 使用 __invoke() 方法
// \App\Http\Controllers\UpdateUserAvatar.php
public function __invoke(User $user)
{
// Update the user's avatar image
}
// routes/web.php
Route::post('users/{user}/update-avatar', UpdateUserAvatar::class);
路由模型绑定
最常见的路由模式之一是任何控制器方法的第一行尝试查找具有给定 ID 的资源,例如 示例 3-31。
示例 3-31. 为每个路由获取资源
Route::get('conferences/{id}', function ($id) {
$conference = Conference::findOrFail($id);
});
Laravel 提供了一个简化此模式的功能称为 路由模型绑定。这允许你定义一个特定的参数名称(例如 {conference}),表示路由解析器应该查找具有该 ID 的 Eloquent 数据库记录,然后将其作为参数传递给闭包或控制器方法 而不是 仅仅传递 ID。
有两种类型的路由模型绑定:隐式和自定义(或显式)。
隐式路由模型绑定
使用路由模型绑定的最简单方法是将路由参数命名为该模型独有的名称(例如,将其命名为 $conference 而不是 $id),然后在闭包/控制器方法中对该参数进行类型提示并在那里使用相同的变量名。展示起来比描述容易,因此请查看 示例 3-32。
示例 3-32. 使用隐式路由模型绑定
Route::get('conferences/{conference}', function (Conference $conference) {
return view('conferences.show')->with('conference', $conference);
});
因为路由参数({conference})与方法参数($conference)相同,并且方法参数是用 Conference 模型进行类型提示的(Conference $conference),Laravel 将其视为路由模型绑定。每次访问此路由时,应用程序将假定传入 URL 中的任何内容代替 {conference} 都是一个 ID,应该用于查找 Conference,然后将生成的模型实例传递给你的闭包或控制器方法。
自定义 Eloquent 模型的路由键
每当通过 URL 段查找 Eloquent 模型(通常是因为路由模型绑定),Eloquent 将使用其主键(ID)进行查找。
要更改你的 Eloquent 模型在所有路由中用于 URL 查找的列,请在模型中添加一个名为 getRouteKeyName() 的方法:
public function getRouteKeyName()
{
return 'slug';
}
现在,像 conferences/{conference} 这样的 URL 将期望从 slug 列获取条目而不是 ID,并且将根据其进行查找。
在特定路由中自定义路由键
在 Laravel 中,你还可以通过在路由定义中追加冒号和列名来在特定路由上更改路由键:
Route::get(
'conferences/{conference:slug}',
function (Conference $conference) {
return view('conferences.show')
->with('conference', $conference);
});
如果你的 URL 中有两个动态段(例如:organizers/{organizer}/conferences/{conference:slug}),Laravel 将自动尝试将第二个模型的查询范围限定为仅与第一个相关联的内容。因此,它将检查 Organizer 模型是否具有 conferences 关系,如果存在,则只返回与第一个段找到的 Organizer 相关联的 Conferences。
use App\Models\Conference;
use App\Models\Organizer;
Route::get(
'organizers/{organizer}/conferences/{conference:slug}',
function (Organizer $organizer, Conference $conference) {
return $conference;
});
自定义路由模型绑定
要手动配置路由模型绑定,请在 App\Providers\RouteServiceProvider 的 boot() 方法中添加类似 Example 3-33 的行。
示例 3-33. 添加路由模型绑定
public function boot()
{
// Perform the binding
Route::model('event', Conference::class);
}
现在你指定了当路由定义中有一个名为 {event} 的参数时(例如 Example 3-34 中演示的),路由解析器将返回一个带有该 URL 参数 ID 的 Conference 类的实例。
示例 3-34. 使用显式路由模型绑定
Route::get('events/{event}', function (Conference $event) {
return view('events.show')->with('event', $event);
});
路由缓存
如果你想要尽可能减少加载时间,你可能需要关注 路由缓存。 Laravel 启动过程中的一个步骤是解析 routes/* 文件,可能需要几十到几百毫秒,而路由缓存可以极大加快这一过程。
要缓存你的路由文件,你需要使用所有控制器、重定向、视图和资源路由(不使用路由闭包)。如果你的应用程序不使用任何路由闭包,你可以运行 php artisan route:cache,Laravel 将序列化你的 routes/* 文件的结果。如果想删除缓存,请运行 php artisan route:clear。
这里的问题是:Laravel 现在将匹配路由与缓存文件而不是实际的 routes/* 文件。你可以对路由文件进行无限更改,但在再次运行 route:cache 之前,这些更改不会生效。这意味着每次更改都需要重新缓存,这可能会带来很多混乱的潜力。
我建议的替代方案是:由于 Git 默认会忽略路由缓存文件,因此考虑仅在生产服务器上使用路由缓存,并在每次部署新代码时运行 php artisan route:cache 命令(无论是通过 Git 的后处理挂钩、Forge 部署命令还是其他部署系统的一部分)。这样,你不会在本地开发时遇到混乱的问题,但远程环境仍然可以从路由缓存中受益。
表单方法欺骗
有时你需要手动定义表单应发送的 HTTP 动词。HTML 表单只允许 GET 或 POST,所以如果你想使用其他动词,就需要自行指定。
Laravel 中的 HTTP 动词
正如我们已经看到的,你可以使用 Route::get()、Route::post()、Route::any() 或 Route::match() 来定义路由匹配的动词。你还可以使用 Route::patch()、Route::put() 和 Route::delete() 进行匹配。
但是如何通过 Web 浏览器发送除了 GET 之外的请求呢?首先,HTML 表单中的 method 属性决定了它的 HTTP 动词:如果你的表单的 method 是 "GET",它将通过查询参数和 GET 方法提交;如果表单的 method 是 "POST",它将通过 post body 和 POST 方法提交。
JavaScript 框架使得发送其他请求变得容易,比如 DELETE 和 PATCH。但是如果你发现自己需要在 Laravel 中提交除了 GET 或 POST 之外的动词的 HTML 表单,你需要使用 表单方法伪造(form method spoofing),这意味着在 HTML 表单中伪造 HTTP 方法。
HTML 表单中的 HTTP 方法伪造
要告诉 Laravel 当前提交的表单应被视为非 POST 方法的请求,需添加一个名为 _method 的隐藏变量,并赋值为 "PUT", "PATCH" 或 "DELETE",Laravel 将会根据该动词匹配和路由该表单提交。
示例 3-35 中的表单,因为它传递了方法 "DELETE" 给 Laravel,将匹配使用 Route::delete() 定义的路由,但不会匹配使用 Route::post() 定义的路由。
示例 3-35. 表单方法伪造
<form action="/tasks/5" method="POST">
<input type="hidden" name="_method" value="DELETE">
<!-- or: -->
@method('DELETE')
</form>
CSRF 保护
如果你已经尝试在 Laravel 应用中提交表单,包括 示例 3-35 中的表单,你很可能遇到了可怕的 TokenMismatchException。
在 Laravel 中,默认情况下,除了“只读”路由(使用 GET、HEAD 或 OPTIONS)之外的所有路由都受到跨站请求伪造(CSRF)攻击的保护,要求在每个请求中传递一个名为 _token 的输入。此 token 在每个会话开始时生成,并且每个非“只读”路由会将提交的 _token 与会话 token 进行比较。
什么是 CSRF?
跨站请求伪造 是指一个网站假装成另一个网站。其目的是通过在已登录用户的浏览器中从 他们 的网站提交到 你 的网站上的表单,来劫持你用户对你网站的访问权限。
避免 CSRF 攻击的最佳方法是默认保护所有入站路由——POST、DELETE 等——通过一个 token,Laravel 已经内置支持。
你有两种方法可以解决 CSRF 错误。第一种,也是首选的方法,是在每个提交中添加 _token 输入。在 HTML 表单中,有一种简单的方法,如 示例 3-36 中所示。
示例 3-36. CSRF tokens
<form action="/tasks/5" method="POST">
@csrf
</form>
在 JavaScript 应用中,需要更多的工作,但并不复杂。对于使用 JavaScript 框架的站点,最常见的解决方案是在每个页面的 <meta> 标签中存储 token,例如:
<meta name="csrf-token" content="<?php echo csrf_token(); ?>">
将 token 存储在 <meta> 标签中使得可以轻松地绑定到正确的 HTTP 头部,你可以像在 示例 3-37 中一样,全局为你的 JavaScript 框架的所有请求绑定它。
示例 3-37. 全局绑定 CSRF 的头部
// In jQuery:
$.ajaxSetup({
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
}
});
// With Axios: it automatically retrieves it from a cookie. Nothing to do!
Laravel 将在每个请求中检查 X-CSRF-TOKEN(以及 Axios 和其他 JavaScript 框架如 Angular 使用的 X-XSRF-TOKEN),并且传递有效的令牌将标记 CSRF 保护为满足状态。
使用 Vue Resource 绑定 CSRF 令牌的方法有些不同于 Laravel;查看 Vue Resource 文档 获取示例。
将 CSRF 令牌引导到 Vue Resource 看起来与 Laravel 的方式有所不同;查看 Vue Resource 文档 获取示例。
重定向
到目前为止,我们明确讨论过从控制器方法或路由定义中返回的仅仅是视图。但是还有一些其他结构可以返回,以便向浏览器提供行为指令。
首先,让我们来讨论 重定向。你在其他示例中已经看到了一些这样的示例。生成重定向有两种常见的方法;我们将在这里使用 redirect() 全局辅助方法,但你可能更喜欢门面。两者都会创建一个 Illuminate\Http\RedirectResponse 实例,对其执行一些便捷方法,然后返回它。你也可以手动执行这些操作,但你将需要做更多的工作。看看 示例 3-38,你可以看到几种返回重定向的方法。
示例 3-38. 返回重定向的不同方式
// Using the global helper to generate a redirect response
Route::get('redirect-with-helper', function () {
return redirect()->to('login');
});
// Using the global helper shortcut
Route::get('redirect-with-helper-shortcut', function () {
return redirect('login');
});
// Using the facade to generate a redirect response
Route::get('redirect-with-facade', function () {
return Redirect::to('login');
});
// Using the Route::redirect shortcut
Route::redirect('redirect-by-route', 'login');
请注意,redirect() 辅助方法暴露了与 Redirect 门面相同的方法,但它还有一个快捷方式;如果你直接将参数传递给辅助方法,而不是在其后链接方法,那么这是 to() 重定向方法的快捷方式。
还要注意,Route::redirect() 路由辅助方法的(可选的)第三个参数可以是重定向的状态码(例如,302)。
redirect()->to()
用于重定向的 to() 方法的方法签名如下:
function to($to = null, $status = 302, $headers = [], $secure = null)
$to 是有效的内部路径,$status 是 HTTP 状态(默认为 302),$headers 允许你定义随重定向发送的 HTTP 头,$secure 允许你覆盖默认的 http 或 https 选择(通常基于当前请求 URL 设置)。示例 3-39 展示了其使用示例。
示例 3-39. redirect()->to()
Route::get('redirect', function () {
return redirect()->to('home');
// Or same, using the shortcut:
return redirect('home');
});
redirect()->route()
route() 方法与 to() 方法相同,但不是指向特定路径,而是指向特定路由名称(参见 示例 3-40)。
示例 3-40. redirect()->route()
Route::get('redirect', function () {
return redirect()->route('conferences.index');
});
请注意,由于某些路由名称需要参数,其参数顺序有点不同。route() 方法有一个可选的第二个参数用于路由参数:
function route($to = null, $parameters = [], $status = 302, $headers = [])
因此,使用它可能看起来有点像 示例 3-41。
示例 3-41. redirect()->route() 带有参数
Route::get('redirect', function () {
return to_route('conferences.show', [
'conference' => 99,
];
});
使用 to_route() 辅助方法
你可以使用 to_route() 辅助方法作为 redirect()->route() 方法的别名。它们的签名都是一样的:
Route::get('redirect', function () {
return to_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()
虽然它的结构与您在redirect()上调用的其他方法类似,但with()不同之处在于它不定义您要重定向到哪里,而是定义您要传递的数据。当您将用户重定向到不同页面时,通常希望将某些数据传递给他们。您可以手动将数据闪存到会话中,但 Laravel 提供了一些便利方法来帮助您完成这些操作。
最常见的是,您可以使用with()传递键和值的数组或单个键和值,就像示例 3-42 中的一样。这会将您的with()数据保存到会话中,只用于下一次页面加载。
示例 3-42. 带数据的重定向
Route::get('redirect-with-key-value', function () {
return redirect('dashboard')
->with('error', true);
});
Route::get('redirect-with-array', function () {
return redirect('dashboard')
->with(['error' => true, 'message' => 'Whoops!']);
});
在重定向上链接方法
与许多其他门面一样,Redirect门面的大多数调用可以接受流畅的方法链,就像示例 3-42 中的with()调用一样。您将在“什么是流畅接口?”中了解更多信息。
您还可以使用withInput(),如示例 3-43 中所示,以闪存用户的表单输入重定向;这在验证错误的情况下最常见,您希望将用户发送回他们刚刚来自的表单。
示例 3-43. 带表单输入的重定向
Route::get('form', function () {
return view('form');
});
Route::post('form', function () {
return redirect('form')
->withInput()
->with(['error' => true, 'message' => 'Whoops!']);
});
获取通过withInput()传递的闪存输入的最简单方法是使用old()辅助函数,它可以用于获取所有旧输入(old())或只是特定键的值,如下例所示,如果没有旧值,则第二个参数作为默认值。你通常会在视图中看到这一点,这使得这段 HTML 可以在此表单的“创建”和“编辑”视图中通用:
<input name="username" value="<?=
old('username', 'Default username instructions here');
?>">
谈到验证,还有一种有用的方法可以将错误与重定向响应一起传递:withErrors()。你可以传递任何“错误提供者”,它可以是一个错误字符串,一个错误数组,或者,最常见的情况是,Illuminate Validator 的一个实例,我们将在第十章中详细介绍。示例 3-44 展示了其使用示例。
示例 3-44. 带错误的重定向
Route::post('form', function (Illuminate\Http\Request $request) {
$validator = Validator::make($request->all(), $this->validationRules);
if ($validator->fails()) {
return back()
->withErrors($validator)
->withInput();
}
});
withErrors()会自动与它重定向到的页面的视图共享一个$errors变量,以便你可以按照自己的意愿处理。
请求对象上的 validate()方法
不喜欢示例 3-44 的外观?有一个简单而强大的工具,可以帮助你轻松清理代码。在“请求对象上的 validate()”中详细了解更多。
中止请求
除了返回视图和重定向之外,退出路由的最常见方式是中止。有几种全局可用的方法(abort()、abort_if()和abort_unless()),可以选择使用 HTTP 状态码、消息和头数组作为参数。
如示例 3-45 所示,abort_if()和abort_unless()接受一个首要参数,该参数根据其真实性进行评估,并根据结果执行中止。
示例 3-45. 403 Forbidden 中止
Route::post('something-you-cant-do', function (Illuminate\Http\Request $request) {
abort(403, 'You cannot do that!');
abort_unless($request->has('magicToken'), 403);
abort_if($request->user()->isBanned, 403);
});
自定义响应
对于我们返回的几种其他选项,让我们先了解一下最常见的视图、重定向和中止响应之后的响应。与重定向一样,你可以在response()辅助函数或Response外观上运行这些方法。
response()->make()
如果你想手动创建 HTTP 响应,只需将你的数据传递给response()->make()的第一个参数:例如 return response()->make(*Hello, World!*)。再次提醒,第二个参数是 HTTP 状态代码,第三个是你的头部。
response()->json()和->jsonp()
要手动创建 JSON 编码的 HTTP 响应,请将你的可 JSON 化内容(数组、集合或其他内容)传递给json()方法:例如 return response()->json(User::all())。它与make()类似,只是json_encode了你的内容并设置了适当的头。
response()->download()、->streamDownload()和->file()
要发送文件供最终用户下载,请将download()传递给SplFileInfo实例或字符串文件名,第二个可选参数是下载文件名:例如,return response()->download('file501751.pdf', 'myFile.pdf'),这将发送名为file501751.pdf的文件,并在发送时重命名为myFile.pdf。
要在浏览器中显示相同的文件(如果是 PDF 或浏览器可以处理的图像或其他内容),请改用response()->file(),它接受与response->download()相同的参数。
如果您希望将外部服务的某些内容作为下载可用,而无需直接将其写入服务器磁盘,则可以使用response()->streamDownload()来流式下载。该方法期望的参数包括一个回调函数(回显一个字符串)、一个文件名,以及可选的头部数组;参见示例 3-46。
示例 3-46. 从外部服务器进行流式下载
return response()->streamDownload(function () {
echo DocumentService::file('myFile')->getContent();
}, 'myFile.pdf');
测试
在其他一些社区中,单元测试控制器方法的想法很常见,但在 Laravel(以及大多数 PHP 社区)中,通常依赖应用程序测试来测试路由的功能。
例如,要验证POST路由是否正常工作,我们可以编写类似于示例 3-47 的测试。
示例 3-47. 编写简单的POST路由测试
// tests/Feature/AssignmentTest.php
public function test_post_creates_new_assignment()
{
$this->post('/assignments', [
'title' => 'My great assignment',
]);
$this->assertDatabaseHas('assignments', [
'title' => 'My great assignment',
]);
}
我们直接调用了控制器方法吗?没有。但我们确保了该路由的目标——接收POST并将其重要信息保存到数据库中——得到了实现。
您还可以使用类似的语法访问一个路由,并验证页面上是否显示了某些文本,或者点击某些按钮是否执行了某些操作(参见示例 3-48)。
示例 3-48. 编写简单的GET路由测试
// AssignmentTest.php
public function test_list_page_shows_all_assignments()
{
$assignment = Assignment::create([
'title' => 'My great assignment',
]);
$this->get('/assignments')
->assertSee('My great assignment');
}
简而言之
Laravel 的路由定义在routes/web.php和routes/api.php中。您可以为每个路由定义预期的路径,哪些段是静态的,哪些是参数,哪些 HTTP 动词可以访问路由,以及如何解析它。您还可以将中间件附加到路由上,对它们进行分组,并为它们命名。
路由闭包或控制器方法返回的内容决定了 Laravel 如何响应用户。如果是字符串或视图,它会呈现给用户;如果是其他类型的数据,它会转换为 JSON 并呈现给用户;如果是重定向,它会强制进行重定向。
Laravel 提供了一系列工具和便利功能,用于简化常见的与路由相关的任务和结构。这些包括资源控制器、路由模型绑定和表单方法欺骗。