在构建 Laravel 应用程序时,几乎可以肯定您会在某些时候需要处理会话。它们是 Web 开发的基础部分。
本文将快速介绍会话是什么,它们如何在 Laravel 中工作,以及您如何在 Laravel 应用程序中使用它们。
然后,我们将更进一步,深入探讨如何使用“会话类”与会话交互,以避免我在处理 Laravel 应用程序时经常遇到的常见陷阱。
最后,我们将了解如何在 Laravel 中测试会话数据。
默认情况下,Web 应用程序是无状态的,这意味着请求通常彼此不了解。因此,我们需要一种方法来存储请求之间的数 据。例如,当用户登录网站时,我们需要记住他们在访问期间已登录。这就是会话的用武之地。
简而言之,会话是一种在多个请求之间持久保存数据安全的方式。
会话数据可能用于存储以下内容:
会话数据可以存储在各种位置,例如:
要了解会话是什么,让我们看看它们如何在 Laravel 中工作。
以下是您可能在 Laravel 应用程序的会话中找到的一些示例数据:
<code>[ '_token' => 'bKmSfoegonZLeIe8B6TWvSm1dKwftKsvcT40xaaW' '_previous' => [ 'url' => 'https://my-app.com/users' ] '_flash' => [ 'old' => [ 'success', ], 'new' => [] ] 'success' => 'User created successfully.' 'current_team_id' => 123 ]</code>
让我们分解每个键可能代表的内容。
以下键由 Laravel 框架本身添加:
_token
值用于防止 CSRF 攻击。_previous.url
值用于存储先前请求的 URL。_flash.old
值用于存储先前请求中闪存会话数据的键。在本例中,它表示在先前请求中闪存了 success
值。_flash.new
值用于存储当前请求中闪存会话数据的键。以下键由我添加:
success
值用于存储可能显示给用户的成功消息。current_team_id
值用于存储用户正在查看的当前团队的 ID。默认情况下,Laravel 支持以下会话驱动程序:
cookie
- 会话数据存储在安全且加密的 Cookie 中。database
- 会话存储在您的数据库中(例如 MySQL、PostgreSQL、SQLite)。memcached
/ redis
- 会话数据存储在这些快速的缓存存储中。dynamodb
- 会话数据存储在 AWS DynamoDB 中。file
- 会话数据存储在 storage/framework/sessions
中。array
- 会话数据存储在内存中的 PHP 数组中,不会持久保存。其中一些驱动程序有设置要求。因此,在使用它们之前,务必检查 Laravel 文档以了解如何设置它们。
Laravel 使使用会话变得非常简单。文档很好地解释了如何与会话交互。但是,让我们快速回顾一下基础知识。
对于我们的示例,我们将假设我们正在构建一个跨越多个页面的分步向导。我们将存储当前步骤以及每个步骤中输入的数据到会话中。这样,当用户完成所有步骤时,我们可以在向导结束时读取所有提交的数据。
为了使示例简单,我们还将使用 session()
辅助函数。但稍后我们将讨论使用 Session
门面或请求类访问会话数据。
要从会话读取数据,您可以使用 get
方法,如下所示:
<code>[ '_token' => 'bKmSfoegonZLeIe8B6TWvSm1dKwftKsvcT40xaaW' '_previous' => [ 'url' => 'https://my-app.com/users' ] '_flash' => [ 'old' => [ 'success', ], 'new' => [] ] 'success' => 'User created successfully.' 'current_team_id' => 123 ]</code>
运行上述代码将返回为 wizard:current_step
键存储在会话中的值。如果该键在会话中没有存储的值,它将返回 null
。
此方法还允许您定义一个默认值,如果键不存在则返回:
<code>$currentStep = session()->get(key: 'wizard:current_step');</code>
运行上述代码将返回为 wizard:current_step
键存储在会话中的值。如果该键在会话中没有存储的值,它将返回 1
。
还可能有一些时候您想从会话读取数据并同时将其删除(因此无法再次访问)。您可以为此使用 pull
函数:
<code>$currentStep = session()->get(key: 'wizard:current_step', default: 1);</code>
运行上述代码将返回为 wizard:current_step
键存储在会话中的值,然后将其从会话中删除。
要将数据写入会话,您可以使用 put
函数,如下所示:
<code>$currentStep = session()->pull(key: 'wizard:current_step');</code>
运行上述代码将把数组(在第二个参数中传递)存储为 wizard:step_one:form_data
键的值。
类似地,您还可以使用 push
方法将数据推送到会话中的数组:
<code>session()->put( key: 'wizard:step_one:form_data', value: [ 'name' => 'Ash Allen', 'email' => 'ash@example.com', ], );</code>
假设 wizard:step_one:form_data:languages
键具有以下数据:
<code>session()->push( key: 'wizard:step_one:form_data:languages', value: 'javascript', );</code>
上述代码(调用 push
方法)将更新会话值:
<code>[ '_token' => 'bKmSfoegonZLeIe8B6TWvSm1dKwftKsvcT40xaaW' '_previous' => [ 'url' => 'https://my-app.com/users' ] '_flash' => [ 'old' => [ 'success', ], 'new' => [] ] 'success' => 'User created successfully.' 'current_team_id' => 123 ]</code>
如果 wizard:step_one:form_data:languages
值尚不存在于会话中,则使用 push
将创建会话键并将值设置为包含您传入的值的数组。
Laravel 还提供了一些方便的辅助方法,允许您增加和减少会话中的值:
您可以像这样增加会话中的值:
<code>$currentStep = session()->get(key: 'wizard:current_step');</code>
当我们运行上面的代码时,如果 wizard:current_step
会话值为 3,它现在将增加到 4。
您还可以像这样减少会话中的值:
<code>$currentStep = session()->get(key: 'wizard:current_step', default: 1);</code>
如果值尚不存在于会话中,则将它们视为 0。因此,对空会话值调用 increment
将值设置为 1。对空会话值调用 decrement
将值设置为 -1。
这两种方法都允许您指定要增加或减少的数量:
<code>$currentStep = session()->pull(key: 'wizard:current_step');</code>
您还可以使用 forget
方法从会话中删除数据:
<code>session()->put( key: 'wizard:step_one:form_data', value: [ 'name' => 'Ash Allen', 'email' => 'ash@example.com', ], );</code>
运行上述代码将从会话中删除属于 wizard:current_step
键的数据。
如果您想一次删除多个键,您可以将键数组传递给 forget
函数:
<code>session()->push( key: 'wizard:step_one:form_data:languages', value: 'javascript', );</code>
或者,如果您希望从会话中删除所有数据,您可以使用 flush
函数:
<code>[ `php`, ]</code>
Laravel 还提供了一些方便的辅助函数来检查会话中是否存在数据。
您可以使用 has
方法检查会话中是否存在键以及其值是否不是 null
:
<code>[ `php`, `javascript`, ]</code>
如果值存在且不是 null
,则上述代码将返回 true
。如果值为 null
或键不存在,它将返回 false
。
类似地,您还可以使用 exists
方法检查会话中是否存在键(无论值是否为 null):
<code>session()->increment(key: 'wizard:current_step');</code>
您还可以检查会话中是否根本不存在会话:
<code>session()->decrement(key: 'wizard:current_step');</code>
可能有时您想在会话中持久保存一些数据,但仅限于下一个请求。例如,您可能希望在用户提交表单后向用户显示成功通知。
为此,您可以使用 flash
方法:
<code>session()->increment(key: 'wizard:current_step', amount: 2); session()->decrement(key: 'wizard:current_step', amount: 2);</code>
如果您要运行上述代码,在下一次请求中,您可以从会话中读取该值(使用类似 session()->get('success')
的内容)进行显示。然后将其删除,以便在下一次请求中不可用。
可能有时您有一些闪存数据(在先前的请求中添加),并且您想将其保留到下一个请求。
您可以使用 reflash
方法刷新所有闪存数据:
<code>session()->forget(keys: 'wizard:current_step');</code>
或者,如果您只想保留一些闪存数据,您可以使用 keep
方法:
<code>[ '_token' => 'bKmSfoegonZLeIe8B6TWvSm1dKwftKsvcT40xaaW' '_previous' => [ 'url' => 'https://my-app.com/users' ] '_flash' => [ 'old' => [ 'success', ], 'new' => [] ] 'success' => 'User created successfully.' 'current_team_id' => 123 ]</code>
运行上述代码将保留 success
和 error
闪存会话值,但会删除下一个请求的任何其他闪存数据。
到目前为止,我们只在示例中使用了 session()
辅助函数。
但您也可以使用 IlluminateSupportFacadesSession
门面或 IlluminateHttpRequest
类与会话交互。
无论您使用哪种方法,您仍然可以使用本文前面介绍的相同方法。这些方法只是与会话数据交互的不同方式。
要使用 Session
门面,您可以像这样调用方法:
<code>$currentStep = session()->get(key: 'wizard:current_step');</code>
或者,您可以通过调用注入到控制器方法中的 IlluminateHttpRequest
实例上的 session
方法来访问会话。假设您有如下控制器方法:
<code>$currentStep = session()->get(key: 'wizard:current_step', default: 1);</code>
每种方法都是完全有效的,因此您可以决定您和您的团队更喜欢哪一种。
对于较小的项目,使用我们前面讨论过的方法与会话交互是完全可以的。但是,随着 Laravel 项目的增长,您可能会遇到一些问题,这些问题会导致错误并使您的代码更难以维护。
因此,我们现在将介绍一些潜在的陷阱以及如何避免它们。
我看到的一个常见陷阱(我自己也经历过很多次)是会话键中的错字。
坚持使用我们的向导示例,假设我们要将当前步骤存储在会话中。因此,我们的代码可能如下所示:
<code>$currentStep = session()->pull(key: 'wizard:current_step');</code>
然后稍后,在代码库的不同部分,我们可能想从会话中读取当前步骤:
<code>session()->put( key: 'wizard:step_one:form_data', value: [ 'name' => 'Ash Allen', 'email' => 'ash@example.com', ], );</code>
您看到我刚才犯的错误了吗?我意外地尝试读取 wizard:step
键而不是 wizard:current_step
键。
这是一个简单的示例,但在大型代码库中,很容易犯这种错误。这些显而易见的错误也可能是最难发现的。
因此,避免这些错字的一种有用方法是使用常量或方法来生成会话键。
例如,如果会话键是静态的,您可以定义一个常量(可能在稍后我们将介绍的会话类中),如下所示:
<code>session()->push( key: 'wizard:step_one:form_data:languages', value: 'javascript', );</code>
这意味着我们减少了在代码库中使用的原始字符串的数量,这有助于减少错字的数量。
但是,有时您可能需要动态生成会话键。例如,假设我们希望我们的 wizard:current_step
键包含一个团队 ID 字段。我们可以创建一个方法来生成密钥,如下所示:
<code>[ `php`, ]</code>
正如我们在上面的代码中看到的,我们正在动态生成会话键,以便它可以在不同的方法中使用。例如,如果我们试图查找 ID 为 1 的团队的当前步骤,则密钥将是 wizard:1:current_step
。
我在处理存在一段时间的项目时看到的另一个陷阱是会话键冲突。
例如,想象一下,几年前您构建了一个用于创建新用户帐户的向导。因此,您可能像这样存储会话数据:
<code>[ '_token' => 'bKmSfoegonZLeIe8B6TWvSm1dKwftKsvcT40xaaW' '_previous' => [ 'url' => 'https://my-app.com/users' ] '_flash' => [ 'old' => [ 'success', ], 'new' => [] ] 'success' => 'User created successfully.' 'current_team_id' => 123 ]</code>
现在,您已被分配构建一个也具有向导的新功能,并且您完全忘记了旧的向导和您使用的命名约定。您可能会意外地为新向导使用相同的键,导致数据冲突并引入潜在的错误。
为了避免这种情况,我喜欢用功能名称作为会话键的前缀。因此,对于保存用于创建新用户的向导数据,我可能有以下键:
new_user_wizard:current_step
new_user_wizard:step_one:form_data
new_user_wizard:step_two:form_data
然后在我的用于创建新团队的新向导中,我可能有以下键:
new_team_wizard:current_step
new_team_wizard:step_one:form_data
new_team_wizard:step_two:form_data
我们将在本文后面介绍如何在会话类中添加这些前缀。
你能告诉我此会话值中存储的数据类型是什么吗?
<code>$currentStep = session()->get(key: 'wizard:current_step');</code>
如果您猜到它是一个 AppDataTransferObjectsWizardsFormData
实例,那么您是对的。
说笑归说笑,我想说明的是,当您从会话中读取数据时,并不总是立即清楚您正在使用什么类型的数据。您最终必须查看将数据写入会话的代码才能弄清楚它是什么。这可能会分散注意力且耗时,并可能导致错误。
您可以向读取会话数据的代码添加注释或文档块。但这只是一个提示。如果注释没有保持最新(如果会话数据类型发生更改),那么它就没有帮助,并且会增加错误的可能性。
我喜欢使用的另一种方法是在方法内部读取会话数据并向该方法添加返回类型。这样,您可以确保您正在使用的数据类型正确。它还有助于您的 IDE 和阅读代码的人员。
例如,让我们来看这段代码:
<code>$currentStep = session()->get(key: 'wizard:current_step', default: 1);</code>
我们现在可以立即看到 stepOneFormData
方法返回一个 AppDataTransferObjectsWizardsFormData
实例。这清楚地说明了我们正在使用的数据类型。然后,我们可以在控制器中像这样调用此方法:
<code>$currentStep = session()->pull(key: 'wizard:current_step');</code>
正如我们在前面几节中看到的,在 Laravel 中使用会话时,有一些很容易避免(但很常见)的陷阱。
通过使用“会话类”,可以避免(或至少减少)这些陷阱中的每一个。我喜欢使用会话类来在一个地方封装与单个功能相关的会话数据处理逻辑。
例如,假设我们有一个用于创建用户和另一个用于创建团队的向导。我将为这些向导中的每一个创建一个会话类:
AppSessionsUsersNewUserWizardSession
AppSessionsTeamsNewTeamWizardSession
通过使用会话类,您可以:
在处理会话数据时使用这种基于类的方法在处理大型 Laravel 项目时为我节省了无数次时间。这是一种简单的方法,可以产生很大的影响。
在前面的示例中,我已经暗示过使用会话类。但是,让我们更深入地了解我喜欢如何构建这些类。
假设我们有以下用于新用户向导的会话类。乍一看它可能有点让人不知所措,但让我们看看代码,然后分解它:
<code>[ '_token' => 'bKmSfoegonZLeIe8B6TWvSm1dKwftKsvcT40xaaW' '_previous' => [ 'url' => 'https://my-app.com/users' ] '_flash' => [ 'old' => [ 'success', ], 'new' => [] ] 'success' => 'User created successfully.' 'current_team_id' => 123 ]</code>
在上面的 AppSessionsUsersWizardSession
类中,我们首先定义了接受 IlluminateContractsSessionSession
实例的构造函数。通过这样做,当我们从服务容器中解析 AppSessionsUsersWizardSession
类时,Laravel 将自动为我们注入会话实例。我将稍后向您展示如何在控制器中执行此操作。
然后,我们定义了 5 个基本的公共方法:
getCurrentStep
- 返回向导中的当前步骤。如果没有设置步骤,则默认为 1
。setCurrentStep
- 设置向导中的当前步骤。setFormDataForStep
- 设置向导中给定步骤的表单数据。此方法采用步骤号和 AppDataTransferObjectsWizardsUsersFormData
实例。getFormDataForStep
- 获取向导中给定步骤的表单数据。此方法采用步骤号并返回 AppDataTransferObjectsWizardsUsersFormData
实例或如果数据不存在则返回 null
。flush
- 从会话中删除与向导相关的所有数据。如果已完成或取消向导,您可能希望调用此方法。您可能已经注意到所有键都在方法内部生成。我喜欢这样做是为了减少使用的原始字符串的数量(并减少错字的机会)。这也意味着如果我们想添加另一个访问特定键的方法,我们可以很容易地做到这一点。
使用这些键生成方法的额外好处是我们可以轻松地向键添加前缀以避免冲突。在此示例中,我们通过使用 sessionKey
方法将所有键的前缀设置为 new_user_wizard:
。
现在此类已设置,让我们看看我们如何在控制器中与它交互:
<code>[ '_token' => 'bKmSfoegonZLeIe8B6TWvSm1dKwftKsvcT40xaaW' '_previous' => [ 'url' => 'https://my-app.com/users' ] '_flash' => [ 'old' => [ 'success', ], 'new' => [] ] 'success' => 'User created successfully.' 'current_team_id' => 123 ]</code>
正如我们看到的,在上面的示例中,我们将 AppSessionsUsersWizardSession
类注入到我们的控制器方法中。Laravel 将自动为我们解析会话实例。
然后,我们可以像使用任何其他类一样与它交互。
起初,这可能感觉像过度抽象和需要维护更多代码。但是,随着项目的增长,类型提示、返回类型、键生成方法,甚至方法的命名(使您的操作更具描述性)都非常有用。
就像代码库的任何其他部分一样,您应该确保您拥有会话数据的覆盖范围,以确保正在读取和写入正确的字段。
使用会话类的一大好处是您可以轻松地为类中的每个方法编写集中的单元样式测试。
例如,我们可以为 AppSessionsUsersWizardSession
类的 getFormDataForStep
方法编写一些测试。作为提醒,以下是该方法:
<code>$currentStep = session()->get(key: 'wizard:current_step');</code>
我们可以在这里测试几个场景:
AppDataTransferObjectsWizardsUsersFormData
对象。null
。我们的测试类可能如下所示:
<code>$currentStep = session()->get(key: 'wizard:current_step', default: 1);</code>
在上面的测试类中,我们有两个测试涵盖了我们前面提到的两种情况。
这些单元样式测试非常适合确保您的会话类配置正确以读取和写入会话数据。但它们并不一定能让你相信它们在代码库的其余部分被正确使用。例如,您可能正在调用 getFormDataForStep(1)
,而您应该调用 getFormDataForStep(2)
。
出于这个原因,您可能还想考虑在您的功能测试(您通常为控制器编写的测试)中断言会话数据。
例如,假设您的控制器中有以下基本方法,它会转到向导中的下一步:
<code>$currentStep = session()->pull(key: 'wizard:current_step');</code>
在上面的方法中,我们首先从会话中读取当前步骤。然后,我们将当前步骤的表单数据存储在会话中。最后,我们递增当前步骤并重定向到向导中的下一步。
我们将假设我们的 AppHttpRequestsUsersWizardNextStepRequest
类负责验证表单数据,并在我们调用 toDto
方法时返回 AppDataTransferObjectsWizardsUsersFormData
实例。
我们还将假设 nextStep
控制器方法可通过对 /users/wizard/next-step
路由(名为 users.wizard.next-step
)的 POST 请求访问。
我们可能想编写如下测试以确保表单数据被正确存储在会话中:
<code>[ '_token' => 'bKmSfoegonZLeIe8B6TWvSm1dKwftKsvcT40xaaW' '_previous' => [ 'url' => 'https://my-app.com/users' ] '_flash' => [ 'old' => [ 'success', ], 'new' => [] ] 'success' => 'User created successfully.' 'current_team_id' => 123 ]</code>
在上面的测试中,我们正在使用一些表单数据向 /users/wizard/next-step
路由发出 POST 请求。您可能会注意到我们正在使用 withSession
。此方法允许我们设置会话数据,以便我们可以断言它被正确读取。
然后,我们断言用户被重定向到向导中的下一步,并且会话中的当前步骤设置为 3
。我们还断言步骤 2
的表单数据被正确存储在会话中。
正如我们在测试中看到的,我们还通过两种方式从会话中读取:
assertSessionHas
方法检查会话数据是否正确设置。session()
辅助函数直接读取会话数据。这两种方法都是有效的,因此您可以决定更喜欢哪一种。我在上面的测试中使用了这两种方法来向您展示您有多种选择。
希望本文能帮助您很好地理解会话是什么以及它们如何在 Laravel 中工作。我还希望它们能为您提供一些关于如何使用基于类的方法与会话数据交互以避免一些常见陷阱的想法。
以上是深入研究拉拉维尔的会议的详细内容。更多信息请关注PHP中文网其他相关文章!