Laravel-Eloquent-学习指南-全-

Laravel Eloquent 学习指南(全)

原文:zh.annas-archive.org/md5/143302d39935f3904781bde8bcbab9d1

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

如果你与 Web 开发领域有关联,你知道数据有多么重要。网络运行在数据之上,因此对于开发者来说,考虑快速有效的处理数据的方法至关重要。Eloquent 是 Laravel PHP 框架附带的一个出色的 ORM,它独特且对开发者非常有用,因为它允许他们使用真正简单直观的语法定义模型、关系和许多复杂操作,而不会牺牲性能。在不编写长查询对象的情况下对多个表执行大量操作将变得容易如玫瑰床。

本书将带你通过 Eloquent,Laravel 框架的 ORM,开发卓越的数据驱动应用程序。

你将执行以下操作:

  • 使用表达性语法构建高度高效的 Eloquent ORM 应用程序

  • 掌握关系的力量以及 Eloquent 如何处理它们

  • 通过各种逐步的代码示例超越简单的理论

因此,让我们开始吧!

本书涵盖的内容

第一章,设置我们的第一个项目,将讨论如何处理 Composer 和 Homestead。我们还将介绍我们的第一个 Laravel 项目的安装过程。

第二章,使用 Schema Builder 类构建数据库,将讨论 Schema Builder 类。我们将分析你可以使用该类执行的所有操作,查看不同类型的索引,并了解 Schema 类提供的方法。

第三章,最重要的元素——模型!,将帮助我们为我们的项目实现一些“创建”、“读取”、“更新”和“删除”逻辑。我们还将探索模型类的一些有用方法和功能。

第四章,探索关系的世界,将帮助我们了解如何处理不同类型的关系,以及如何以舒适和整洁的方式查询和使用它们。此外,我们还将学习如何在数据库中插入和删除相关模型,或更新现有的模型。

第五章,使用集合增强结果,将讨论集合。我们将处理一些结果转换方法以及构成集合的元素。

第六章,使用事件和观察者全面控制,将使我们了解 Eloquent 模型上下文中的所有事件。紧接着,我们将介绍模型事件和模型观察者。

第七章,优雅地使用 Laravel!,将探讨数据库包的结构,并查看其中包含的内容。之后,我们将学习如何单独为你的项目安装"illuminate/database"包,以及如何为其首次使用进行配置。是的,正是:没有 Laravel 的 Eloquent!

第八章,还不够!扩展 Eloquent,高级概念,将探讨两种不同的扩展 Eloquent 的方法,并继续学习关于仓库模式的内容。

你需要为本书准备

本书对硬件或软件没有具体要求;我们将使用 Vagrant 设置一个标准的虚拟机作为示例项目和代码片段的基础。所以,别担心,我的朋友!你不会在 Apache 或 Nginx 配置文件上花费漫长的时间或夜晚。

本书面向对象

本书非常适合对 PHP 开发有基本知识的开发者,但却是 Eloquent ORM 的新手。然而,有之前 Laravel 和 Eloquent 经验的开发者也能从书中对特定类和方法论进行深入分析中受益。

惯例

在本书中,你会找到多种文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。

文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称将如下所示:"在EventServiceProvider类中,你可以添加一个特殊的事件监听器并将其绑定到某个闭包上。"

代码块设置如下:

  User::saved(function($user)
    {
        // doing something here, after User save operation (both create and update)...
    });

新术语重要词汇将以粗体显示。你会在屏幕上看到这些词,例如在菜单或对话框中,文本中将如下显示:"你所要做的就是访问 Composer 网站的下载页面,并找到适合你操作系统的正确方法。"

注意

警告或重要注意事项将以如下框中的形式出现。

小贴士

小贴士和技巧将以如下形式出现。

读者反馈

我们欢迎读者的反馈。告诉我们你对这本书的看法——你喜欢什么或可能不喜欢什么。读者反馈对我们开发你真正能从中获得最大收益的标题非常重要。

要向我们发送一般反馈,只需发送一封电子邮件到<feedback@packtpub.com>,并在邮件主题中提及书名。

如果你在某个领域有专业知识,并且对撰写或参与书籍感兴趣,请参阅我们的作者指南www.packtpub.com/authors

客户支持

现在你已经是 Packt 图书的骄傲拥有者,我们有一些事情可以帮助你从购买中获得最大收益。

错误清单

尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果你在我们的某本书中发现错误——可能是文本或代码中的错误——如果你能向我们报告这一点,我们将不胜感激。这样做可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果你发现任何勘误,请通过访问 www.packtpub.com/submit-errata,选择你的书籍,点击勘误提交表单链接,并输入你的勘误详情来报告。一旦你的勘误得到验证,你的提交将被接受,勘误将被上传到我们的网站,或添加到该标题的勘误部分下的现有勘误列表中。任何现有的勘误都可以通过从 www.packtpub.com/support 选择你的标题来查看。

盗版

互联网上对版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果你在网上发现我们作品的任何非法副本,无论形式如何,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

请通过 <copyright@packtpub.com> 联系我们,并提供疑似盗版材料的链接。

我们感谢你在保护我们的作者和提供有价值内容的能力方面的帮助。

询问

如果你在本书的任何方面遇到问题,可以通过 <questions@packtpub.com> 联系我们,我们将尽力解决。

第一章. 设置我们的第一个项目

"Chi ben comincia è a metà dell'opera."

(意大利语,意为“良好的开端是成功的一半。”)

每次旅行都有一个起点,只有装备齐全的英雄才能取得胜利。当然,21 世纪的英雄——开发者也不例外!

为了避免问题和与不良(以及故障)代码怪物作斗争,优秀的代码工匠会在开始之前准备一切必要的东西。

开发者必须熟悉他们将要使用的工具,一个好的开发环境可以极大地提高开发过程。所以,在我们动手之前,在本章中,我们将了解如何处理 Composer 和 Homestead。

Composer 是一个出色的依赖管理工具,被世界各地的许多 PHP 项目使用。Homestead 是官方的 Laravel Vagrant 箱子,它允许你在几分钟内创建一个完全功能化的开发环境。最后,我们将介绍我们的第一个 Laravel 项目的安装过程。

我知道你在想什么:你只想写代码,代码,更多的代码。

有耐心一点:如果你知道我们将在本章末尾分析的工具,你将感受到巨大的差异。

相信我。

  • 你的瑞士军刀:Composer

  • 你的安全之地:Homestead

  • 新的藏身之处:改进后的 Homestead

  • 一个额外的工具:Adminer

  • 你的最佳伙伴:Laravel

  • 你的第一个项目:EloquentJourney

  • 摘要

你的瑞士军刀 – Composer

你首先需要与 Laravel(以及 Eloquent)一起工作的工具是 Composer。Composer 是一个 PHP 的依赖管理工具。使用这个工具,你可以轻松地将项目中需要的每个依赖项包含进来。这只需几秒钟,使用一个名为 composer.json 的 JSON 配置文件即可完成。

通常,PHP 项目的依赖项管理使用 PEAR 或其他方法。Composer 有不同的策略:所有操作都是基于项目的。这意味着你可以在同一服务器上拥有两个项目,它们使用相同依赖项包的不同版本。

安装 Composer

安装程序非常简单。你所要做的,就是访问 Composer 网站的 下载 页面,并找到适合你操作系统的正确方法。

  • 如果你使用 Linux 或 Mac,只需这样做:

    curl -sS https://getcomposer.org/installer | php
    
    

    或者,如果你没有 cURL,那么使用这个:

    php -r "readfile('https://getcomposer.org/installer');" | php
    
    

    此外,composer.phar 文件将下载到你的当前目录。

  • 在 Windows 上,你可以简单地下载专门的安装程序。

一旦 Composer 安装完成,我建议将其路径添加到系统的 PATH 变量中,以便在任何地方使用它。这有很多方法,取决于你的操作系统。让我们看看每种方法。

  • 在 Linux 上,你可以使用以下命令将 Composer 移动到正确的目录:

    mv composer.phar /usr/local/bin/composer
    
  • 对于 OS X,情况也相同,但有时 usr 目录不存在。你必须手动创建 usr/local/bin

  • 最后,在 Windows 上,你必须打开控制面板,输入环境变量或类似的内容。搜索实用程序会为你完成剩下的工作。一旦进入正确的窗口,你将看到一个所有环境变量的列表。找到PATH并将 Composer 安装路径添加到其中。

composer.json 和 autoload 文件

在我们深入我们的项目之前,让我们看看 Composer 是如何工作的。

composer.json文件中,开发者指定了其项目的每个单个依赖项。你还可以创建自己的包,但在这本书中我们不会探讨如何创建它们。

假设你想创建一个使用 Monolog 进行日志记录的项目。

  1. 为项目创建一个文件夹,然后创建一个空白的文本文件,并将其命名为composer.json

  2. 打开它,你只需按照以下所示包含你的依赖项:

      {
          "require": {
              "monolog/monolog": "1.12.0"
          }
      }
    
  3. 之后,保存文件,并在你的项目目录中输入以下内容:

    composer update
    
    

等待一下,下载所有内容,然后你就完成了!

什么?好吧,这是它的工作方式:Composer 下载你可能需要的每个包,并自动为所有包创建一个加载器。所以,为了在你的项目中使用依赖项,你只需包含vendor/autoload.php,然后你就可以开始了。

假设你有一个index.php文件作为你应用程序的起始文件。你将不得不执行以下操作:

  <?php // index.php file

    require('vendor/autoload.php');

    // your code here...

没有更多了!

为什么我要向你展示这个?好吧,Laravel 和 Eloquent 是 Composer 包。所以,为了使用它并创建一个 Laravel 应用程序,你必须了解这个机制是如何工作的!

最常用的命令

Composer 是一个命令行工具。每个好的 CLI 工具都有一些重要的命令,在这个小节中,我将向你展示我们将要使用最多的命令。

  • 首先,我们有以下内容:

    composer create-project
    
    

    使用此命令,你可以使用特定的包作为基础创建一个新的项目。你将使用此命令创建一个新的 Laravel 项目,使用以下语法:

    composer create-project laravel/laravel my_project_folder
    
    
  • 然后,你可以找到:

    composer install
    composer update
    
    

    这两个命令很相似;它们很相似,但并不相同。当你指定composer.json文件中的依赖项时,你可以使用install来安装它们。如果你已经安装了它们,但想将依赖项更新到新版本,请使用update

    注意

    为了知道必须更新什么和不应该更新什么,Composer 使用composer.lock文件,你可以在项目的根目录中看到它。实际上,你永远不需要与它打交道,但重要的是要知道 Composer 将其用作日志

  • 有时,你也会看到这个:

    composer require
    
    

    你可以使用require在运行时将依赖项包含到你的项目中。以下是一个使用require包含 Monolog 的示例:

    composer.phar require monolog/monolog:1.12.0
    
    
  • 另一个常用的命令是:

    composer dump-autoload
    
    

    此命令重新生成autoload.php文件。如果你在项目中添加了一些没有使用命名空间或 PSR 约定和规则的类,这可能很有用。

  • 有时,你必须在警告之后使用():

    composer self-update
    
    

    此命令更新 Composer 本身。只需几秒钟,你就可以再次运行了!

  • 最后,你可以使用以下特殊命令:

    composer global COMMAND_HERE
    
    

    使用它来在 Composer 主目录中执行特定命令。如我之前提到的,Composer 是基于每个项目工作的,但有时你需要全局安装一些工具。使用全局命令,你可以轻松完成。

现在你需要知道的关于 Composer 的就这么多,是的,还有很多其他命令,但现在我们不需要它们。

让我们更进一步:现在是时候了解 Homestead 了!

你的安全之地 – Homestead

当我们开始一个新的项目时,我们可能会遇到许多兼容性和环境问题。首先需要考虑的是 PHP 版本。也许你正在使用 XAMPP 或你本地机器上的一些预配置的栈。对于你的新项目,你希望使用 PHP 5.6,但安装的版本是 5.3(因为你用它处理了一些旧项目)。好吧,没问题;你只需安装 5.6,然后就可以继续了。

是的,但两天后,电话响了。是你的客户;终于,是时候做一些改进并添加新功能了!所以,你启动你的栈服务,浏览你的旧项目索引,然后 BOOM!兼容性问题,到处都是兼容性问题!这绝对不是开始新的一天的好方法。

这不是一个代码问题,而是一个环境问题。

实际上,最好的解决方案是开始使用 Vagrant。Vagrant 是一个出色的工具,它允许你创建一个带有无头操作系统的虚拟机,以便根据每个项目配置虚拟机。此外,你还可以将本地机器上的某些文件夹与该机器共享,这样你就可以在一个隔离的环境中工作,同时使用你喜欢的 IDE 和操作系统。

注意

注意,基于每个项目的基础是整个问题的关键部分。如果你为单个项目配置一个单独的机器,你可以调整你想要的一切以达到完美的环境。此外,使用 Vagrant,你将能够以与生产机器相同的方式设置你的本地环境。所以,再也没有 本地到生产 的错误和问题!

最后但同样重要的是,Vagrant 的有趣(且有用)之处在于,你可以将特定的盒子置于版本控制之下。因此,对于每个新团队成员,你所要做的就是克隆仓库并启动机器。

这看起来很复杂,但实际上并不复杂。使用 Vagrant,你可以轻松下载一个盒子(一个包含所有所需工具和应用程序的现成虚拟机),然后从 shell 中使用简单命令启动它,如下所示:

$ vagrant up

Laravel 社区对 Vagrant 了解一些,并制作了一个 Vagrant Box 来帮助你完成工作。

你的安全之地 – Homestead

Homestead 是 Laravel 的官方 Vagrant Box,已经包含了你开始所需的一切。你将默认找到它(已安装并运行):

Ubuntu 14.04 Node(带有 Bower、Grunt 和 Gulp)
PHP 5.6 Redis
HHVM Memcached
nginx Beanstalkd
MySQL Laravel Envoy
PostgreSQL Fabric + Hipchat Extension

对于一个可以在几分钟内准备好的工具箱来说,这已经相当不错了!

现在让我们停止讨论,开始安装 Homestead。

安装 Homestead

首先,确保你已经安装了 VirtualBox([www.virtualbox.org/](https://www.virtualbox.org/))和 Vagrant([https://www.vagrantup.com/](https://www.vagrantup.com/))。你可以在任何操作系统上安装它们,所以请随意选择你想要的。

提示

如果你想在 Windows 上使用一个好的 shell,我建议你使用 Cmder([http://bliker.github.io/cmder/](http://bliker.github.io/cmder/))。在撰写这本书时,我参考了相同的链接。

接下来,我们可以将 Homestead 添加到我们的本地虚拟机中。这意味着 Vagrant 将会下载 Homestead 虚拟机以便本地使用。

你可以用一个简单的命令来完成:

vagrant box add laravel/homestead

你可能需要等待几分钟来下载虚拟机。所以,如果你想喝杯咖啡,这是一个完美的时刻。

注意

在这里,你不必担心 Vagrant 将虚拟机放在哪里,因为它将会在 Vagrant 文件夹中本地保存。将来,每次你需要一个特定的虚拟机时,Vagrant 都会克隆并使用它。

好的,你的虚拟机现在已经在你的本地机器上了,并且已经准备好启动。然而,根据你的本地机器设置,你可以以两种不同的方式安装 Homestead。这两种方法都在官方 Laravel 文档中有介绍,所以它们都是官方的

Composer 和 PHP 工具方法

让我们从第一个开始:如果你已经在本地机器上安装了 Composer 和 PHP,这是一个完美的选择。请注意,你只会在第一次执行这些步骤。

使用此命令安装 Homestead CLI 工具。

composer global require "laravel/homestead=~2.0"

然后,确保将~/.composer/vendor/bin目录放入PATH环境变量中,以便在任何地方使用该工具。

之后,你可以使用init命令初始化你的机器。

homestead init

这将会创建一个包含Homestead.yaml文件的~/.homestead文件夹。这个文件将在虚拟机启动时被 Vagrant 使用。

Composer 和 PHP 工具方法

Git 方法

如果你没有在本地机器上安装 PHP 和 Composer(或者你可能只是不想使用它们),没有问题。你可以简单地使用 Git。

选择一个你想要保存虚拟机文件夹的位置。然后,使用以下命令克隆仓库:

git clone https://github.com/laravel/homestead.git HomesteadFolder

在这里,HomesteadFolder是你为 VM 文件选择的位置。在克隆过程之后,使用cd进入文件夹,并使用以下命令启动init脚本:

bash init.sh

这个脚本将在~/.homestead目录中创建一个Homestead.yaml文件,这就完成了!

你刚才看到的两种安装方法的后续步骤是相同的。

配置 Homestead

在我们继续之前,让我们看看默认的Homestead.yaml文件。

  ---
  ip: "192.168.10.10"
  memory: 2048
  cpus: 1

  authorize: ~/.ssh/id_rsa.pub

  keys:
      - ~/.ssh/id_rsa

  folders:
      - map: ~/Code
        to: /home/vagrant/Code

  sites:
      - map: homestead.app
        to: /home/vagrant/Code/Laravel/public

  databases:
      - homestead

  variables:
      - key: APP_ENV
        value: local

如果你对这个语法不熟悉,没问题;这是一个简单的 YAML(YAML 并非一种标记语言)标记文件。这是一种非常易于阅读的方式来指定设置,Homestead 就使用它。在这里,你可以为你的虚拟机选择 IP 地址和其他设置。根据你的需求相应地调整配置文件。

  1. 你在 Homestead.yaml 文件中看到了 authorize 属性吗?好吧,我们将设置我们的 SSH 密钥并将其路径放在那里。如果你感到害怕,不要担心;它只是一个命令。

    ssh-keygen -t rsa -C "you@homestead"
    
    

    如果你使用 Windows,Laravel 文档推荐使用 Git Bash。就我个人而言,如我之前提到的,我更喜欢使用 Cmder。然而,你也可以使用 PuTTY 或你想要的任何东西。使用 ssh-keygen –t rsa –C "you@homestead" 生成你的 SSH 密钥。如下所示:

    配置 Homestead

  2. 将生成的 SSH 密钥路径放在 Homestead.yaml 文件的 authorize 属性中,如下所示:

    authorize: ~/.ssh/id_rsa.pub
    
    

    完成了吗?很好。现在,你可以看到一个 folders 属性了。

    注意

    如我之前提到的,Vagrant 允许开发者在本地和虚拟机之间共享一些文件夹。

    这样做的目的是什么?好吧,这非常重要,因为有了这个系统,我们可以在一个单独的机器上工作我们的项目,同时能够从我们的本地机器使用任何 IDE 或工具。例如,即使 VM 上有 Ubuntu,我也可以轻松地使用 Windows 8.1 和 PHPStorm。两者之最佳!

  3. 默认情况下,Homestead 建议以下结构:

    folders:
          - map: ~/Code
            to: /home/vagrant/Code
    

    此外,这也意味着你将不得不在你的用户文件夹中创建一个 Code 文件夹。这个本地文件夹将被映射到 VM 上的 /home/vagrant/Code 文件夹;你在那里所做的任何更改都将反映在虚拟机上,反之亦然。

    注意

    你可以根据你的需求自定义这个映射。

  4. 接下来,让我们看看 sites 属性。在默认设置中你可以看到以下内容:

      sites:
          - map: homestead.app
            to: /home/vagrant/Code/Laravel/public
    

    你可以为每个项目定义一个自定义域名,这是一个非常舒适的工作方式,因为你将不再需要用 IP 地址(如 192.168.10.10)测试你的项目,只需一个简单的本地域名,例如 myproject.dev

  5. 这是一个为我们的项目定义单独站点的良好时机。所以,请随意将以下行添加到你的文件中:

      - map: eloquent.dev
          to: /home/vagrant/Code/EloquentJourney/public
    
  6. 接下来,转到你的主机文件(在主机机器上)并添加以下记录:

      192.168.10.10 eloquent.dev
    

    你可以在下面的屏幕截图中看到你需要如何添加它:

    配置 Homestead

    注意

    当然,你必须插入你在 Homestead.yaml 文件中指定的相同 IP。

  7. 我们接下来要看到的是 database 属性。对于你添加的每个名称,Homestead 都会自动创建一个数据库来工作。所以,将属性编辑成如下所示:

      databases:
          - homestead
          - eloquent_journey
    

这是因为我们将为我们的测试应用程序使用一个单独的 eloquent_journey MySQL 数据库。

注意

MySQL 服务器的默认用户名是 homestead,默认密码是 secret

我们在这里没有更多的事情要做;我们的设置已经完成,现在我们准备好启动我们的虚拟机并使用它了。

新的藏身之处:Homestead 改进版

即使 Homestead 是一个出色的盒子,许多人抱怨其一些结构上的选择。正如我之前提到的,Vagrant 用于基于项目的虚拟机创建。这意味着,在理想情况下,每个项目都必须有自己的 VM。现在,有了 Homestead,你可以创建一个单独的 VM 并管理你所有的项目。有些人喜欢这个主意,而且这与传统的 XAMPP 方法非常相似。非常熟悉!

然而,其他人更喜欢对 Vagrant 的更纯粹的方法。在研究这个概念的过程中,我偶然发现了由Swader在 GitHub 上创建的Homestead 改进版 (github.com/Swader/homestead_improved)。

这是 Homestead 的改进版本,你可以安装并运行,而无需在用户文件夹中保存文件。这是一个非常好的方法!而且,你也不必配置任何 SSH 密钥或执行apt-get updatecomposer auto-update。一切都将自动完成。

如果你想使用 Homestead 改进版,只需打开你的终端并输入以下命令:

git clone https://github.com/Swader/homestead_improved.git MyHomesteadImprovedVM

在这里,MyHomesteadImprovedVM将是所有虚拟机文件的包含文件夹。

在克隆过程之后,只需输入以下内容:

vagrant up

所以,你已经完成了!比以前更容易,不是吗?

一个额外的工具 – Adminer

在我们深入旅程之前,还有一个非常有用的工具我想向你展示。我正在谈论 Adminer,这是一个完全包含在单个.php文件中的数据库管理工具。你可以在www.adminer.org/下载它。

也许你会发现 Adminer 界面与 phpMyAdmin 界面非常相似。这是真的,但 Adminer 有更多功能。仅举一个简单的例子,phpMyAdmin 只支持 MySQL。相反,Adminer 支持 MySQL、PostgreSQL、SQLite、Oracle 和 MS SQL。

一个额外的工具 – Adminer

显然,你可以使用任何你想要的东西来处理你的数据库。然而,我想向你展示 Adminer,因为它是我偶尔用来展示一些查询结果或各种示例的工具。所以,如果你能更熟悉这个工具就更好了。

你最好的朋友:Laravel

我们即将结束。你有了武器(Composer)和一个安全的地方来做你想要的一切,无需担心问题(Homestead)。那么,盟友呢?Laravel 可能是一个不错的选择,你不这么认为吗?此外,Laravel 是 Eloquent 容器:我们将用它来创建一个新的项目,以充分利用其功能。

安装 Laravel

在继续之前,请记住 Laravel 有一些先决条件。你需要以下内容:

  • PHP 5.4(或更高版本)

  • PHP Mcrypt 扩展

  • PHP OpenSSL 扩展

  • PHP Mbstring 扩展

注意

如果你使用的是 PHP 5.5,你可能需要安装 JSON PHP 扩展。如果是这种情况,只需输入以下内容:

apt-get install php5-json

所以,你可以开始了。

显然,如果你已经安装了 Homestead,所有东西都已经在其正确的位置。

  1. 你需要做的就是使用以下命令启动虚拟机:

    homestead up
    
    
  2. 当引导过程完成后,使用以下命令通过 SSH 进入机器:

    homestead ssh
    
    

话虽如此,正如你可能从 Homestead 经验中了解到的,Laravel 也提供了两种不同的方式来安装它并创建一个新项目。

  • 第一个是通过一个特定的工具完成的,即 Laravel 安装器工具。这是一个 CLI 工具,你可以将其作为全局 Composer 包进行安装。

  • 第二种是一个简单的 composer create-project 命令。当然,我们现在将看到两种方法。

使用 Laravel 安装器工具

Laravel 安装器工具是一个很好的实用工具,它允许你使用非常简单的语法创建一个新的 Laravel 项目。想象一下,你想要在一个名为 my_project 的文件夹中创建一个新的项目。如果你已经安装了该工具,你只需要输入以下内容,无需更多操作:

laravel new my_project

安装工具很简单。只需打开终端,输入以下内容:

composer global require "laravel/installer=~1.1"

如你所见,我们正在使用 global 关键字执行 require 命令。这意味着安装器工具包将被保存在 Composer 的 global 文件夹中,并且工具将在任何地方可用。

注意

如果你运行工具时遇到任何问题,请确保将 ~/.composer/vendor/bin 添加到 PATH 环境变量中。否则,它将无法工作!

使用 Composer create-project 命令

如果你不想安装 Laravel 安装器工具,你可以简单地使用 Composer 的 create-project 命令。

在这种情况下,你只需要使用以下命令:

composer create-project laravel/laravel ProjectName

在这里,ProjectName 代表你想要用作新 Laravel 项目根目录的文件夹名称。

这里没有更多的事情要做!你的 Laravel 项目现在已经完全安装在你指定的文件夹中。

注意

请确保为你的文件夹配置正确的权限,并确保仔细查看 URL 重写规则。如果你查看 Laravel 专门的文档页面 (laravel.com/docs/5.0/installation#pretty-urls),你可以学习如何在 Apache 或 nginx 上进行操作。

第一个项目 – EloquentJourney

一个新的项目将是我们新、精彩的旅程的完美隐喻!在学习优雅(Eloquent)的过程中,我们将构建一个简单的项目。更具体地说,我们将分析一个假设的图书馆管理系统数据相关部分及其组件。

你还在等什么?让我们开始吧!首先,创建一个新的项目(使用你喜欢的任何方法)。我们将我们的新项目命名为 EloquentJourney。在你的服务器文件夹中输入以下内容:

laravel new EloquentJourney

如果你更喜欢,请输入以下内容:

composer create-project laravel/laravel EloquentJourney

等待几秒钟来构建项目,安装过程完成后,你就完成了!你可以使用 cd 命令进入你的新文件夹,看看里面有什么。

太棒了!好吧,但我们接下来要做什么?这里和其他子文件夹中有成千上万的文件!别担心。深呼吸,跟我来。首先,我们需要对 Laravel 配置系统进行一些练习,以便设置合适的数据库连接。

没有它,我们就无法使用 Eloquent!

配置系统

你需要的所有配置信息都存储在 config 目录中。这里的每个文件都有一个相当描述性的名称:app.phpdatabase.phpfilesystems.phpcache.php 等等。实际上,我们将使用其中的两个文件:app.php 用于一些基本设置,以及 database.php,原因很明显。

首先,让我们打开 app.php 文件,看看里面有什么。

<?php

return [

  'debug' => true,

  'url' => 'http://localhost',

  'timezone' => 'Europe/Rome',

  'locale' => 'en',

  'fallback_locale' => 'en',

  'key' => env('APP_KEY', 'SomeRandomString'),

  'cipher' => MCRYPT_RIJNDAEL_128,

  'log' => 'daily',

  // other items here...

];

一个 Laravel 配置文件包含一个返回指令。返回的值是一个关联数组。正如你可以想象的那样,键值系统代表配置项的名称及其值。例如,让我们检查第一个项:

  'debug' => true,

这意味着 app.debug 配置项被设置为布尔值 true。

Laravel 在整个框架代码中使用了这些值,你也可以通过 \Config 类来使用它们。

具体来说,如果你想检索特定项的值,你必须调用 get() 方法,如下所示:

  $myItem = \Config::get('item.name');
  var_dump($myItem);

  // true

你也可以在运行时设置特定的 Config 值,这次使用 set() 方法,如下所示:

  \Config::set('item.name', 'my value!');

  $myItem = \Config::get('item.name');
  var_dump($myItem);

  // "my value!"

设置数据库连接

是的,我们终于到达了这一章的结尾。在这里我们需要做的最后一件事,就是设置数据库连接。

让我们打开 config 下的 database.php 文件。你应该会看到如下内容:

<?php

return [

  'fetch' => PDO::FETCH_CLASS,

  'default' => 'mysql',

  'connections' => [

    'sqlite' => [
      'driver'   => 'sqlite',
      'database' => storage_path().'/database.sqlite',
      'prefix'   => '',
    ],

    'mysql' => [
      'driver'    => 'mysql',
      'host'      => 'localhost',
      'database'  => 'homestead',
      'username'  => 'homestead',
      'password'  => 'secret',
      'charset'   => 'utf8',
      'collation' => 'utf8_unicode_ci',
      'prefix'    => '',
      'strict'    => false,
    ],

    'pgsql' => [
      'driver'   => 'pgsql',
      'host'     => env('DB_HOST', 'localhost'),
      'database' => env('DB_DATABASE', 'forge'),
      'username' => env('DB_USERNAME', 'forge'),
      'password' => env('DB_PASSWORD', ''),
      'charset'  => 'utf8',
      'prefix'   => '',
      'schema'   => 'public',
    ],

    'sqlsrv' => [
      'driver'   => 'sqlsrv',
      'host'     => env('DB_HOST', 'localhost'),
      'database' => env('DB_DATABASE', 'forge'),
      'username' => env('DB_USERNAME', 'forge'),
      'password' => env('DB_PASSWORD', ''),
      'prefix'   => '',
    ],

  ],

  'migrations' => 'migrations',

  'redis' => [

    'cluster' => false,

    'default' => [
      'host'     => '127.0.0.1',
      'port'     => 6379,
      'database' => 0,
    ],

  ],

];

最重要的两项是 defaultconnections。在这个第二项 connections 中,我们存储了连接到我们的数据库所需的所有信息。默认情况下,你会找到许多示例。事实上,在这里你可以看到 sqlitemysql 以及 sqlsrv 连接。

每个连接都有一个驱动。driver 元素表示该连接使用的数据库。如果你需要,你可以指定多个连接。default 元素代表所选连接。

让我们删除所有内容,并用以下内容替换 defaultconnections 元素:

  'default' => 'eloquentJourney',

  'connections' => [

    'eloquentJourney' => [
      'driver'    => 'mysql',
      'host'      => 'localhost',
      'database'  => 'eloquent_journey',
      'username'  => 'homestead',
      'password'  => 'secret',
      'charset'   => 'utf8',
      'collation' => 'utf8_unicode_ci',
      'prefix'    => '',
      'strict'    => false,
    ],
  ],

我们刚才做了什么?

非常简单!我们已经定义了一个 eloquentJourney 连接。这个连接将使用 mysql 驱动。因此,我们将连接 Laravel 到一个 MySQL 服务器。我不会解释其他属性,因为它们的意义很容易理解。

之后,我们指定了连接名称作为默认选项。这意味着,对于未来的每一个数据库相关操作的调用,Laravel 都将连接到 eloquentJourney 连接指定的服务器,并使用给定的凭证。

摘要

我们做到了!

我们为使用 Laravel 和 Eloquent 准备了一切所需。我们搭建了本地开发服务器,学习了 Composer 的基础知识以正确管理我们的依赖,安装了几款更有用的工具,最后成功配置了数据库连接。对于第一章来说,这已经很不错了,不是吗?

然而,我们才刚刚开始,我们在 Eloquent 中的旅程才刚刚起步。我们准备好离开我们的安全屋,深入 Eloquent ORM 的最黑暗角落去探索它,并了解它的所有秘密。

这将是一次美妙的旅程。现在,让我们探索我们路上的第一个主题:模式构建器和迁移系统,以构建完美的数据库!

第二章. 使用 Schema Builder 类构建数据库

嘿,英雄!

我们的开发环境现在已经准备好了。不再有担忧,一个全新的世界在等待:英雄离开了他的家。一步一步,他朝着目标前进。英雄知道什么才是真正重要的:保持脚踏实地。有一个良好的基础是有帮助的。对于应用程序来说,没有太大的区别:为你的项目设计一个良好的数据库始终是最好的开始。

从这个重要的假设开始,问题自然而然地产生:在使用 Laravel 之前,是否有处理数据库设计的方法,也许是一种聪明的方法,你可以轻松管理?答案是,正如你所想象的那样,是的。

在这一章中,我们将探讨 Schema Builder 类。一个非常重要的类,它让你能够不写一行 SQL 就设计整个数据库!如果你考虑一下,这确实很令人印象深刻,考虑到你完全可能在没有使用 SQL 的情况下构建整个基于 SQL 的数据库!我们将分析你可以使用这个类做的一切:创建、删除和更新表;添加、删除和重命名列。我们还将从许多方面探讨索引:不仅包括简单的索引,还包括唯一索引和外键。此外,我们还将探讨 Schema 类提供的一些方法,以便对一切拥有真正的和完全的控制。有时,你需要确保你的数据库中存在什么!

在所有这些之后,事情还没有结束。事实上,我们将探索迁移的世界,这是 Laravel 用于对数据库进行版本控制的方法。结合 Schema 类和迁移系统的力量,将赋予你对数据设计极大的控制权。此外,将你的应用程序与团队的新成员共享将变得非常容易!你知道版本控制始终是最好的选择。

在继续之前的一个重要注意事项:即使 Schema Builder 类和迁移系统与 Eloquent 并没有紧密相关,它们也能轻松地创建一个结构完美适合 Eloquent 标准和约定的数据库。此外,Schema Builder 和迁移系统是 illuminate/database (github.com/illuminate/database) 包的一部分;是的,就是 Eloquent 的同一个包!让我们开始吧。

  • Schema Builder 类

  • 使用迁移系统进行数据库版本控制

Schema Builder 类

Schema Builder 类是一个工具,它为你提供了一个数据库无关的方式来处理你的数据库设计。术语数据库无关意味着你永远不必担心你使用的是哪种数据库:唯一重要的是使用正确的驱动程序,就像我们在第一章的配置部分所看到的那样。所以,如果你考虑将你的数据库系统从 SQLite 切换到 MySQL 或 SQL Server,不用担心:你总是有你的结构准备好使用。

在本章的第一部分,我们将使用一些简单的路由。你所要做的就是将它们添加到 app/routes.php 文件中。

处理表

让我们从最基本的物理现象开始。以下是你可以在数据库中创建新表的 Schema::create() 方法:

处理表

create() 方法接受两个参数:第一个参数是你想要创建的表名,第二个参数是一个闭包,我们将在这里指定所有表字段。更准确地说,闭包参数是一个 Blueprint 对象。

提示

当可能时,我喜欢使用类型提示来提高代码的可读性。

因此,你可能读到如下内容:

Schema::create('users', function(Blueprint $table)
{
$table->increments('id');
});

别担心,它是一样的。

你还将能够重命名一个表:在这种情况下,只需使用 rename() 方法。

Schema::rename($previousName, $newName);

最后,关于删除表怎么办?drop() 方法就是为你准备的。

  Schema::drop($tableName);

还有一个类似的方法,dropIfExists(),语法相同。

  Schema::dropIfExists($tableName);

正如令人惊叹的表达性名称所暗示的,该方法仅在表存在于数据库中时才会删除表。

提示

我经常使用这个方法而不是简单的 drop() 方法。

记住,如果你想检查一个表是否存在,你可以使用 hasTable() 方法,如下所示:

  if (Schema::hasTable('books')) 
  { 
    // the table "books" exists...
  }

现在,让我们看看如何处理表列。

处理列

处理列相当简单。首先,让我们看看在向数据库添加新表时如何创建新列。

正如我之前告诉你的,我们将使用 Schema::create()$table 参数。

  Schema::create('books', function(Blueprint $table)
  {
      $table->increments('id');

      $table->string('title');

      $table->integer('pages_count');
      $table->decimal('price', 5, 2);

      $table->text('description');

      $table->timestamps();
  });

Blueprint $table 对象有许多方法,这些方法与创建单个新列有关。你可以很容易地看到所有方法的名字代表了一种特定的列类型。例如,string 是 MySQL 中的 VARCHAR 的等价物,而 integerINT 的等价物。

此外,还有一些实用方法:如果你查看闭包的第一个代码行,你会看到一个 increments() 方法。然而,这并不复杂,它只是创建了一个带有 autoincrementid 整数字段(并将其设置为主键)。

最后,你可以在最后一行看到 timestamps() 方法。Schema Builder 类还有一些与 Eloquent 功能相关的方法。在这种情况下,timestamps() 方法自动创建了两个 DATETIME 等效字段,分别命名为 created_atupdated_at。如果你想要跟踪插入和最后更新操作的时间,它们可能非常有用。

现在,在我们继续前进之前,为什么不进行一些测试呢?

这只需要几秒钟:只需在 app/routes.php 文件中创建一个新的 GET 路由,并添加我们看到的 Schema::create() 方法,如下所示:

  // remember this "use" directive!
  use Illuminate\Database\Schema\Blueprint;

  Route::get('create_books_table', function(){

    Schema::create('books', function(Blueprint $table)
    {
        $table->increments('id');

        $table->string('title', 30);

        $table->integer('pages_count');
        $table->decimal('price', 5, 2);

        $table->text('description');

        $table->timestamps();
    });

  });

我们将在数据库中创建一个名为 books 的表。这个表将包含七个列:

  • 唯一的主键 id 是一个整数

  • title 列作为一个字符串(最多 30 个字符)

  • pages_count 整数列

  • price 小数列

  • description 文本列

  • timestamps() 方法创建的 created_atupdated_at 字段

让我们导航到 /create_books_table URL,然后打开你的 Adminer(或你喜欢的任何数据库处理工具)来验证是否一切顺利。

这就是结果!

处理列

干得好!让我们看看结构。

处理列

完美。正是我们想要的——没有一行 SQL 代码!

注意,就像我们之前为表所做的那样,我们也可以使用 hasColumn() 方法来检查是否存在特定的列:

  if (Schema::hasColumn('books', 'title')) 
  { 
    // the column "title" in the "books" table exists...
  }

列的方法参考

这里是所有可以在 Blueprint $table 实例上使用的所有方法的快速参考:

方法 描述
$table->bigIncrements('id'); 使用 大整数 等价项进行 ID 增量
$table->bigInteger('votes'); BIGINT 等价于表中的字段
$table->binary('data'); BLOB 等价于表中的字段
$table->boolean('confirmed'); BOOLEAN 等价于表中的字段
$table->char('name', 4); CHAR 等价,长度为
$table->date('created_at'); DATE 等价于表中的字段
$table->dateTime('created_at'); DATETIME 等价于表中的字段
$table->decimal('amount', 5, 2); DECIMAL 等价,具有精度和小数位数
$table->double('column', 15, 8); DOUBLE 等价,精度为总精度 15 位,小数点后 8 位
$table->enum('choices', ['foo', 'bar']); ENUM 等价于表中的字段
$table->float('amount'); FLOAT 等价于表中的字段
$table->increments('id'); ID 增量到表中(主键)
$table->integer('votes'); INTEGER 等价于表中的字段
$table->json('options'); JSON 等价于表中的字段
$table->longText('description'); LONGTEXT 等价于表中的字段
$table->mediumInteger('numbers'); MEDIUMINT 等价于表中的字段
$table->mediumText('description'); MEDIUMTEXT 等价于表中的字段
$table->nullableTimestamps(); timestamps() 相同,但允许 NULL
$table->smallInteger('votes'); SMALLINT 等价于表中的字段
$table->tinyInteger('numbers'); TINYINT 等价于表中的字段
$table->string('email'); VARCHAR 等价列
$table->string('name', 100); VARCHAR 等价,字符串长度为
$table->text('description'); TEXT 等价于表中的字段

注意

你也可以在 Laravel 官方文档的 laravel.com/docs/5.0/schema#adding-columns 页面找到完整的参考。

其他 $table 对象方法

如果你查看 Laravel 文档中的方法参考,你可能会看到一些你刚才看到的列表中没有的内容。

第一个方法参考是:

  $table->timestamps();

我已经在示例中使用了这个方法。它创建了一个包含 created_atupdated_at 列的表,如果你想要跟踪特定记录的创建或更新时间,这些列非常有用。

Eloquent 还会自动管理时间戳列,所以你不需要在你的代码中担心它们。

另一个重要(并且非常相似)的方法如下:

  $table->softDeletes();

它用于向目标表添加一个 deleted_at 列。

有时候,在你的应用程序中,保留一些信息即使你不想向最终用户展示也是非常有用的。想想电子商务订单的历史记录:客户可以选择清理他的历史记录,但你不能允许他/她物理删除记录!

软删除系统通过向表中添加一个 deleted_at 列来跟踪记录的删除日期,从而解决了这个问题。

Eloquent 会自动处理这个系统,所以你将能够向客户展示一个干净的历史记录,并向商店管理员展示完整的订单列表。

接下来,你可以看到这个:

  $table->rememberToken();

此方法添加一个 remember_token 列。

Laravel 认证系统使用这个令牌(一个简单的 VARCHAR 100)来跟踪用户状态。当你在应用程序的登录页面点击 记住我 复选框时,它会用到这个令牌。

在某些表格列定义之后,还有一些你可以链式调用的方法。

如果你想要使一个数值列无符号,可以使用以下方法:

  $table->integer('my_column')->unsigned();

当然,你可以将它与每个数值字段(浮点数、十进制数等)一起使用。

你也可以使用 nullable() 来指定一个列是否可以为空:

  $table->string('my_column')->nullable();

如果你想要指定一个默认值,可以使用以下方法:

  $table->string('my_column')->default('my_default_value');

小贴士

有时候,你可能需要为你的主键使用 BIGINT 或等效类型。如果是这种情况,请使用 bigIncrements() 而不是 increments()(它使用 INT 或等效类型)。

更新表格和列

什么?是的,我知道你在想什么——如果我必须添加一个名为《了不起的弗朗切斯科·马拉泰斯塔的超级精彩生活》的书名怎么办?这是一个 60 个字符长的名字!

事情会变化,所以我们的数据库也必须随之改变。简单来说,你会使用 table 方法来更新现有的表格。

这里你可以看到一个示例:

  Schema::table('table_name', function(Blueprint $table)
  {
    // update operations here...
  });

那么,让我们进行一点更新。在我们的路由文件中创建一个新的路由,并将其命名为 update_books_table

  Route::get('update_books_table', function(){

      Schema::table('books', function(Blueprint $table)
      {
          $table->string('title', 250)->change();
      });

  });

注意,我们在 string() 方法之后立即使用了 change() 方法。你将在更改列时使用 change()。如果你只是想向现有表中添加一个新列,只需像平常一样使用它:

  $table->string('title', 250);

注意

你需要 doctrine/dbal 包来更改列。要获取它,只需将 doctrine/dbal 添加到你的 composer.json 文件中的 2.5.0 依赖项。

然后,在你的终端中输入 composer update,然后你就可以开始了。

浏览到新的 URL 并更新表格。现在你将能够在未来将我的精彩传记添加到你的图书馆中。为此感到自豪吧。

索引和外键

让我们再向前迈一步。让我们想象一下,我们正在升级我们的应用程序,并希望添加存储书籍作者和相关数据的功能。此外,我们还将给用户提供通过书名搜索书籍的可能性。

我们可能需要做以下几件事情:

  • 创建authors表以存储作者数据

  • 更新books表,插入一个外部外键和一个索引到title

你已经知道如何更新现有的表,但关于索引列和创建外键呢?

使用 Schema Builder 添加索引相当简单。有一些表达性的方法你可以使用。

首先,你可以使用primary()方法创建一个主键。参数是你想要索引的字段。

  $table->primary('id');

如果你需要,你可以指定一个列的数组作为单个索引。

  $table->primary(['first_name', 'last_name', 'document_number']);

使用unique()方法来创建一个唯一键:

  $table->unique('email');

最后,如果你想添加一个简单的索引,你可以使用index()方法!

  $table->index('title');

如果需要,你也可以添加外键。在下面的例子中,我们正在将假设的书籍表中的author_id列(可能)引用到authors表中的id列。

  $table->foreign('author_id')->references('id')->on('authors');

指定onDeleteonUpdate约束操作不是问题。

  $table->foreign('author_id')
      ->references('id')
      ->on('authors')
      ->onDelete('cascade');

如果你不再需要你的索引,你可以轻松地删除它们:

  $table->dropPrimary('authors_id_primary');
  $table->dropUnique('authors_email_unique');
  $table->dropIndex('books_title_index');
  $table->dropForeign('books_author_id_foreign');

注意

创建索引名称的约定是table-name_column-name_index-type

好吧,现在你对 Schema Builder 类中的索引有了很好的了解,让我们给我们的示例添加新的功能。

创建一个新的GET路由。这次它被命名为update_books_table_2

  Route::get('update_books_table_2', function(){

      // creating the authors table...
      Schema::create('authors', function(Blueprint $table)
      {
          $table->increments('id');

          $table->string('first_name');
          $table->string('last_name');

          $table->timestamps();
      });

      Schema::table('books', function(Blueprint $table)
      {
          // creating the index on the title column...
          $table->index('title');

          // creating the foreign key...
          $table->integer('author_id')->unsigned();
          $table->foreign('author_id')->references('id')->on('authors');
      });

  });

就这样!让我们用 Adminer 验证所有内容,就像我们之前做的那样:

索引和外键

哟!它成功了。

使用迁移系统进行数据库版本控制

到目前为止,我们使用 Schema 类和一些简单的路由进行工作。相当简单,但这并不是一个好的实践。你知道:单一责任原则不仅仅是一个睡前故事。

此外,这不仅仅关乎代码:数据库也应该有一个功能齐全的版本控制系统,以便跟踪所有更新并帮助新成员加入你的团队。

好吧,迁移系统就在这里帮助你。你可以把它看作是你数据库的版本控制系统,它由许多文件组成。这些文件中的每一个都是一个类,包含两个方法:up()down()。在up()方法中,你会放置所有数据库构建逻辑。而在down()方法中,你将放置与在up()方法中执行的操作相关的任何回滚操作。

创建迁移

让我们制作第一个基本示例。我们已经有booksauthors表。想象一下,我们想要收集关于我们书籍出版者的数据。

我们将不得不:

  • 创建一个名为publishers的新表,至少包含出版者的名称

  • books表中创建一个新的外键publisher_id

让我们通过迁移文件来实现!打开你的终端并输入以下命令:

  php artisan make:migration publishers_update

等待几秒钟,然后转到database/migrations文件夹。你将找到一个类似这样的文件:

  2015_03_06_105131_publishers_update.php

打开它。这个类已经有了空的up()down()方法:

  <?php

    use Illuminate\Database\Schema\Blueprint;
    use Illuminate\Database\Migrations\Migration;

    class PublishersUpdate extends Migration {

      public function up()
      {
      //
    }

      public function down()
      {
        //
      }

    }

现在,用一些 Schema Builder 指令填充它们,就像这样。

  <?php

    use Illuminate\Database\Schema\Blueprint;
    use Illuminate\Database\Migrations\Migration;

    class PublishersUpdate extends Migration {

      public function up()
      {
        Schema::create('publishers', function(Blueprint $table)
        {
          $table->increments('id');

          $table->string('name');

          $table->timestamps();
        });

        Schema::table('books', function(Blueprint $table)
        {
          $table->integer('publisher_id')->unsigned();
          $table->foreign('publisher_id')->references('id')->on('publishers');
        });
      }

      public function down()
      {
        Schema::table('books', function(Blueprint $table)
        {
          $table->dropForeign('books_publisher_id_foreign');
          $table->dropColumn('publisher_id');
        });

        Schema::drop('publishers');
      }

    }


非常线性:我们在up()中做的,我们在down()中撤销。当然,顺序相反。记住,这是一个镜像操作!

现在,是时候运行你的迁移了。你所要做的就是运行php artisan migrate;它加载所有迁移类并执行它们的up()方法。这样,你可以根据你的文件构建你的数据库。执行顺序由文件名决定。我们刚刚创建的文件名为2015_03_06_105131_publishers_update.php

这意味着这个文件是在 2015 年 3 月 6 日 10:51:31 创建的。正如你可以轻易想象的,所有的迁移都将按时间顺序执行。为了使事情更清晰,删除两个2014_10_12_000000_create_users_table.php2014_10_12_100000_create_password_resets_table.php。我们不需要它们。

回到你的终端并输入:

  php artisan migrate

等待几秒钟,你将得到一个与以下输出非常相似的结果:

  Migration table created successfully.
  Migrated: 2015_03_06_105131_publishers_update

现在,回到你的数据库。更新已经成功完成,但我们还有一个新的表:migrations表。Laravel 使用这个表来跟踪数据库的每一次更新。所以,如果你对你的应用程序(通过另一个新的迁移)进行另一次更新,并且再次运行php artisan migrate命令,你将只执行新的迁移,而不是旧的迁移。没错,就像我之前提到的,这是版本控制。

回滚迁移

让我们现在尝试这个rollback操作。在你的终端中输入

  php artisan migrate:rollback

然后,回到你的数据库:没有更多的publishers表和没有更多的相关外键!太酷了!

明白了?好。现在,再次输入这个:

  php artisan migrate

让我们继续前进。

小贴士

在执行php artisan migrate命令时,你可能会遇到class not found错误。

别担心,只需在终端中输入composer dump-autoload并再次尝试!即使是最好的,有时也会犯错误。

更多示例,更多迁移

现在,关于另一个示例呢?

让我们再想象另一个功能。好吧,我有一个想法:想象一下,你想为你的应用程序中存储的每一本书指定一些标签。这次你将有两件事情要做:

  • 创建一个新的tags表来存储标签。

  • 创建一个新的book_tag表,因为我们将要表示多对多关系。在这个表中,你会找到book_idtag_id

首先,创建一个新的迁移。打开你的终端并输入:

  php artisan make:migration tags_update

此外,更新文件如下:

  <?php

    use Illuminate\Database\Schema\Blueprint;
    use Illuminate\Database\Migrations\Migration;

    class TagsUpdate extends Migration {

      /**
       * Run the migrations.
       *
       * @return void
       */
      public function up()
      {
        Schema::create('tags', function(Blueprint $table)
        {
          $table->increments('id');

          $table->string('name');

          $table->timestamps();
        });

        Schema::create('book_tag', function(Blueprint $table)
        {
          $table->increments('id');

          $table->integer('book_id')->unsigned();
          $table->integer('tag_id')->unsigned();

          $table->foreign('book_id')->references('id')->on('books');
          $table->foreign('tag_id')->references('id')->on('tags');
        });
      }

      /**
       * Reverse the migrations.
       *
       * @return void
       */
      public function down()
      {
        Schema::drop('book_tag');
        Schema::drop('tags');
      }

    }

注意

我喜欢同时编写up()down()方法。在你刚才看到的例子中,我把tagscreate方法放入了up()中。然后,我传递到down()方法并添加了drop('tags')方法调用。然后,我回到up()并添加了book_tag表,最后我又回到down()来删除book_tag表。

以这种方式工作有助于我减少分心错误。

现在,打开终端并输入:

  php artisan migrate

前往你的数据库。打开migrations表来查看发生了什么:

更多示例,更多迁移

这里有两个不同的批次。第一个批次(1)与发布者更新相关。第二个批次(2)与标签系统更新相关。

我为什么要告诉你这些?谁在乎呢?

嗯,当你你在终端中输入php artisan migrate:rollback时,迁移系统会寻找最后一个批次并将其回滚。不是每个批次,而是最后一个批次。这意味着第一个rollback命令将撤销与标签系统相关的一切。如果你再次输入,迁移系统也将撤销与发布者更新相关的一切。

通过批次号,你可以知道你在数据库上进行了多少次迭代。然而,还有另一件重要的事情你需要知道:关于我们的例子,如果你回滚两次然后再次迁移,这两个迁移文件都将被归入批次 1。

同样,你也可以使用以下命令回滚所有内容:

  php artisan migrate:reset

你可以使用以下命令回滚并再次迁移所有内容:

  php artisan migrate:refresh

最后,你可以使用以下命令获取所有已迁移/回滚的迁移列表:

  php artisan migrate:status

那就结束了!使用这个最后的命令,我们也完成了迁移。

摘要

最后,英雄离开了他的家。经过培训和获得正确的基石后,他真的准备好了。

记住,Schema Builder 类和迁移系统不仅仅是关于构建你的数据库。在本章中,你学习了如何以更智能的方式改进你的数据库设计方法,而不需要写一行 SQL 语句。此外,通过使用迁移,你将不再需要下载数据库备份,或者在某些极端情况下,安装任何额外的工具来处理它。

所有操作都是通过artisan migrate命令完成的。然而,正如我已经告诉你的,这只是一个示例。真正的事情即将到来。

在下一章中,我们将深入探讨最重要的、原子的 Eloquent 组件:模型。

你准备好了吗,英雄?

第三章。最重要的元素——模型!

最后,我可以说:从现在开始,事情变得真的、真的严肃了。在前面的章节中,你学习了开始所需的一切。也许有点烦人,但你都知道你需要它。现在,别再谈论过去;它已经过去了。在这一章中,许多令人惊奇的事情等着你。

如标题所示,这部分将关于最重要的、原子的 Eloquent 元素:模型。我们将分析MVC中的M。考虑到你正在阅读一本关于创建基于数据的应用程序的书,这非常重要!然而,我不想再让你感到无聊了。让我们谈谈下一节我们将看到的内容。

通常,查看 Eloquent 文档有一种标准的方式。它和你在 Laravel 网站上看到的是一样的。在我最初的 Laravel 日子里,我使用过那些页面,但我感觉有点不完整。所以,我稍微改变了一下,但以一种更简化的方式。如果你一生中曾经开发过网络应用程序,我相当确信你曾经制作过一个基于数据的带有表格和记录的应用程序。对吧?嗯...

如果你这么想,你可以在它们上执行四个基本操作。无论你开发什么应用程序,你都有很高的可能性将实现一些创建读取更新删除逻辑。这正是我们将在本章中看到的内容。

首先,我们将介绍一个简单的、基本的模型。它将是一行代码!然后,我们将讨论使用 Eloquent 的 CRUD(创建、读取、更新和删除)操作。从那时起,你将拥有所有基础和机制的概述。我们还将处理where()方法和所有相关内容,解开有关条件和选择的一切。

之后,我们将深入探讨模型类,研究批量赋值作为存储和更新我们数据的一种方式。然后,我们将讨论时间戳软删除,了解 Laravel 和 Eloquent 如何处理日期。之后,你将了解查询范围以及如何使用它们来提高你的开发过程。

此外,我们还将查看许多酷炫的方法来转换我们的数据,以便正确显示(或存储):属性转换、修改器、日期修改器和访问器。如果还不够,我们将探索所有与模型相关的你可以用来在不破坏代码的情况下引入新行为的事件。当事件不够时,我们还将分析模型观察者。

在本章的最后部分,我们将探索一些有用的方法和模型类的功能,直接深入基本模型类的代码!不错吧?让我们开始!以下是主题:

  • 创建模型

  • 创建、读取、更新和删除操作基础

  • 哪里,聚合和其他实用工具

  • 批量赋值——面向大众

  • 查询范围

  • 属性转换、访问器和修改器

  • 模型事件和观察者

  • 代码深入

创建模型

首先,让我们看看如何创建一个模型以及其基本结构是如何构成的。

创建模型最快的方式是使用以下命令,你可以使用参数来指定模型名称:

php artisan make:model

所以,让我们想象一下,你想要创建一个 Book 模型。步骤如下:

  1. 你需要做的只是使用以下命令:

    php artisan make:model Book
    
    

    当然,你也可以手动创建它;通常,Laravel 将模型放在 app 文件夹中。

  2. 一旦你完成了这些,让我们打开 app 目录下的 Book.php 文件,看看里面有什么。创建模型

等等。什么?一个空类?真的吗?

是的。

Laravel 设计得非常快速地创建一个优秀的网络应用程序。Eloquent(及其模型)也不例外。你在这里看到的类已经准备好在你的应用程序中使用;使用它,你将能够完成与你的书籍相关的所有操作。

谈到 SQL 数据库,你可以想到每个模型和表之间的连接。如果你遵守某种约定,Laravel 会自动根据模型名称猜测表名。所以,如果我有一个名为 Book 的模型,Laravel 将会在数据库中搜索一个名为 books 的表,无需明确指定。

如果你需要将某个模型与另一个表绑定,你可以指定名称,将其作为 $table 属性添加,如下所示:

  <?php 

namespace App;

use Illuminate\Database\Eloquent\Model;

class Book extends Model {

    protected $table = 'my_books';

}

好吧,这里没有更多要说的了。现在,让我们来玩我们的新模型。

创建、读取、更新和删除操作的基本知识

我读过的每一篇关于 Eloquent 的文章通常都以一些读取操作开始。我不喜欢这样。我将教你如何创建和插入新记录,然后我们将使用一些读取操作来检索它们。

没有使用外部管理工具的枯燥测试插入。

创建操作

让我们创建我们的第一本书!作为记录结构的参考,我们将使用我们在上一章中创建的 books 表。该表结构非常简单:标题、页数(pages_count)、价格和描述。

这个过程就像创建一个对象一样简单。

嗯,实际上完全一样。在 app/Http/ 目录下的 routes.php 文件中创建一个新的 GET 路由,命名为 book_create 并输入以下内容:

  Route::get('book_create', function(){

    $book = new \App\Book;

    $book->title = 'My First Book!';
    $book->pages_count = 230;
    $book->price = 10.5;
    $book->description = 'A very original lorem ipsum dolor sit amet...';

  });

如果你仔细想想,这里有一些奇怪的地方!在检查模型文件后,你可以看到没有声明 titlepages_count 属性。在其他方面,Eloquent 大量使用魔法方法。当最终查询构建时,Laravel 将使用属性名称作为表列来填充。这很重要!

现在,如果你运行这段代码然后检查你的表,你将找不到任何记录。你必须添加一个单一的最终指令:调用 save() 方法。

  Route::get('book_create', function() {

      $book = new \App\Book;

      $book->title = 'My First Book!';
      $book->pages_count = 230;
      $book->price = 10.5;
      $book->description = 'A very original lorem ipsum dolor sit amet...';

      $book->save();

  });

执行它。现在,你的书已经保存在数据库中了。

如果你愿意,你甚至可以在保存后访问特定记录的字段。让我们再举一个例子。

  Route::get('book_create', function() {

      $book = new \App\Book;

      $book->title = 'My First Book!';
      $book->pages_count = 230;
      $book->price = 10.5;
      $book->description = 'A very original lorem ipsum dolor sit amet...';

      $book->save();

      echo 'Book: ' . $book->id;

  });

注意

关于 Eloquent 约定还有另一件小事;那就是,每个表都有一个 ID,自动递增为主键。

读取操作

现在我们已经创建了一些示例记录,为什么不尝试读取它们呢?一个例子胜过千言万语。

  Route::get('book_get_all', function(){

    return \App\Book::all();

  });

在这里,我们用一条指令返回所有表记录。输出将非常类似于以下内容:

  [
    {
      id: 1,
      title: "My First Book!",
      pages_count: 230,
      price: "10.50",
      description: "A very original lorem ipsum dolor sit amet...",
      created_at: "2015-03-24 16:45:59",
      updated_at: "2015-03-24 16:45:59"
    }
  ]

注意

如果您对此感到奇怪,请不要担心。如果您在一个路由(或控制器方法)中返回 Eloquent 模型查询的结果,结果将自动转换为 JSON。如果您正在考虑构建 RESTful API,这是一个非常有用的快捷方式。

让我们再创建一本书,给我们的测试增加更多元素。

    $book = new \App\Book;

    $book->title = 'My Second Book!';
    $book->pages_count = 122;
    $book->price = 9.5;
    $book->description = 'Another very original lorem ipsum dolor sit amet...';

    $book->save();

在这里,再次在book_get_all路由中执行您的代码。结果将如下所示:

  [
    {
      id: 1,
      title: "My First Book!",
      pages_count: 230,
      price: "10.50",
      description: "A very original lorem ipsum dolor sit amet...",
      created_at: "2015-03-24 16:45:59",
      updated_at: "2015-03-24 16:45:59"
    },
    {
      id: 2,
      title: "My Second Book!",
      pages_count: 122,
      price: "9.50",
      description: "Another very original lorem ipsum dolor sit amet...",
      created_at: "2015-03-24 16:57:15",
      updated_at: "2015-03-24 16:57:15"
    }
  ]

然而,我们可以做得更多。实际上,另一个很好的方法是find()方法。您可以使用它这样:

  Route::get('book_get_2', function(){

      return \App\Book::find(2);

  });

此方法以主 ID 作为参数,并返回单个记录作为模型的实例。

看看它的输出:

  {
    id: 2,
    title: "My Second Book!",
    pages_count: 122,
    price: "9.50",
    description: "Another very original lorem ipsum dolor sit amet...",
    created_at: "2015-03-24 16:57:15",
    updated_at: "2015-03-24 16:57:15"
  }

注意,这次您有一个单个对象而不是数组。当然,这些静态方法并不是 Eloquent 所能提供的一切。有趣的部分从这里开始。

看看这个:

  Route::get('book_get_where', function(){

      $result = \App\Book::where('pages_count', '<', 1000)->get();
      return $result;

  });

您可以使用where()方法来过滤您的结果。然后,在指定您的标准后,get()方法从数据库中检索结果。为了更好地理解,想象一下where()方法正在构建一个查询。get()方法执行它。最后一个方法是触发方法。

如果您只想检索第一个结果而不是所有结果,您可以使用first()方法而不是get()

  Route::get('book_get_where', function(){

      $result = \App\Book::where('pages_count', '<', 1000)->first();
      return $result;

  });

提示

在我们继续之前,这里有一个可以节省您很多时间的提醒。实际上,当您使用触发方法,如get()first()时,您可以得到两种不同类型的结果。

当您使用first()时,您正在选择一个单一实例。因此,您将作为结果(如果存在)收到某个模型的单个实例。否则,如果您使用all()get(),您将获得实例的集合。

回到我们的where(),您可以按需链式调用多个调用。

  Route::get('book_get_where_chained', function(){

      $result = \App\Book::where('pages_count', '<', 1000)
              ->where('title', '=', 'My First Book!')
              ->get();

      return $result;

  });

您可以通过非常简单的方式遍历结果:简单的 for each 就足够了。

  Route::get('book_get_where_iterate', function(){

      $results = \App\Book::where('pages_count', '<', 1000)->get();

      if(count($results) > 0)
      {
        foreach($results as $book){

            echo 'Book: ' . $book->title . ' - Pages: ' . $book->pages_count . ' <br/>';

        }
      }
      else
        echo 'No Results!';

      return '';
  });

$results对象是可计数的,因此您也可以检查是否有结果。

看看这一行:

  echo 'Book: ' . $book->title . ' - Pages: ' . $book->pages_count . ' <br/>';

您可能已经注意到,您可以用与设置它们相同的方式访问单个记录字段:魔法方法。

注意

如果您喜欢,您可以通过数组的方式访问记录字段。

尝试将$book->title切换到$book['title']并看看会发生什么。

更新操作

更新记录与创建它一样简单。说实话,这完全一样,只是在第一条指令中有一个小小的变化。

  Route::get('book_update', function() {

      $book = \App\Book::find(1);

      $book->title = 'My Updated First Book!';
      $book->pages_count = 150;

      $book->save();

  });

我们不是在创建它,而是在数据库中检索我们想要的模型实例。之后,使用魔法方法,我们修改并保存了它。就像插入过程一样,你做的更改在save()调用后变得持久。

删除操作

删除记录是最简单的事情。

  Route::get('book_delete_1', function() {

      \App\Book::find(1)->delete();

  });

是时候让Book消亡了!

注意

这是一个关键概念。当你使用update()delete()方法时,你正在对一个模型实例进行操作,就像你之前在创建它时做的那样。

所以,如果你运行\App\Book::find(1)指令,你将得到一个Book类实例作为结果。对于一些人来说,这很明显,但许多新来者经常会遇到这个问题。

聚合、where 和其他实用工具

毫无疑问,where()方法将在你使用 Eloquent 构建查询和选择记录时成为你的最佳拍档。你不认为详细了解一下它很有价值吗?

让我们回顾一下我们已经知道的内容。

你可以使用where()方法来过滤结果。你必须使用的正确语法是:

  where('field_name', 'operator', 'term')

例如,你可以使用以下方式过滤所有页数少于 100 页的书籍:

  where('pages_count', '<', 100)

你还可以链式调用更多的where方法,一个接一个,来构建更复杂的查询。让我们选择所有那些页数少于 100 页,标题以M开头的书籍。

  where('pages_count', '<', 100)->where('title', 'LIKE', 'M%')

注意

两个链式条件等同于condition1 AND condition2

太棒了!

where 和 orWhere

然而,你知道这还不够。通常,在现实世界的应用中,你可能需要更复杂的条件。首先,你可能需要得到满足一个条件或另一个条件的查询结果。AND条件不是标准的;所以,这里有一个解决方案:orWhere()

  Route::get('book_get_where_complex', function(){

      $results = \App\Book::where('title', 'LIKE', '%Second%')
              ->orWhere('pages_count', '>', 140)
              ->get();

      return $results;

  });

在这段代码中,我们告诉 Eloquent 获取所有标题中包含单词Second的书籍或所有页数超过 140 页的书籍。

是的,这比之前好多了。

现在让我们尝试想象一个更复杂的条件:我们想要找到所有页数超过 120 页且标题中包含单词Book的书籍,或者所有页数少于 200 页且描述为空的书籍。

  Route::get('book_get_where_more_complex', function(){

      $results = \App\Book::where(function($query){

          $query
              ->where('pages_count', '>', 120)
              ->where('title', 'LIKE', '%Book%');

      })->orWhere(function($query){

          $query
              ->where('pages_count', '<', 200)
              ->orWhere('description', '=', '');

      })->get();

      return $results;

  });

你不需要为where()orWhere()方法指定三个参数,你可以使用一个接受$query参数的单个闭包参数。从那个$query对象开始,你将能够以你喜欢的任何方式执行选择和过滤。

当然,你可以嵌套很多这样的工具:

  Route::get('...', function(){

      $results = \App\Book::where(function($query){

          $query
              ->where(function($query){

                // other conditions here...

                $query->where(function($query){

                  // deeper and deeper in the seas of conditions...

                });

              })
              ->orWhere('field', 'operator', 'condition');

      })->orWhere(function($query){

          $query
              ->where('field', 'operator', 'condition')
              ->orWhere(function($query){

          // other conditions here...

              });

      })->get();

      return $results;

  });

好吧,停下来。我认为这个概念现在已经很清晰了。让我们看看where的其他形式。

首先,这里有whereBetween,你可以用它来使用范围而不是单个值来过滤某些字段。

  $results = \App\Book::whereBetween('pages_count', [100, 200])->get();

就这样,我们得到了所有页数在 100 到 200 之间的书籍。

你也可以使用whereIn来检查一个特定字段是否在其它值的数组中。

  $results = \App\Book::whereIn('title', ['My First Book!', 'My Second Book!'])->get();

最后,如果你想获取所有某个列等于 null 的记录,可以使用whereNull

  $booksThatDontExist = \App\Book::whereNull('title')->get();

魔法 where

另一个很棒且酷炫的特性是 魔法 where。你知道 Laravel 中到处都有一些魔法。显然,Eloquent 也不例外。

实际上,你可以为你的 where 子句使用一种替代语法,这是一种魔法语法,允许你将感兴趣的字段定义为方法名。

你已经知道了这种语法:

Route::get('book_get_where', function(){

      $result = \App\Book::where('pages_count', '=', 1000)->first();
      return $result;

  });

现在,你可以用这段代码得到相同的结果:

Route::get('book_get_where', function(){

      $result = \App\Book::wherePagesCount(1000)->first();
      return $result;

  });

显然,wherePagesCount 方法不存在,但 Laravel 会自动使用 PHP 魔术方法创建一个快速的 where 子句。正如你从示例中看到的那样(以及语法所暗示的),你并不能每次都使用这种技术,因为它只适用于等号。

然而,了解一个类似的快捷方式是很好的,对吧?

聚合

有时候,你需要使用聚合函数。没问题!这是你如何使用 Eloquent 来做到这一点的示例。

  \App\Book::count();

这是一个其实际应用的例子:

  Route::get('book_get_books_count', function(){

      $booksCount = \App\Book::count();
      return $booksCount;

  });

正如你之前在 get()first() 方法中看到的,你可以使用聚合方法与 where 方法一起使用。在这里,我们正在计算页数超过 140 页的书籍数量。

  Route::get('book_get_books_count', function(){

      $booksCount = \App\Book::where('pages_count', '>', 140)->count();
      return $booksCount;

  });

显然,count() 不是唯一的聚合。让我们通过一些其他的 where() 示例来看看它们,以便进行更多的练习。这次,我们正在寻找页数最少的书籍(但至少是超过 120 页的)。

  Route::get('book_get_books_min_pages_count', function(){

      $minPagesCount = \App\Book::where('pages_count', '>', 120)->min('pages_count');
      return $minPagesCount;

  });

你也可以用 max() 做同样的事情:

  Route::get('book_get_books_max_pages_count', function(){

      $maxPagesCount = \App\Book::where('pages_count', '>', 180)->max('pages_count');
      return $maxPagesCount;

  });

现在,让我们找到所有书名中包含 Book 单词的书籍的平均价格:

  Route::get('book_get_books_avg_price', function(){

    $avgPrice = \App\Book::where('title', 'LIKE', '%Book%')->avg('price');
      return $avgPrice;

  });

最后,让我们得到所有页数超过 100 页的书籍的总页数。

  Route::get('book_get_books_avg_price', function(){

    $countTotal = \App\Book::where('pages_count', '>', 100)->avg('price');
      return $countTotal;

  });

实用方法

如你所想象的那样,Eloquent 有很多实用工具和方法可以提升你的开发者生活。试图涵盖每一个单独的方法可能会很困难,但除了最常见的方法之外,还有一些其他的方法值得在这里提及。

首先是 skip()take() 方法,这些方法用于在你的查询中实现分页。

  $books = \App\Book::skip(10)->take(10)->get();

我们正在告诉 Eloquent 取前 10 条记录并跳过前 10 条。所以,如果 skip(0)->take(10) 会取第 1 到第 10 条记录,skip(10)->take(10) 会取第 11 到第 20 条,以此类推。

当然,你也不能错过 orderBygroupByhaving 方法。如果你对 SQL 数据库有些了解(而且如果你在这里,我想你确实了解),你不会对理解这段代码有任何问题:

  // orderBy
  \App\Book::orderBy('title', 'asc')->get();

  // groupBy
  \App\Book::groupBy('price')->get();

  // having
  \App\Book::having('count', '<', 20)->get();

大量赋值...针对大众

你已经掌握了基础。太棒了!

现在,你必须了解另一种将记录插入 Eloquent 的方法。

第一个就是模型构造函数。你可以将关联数组作为构造函数调用的参数传递,就像这样:

  $book = new \App\Book([
    'title' => $title,
    'pages_count' => $pagesCount,
    'price' => $price
  ]);

另一个是 create() 方法,你可以从模型本身静态地调用它。

  $book = \App\Book::create([
    'title' => 'My First Book!',
    'price' => 10.50,
    'pages_count' => 150,
    'description' => 'My lorem ipsum dolor description here...'
  ]);

与构造函数类似,这个 create() 方法也接受一个关联数组作为参数,这个数组中的每个键都对应于表中的一个列(以及模型上的一个 magic 属性)。返回的值(存储在 $book 中的那个)是 Book 类的一个实例。

这被称为批量赋值,直到目前为止,一切都很正常,除了一个严重的安全问题。事实上,有时你可以将整个请求输入数组作为参数传递。相信我:迟早你会这么做。

注意

在以下示例中,你将要看到的$request对象是一个请求实例,你可以用它来处理当前请求的数据。我将使用它从表单(使用all()方法)中检索一些假设的POST数据,我们将使用这些数据创建一本新书。

类似这样的内容可以被使用:

  $book = new \App\Book($request->all());

  // or...

  $book = \App\Book:create($request->all());

技术上,你不知道$request->all()中确切是什么,所以这不是最佳实践,对吧?然而,你可以通过向模型添加一个单一属性——fillableguarded——来轻松解决这个问题。

让我们用一个例子来更好地理解这个概念。想象一下,由于某种原因,你在用于新书插入的表单中指定了其他字段,比如当前用户 ID 用于登录目的。

然而,你只需要插入四个单字段:titlepages_countpricedescription。其他什么也不要!

关键属性是fillable

  <?php 

  namespace App;

  use Illuminate\Database\Eloquent\Model;

  class Book extends Model {

      protected $fillable = [
          'title',
          'price',
          'pages_count',
          'description'
      ];

  }

你可以像这样执行代码:

  $book = \App\Book:create($request->all());

从你执行代码的那一刻起,模型将搜索titlepages_countpricedescription项。没有更多!如果你在请求数据数组中也有user_id字段,它将被模型忽略。

当然,要小心,并仔细检查你放入模型$fillable数组中的内容。有时,开发者很容易忘记正确的字段,并在空数据库记录上花费数小时。

所以,如果你实际上遇到了空数据库记录的问题,请记住:检查你的fillable

guarded属性有类似的行为,但做的是相反的事情:如果fillable是白名单,那么guarded就是黑名单。如果存储了一些重要且敏感的信息,你不想以任何方式让用户更新,那么黑名单机制可以非常有用。

此外,有时你需要保护模型中的每一个属性。没问题,使用guarded如下所示:

  <?php 

  namespace App;

  use Illuminate\Database\Eloquent\Model;

  class Book extends Model {

      protected $guarded = ['*'];

  }

注意

如果你将一个字段同时放入fillableguarded数组中,模型的行为将优先考虑fillable,该字段将被填充。

此外,明智地使用fillableguarded。记住,如果你正在使用guarded数组,并且将经典的全请求数据数组传递给模型,你可能会更新一些不希望更新的字段,并得到一些非常不希望出现的错误。

无聊的错误!避免它们。

时间戳和软删除

现在来介绍模型类的两个酷炫特性:时间戳和软删除。你有多少次不得不手动处理记录的创建日期和最后更新时间?模型时间戳就是为了帮助解决这个问题。

此外,你有多少次在维护数据的一些信息的同时创建删除功能,如果不是全部的话?是的,软删除功能也是为了帮助解决这个问题。

时间戳

你还记得我们在 books 表迁移中调用的最后一个方法吗?不记得了吗?

别担心,下面就是:

  Schema::create('books', function(Blueprint $table)
  {
    // other fields...

      $table->timestamps();
  });

正确,它是 timestamps() 方法。这个特殊的 Schema Builder 方法用于创建两个独立的字段:created_atupdated_at,两者都是 MySQL 的 DATETIME 或等效类型。Eloquent 在你创建或更新记录时会自动处理这两个字段。

它可以非常实用:你有多少次不得不处理特定表上的某些最后编辑数据?想象一下,有了这两个字段处理一些计划发布的文章会容易多少。

然而,有时它们并不那么有用:你只需将模型中的 timestamps 属性设置为 false 就可以禁用它们。

  <?php

  namespace App;

  class Book extends Model {

      public $timestamps = false;

  }

注意

显然,如果你打算禁用时间戳,你可以在迁移中删除相应的 timestamps() Schema Builder 调用。你将不再需要它。

如果需要,你可以最终指定时间戳的格式。这里有一个例子:

  <?php

  namespace App;

  class Book extends Model {

      protected $table = 'books'';

      protected function getDateFormat()
      {
          // returining a different timestamp format!
          return 'd/m/Y';
      }

  }

你所需要做的就是实现模型中的 getDateFormat 方法,并让它返回一个描述所需格式的字符串。

注意

在返回的字符串中,你可以插入任何 date() PHP 函数的有效格式(你可以在 php.net/manual/en/function.date.php 找到完整的参考)。

软删除

软删除功能是一个非常有趣的功能,在许多场合都可能很有用。

如果你决定激活软删除,你实际上永远不会删除一条记录:相反,deleted_at 列将被更新为操作日期,但仅此而已。Eloquent 将像以前一样工作,但你永远不会丢失任何东西。

它非常适合处理订单的电子商务数据库。客户可以决定清除他的订单历史。然而,为了保持账户完美,店主会持续需要每个订单的详细信息。

如你很容易想象的,从上一章中,为了激活软删除功能,你必须调用 softDeletes() Schema Builder 方法。

  Schema::create('books', function(Blueprint $table)
  {
    // other fields...

      $table->softDeletes();
  });

在此之后,你必须将 SoftDeletes 特性包含在模型中。

  <?php

  namespace App;

  use Illuminate\Database\Eloquent\SoftDeletes;

  class Book extends Model {

      use SoftDeletes;

      protected $dates = ['deleted_at'];

  }

没有更多了!

此外,处理软删除数据真的非常简单。让我们基于之前描述的情况(客户订单的详情和店主的需求)来实现一个简单的例子。

首先,这里有一个示例模型:

  <?php // Order.php

  namespace App;

  use Illuminate\Database\Eloquent\SoftDeletes;

  class Order extends Model {

      use SoftDeletes;

      protected $dates = ['deleted_at'];

  }

在他的/她的订单历史中,客户应该只能看到他/她的存在订单,而不是已删除订单。我之所以使用引号,是因为我们谈论的是始终存在的记录。

  // getting all the orders

$orders = \App\Order::orderBy('created_at', 'desc')->get();

没有什么特别之处!

那店主怎么办呢?神奇的字眼是 withTrashed

  // getting all the orders, including the "deleted" ones...

$orders = \App\Order::withTrashed()->orderBy('created_at', 'desc')->get();

withTrashed 方法自动包含表中实际存在的每个结果,无论 deleted_at 字段值如何。

此外,如果你只想看到软删除的字段,将 withTrashed 改为 onlyTrashed

  $trashedOrders = \App\Order::onlyTrashed()->orderBy('created_at', 'desc')->get();

最后,你可以使用restore方法恢复已被删除的记录。

  $trashedOrder = \App\Order::find($trashedOrderId);
  $trashedOrder->restore();

  // $trashedOrder is not so trashed anymore...

如果你愿意,你可以使用查询作为filter来执行恢复操作。

  \App\Order::where('customer_id', '=', $customerId)->restore();

好吧,我知道你在想什么:这个功能很酷,但如果我想真正删除一个字段怎么办?

没问题;只需使用forceDelete

  order = \App\Order::find($orderId);

  // bye bye... forever :'(
  $order->forceDelete();

查询范围

查询范围非常有趣且强大。我真的很喜欢它们,因为像许多程序员一样,我绝对且非常懒惰。我也有一个很好的理由来证明我的懒惰:不要重复自己DRY)原则。

简而言之,它们允许你在查询中重用一些逻辑。如果你在应用程序中有类似的查询并且不想每次都重复编写它们,这很有用。

让我们举一个例子。

  <?php // Book.php

  namespace App;

  class Book extends Model {

      public function scopeCheapButBig($query)
      {
          return $query->where('price', '<', 10)->where('pages_count', '>', 300);
      }

  }

发生了什么?我声明了一个scopeCheaperButBig方法。scope前缀用于指定这将用作范围。

那么,我该如何使用范围?

下面是示例:

  <?php

  $bigAndCheaperBooks = \App\Book::cheapButBig()->get();

这是一个很酷的功能。如果你也认为你可以以更智能的方式拆分你的逻辑,你可以这样做:

  <?php // Book.php

  namespace App;

  class Book extends Model {

      public function scopeCheap($query)
      {
          return $query->where('price', '<', 10);
      }

      public function scopeExpensive($query)
      {
      return $query->where('price', '>', 100);
      }

      public function scopeLong($query)
      {
        return $query->where('pages_count', '>', 700);
      }

      public function scopeShort($query)
      {
      return $query->where('pages_count', '<', 100);
      }

  }

根据需要使用,减少重复代码。

  <?php

    // getting cheaper and longer books;
    $cheapAndLongBooks = \App\Book::cheap()->long()->get();

    // getting most expensive and longer books;
    $expensiveAndLongBooks = \App\Book::expensive()->long()->get();

    // getting cheaper and shorter books;
    $cheapAndShortBooks = \App\Book::cheap()->short()->get();

    // getting expensive and shorter books;
    $expensiveAndShortBooks = \App\Book::expensive()->short()->get();

如果需要,你还可以定义动态范围,以便向范围传递参数。如果你喜欢,在另一个范围内调用范围不是问题。

  <?php // Book.php

  namespace App;

  class Book extends Model {

    public function scopeLong($query)
      {
        return $query->where('pages_count', '>', 700);
      }

      public function scopeLongAndCheaperThan($query, $amount)
      {
          return $query->long()->where('price', '<', $amount);
      }

  }

属性转换、访问器和修改器

Eloquent 有多种方法将模型数据转换为更易读或易用的形式(反之亦然)。在本章中,我们将分析其中的三种:属性转换、访问器和修改器。

属性转换

在访问模型属性的同时转换它们的最简单方法是属性转换。简而言之,它允许你定义你想要转换的属性以及它将变成的目标类型。

假设你在books表中还有一个整数字段:is_rare。如果这本书是罕见的,这将等于1,否则为0。然而,当你处理它时,从逻辑上讲最好的做法是如下所示:

  if($book->is_rare)
  {
    // do wow things here...
  }
  else
  {
    // do common things here...
  }

它不会是这样的:

  if($book->is_rare === 1)
  {
    // ...
  }

对吗?很好。

因此,要解决这个问题,你只需在模型中指定casts数组:

  <?php // Book.php

  namespace App;

  class Book extends Model {

    protected $casts = [
        'is_rare' => 'boolean',
    ];

  }

从此刻起,每次你调用is_rare属性时,它将自动转换为相应的布尔值并返回。

支持的转换类型有:整数、实数、浮点数、双精度浮点数、字符串、布尔值、对象和数组。

注意

正如文档所述,当你在特定表列中存储了 JSON 数组并且想要快速处理它时,数组类型转换非常有用。

访问器和修改器

属性转换非常有用,但有一些限制。有时,从数据库中存储的简单值开始,你可能需要对它进行更复杂的工作。访问器和修改器就在这里帮助你。

更具体地说,访问器是一种在用户读取特定属性时执行的方法。访问器作用于存储在数据库中的属性,并返回它。相反,修改器以相反的方式工作:当你存储一个值时,修改器工作,完成其任务,然后将它保存在表中。让我们说它们是获取器和设置器的一种。

定义一个访问器(或修改器)并不难:你所要做的就是遵循一个命名规范。

让我们从简单的东西开始。想象一下,每次你访问你书的定价时,你都想在字符串的开头加上美元符号$

  <?php // Book.php

    namespace App;

    class Book extends Model {

      public function getPriceAttribute()
        {
            return '$ ' . $value;
        }

    }

访问器的命名规范很简单,如下所示:

  • 方法名称以get开头

  • 名称的中间部分是属性名称,它是驼峰式命名的

  • 方法名称以Attribute结尾

没有更多。

现在,看看这个:

  $book = \App\Book::find(1);

  echo $book->price;
  // output: $ 10.50

每次你有前面的代码,修改器都非常相似。这次,我们想要存储标题的小写版本。

  <?php // Book.php

    namespace App;

    class Book extends Model {

      public function setTitleAttribute($value)
        {
            $this->attributes['title'] = strtolower($value);
        }

    }

习惯用法并没有改变太多:唯一的区别是,现在方法名称以set开头,而不是get。这完全是关于获取器和设置器,我的朋友。

修改器的一个实际常见用途是在应用程序存储用户的密码时。可以使用修改器来散列选定的密码,然后将结果存储起来。

  <?php // User.php

    namespace App;

    class User extends Model {

      public function setPasswordAttribute($value)
        {
            $this->attributes['password'] = \Hash::make($value);
        }

    }

在代码中向下深入

在前面的文本中,我们分析了 Eloquent 模型许多方面。如果你想想你的路径,在几页之内,你学到了你需要做许多数据操作的所有东西。此外,你还看到了许多添加行为(从访问器和修改器到模型观察器)的有趣方式,以提高你的代码的可用性、可读性和可维护性。

在本节中,我将深入分析模型类,比通常更深入一些。没有什么太高级的,所以不用担心:我将只使用类代码来展示你可以用你的模型做什么。

把它当作一个有用的提示和技巧列表

一个大文件

当我写这一章的时候,Illuminate\Database\Eloquent下的Model类有 3361 行代码,这是一个具有许多方法和功能的大家伙。然而,考虑到我们可以以许多不同的方式做同样的事情(想想save()方法,::create()方法,以及在模型构造函数中指定关联数组的可能性:三种插入新记录的方式),这个长度是可以接受的。

在类的第一部分,你可以看到我们之前看到的所有属性,都设置为它们的默认值。

  ...

  protected $fillable = array();

  ...

  protected $guarded = array('*');

  ...

  protected $casts = array();

  ...

是的,默认情况下,每个属性都是受保护的。这是一个完全黑名单,以实现最大安全性!

快速转换为数组或 JSON

你还记得这一章开头我告诉你的关于模型属性自动转换为 JSON 的事情吗?如果你查看模型中的代码,你会看到两个方法:toArraytoJson。这两个方法就是让魔法发生的地方。

  // from the Illuminate\Database\Eloquent\Model class

  ...

  /**
   * Convert the model instance to an array.
   *
   * @return array
   */
  public function toArray()
  {
    $attributes = $this->attributesToArray();

    return array_merge($attributes, $this->relationsToArray());
  }

  ...

  /**
   * Convert the model instance to JSON.
   *
   * @param  int  $options
   * @return string
   */
  public function toJson($options = 0)
  {
    return json_encode($this->toArray(), $options);
  }

注意

你还可以看到第二个方法使用了第一个。如果你不怕大文件,我建议你深入了解一下这个类中的所有方法。你将看到一些代码重用和函数编写的精彩示例。在本章中,我们只是触及了表面。

你可以将单个模型和模型集合都转换为数组或 JSON:

  return \App\Book::all()->toJson();

  // or ...

  return \App\Book::where('title', 'LIKE', 'My%')->get()->toArray();

  // or

  return \App\Book::find(1)->toJson();

显然,出于安全原因,你可以选择将一些特定字段标记为 hidden。当记录转换为数组或 JSON 时,它们将不会显示。

  <?php

  namespace App;

  use Illuminate\Database\Eloquent\Model;

  class User extends Model {

      protected $hidden = ['password', 'credit_card_number'];

  }

在这种情况下,我们通过 hidden 指定了一个黑名单。你还可以使用 visible 属性定义一个白名单。

  <?php

  namespace App;

  use Illuminate\Database\Eloquent\Model;

  class User extends Model {

      protected $visible = ['first_name', 'last_name'];

  }

虚拟属性

使用访问器和模型的特定属性 appends,即使这些属性不是作为表列存在,你也能创建一些属性。

为了举例说明,让我们假设我们想要创建一个 complete_name 属性,由 first_namelast_name 这两个属性组成。

首先,让我们在模型中创建适当的访问器:

  <?php

  namespace App;

  use Illuminate\Database\Eloquent\Model;

  class User extends Model {

      public function getCompleteNameAttribute()
    {
        return $this->attributes['first_name']. ' ' . $this->attributes['last_name'];
    }

  }

然后作为最后一步,我们可以将其包含在 appends 数组属性中。

  <?php

  namespace App;

  use Illuminate\Database\Eloquent\Model;

  class User extends Model {

    protected $appends = ['complete_name'];

      public function getCompleteNameAttribute()
    {
        return $this->attributes['first_name']. ' ' . $this->attributes['last_name'];
    }

  }

没有其他了!

现在,我们可以像这样使用我们的属性:

  echo $user->complete_name;
  // outputs: Francesco Malatesta

注意

你使用 appends 数组创建的每个属性都尊重你在 visiblehidden 数组中最终指定的规则。

路由模型绑定

另一个很酷的快捷方式是 模型绑定。简单来说,它允许你将某个路由参数链接到定义的模型的一个实例。

让我们来测试一下:首先,前往 app/Providers 文件夹中的 RouteServiceProvider。现在按照以下步骤操作:

  1. 将此行添加到 boot 方法中:

      public function boot(Router $router)
      {
          parent::boot($router);
    
          $router->model('book', 'App\Book');
      }
    
  2. 然后前往你的路由文件,创建一个新的路由,使用这个参数:

      Route::get('books/{book}', function(App\Book $book)
      {
          return $book->title;
      });
    
  3. 因此,调用 books/1 URL 将输出类似的内容:

      My First Book!
    

发生了什么?Laravel 解析并自动在 $book 变量中实例化了所需的对象,使用给定的参数(1)作为基于主键的搜索词。然后,使用该实例输出了标题。

这是一个小技巧,但有时可能很有用。

你还可以自定义解析逻辑:这里有一个使用电子邮件地址的另一个示例。

  Route::bind('user', function($value)
  {
      return User::where('email', '=', $value)->first();
  });

如果指定的主键记录不存在,应用程序将返回 404 not found 错误。显然,你可以通过为 model() 绑定指定第三个参数来改变这种行为。

  Route::model('user', 'User', function()
  {
      throw new MyCustomNotFoundHttpException;
  });

记录分块以优化内存

有时候,你可能需要处理成千上万的记录。你知道这些操作对你的内存来说非常沉重,但 Eloquent 有一个有用的方法可以将查询结果分块在 中,以优化你的加载。

  \App\Book::chunk(200, function($books)
  {
      foreach ($books as $book)
      {
          // heavy operations on the book here...
      }
  });

第一个参数定义了你想要使用的块的大小。在这种情况下,我们将加载 200 个结果,处理它们,卸载它们,然后对下一个 200 个重复同样的操作。

第二个参数是一个闭包,它定义了如何处理这个块:books 闭包参数是返回的记录集合。

摘要

嘿,我们又做到了!Eloquent 的这个第一部分概述已经完成,现在你能够自己进行第一次实验了!实际上,你能够进行许多实验,而不仅仅是基础操作。我们学习了所有关于 CRUD、批量赋值以及许多其他有趣的内容。

如果你觉得所有这些信息让你感到不知所措,别担心。花时间进行多次测试,熟悉所有机制,包括你在旅途中发现的约定。我们才刚刚进行到第三章!

在下一章中,我们将深入探讨 Eloquent 最令人惊叹的方面之一:关系。你将体验到 Eloquent 的力量以及它是如何处理模型之间各种关系的,从一对一关系到多对多关系,展现其全部的美丽。

加油,加油,英雄!

第四章:探索关系的世界

团结(和关联)使我们站立,分裂使我们倒下。

在现实世界的背景下,一切事物都是相互关联的;例如,一辆车有一个车主,一本书有一个作者(或者可能不止一个),或者一个电子商务订单与一个或多个客户(另一个关系!)所订购的产品相关联。

实际上,一切事物都是相互关联的!

在应用开发的世界里没有区别;通常,你创建软件来解决现实世界的问题。现实世界是由相关事物构成的,所以你可能需要在你的实体之间定义许多关系。

然而,让我们明确一点:我并没有说什么新东西。只需去维基百科上搜索实体-关系模型

通常,在你的学校教科书中,你可以找到三种基本的关系类型:

  • 一对一:这是用来关联一个单一实体与另一个单一实体(例如,一个人和身份证)

  • 一对一:这是用来定义一个实体与同一类型的其他实体之间的连接(例如,同一作者的所有书籍)

  • 多对多:这是用来关联多个实体与许多其他实体(例如,一本书可以属于多个类别,一个类别可以包含多本书)

当然,网络开发也不例外。Eloquent 也不例外。

按照你迄今为止所看到的约定,Eloquent 有一个处理关系、用于定义它们的方法以及你可以采用的与它们一起工作的技术的好方法。

因此,让我们探索我们将在本章中做什么。

首先,我们将处理我们刚刚看到的基本关系类型。Eloquent 是如何处理它们的?你将发现诸如hasManybelongsTo等强大方法的美丽。这次,没有更多的代码片段;我们将跟随我们的图书馆管理工具类的创建,定义每一个实体和每一个关系。

在基础之后,你将发现如何处理这些关系:如何查询和使用它们,以舒适和整洁的方式。此外,我们还将看到如何在数据库中插入和删除相关模型,或者更新现有模型。

有时候,处理多对多关系可能意味着存储一些特定于该关系的特定数据。Eloquent 有一个非常有用的属性名为pivot,你可以用它来查询所需的枢纽表。

因此,这次有很多东西要看看!然而,还没有结束!Eloquent 提供了两种其他你可以使用的关系通过 has many和多态多对多关系。

好吧,别再闲聊了!我现在不会泄露任何东西。跟随这一章节,你会爱上它的。

显然,我将为每个概念展示一个现实世界的例子。来吧,英雄!

  • 三位一体:一对一、一对多和多对多

  • 查询相关模型

  • 懒加载(以及 N + 1 问题)

  • 插入和更新相关模型

  • 访问 远程 关系

  • 更强大的功能!多态关系

三位一体——一对一、一对多、多对多

如我之前提到的,我们将从基础知识开始。所以,我们将首先看到如何定义 Eloquent 中实体之间的关系。这真的很简单,通常你只需要为每个关系添加一行代码。

一对一

我们的库非常关注追踪借书的人。因此,每个新用户都必须向图书馆提供一些身份证明文件数据。

现在,每个用户都有一个唯一的身份证明文件,每个文件都是独一无二的。如果你仔细想想,这是一个完美的一对一关系。当你构建数据库时,最遵循的规则告诉你,你必须首先在第一个表中添加必要的列。在这个特定例子中,我们会向用户表添加列。

然而,有人可能会说“是的,但这是一个完全不同的实体!”

此外,我们可能需要存储每个用户身份证明文件的许多详细信息:号码、类型、到期日、城市等等。

在这种情况下,你会在一个现有的表中添加四到五列。许多人不喜欢这种解决方案,所以想象一下,你有了 UserIdentityDocument 模型。

这是我们的默认 User 模型:

  <?php namespace App;

  use Illuminate\Auth\Authenticatable;
  use Illuminate\Database\Eloquent\Model;
  use Illuminate\Auth\Passwords\CanResetPassword;
  use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
  use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;

  class User extends Model implements AuthenticatableContract, CanResetPasswordContract {

    use Authenticatable, CanResetPassword;

    /**
     * The database table used by the model.
     *
     * @var string
     */
    protected $table = 'users';

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = ['name', 'email', 'password'];

    /**
     * The attributes excluded from the model's JSON form.
     *
     * @var array
     */
    protected $hidden = ['password', 'remember_token'];

  }

以下是我们 IdentityDocument 模型:

  <?php namespace App;

  use Illuminate\Database\Eloquent\Model;

  class IdentityDocument extends Model {

    //

  }

现在,让我们看看如何定义它们之间的连接。对于一对一关系,使用的方法是 hasOne()belongsTo()hasOne() 方法的使用方式如下:

  $this->hasOne('App\IdentityDocument');

此外,belongsTo() 方法的使用方式如下:

  $this->belongsTo('App\User');

语法是合理的,对吧?一个用户有一个文件,一个文件属于某个用户!我知道你在想什么:你不能随意在类中放置这个方法调用。

实际上,你必须定义一个返回该方法调用的方法,如下所示:

  public function identityDocument()
  {
    return $this->hasOne('App\IdentityDocument');
  }

同样,belongsTo() 必须定义为以下方式:

  public function user()
  {
    return $this->belongsTo(' App\User');
  }

因此,最终模型的代码将如下所示:

  <?php namespace App;

  use Illuminate\Auth\Authenticatable;
  use Illuminate\Database\Eloquent\Model;
  use Illuminate\Auth\Passwords\CanResetPassword;
  use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
  use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;

  class User extends Model implements AuthenticatableContract, CanResetPasswordContract {

    use Authenticatable, CanResetPassword;

    /**
     * The database table used by the model.
     *
     * @var string
     */
    protected $table = 'users';

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = ['name', 'email', 'password'];

    /**
     * The attributes excluded from the model's JSON form.
     *
     * @var array
     */
    protected $hidden = ['password', 'remember_token'];

    public function identityDocument()
    {
      return $this->hasOne(' App\IdentityDocument');
    }

  }

User 类的代码如下:

  <?php namespace App;

  use Illuminate\Database\Eloquent\Model;

  class IdentityDocument extends Model {

    public function user()
    {
      return $this->belongsTo(' App\User');
    }

  }

哎呀!

究竟发生了什么?

在数据库层面,我们创建了一个包含一些数据列和 user_id 外键的 identitydocuments 表。这个外键很重要,因为它会被 Eloquent 自动用来解析关系。

如果你愿意,你可以在两个方法中指定不同的外键作为第二个参数:

  $this->hasOne('App\IdentityDocument', 'another_user_external_id');

否则,你可以使用这个:

  $this->belongsTo(' App\User', 'another_user_external_id');

我们指定的字段是相同的。当然,hasOne 方法(User 模型)会将其视为外键,而 belongsTo 方法(IdentityDocument 模型)会将其视为本地键。

在这两个方法中,还有一个你可以使用的第三个参数。在 hasOne() 中,它用于指定本地键(默认是 id 字段)。在 belongsTo() 方法中,它用于在父表上定义父键(再次,默认是 id 字段)。

让我们用相同的模型再举一个例子。想象一下,我们有一个名为 IdentityDocuments 的表,其主键名为 documentidentifier。此外,我们需要遵循一定的标准,我们不能使用 user_id 作为外部外键的名称。我们必须使用 documentowner_id

没有问题。首先,你将像这样定义你的 hasOne

    $this->hasOne('App\IdentityDocument', 'owner_id');

我们不需要定义第三个参数,因为我们的 users 主键是 id

然后,你定义 belongsTo

    $this->belongsTo('App\IdentityDocument', 'documentidentifier', 'owner_id');

现在你完成了!注意,这个概念适用于我们将要看到的其他关系方法。

一对多

这次比之前更容易:每本书都有一个作者,对吧?有时,一本书可能有多个作者,但让我们假设一个基本的情况。我们将要分析的第二种关系类型是一对多。每个作者可以有多本书。让我们考虑涉及到的模型:AuthorBook

这是 Author 类:

  <?php namespace App;

  use Illuminate\Database\Eloquent\Model;

  class Author extends Model {

    //

  }

Book 类如下:

  <?php namespace App;

  use Illuminate\Database\Eloquent\Model;

  class Book extends Model {

    //

  }

这些只是简单的 Eloquent 模型。现在,为了定义一个一对多关系,我们必须使用 hasMany() 方法。

因此,Author 类将看起来像这样:

  <?php namespace App;

  use Illuminate\Database\Eloquent\Model;

  class Author extends Model {

    public function books()
    {
      return $this->hasMany('App\Book');
    }

  }

然后,你可以像之前一样使用 belongsTo() 方法来定义这种关系的 inverse

  <?php namespace App;

  use Illuminate\Database\Eloquent\Model;

  class Book extends Model {

    public function author()
    {
      return $this->belongsTo('App\Author');
    }

  }

所以,我们完成了!我们再次使用了 belongsTo(),因为 属于 的概念完全相同;没有差异。

在数据库结构层面,我在 books 表中添加了一个外部的 author_id 键。

所以,是的,完成了!我在之前的笔记中提到过,但我会重复一遍:记住,你可以通过在 hasMany()belongsTo() 方法中指定它作为第二个参数来更改你的外键。

多对多

让我们想象一个多对多关系的好例子。好吧,书籍/类别的关系是完美的。事实上,想象一下 海底两万里儒勒·凡尔纳

这不仅是一部冒险小说,也是一部经典。所以,你需要将其归类为两个不同的类别:经典冒险。我们的图书馆也可能包含另一部经典作品 地球中心之旅,它也是一部冒险小说。同样如此!

如你所见,这次一个多对多关系是绝对必要的。让我们来看看 Eloquent 如何处理多对多关系以及如何在模型上定义它们。

这是我们的 Book 模型的代码:

  <?php namespace App;

  use Illuminate\Database\Eloquent\Model;

  class Book extends Model {

    public function author()
    {
      return $this->belongsTo('App\Author');
    }

  }

Category 模型的代码如下:

  <?php namespace App;

  use Illuminate\Database\Eloquent\Model;

  class Category extends Model {

    //

  }

这次,我们没有任何 方向 或关系的可能 反向

特别是,对于许多类别,有许多书籍。所以,在这种情况下,你需要使用的唯一方法是 belongsToMany()。像这样使用该方法:

  <?php namespace App;

  use Illuminate\Database\Eloquent\Model;

  class Book extends Model {

    public function author()
    {
      return $this->belongsTo('App\Author');
    }

    public function categories()
    {
      return $this->belongsToMany('App\Category');
    }

  }

另一种方法的使用方式如下:

  <?php namespace App;

  use Illuminate\Database\Eloquent\Model;

  class Category extends Model {

    public function books()
    {
      return $this->belongsToMany('App\Book');
    }

  }

没有其他的事情!

让我们看看在数据库层面需要什么来处理这种关系。正如你很容易想象的,在这种情况下,你将不得不与一个交叉表(pivot table)一起工作。

所以,你需要在迁移文件中使用 up() 方法指定一个合适的额外表,如下所示:

  Schema::create('book_category', function(Blueprint $table)
  {
    $this->increments('id');

    $this->integer('book_id')->unsigned();
    $this->integer('category_id')->unsigned();

    $this->text('notes');

    $this->timestamps();
  });

你也有一些约定要遵循。这些在这里给出:

  • 表名由实体的名称组成,这些名称是单数,由下划线分隔

  • 表格将包含两个以感兴趣实体命名的列(author_idbook_id

注意

当你指定一个关系时,请记住在适当的方法调用之前使用 return。我知道这有点明显,但新手经常忘记这一点。

真的,真的非常重要,你必须遵循定义的约定。Laravel 和 Eloquent 可以显著改变你的工作流程时间表,但为了得到结果,你必须遵循约定。你越早这样做,感觉就会越好。

反向问题

在我们继续前进之前,有一些额外的事情。我们刚刚看到了如何定义一个关系及其反向。然而,如果你不定义反向,或者如果你定义了反向但没有定义 反向的反向,你会遇到什么后果?

实际上没有什么特别的!

最好的规则是定义你需要的关联。让我们想象这种情况:在你的软件中,你需要知道一本书的类别,但不需要知道某个类别中的所有书籍。

这是一个奇怪的情况,但这种情况确实会发生。在这种情况下,你只需在 Book 中定义 categories() 关系,无需其他。

反之,如果你的应用程序只需要从类别获取书籍列表,而不需要其他内容,你将只在 Category 模型中定义 books() 关系。仅此而已。

完成!已经涵盖了三种基本的关系类型!你不需要做更多的事情;实际上,Eloquent 会自动处理一切,所以你只需要编写代码,使用模型,提出查询。

哦,关于查询...

查询相关模型

现在你已经学会了如何定义你的关系,我认为你准备好学习如何查询它们了。让我们从一个非常基础的例子开始。

假设我们正在搜索特定用户的文档编号。我们将使用我们刚才看到的 UserIdentityDocument 实体。为了这个示例,想象你有一个名为 identitydocuments 的表,其中包含以下列:

  • 编号: 这表示文档编号

  • 类型: 这表示文档类型

  • due_date: 这表示文档的到期日期

  • 城市: 这表示文档发布的城市

这是获取文档身份编号的代码,从一个 User 实例开始:

  $user = \App\User::where('first_name', '=', 'Francesco')->where('last_name', '=', 'Malatesta')->first();

  $identityDocumentNumber = $user->identityDocument->number;

如果你输出 $identityDocumentNumber,你会读取所需的信息。不错,对吧?

嗯,这就是 Laravel 和 Eloquent 处理查询你的关系的方式。一旦你定义了它,你所要做的就是像访问一个简单的属性或方法一样访问它。

所有其他查询将由 Laravel 自动执行。实际上,请遵循以下简单说明:

  $user = \App\User::where('first_name', '=', 'Francesco')->where('last_name', '=', 'Malatesta')->first();

  $identityDocumentNumber = $user->identityDocument->number;

你刚刚执行了这些查询:

  // the user Francesco Malatesta as an ID = 1...
  select * from users where first_name = 'Francesco' AND last_name = 'Malatesta';

  select * from identitydocuments where user_id = 1

现在将结果放入 $identityDocumentNumber。对于一对一关系来说,这是显而易见的;然而,对于一对一多关系也是如此。

让我们考虑另一个例子:老牌的 Jules 将是一个完美的选择。假设我们想要获取我们拥有的所有儒勒·凡尔纳书籍的列表,代码如下:

  $author = \App\Author::where('first_name', '=', 'Jules')->where('last_name', '=', 'Verne')->first();

  foreach($author->books as $book)
  {
    echo $book->title . <br/>;
  }

  // outputs:
  //
  // Journey to the Center of the Earth
  // Twenty Thousand Leagues Under the Sea
  // Around the World in Eighty Days
  // Michel Strogoff

注意

正如我之前告诉你的,你可以通过一个简单的属性或方法调用来访问你的关系。有什么区别呢?嗯,通过方法调用,你可以进行一些过滤,并且之前看到的一切都是为了得到期望的结果。实际上,你可以在关系上发起一个查询。

想象一下,我们想要获取所有标题中包含the的书籍。以下是代码:

  $author = \App\Author::where('first_name', '=', 'Jules')->where('last_name', '=', 'Verne')->first();

  $theBooks = $author->books()->where('title', 'LIKE', '%the%')->get();

  foreach($theBooks as $book)
  {
    echo $book->title . <br/>;
  }

  // outputs:
  //
  // Journey to the Center of the Earth
  // Twenty Thousand Leagues Under the Sea
  // Around the World in Eighty Days

很酷,对吧?但这还没有结束,这只是触及了表面!

访问交叉表

在处理多对多关系时,并不仅仅是定义几个外部键。你可以选择在你的交叉表中添加额外的数据,以便存储实体之间特定连接的一些信息。

你已经知道如何创建一个交叉表,但如何访问它呢?这并不复杂:你只需要使用你关系的pivot属性。让我们用一个例子来说明,这个例子是我们之前创建的书籍/类别关系。

  1. 首先,你必须定义你想要从表中获取哪个属性,修改你的模型中的belongsToMany()调用:

      return $this->belongsToMany('App\Category')->withPivot('created_at', 'notes');
    
  2. 然后,你的代码应该是这样的:

      $book = App\Book::find(23);
    
      foreach($book->categories as $category)
      {
        echo 'Association Date: ' . $category->pivot->created_at;
        echo 'Association Notes: ' . $category->pivot->notes;
      } 
    

在这个小例子中,我们只是打印了所有我们将特定类别附加到id = 23的书籍上的日期。作为额外信息,我们还打印了一些额外的注释。这意味着在交叉表中,我们有created_atnotes字段。

作为快捷方式,你也可以使用:

  return $this->belongsToMany('App\Category')->withTimestamps();

这用于你只想从交叉表中导入时间戳数据时。

查询关系

Eloquent 允许你查询一个关系。换句话说,你可以根据某个关系的存在来获取一些结果。想象一下,我们想要获取数据库中至少有一本书的所有作者。

使用 Eloquent,我们可以这样做:

  $authorsWithABook = \App\Author::has('books')->get();

在这种情况下,你必须使用has方法,指定你想要检查的关系的所需方法。只有当至少找到一本相关书籍时,作者才会被添加到$authorsWithABook中。

如果你不喜欢这种布尔方法,不要担心;让我们看看如何找到数据库中至少有五本书的每个作者。

  $authorsWithAtLeastFiveBooks = \App\Author::has('books', '>=', 5)->get();

是的,你可以指定第二个和第三个参数作为操作符和比较项,分别用于这个计数检查

我知道,我知道,很酷,但还不够。好吧,那么获取所有至少有一本书在 1864 年出版的作者怎么办?

这里我们来了,这次使用whereHas方法:

  $authorsWithABookFromThe1864 = \App\Author::whereHas('books', function($q)
  {
      $q->where('year', '=', 1864);

  })->get();

如你所见,你可以以一种相当优雅的方式做到这一点。指定的第一个参数是你想要查询的关系的名称。第二个参数是一个闭包,它接受一个$q查询参数,你可以使用它来定义条件。

注意

你刚才看到的关于一对一关系的相同概念也适用于多对多关系。

预加载(以及 N + 1 问题)

每个强大的工具都必须明智地使用。Eloquent 中的关系也不例外。实际上,使用 Eloquent 最常见的问题之一就是 N + 1 问题。为了解释它,我将像往常一样使用一个例子。

假设我正在展示前 100 本书的一些数据。从这些数据开始,我还想打印每本书的作者姓名。

使用我们之前学到的知识,以下是代码:

  $books = \App\Book::take(100)->get();

  foreach($books as $book)
  {
    $author = $book->author;

    echo $author->first_name . ' ' . $author->last_name;
  }

即使语法很简单,在底层,Eloquent 正在进行 101 个查询!第一个查询是获取 100 本书列表,然后对每一本书进行查询以获取作者。这并不完全符合性能友好,对吧?

别担心,有解决方案!

基本预加载

预加载解决了你的问题。这次使用 Book 模型的 with() 方法,如下所示:

  $books = \App\Book::with('author')->take(100)->get();

  foreach($books as $book)
  {
    $author = $book->author;

    echo $author->first_name . ' ' . $author->last_name;
  }

现在,执行查询的数量将急剧下降到两个,使用 where in

  select * from books;
  select * from authors where id in (1, 2, 3, ...);

如果你愿意,你还可以在你的最终结果中包含多个关系。让我们也包含每本书的分类数据!

  $books = \App\Book::with('author', 'categories')->take(100)->get();

  foreach($books as $book)
  {
    $author = $book->author;

    echo 'Author: ' . $author->first_name . ' ' . $author->last_name;

    echo 'Categories:';

    foreach($book->categories as $category)
    {
      echo $category->name . ', ';
    }
  }

似乎还不够,你还可以包含来自 嵌套关系 的数据。

假设你正在获取应用中的分类列表。然后,你想要包含每个分类的书籍数据,并且对于每个相关书籍,你想要包含作者的数据。

你只需这样做:

  $categories = \App\Categories::with('books.author')->get();

  foreach($categories as $category)
  {
    echo $category->name;

    foreach($category->books as $book)
    {
      echo 'Title: ' . $book->title;
      echo 'Author: ' . $book->author->first_name . ' ' . $book->author->last_name;
    }
  }

高级预加载

如果你想更好地控制预加载,你可以定义一些约束或条件。假设你正在获取一个作者列表。从这个列表中,你想要获取从最古老到最新出版的每一本书。

你可以这样操作:

  $authors = \App\Author::with(['books' => function($query)
  {
    $query->orderBy('year', 'asc');

  }])->get();

你所要做的就是指定所需的预加载关系作为关联数组的元素,使用这种语法:

  ['relationship' => function($query){

    // conditions here, using the $query object

  }]

懒预加载

有时候你会使用预加载,但不是每次都需要。有时候你需要它,有时候不需要。

如果你愿意,你可以在下一刻手动预加载一个特定的关系。

如何?可以使用 load() 方法来完成:

  $books = Book::all();

  // some operations here...

  $books->load('author', 'categories');

语法与之前看到的一样;你所要做的就是指定所需的关联关系作为参数。

当然,你还可以使用以下方式定义条件:

  $books->load(['categories' => function($query)
  {
      $query->orderBy('name', 'asc');

  }]);

同样的方式,不多也不少!

注意

记住,预加载是一个解决许多性能问题的绝佳方案。特别是在我最初的实验中,它通过让我达到更低的查询数量而极大地帮助了我。仅举一个例子,在 Laravel-Italia 论坛上,我展示了包含一些回复、信息和线程作者数据的线程列表,只需三个查询即可。

插入和更新相关模型

到目前为止,你已经学会了如何定义关系并查询它们,以便从相关模型中获取数据。然而,你也可以轻松地插入和更新相关模型。

让我们选择一个真正基础的例子来开始:想象一下我们要添加一本新书(《米哈伊尔·斯特罗戈夫》,儒勒·凡尔纳*)。我们必须指定以下一些细节:

  $book = new Book;

  $book->title = 'Michael Strogoff';

然后,我们指定正确的作者 ID:

$author = Author::where('first_name', '=', 'Jules')->where('last_name', '=', 'Verne')->first();

  $book = new Book;

  $book->title = 'Michael Strogoff';
  // other data...

  $book->author_id = $author->id;

  // and finally...
  $book->save();

save()和 associate()方法

让我们明确一点;这确实很有效。然而,这并不是你能做的最好的方法。实际上,使用 Eloquent,你可以使用一些其他特定方法来处理相关模型。

让我们使用关系上的save()方法重写这个例子。同时,我们将使用关联数组作为构造函数参数来分配属性。

  $author = Author::where('first_name', '=', 'Jules')->where('last_name', '=', 'Verne')->first();

  $author->books->save(new Book([
    'title' => 'Michael Strogoff',
    // other attributes...
  ]));

完成!这只需要几条指令。save()方法将自动为刚刚传入作为参数的书籍设置author_id键。

然而,这不仅仅是一个节省时间的技巧;如果你仔细阅读代码,你会注意到我们实际上正在创建非常易于阅读的代码。我们不是设置外部键,而是以一种更物理的方式处理关系,我们将书籍保存为某个作者书籍集合中的一个元素。大不相同!

你也可以使用saveMany()方法与对象数组做同样的事情:

  $author = Author::where('first_name', '=', 'Jules')->where('last_name', '=', 'Verne')->first();

  $author->books->saveMany([
    new Book(['title' => 'Michael Strogoff']),
    new Book(['title' => 'The Mysterious Island']),
    new Book(['title' => 'Off on a Comet'])
  ]);

如前所述,saveMany方法将为数组中传递的每个Book实例设置外部的author_id键。你还可以更新现有关系,更改其与另一个模型的关系。

在这种情况下,你必须使用associate()方法。看看这个例子:

  $wrongAuthor = Author::where('first_name', '=', 'Jules')->where('last_name', '=', 'Verne')->first();

  $wrongAuthor->books->save(new Book([
    'title' => 'The Alchemist'
  ]));

  // oops! wrong author!

  $book = Book::where('title', '=', 'The Alchemist')->first();
  $rightAuthor = Author::where('first_name', '=', 'Paulo')->where('last_name', '=', 'Coelho')->first();

  // done!
  $rightAuthor->books->associate($book);

发生了什么?在示例的第一部分,我将《炼金术士》分配给了儒勒·凡尔纳。在沙漠中忏悔了一年之后,我回来找到正确的作者(通过使用$rightAuthor变量),然后使用associate()方法在书籍关系上操作。

第一个传入的参数是你想要与之工作的模型实例。

小贴士

因此,规则相当简单:在插入新记录时使用save()方法,在更新现有记录时使用associate()方法。

记住,《炼金术士》不是由儒勒·凡尔纳写的,而是由保罗·科埃略写的!

那么多对多关系呢?

我们所看到的一切对于一对一或多对一的关系来说都很棒。然而,对于多对多关系呢?这次机制略有不同;不是在复杂性的意义上,而是在语法上,更多的是在语法上。然而,让我们用一个例子来说明。

  • 我们有几本书:《炼金术士》和《地球中心之旅》。

  • 我们还有一些类别:科幻冒险经典

正如我在本章第一部分提到的,这是一个多对多的关系。我们如何注册《炼金术士》和《冒险》或《地球中心之旅》和《经典》之间的关系呢?

为了在单独的连接表中访问我们的数据,我们需要其他专用方法。我知道这看起来很愚蠢且无聊,但请使用你的数据库管理工具查看当你与数据交互时数据库中发生了什么。熟悉这个过程总是一个好主意。

然而,你将首先使用的是attach()方法:

  $book = new Book();

  $book->title = "The Alchemist";
  // other attributes...

  $book->save();

  // after save() call, an id is created.

  $category = new Category();

  $category->name = "Adventure";

  $category->save();

  // and now...

  $category->books->attach($book->id);

如你容易看到的,attach()方法是你可以调用的另一个关系方法。当然,只适用于多对多关系!在这种情况下,它需要一个参数:我想与该类别关联的书籍的主 ID。

如果你正在你的连接表中存储一些额外数据,你也可以添加一个关联数组作为第二个参数:

  $category = Category::where('name', '=', 'Opera')->first();

  $book = Book::where('title', '=', 'Journey to the Center of the Earth')->first();

  $category->books->attach(
    $book->id, 
    ['notes' => "Well, I'm not so sure about this..."]
  );

关联数组遵循attribute_name => attribute_value格式,就像往常一样。

就像你可以附加一样,你也可以解除。如果你想删除两个模型实例之间的多对多关系,请使用以下detach()方法:

  $category = Category::where('name', '=', 'Opera')->first();

  $book = Book::where('title', '=', 'Journey to the Center of the Earth')->first();

  // oh, come on...
  $category->books->detach($book->id);

所以,你已经完成了!

此外,attach()detach()方法都支持数组作为参数,而不是简单的整数。

  $category->books->attach(
    [4, 8, 15, 17, 22, 42]
  );

  $category->books->detach([17, 22]);

  $category->books->attach([16, 23 => ['notes' => 'be careful next time...']]);

sync()方法

这是一种处理多对多关系的真正酷的方法。然而,附加和解除东西有时可能会有些无聊。让我们尝试sync()方法:

  $category->books->sync(
    [4, 8, 15, 17, 22, 42]
  );

  $category->books->sync(
    [4, 8, 15, 16, 23, 42]
  );

迷惑?让我解释一下。sync方法自动同步关系数据,接受一个 ID 数组。元素一个接一个地检查关系之前是否已创建,并在必要时设置(或取消设置)它们。让我们想象一下,连接表book_category是空的;这是第一条指令:

  $category->books->sync(
    [4, 8, 15, 17, 22, 42]
  );

此指令将在所选类别和 ID 为 4、8、15、17、22 和 42 的书籍之间建立连接。然而,这里是第二个方法调用:

  $category->books->sync(
    [4, 8, 15, 16, 23, 42]
  );

它会检查一切,并计算正差和负差。书籍 17 和 22 不再在数组中。关系将自动解除。相反,书籍 16 和 23 将通过附加方法添加,这是一个非常酷的实用方法,可以节省你大量时间!

你显然可以使用之前使用的方法向连接表中添加数据:

  $category->books->sync(
    [4, 8, 15, 16, 23, 42 => ['notes' => 'We either live together.... or die alone.']]
  );

访问远程关系

另一个非常有趣的 Eloquent 特性是使用hasManyThrough()方法定义(然后访问)一个远程关系。

什么?我看到你有点困惑。没问题:让我们再举一个例子,这个例子与我们的实际环境略有不同。

想象一下,你正在为某个研究团队编写一个研究管理应用程序。在这个软件中,每个用户都将能够创建一个新的研究实体,然后添加一些章节到该研究中,例如:

  • 一个与Research实体存在一对一关系的User实体

  • Research实体与Section实体存在一对一关系

好的。首先,对于模型,你可以写点这样的东西:

  // file: app/User.php
  <?php namespace App;

  use Illuminate\Database\Eloquent\Model;

  class User extends Model {

    public function researches()
    {
      return $this->hasMany('App\Research');
    }

  }

  // file: app/Research.php
  <?php namespace App;

  use Illuminate\Database\Eloquent\Model;

  class Research extends Model {

    public function user()
    {
      return $this->belongsTo('App\User');
    }

    public function sections()
    {
      return $this->hasMany('App\Section');
    }

  }

  // file: app/Section.php
  <?php namespace App;

  use Illuminate\Database\Eloquent\Model;

  class Section extends Model {

    public function research()
    {
      return $this->belongsTo('App\Research');
    }

  }

作为基本设置,它可以工作。现在,如果我们想访问某个用户添加的每个部分呢?可能你会使用类似这样的东西:

  // getting $author with id = 1...
  $author = Author::find(1);

  // getting all $author researches...
  $researches = $author->researches;

  $allSections = [];

  // iterating to get all sections
  foreach($researches as $research)
  {
    $allSections[] = $research->sections;
  }

$allSections数组将包含用户添加的每个部分。使用 Eloquent,如果你想,你可以使用我之前提到的hasManyThrough()方法创建一个快捷方式。

你所要做的就是将其放入如下所示的 User 模型中:

  // file: app/User.php
  <?php namespace App;

  use Illuminate\Database\Eloquent\Model;

  class User extends Model {

    public function researches()
    {
      return $this->hasMany('App\Research');
    }

    public function sections()
    {
      return $this->hasManyThrough('App\Section', 'App\Research');
    }

  }

如果你愿意,你可以指定外部键(对于当前和中间实体)作为第三个和第四个参数:

  return $this->hasManyThrough('App\Section', 'App\Research', 'user_id', 'research_id');

在某些这样的特定情况下,这是一个非常有用的快捷方式。享受它!

更强大的多态关系

可能你正在想 Eloquent 很酷,非常强大。

嗯,是的。然而,有时hasMany()belongsToMany()还不够。在开发流程中的某些情况下,你将不得不处理更复杂的关系,这可能涉及两个以上的实体。

因此,作为本章的最后一部分,我将讨论多态关系。像往常一样,即使它们学习起来并不复杂,我仍会用许多详细的例子来涵盖,以便让你完全理解整个概念。

让我们从简单的多态关系开始。

简单的多态关系

当你有一个实体可以属于一个实体或另一个实体时,可以使用简单的多态关系。

因此,这是我们的第一个例子。想象一下你正在创建一个电子商务应用程序。你将能够上传一些照片:可以是产品、类别或博客文章。

这意味着我们首先将有四个独立的实体:

  • 照片

  • 产品

  • 类别

  • 文章

现在,让我们准备一些代码框架如下:

  // file: app/Photo.php
  <?php namespace App;

  use Illuminate\Database\Eloquent\Model;

  class Photo extends Model {

    //

  }

  // file: app/Product.php
  <?php namespace App;

  use Illuminate\Database\Eloquent\Model;

  class Product extends Model {

    //

  }

  // file: app/Category.php
  <?php namespace App;

  use Illuminate\Database\Eloquent\Model;

  class Category extends Model {

    //

  }

  // file: app/Post.php
  <?php namespace App;

  use Illuminate\Database\Eloquent\Model;

  class Post extends Model {

    //

  }

现在,你可以使用morphTo()morphMany()方法定义这个多态关系。

  • morphTo()方法由与所有其他类相关联的类使用。

  • morphMany()方法由owner类调用。

因此,让我们像这样编辑我们的模型:

  // file: app/Photo.php
  <?php namespace App;

  use Illuminate\Database\Eloquent\Model;

  class Photo extends Model {

    public function imageable()
    {
      return $this->morphTo();
    }

  }

  // file: app/Product.php
  <?php namespace App;

  use Illuminate\Database\Eloquent\Model;

  class Product extends Model {

    public function photos()
    {
      return $this->morphMany('Photo', 'imageable');
    }

  }

  // file: app/Category.php
  <?php namespace App;

  use Illuminate\Database\Eloquent\Model;

  class Category extends Model {

    public function photos()
    {
      return $this->morphMany('Photo', 'imageable');
    }

  }

  // file: app/Post.php
  <?php namespace App;

  use Illuminate\Database\Eloquent\Model;

  class Post extends Model {

    public function photos()
    {
      return $this->morphMany('Photo', 'imageable');
    }

  }

完成!等等,那个既用作方法名称又用作字符串参数的imageable是什么?这是一个你可以自己选择的名字:然而,看看我使用的表结构,以了解。

  products
      id - integer
      name - string

  categories
      id - integer
      name - string

  posts
      id - integer
      name - string

  photos
      id - integer
      path - string
      imageable_id - integer
      imageable_type - string

照片表有两个特殊字段:imageable_idimageable_type。这是一个简单的外部键,唯一的区别是,对于这个照片表中的元素,你可以计算不同类型的所有者

因此,在imageable_id中,你将放入所有者的 ID,在imageable_type中,放入所有者类名!

如果一张照片属于一个产品,你将在imageable_type列中看到Product,如果照片属于一个类别,那么就是Category,依此类推。

显然,处理这种关系非常简单。以下是一个例子:

  // getting a sample product...
  $product = App\Product::find(3);

  foreach($product->photos as $photo)
  {
    // working with photos here...
  }

这适用于其他每个实体!

  // getting a sample category
  $category = App\Category::find(42);

  foreach($category->photos as $photo)
  {
    // working with category photos here...
  }

最后,你也可以反转这些关系。如果你有一张照片,想知道是所有者,你所要做的就是:

  $photo = App\Photo::find(23);

  // getting the owner...
  var_dump($photo->imageable);

无论所有者的类是什么,Eloquent 都会自动解析实例并将其返回给你。如果 所有者 是一篇博客文章,你将得到这篇博客文章。很简单!

多对多多态关系

如果可以将简单的多态关系定义为一种 特殊 的多对一关系,那么多对多关系在多对多多态关系中找到了等价。

正如你在前面的文本中看到的,它的工作方式与多对多关系完全一样。唯一的区别是你可以 连接 某个实体与更多实体。

对于我们的例子,这次,让我们回到我们的图书馆管理系统。在本章的开头,你看到了三个主要实体:

  • 作者

  • 书籍

  • 分类

在书籍和分类之间,存在多对多关系。一本书可以属于多个分类。同样,一个分类可以包含多本书。现在,想象一下,你想要 扩展 这个概念到作者。

让我们以那位久经考验的朱尔斯为例。他写了冒险书籍,所以他很容易被归类为冒险作家。

多对多多态关系是处理这种情况的最佳方式。这次,你将不得不使用 morphMany()morphedByMany() 方法:

  // file: app/Author.php
  <?php namespace App;

  use Illuminate\Database\Eloquent\Model;

  class Author extends Model {

    public function categories()
    {
      return $this->morphToMany('App\Category', 'categorizable');
    }

  }

  // file: app/Book.php
  <?php namespace App;

  use Illuminate\Database\Eloquent\Model;

  class Book extends Model {

    public function categories()
    {
      return $this->morphToMany('App\Category', 'categorizable');
    }

  }

  // file: app/Category.php
  <?php namespace App;

  use Illuminate\Database\Eloquent\Model;

  class Category extends Model {

    public function authors()
    {
      return $this->morphedByMany('App\Author', 'categorizable');
    }

    public function books()
    {
      return $this->morphedByMany('App\Book', 'categorizable');
    }

  }

当然,对于每个多对多关系,你都需要一个具有类似以下结构的枢纽表:

  categorizables
      tag_id - integer
      categorizable_id - integer
      categorizable_type – string

像往常一样,请密切关注名称和约定。categorizable 的第二个参数对于 morphToMany()morphedByMany() 方法与你在枢纽表中指定的相同,即 categorizable_idcategorizable_type

注意

此外,使用的表名是该术语的复数形式(categorizablecategorizables)。

在此设置完成后,你可以在代码中使用这种关系,如下所示:

  // getting a sample author
  $author = App\Author::find(30);

  // accessing categories...
  var_dump($author->categories);

  // getting a sample book
  $book = App\Book::find(60);

  // accessing categories... in the same way!
  var_dump($book->categories);

在创建表时,你需要添加特定的 _id_type 列,例如 categorizable_idcategorizable_type

注意

在 Schema Builder 中,如果你想使用 $table->morphs('categorizable'),可以这样做。它将自动添加你需要的列,只需指定你想要的 -able 名称即可。

摘要

好吧,我认为这就足够了。实际上,这就是全部;是时候休息一下了!

你已经在 Eloquent 中学习了所有关于关系的内容,现在你可以迅速构建复杂的应用程序。你对 Eloquent 的基础知识了如指掌,所以我的建议是:花时间回顾一下所有内容,做一些测试,编写好的代码,并享受模型和关系。

准备好了,翻页,深入探索更高级的内容!

第五章。使用集合增强结果

到目前为止,我告诉你了关于模型以及如何创建它们之间关系的一切。我解释了如何查询你的数据和关系,甚至如何指定复杂的条件和约束。然而,我从未告诉你关于 Eloquent 输出的一切。是的,有时我提到了一个数组或者只是说结果。别担心,这并不错,但还有更多隐藏的东西。

好吧,在本章中,我将讨论集合。当你从查询(例如使用get()all())检索结果时,你得到的是一个集合。这就是正确的术语。

实际上,你可以将集合视为结果数组,但具有一些额外的实用方法。事实上,当你使用集合时,你正在使用Illuminate\Database\Eloquent中的Collection类的一个实例。

这个类实现了AggregateIterator接口,允许你将集合当作数组来处理。你可以使用集合执行许多操作,有时甚至是复杂的操作。首先,你将看到如何使用集合执行一些基本的研究操作和检查。

然后,我们将看到一些结果转换方法。你还记得吗,在第三章中,我提到了模型结果在 JSON 中的自动转换?太棒了!这是这些方法之一。

接下来,我们将深入探讨;毕竟,集合是由元素组成的。我们将与这些元素一起工作。显然,使用集合,你可以遍历其元素。有一些专门的方法用于迭代。此外,你还将学习如何以简单的方式过滤集合,就像 Eloquent 中的许多事情一样简单。最后,我们将讨论集合上的排序操作以及如何处理它们。所以,这些内容并不是真正必要的,但它将帮助你更好地理解 Eloquent 以各种方式的工作。

你准备好了吗?以下是我们要讨论的主题:

  • 基本集合操作

  • 转换集合

  • 迭代和过滤

  • 排序

基本集合操作

让我们从一些真正基本的方法开始。为了更好地理解你将要做什么,我强烈建议你在以下列表中的每个方法都在你的项目上尝试一次:

  • 第一个是contains()。如果集合中包含具有特定 ID 的记录,它将返回 true 或 false。

    这里有一个例子:

      $books = \App\Book::all();
    
      if($books->contains(3))
      {
        return 'yeah, book 3 is here!';
      }
    

    在这里,你只需要指定 ID 作为参数。

  • 如我之前所说,你可以将集合当作数组来使用。所以,如果你想获取集合中的第三个元素,你可以这样做:

      $book = $books[2];
    
  • 然而,如果你出于某种原因不喜欢这种语法,你可以以这种方式使用get()作为替代,如下所示:

      $book = $books->get(2);
    
  • 使用这种增强语法,你还可以指定一个默认值,如果所需的索引不存在:

      $book = $books->get(2, "Not Found!");
    
  • 显然,如果你想检查特定元素的存在,你可以使用 has()

      if($books->has(3))
      {
        $book = $books->get(3);
      }
    
  • get() 相反,你可以使用 put() 添加一个具有特定索引的元素:

      $book = new Book;
      // other attributes assignment here...
    
      $books->put(2, $book);
    

    如你所想,第一个参数是期望的索引,第二个参数是值。

  • 另一个很酷的方法是 prepend(),你可以用它将一个元素添加到特定的集合中。以下是语法:

      $firstBook = new Book;
      // other attributes assignment here...
    
      $books->prepend($book);
    
  • 如果你想要获取包含所有主键的数组,你可以使用专门的 modelKeys() 方法!

      $books = \App\Book::all();
    
      $primaryKeys = $books->modelKeys();
    

    还没有结束;实际上,有许多你可以用于许多不同事情的方法。

  • 例如,random() 方法可以从指定的集合中提取一个随机项:

      $books = \App\Book::all();
    
      $randomBook = $books->random();
    
  • 此外,你可以使用 keys()values() 来获取仅包含键或值的数组。

      $books = \App\Book::all();
    
      $keysArray = $books->keys();
      $valuesArray = $books->values();
    
  • 什么?你想把你的集合当作栈来处理?没问题,pop()push() 就在这里帮助你!

      // let's get the last item!
      $books = \App\Books::all();
      $lastBook = $books->pop();
    
      // lets' add a new item ad the end!
      $book = new Book;
      // other attributes assignment here...
    
      $books->push($book);
    
  • 现在,关于使用类似于你在模型上调用的 here() 语法在集合中搜索一个项,看看这个:

        $books = \App\Book::all();
    
        $book = $books->where('title', 'Michael Strogoff');
    

    注意,这个方法返回另一个分类。这意味着你可以使用多个 where() 的链式调用。以下是一个更好的例子:

      $books = \App\Book::all();
    
      $book = $categories->where('year', 1876)->where('page_count', 254);
    
  • 让我们用 perPage 来结束本章的第一部分,这是一个非常直观的方法,它获取一定数量的项目,而你只需要指定你想要每页显示的项目数量。

    语法类似于这样:

      $books = \App\Book::all();
    
      $secondPageBooks = $books->perPage(2, 10);
    

    通过这个简单的调用,你正在获取从第二页开始的 10 本书。我认为这是一个 Eloquent(以及 Laravel)提供的方法表达性语法的绝佳例子。

注意

如果你想了解更多关于 Collection 类及其提供的内容,请查看 laravel.com/api/5.0/Illuminate/Database/Eloquent/Collection.html 或直接查看 Illuminate\Database\Eloquent\CollectionIlluminate\Support\Collection 类中的代码。

转换集合

很常见,Eloquent 会自动将集合转换成你可以以更好的方式输出的东西。例如,以下是我用来显示杂志网站新闻分类列表的代码:

  $categories = \App\Category::all();
   return $categories;

这是相应的输出:

  [
    {
      id: 1,
      name: "Editorial",
      slug: "editorial",
      description: "Qui est quo asperiores aliquid vitae possimus. Dolor consequuntur similique voluptatem a laborum dolorem ea repellendus. Aspernatur ducimus quis dolorum consequatur vel nam at. Aut omnis rem laborum.",
      created_at: "2015-04-21 10:14:37",
      updated_at: "2015-04-21 10:14:37"
    },
    {
      id: 2,
      name: "Interview",
      slug: "interview",
      description: "Rerum deleniti rerum aliquid laudantium id non voluptatum. Aut quia distinctio consequatur velit natus inventore sunt iusto. Non totam quis quam sint et.",
      created_at: "2015-04-21 10:14:37",
      updated_at: "2015-04-21 10:14:37"
    },
    {
      id: 3,
      name: "Reportage",
      slug: "reportage",
      description: "Adipisci et veritatis excepturi ullam explicabo. Eos dolore quas a vero. Optio voluptatem accusamus ex optio. Rerum rem quaerat qui maiores.",
      created_at: "2015-04-21 10:14:37",
      updated_at: "2015-04-21 10:14:37"
    }
  ]

然而,现在考虑以下代码:

  $categories = \App\Category::all();
   return $categories;

然后,让我们将代码更改为这个:

$categories = \App\Category::all();
dd($categories);

这就是发生的事情:

  Collection {#152
    #items: array:3 [
      0 => Category {#155
        #connection: null
        #table: null
        #primaryKey: "id"
        #perPage: 15
        +incrementing: true
        +timestamps: true
        #attributes: array:6 [
          "id" => 1
          "name" => "News"
          "slug" => "news"
          "description" => "Qui est quo asperiores aliquid vitae possimus. Dolor consequuntur similique voluptatem a laborum dolorem ea repellendus. Aspernatur ducimus quis dolorum consequatur vel nam at. Aut omnis rem laborum."
          "created_at" => "2015-04-21 10:14:37"
          "updated_at" => "2015-04-21 10:14:37"
        ]
        #original: array:6 [
          "id" => 1
          "name" => "News"
          "slug" => "news"
          "description" => "Qui est quo asperiores aliquid vitae possimus. Dolor consequuntur similique voluptatem a laborum dolorem ea repellendus. Aspernatur ducimus quis dolorum consequatur vel nam at. Aut omnis rem laborum."
          "created_at" => "2015-04-21 10:14:37"
          "updated_at" => "2015-04-21 10:14:37"
        ]
        #relations: []
        #hidden: []
        #visible: []
        #appends: []
        #fillable: []
        #guarded: array:1 [
          0 => "*"
        ]
        #dates: []
        #casts: []
        #touches: []
        #observables: []
        #with: []
        #morphClass: null
        +exists: true
      }
      1 => Category {#156 ...}
      2 => Category {#157 ...}
    ]
  }

等等,等等;什么?只是改变一个 dd() 调用并返回?嗯,你可以使用两个特殊方法 toArraytoJSON 来看到这个魔法。如果你需要,你也可以手动使用它们,就像这样:

  $books = \App\Book::all();

  $toArray = $books->toArray();
  $toJson = $books->toJson();

很酷,对吧?

注意

我之前使用的 dd() 函数是 Laravel 的一个实用工具。它是原生 PHP 的 var_dump()die() 的混合。更准确地说,它会显示某个对象或变量的值,然后停止脚本。

迭代和过滤

有时候,你需要做的不只是将集合传递给视图,或者进行简单的 toArray() 调用。Eloquent 集合有许多你可以用来过滤和遍历其元素的方法。让我们看看实际操作!

迭代

首先,让我们从简单的迭代开始。你可以调用 each() 方法来遍历某个集合的元素:

  $books = \App\Book::all();

  $books->each(function($book)
  {
    echo $book->title;
  });

你所要做的就是传递一个闭包作为第一个(也是唯一一个)参数,该闭包有一个参数:将被使用的单个项。在这个例子中,我只是打印了所有的标题。

过滤

如果你想要更复杂地过滤你的集合,你可以使用 filter()。让我们来看一个例子:我想选择所有在 1840 年之后打印的书籍。

  $books = \App\Book:all();

  $books->filter(function($book)
  {
    if($book->year > 1840)
      return true;
    else
      return false;
  });

语法与上一个示例非常相似。你有一个闭包作为参数,传递一个单一参数;即集合项。

然而,这次你必须检查你的条件,如果你想包含(或不包含)这个项在 result 集合中,你需要返回 true 或 false。

因此,在这个特定的情况下,当前的 $book 值是在 1840 年之后打印的吗?太好了,请进。不是在 1840 年之后打印的?再见!

排序

最后,你可以使用某个字段对数据进行排序。这次你必须使用的方法是 sortBysortByDesc。我想你足够聪明,能够理解它们的作用,对吧?

然而,这里有一些示例:

  // ordering books by title, ascending
  $books = $books->sortBy(function($book)
  {
      return $book->title;
  });

  // ordering books by creation date, descending;
  $books = $books->sortByDesc(function($book)
  {
      return $book->created_at;
  });

此外,如果你的闭包逻辑非常简单,你可以使用快捷方式,例如之前的示例:

  $books = $books->sortBy('title');
  $books = $books->sortByDesc('created_at');

摘要

让我们明确一下;在我看来,了解集合的每一个方法并不是真的必不可少。然而,在某些需要特定方法来完成非常具体任务的情况下,它可能非常有用。我该如何表达呢?你知道的事情越多,你就越优秀!

现在,让我们继续前进!在这个短暂的休息之后,是时候进入事件的世界了!

第六章。使用事件和观察者全面控制一切

你听说过单一职责原则吗?我希望你听说过。它是编程中的 SOLID 原则之一,它基本上说一个类只有一个且只有一个职责。换句话说,每个类都必须做一件事,而不是其他任何事情。

通常,当你构建软件的第一个版本时,一切都很顺利。然后,事情发生了。你的老板打电话来:是时候介绍一个新功能了,开发者!特别是如果更新意味着在这里插入这个小小的额外行为,你的代码库很容易变得庞大而杂乱。

非常马虎!然后,你就要与截止日期、测试、问答等等作斗争,这简直是一场 Odyssey。这不是一个好的做法,对吧?

现在,在软件开发的世界里,你可以找到许多技术和方法以优雅的方式向你的软件添加新功能。你可能听说过编程中的事件

简而言之,可以说它包含这样的逻辑:当 X 做这件事时,Y 必须做那件事

想象一下在你的应用程序中类似的情景:你刚刚完成你的应用程序,然后你说,“哦,我刚刚忘记给新用户发送一封电子邮件了!”

使用 Eloquent,你可以以两种方式处理这种情况。第一种方式是使用非常有趣的概念:模型事件。第二种方式是基于一个更高级的概念:模型观察者。

在这一章中,首先,你将学习有关 Eloquent 模型中事件的所有内容。然后,我将介绍模型事件:它们是什么,以及你会在什么情况下使用它们。

然后,我将为模型观察者做同样的事情。你将学习所有这些差异,以及它们的优缺点。显然,对于这两个概念,我将使用一个实际例子来展示如何在现实世界中使用它们。

你准备好了吗,英雄?

  • 我应该在模型中使用事件吗?

  • 模型事件

  • 模型事件的例子

  • 模型观察者

  • 模型观察者的一个例子

我应该在模型中使用事件吗?

什么是事件?如果你在谷歌上搜索这个术语,你会得到多个结果。

例如,它可以定义为发生或被认为发生的事情;一个事件,尤其是某个重要的事件。它也可以定义为在特定时间间隔内发生在特定地点的事情

我喜欢这两个定义,因为它们在这个上下文中非常合适。事实上,你可以将这个特定的时间间隔视为模型的生命周期,从某种意义上说。

你可以创建一个新的实例,更新现有的实例,或者删除它。

你能做的每个操作都与两个事件相关。

从基础开始:我刚刚创建了那条记录,我删除了那条记录,或者我正在更新那条记录,听起来很自然,对吧?

好的。现在,Eloquent 在模型生命周期中发生某些事情时触发一些事件。更准确地说,它们如下:

创建 已保存
创建 删除
更新 已删除
已更新 恢复
保存 恢复

对于每个操作,你都有两个独立的事件。正如你可能想象的那样,它们指的是不同的时刻。让我们以创建操作为例。

你有创建事件,你可以将其读作“创建操作即将发生。”然后,你有已创建,这意味着“创建操作刚刚发生”。

就像科学家说的:

操作 描述
创建 关于t - 1时刻
已创建 t + 1时刻相关

因此,对于三个基本操作:创建、更新和删除,你有两个事件。

你还可以看到两个更多操作:保存和恢复。但是,不用担心,它们并不复杂,事实上:

  • 保存:你所需要知道的是,保存操作与创建更新都有关。假设你想添加一个行为,无论应用程序是创建记录还是保存现有记录。为什么要在两次声明相同的事情上浪费时间?只需使用“保存”通用操作即可完成。

  • 恢复:当你在某个模型上启用了软删除功能并撤销对其的删除操作时,使用恢复操作。

好的,我知道你在想什么:关于深入概念呢?

模型事件

我们将要查看的第一个事件技术被称为模型事件。基本概念非常简单:

  • EventServiceProvider类中,你可以添加一个特殊的事件监听器并将其绑定到某个闭包

  • 在这个闭包中,你将能够指定你的新行为,而无需修改模型代码

  • 这种绑定必须放在类的boot()方法中

这里是一个将创建用户事件与闭包绑定的简单示例,该闭包作为调用方法的参数传递。

闭包的$user参数包含相关用户的实例:

  public function boot(DispatcherContract $events)
  {
      parent::boot($events);

      User::created(function($user)
      {
          // doing something here, after User creation...
      });
  }

如你所想,每个模型都有这些方法,这些方法以我们之前看到的事件命名。所以,如果你想将某个操作绑定到已保存的事件,例如,你必须使用以下方法:

  User::saved(function($user)
    {
        // doing something here, after User save operation (both create and update)...
    });

另一个有趣的功能是使用pre方法停止当前操作的可能性。实际上,如果你使用以下任何一种:

  • 创建

  • 更新

  • 保存

  • 恢复

  • 删除

如果你想要中止操作,可以决定返回布尔值false

假设你想在用户电子邮件以@deniedprovider.com字符串结尾时中止创建操作。你可以这样做:

  User::creating(function($user)
    {
      if(ends_with($user->email, '@deniedprovider.com'))
      {
        return false;
      }
    });

显然,你不能在创建、更新、保存、恢复和删除事件上做同样的事情;事件已经发生,你不能回到过去!

模型事件的示例

我们现在可以看看一些模型事件在实际中的应用示例。

让我们从伟大的经典开始。一位新用户加入了我们,我们想通过欢迎邮件向他们打招呼。这很简单!让我们打开EventServiceProvider并在调用parent::boot()之后添加此代码:

  User::created(function($user){

    Mail::send('emails.welcome', ['user' => $user], function($message) use ($user)
    {
        $message->to($user->email, $user->first_name . ' ' . $user->last_name)->subject('Welcome to My Awesome App, '.$user->first_name.'!');
    });

  });

完成了!不是很容易吗?

注意

我假设你有一个 welcome.blade.php 视图在 resources/views/emails 文件夹下。我也假设你了解 Laravel 中发送电子邮件的基础知识。如果你需要更多信息,请访问 laravel.com/docs/5.0/mail#basic-usage

这里是模型事件在行动中的另一个例子:

让我们假设我们有一个类(为了这个示例的目的),它被委托给向每个想要知道某个作者新书被添加的用户发送电子邮件。这个类的名字是 NewBookNotifier,方法名为 forAuthor($authorId),其中 $authorId 是所需作者的键。

我们可以做一些类似的事情:

  Book::created(function($book){

    $newBookNotifier = new NewBookNotifier();
    $newBookNotifier->forAuthor($book->author->id);

  });

完成!主要观点是这非常简单。正如我之前提到的,最重要的是每个模型都保持不变。你甚至可以添加非常复杂的行为,但你不会触及模型中的任何东西。这是一个很大的优势,因为如果你测试这个模型并且没有触及它,你可以确信它永远不会出错。

现在,我将谈论更复杂的事情

事件观察者

模型事件很酷,我同意。然而,有时你可能需要更高级的功能。

当你使用 Laravel 时,你主要是在进行面向对象编程,并且可能希望对你的模型事件也这样做。你问题的答案是模型观察者,它是模型事件的更高级版本。

要使用它们,你只需要声明一个新的类,如下所示(可能在一个名为 observers 的专用文件夹中):

  class BookObserver {

    public function creating($book)
    {
      // I want to create the $book book, but first...
    }

      public function saving($book)
      {
          // I want to save the $book book, but first...
      }

      public function saved($book)
      {
          // I just saved the $book book, so....
      }

  } 

然后,你可以在 EventServiceProviderboot() 方法中注册它:

  Book::observe(new BookObserver);

EventServiceProvider 类的 boot() 方法中。

没有更多的事情了,概念完全相同。使用观察者,你还可以使用之前在模型事件中学到的每一个概念。你可以声明你想要的每一个方法,要绑定特定的事件,只需使用事件标识符作为方法名。所以,创建 事件将与 creating() 方法相关,依此类推。

显然,如果你使用方法,如创建更新,你也可以中止操作:

  class BookObserver {

    public function creating($book)
    {
      $somethingGoesWrong = true;

      if($somethingGoesWrong)
      {
        return false;
      }
    }

  }

好的,现在让我们看看几个使用模型观察者的例子!

模型观察者的一个例子

首先,这是你如何使用观察者做与第一个模型事件示例中相同的事情的方法。

app/Observers 下创建一个名为 WelcomeUserObserver.php 的新文件。现在,输入以下内容:

  <?php

  namespace App\Observers;

  class WelcomeUserObserver {

    public function created($user){

      Mail::send('emails.welcome', ['user' => $user], function($message) use ($user)
      {
          $message->to($user->email, $user->first_name . ' ' . $user->last_name)->subject('Welcome to My Awesome App, '.$user->first_name.'!');
      });

    }

  }

然后,你可以在 EventServiceProviderboot() 方法中注册观察者:

  /**
   * Register any other events for your application.
   *
   * @param  \Illuminate\Contracts\Events\Dispatcher  $events
   * @return void
   */
  public function boot(DispatcherContract $events)
  {
      parent::boot($events);

      User::observe(new WelcomeUserObserver);
  }

嘿!你完成了。你的观察者现在已经附加到你的模型上了。

现在,让我们想象另一种情况。在开发者会议之后,我们发现图书管理员需要对代码库做一些小的介绍:

  • 当系统添加新作者时,向每个用户发送通知

  • 每次添加或删除作者时发送的电子邮件

最后,每次删除一本书时,图书管理员必须知道在数据库中没有相关书籍的作者数量。

好的。让我们开始。我们将构建三个独立的类:记住,我的朋友,这是单一职责原则

我们将会有:

  • CustomerNewAuthorObserver

  • LibrarianAuthorObserver

  • AuthorsWithoutBooksObservers

注意

你可以按你的喜好命名你的类。我只是用这个约定作为一个例子,以便轻松地将行为与选定的名称联系起来。

然后,让我们创建三个独立的类:

  <?php

  // file: app/Observers/CustomerNewAuthorObserver

  namespace App\Observers;

  class CustomerNewAuthorObserver {

    public function created($author)
    {

    }

  }

  <?php

  // file: app/Observers/LibrarianAuthorObserver

  namespace App\Observers;

  class LibrarianAuthorObserver {

    public function created($author)
    {

    }

    public function deleted($author)
    {

    }

  }

  <?php

  // file: app/Observers/AuthorsWithoutBooksObservers

  namespace App\Observers;

  class AuthorsWithoutBooksObservers {

    public function deleted($author)
    {

    }

  }

好的。现在,是时候添加一些逻辑了。

首先,让我们添加CustomerNewAuthorObserver

  <?php

  // file: app/Observers/CustomerNewAuthorObserver

  namespace App\Observers;

  class CustomerNewAuthorObserver {

    public function created($author)
    {
      // getting all users...
      $users = \App\User::all();

      foreach($users as $user)
      {
        Mail::send('emails.created_author_customer', ['author' => $author], function($message) use ($user)
        {
            $message->to($user->email, $user->first_name . ' ' . $user->last_name)->subject('New Author Added!');
        });
      }
    }

  }

注意

我知道这是一种非常粗鲁的方法。像往常一样,这只是为了教学目的。不要在家里尝试这样做!

然后,我们的LibrarianAuthorObserver类如下:

  <?php

  // file: app/Observers/LibrarianAuthorObserver

  namespace App\Observers;

  class LibrarianAuthorObserver {

    public function created($author) {
      Mail::send('emails.created_author_librarian', ['author' => $author], function($message) use ($author)
      {
          $message->to('librarian@awesomelibrary.com', 'The Librarian')->subject('New Author: ' . $author->first_name . ' ' . $author->last_name);
      });
    }

    public function deleted($author) {
      Mail::send('emails.deleted_author_librarian', ['author' => $author], function($message) use ($author)
      {
          $message->to('librarian@awesomelibrary.com', 'The Librarian')->subject('New Author: ' . $author->first_name . ' ' . $author->last_name);
      });
    }

  }

最后,我们有以下内容:

  <?php

  // file: app/Observers/AuthorsWithoutBooksObservers

  namespace App\Observers;

  class AuthorsWithoutBooksObservers {

    public function deleted($author) {
      $authorsWithoutBooks = \App\Author::has('books', '=', 0)->get();

      if(count($authorsWithoutBooks) > 0){
        Mail::send('emails.author_without_books_librarian', ['authorsWithoutBooks' => $authorsWithoutBooks], function($message)
        {
            $message->to('librarian@awesomelibrary.com', 'The Librarian')->subject('Authors without Books! A check is required!');
        });
      }
    }

  }

注意

如前所述,我假设你们都已经拥有了所有需要的视图并且知道如何处理电子邮件。如果没有,请查看laravel.com/docs/5.0/mail#basic-usage页面。

这还没有结束。你可以使用观察者和事件来解决大量的案例和场景。仅举一个例子,想象你正在写一个博客,并且每次你创建或编辑文章时都想重新生成你的网站地图。观察者是答案,或者你可能想在添加新书时记录一些东西——再次使用事件观察者!

摘要

太棒了!你现在能够处理各种形式的事件,从非常基础的概念到更高级的观察者概念。你刚刚为你的 Eloquent 知识增添了另一个小片段:你走得越远,你将越多地了解如何制作复杂的应用程序。此外,我们也在尊重一些 SOLID 原则!

还不错,不是吗?然而,不要把观察者和事件用于所有事情。有时候,它们并不是最佳选择,你必须使用其他工具。所以,要小心,分析你想要解决的个别问题。一个好的技术并不总是适用于所有事情。

好吧,现在是时候向前迈出另一步了。如果你想,可以休息一下;我们工作的中间部分已经完成了。实际上,在接下来的两个章节中,你将学习一些高级内容。

你准备好了吗?太好了!

翻到下一页,学习如何在没有 Laravel 的情况下使用 Eloquent!

第七章. 无 Laravel 的 Eloquent…

我们的旅程即将结束,英雄。你从最基础的知识学到了关于 Eloquent 的所有内容,包括模型、关系和其他主题。你可能开始喜欢它,并考虑在下一个项目中实现它。

事实上,创建一个不使用单个 SQL 查询的应用程序很有吸引力。也许你也向你的老板展示了它,并说服他/她将其用于下一个生产项目。

我为你感到骄傲,英雄!

然而,有一个小问题。是的,下一个项目并不那么新。它已经存在了,尽管如此,它并没有使用 Laravel!你开始颤抖。这真是太遗憾了,因为你上周一直在学习这个新的 ORM,一个非常酷的 ORM,然后继续前进。

好吧,别抱怨了!总有解决办法!你是一名开发者!此外,解决方案并不难找到。如果你想,你可以使用 Eloquent 而不使用 Laravel。是的,真的!

实际上,Laravel 不是一个 单体 框架。它由几个独立的组件组成,这些组件组合在一起构建了更大的东西。然而,没有任何东西阻止你在另一个应用程序中仅使用所选的包。

一个非常酷的想法!

那么,我们将在本章中看到什么?

首先,我们将探索数据库包的结构,看看里面有什么。然后,你将学习如何为你的项目单独安装 illuminate/database 包,以及如何为其首次使用进行配置。

然后,你将遇到一些示例。首先,我们将查看 Eloquent ORM。你将学习如何定义模型并使用它们。

做完这件事后,作为额外的补充,我将向你展示如何使用 查询构建器(记住,"illuminate/database"包不仅仅是 Eloquent)。也许你也会喜欢 Schema Builder 类。我会涵盖它,不用担心!

由你负责!

我们将涵盖以下内容:

  • 探索目录结构

  • 安装和配置数据库包

  • 使用 ORM

  • 使用查询和模式构建器

  • 摘要

探索目录结构

如我之前所述,要在你的应用程序中使用 Eloquent 而不使用 Laravel 的关键步骤是使用 "illuminate/database" 包。

那么,在我们安装它之前,让我们先稍微了解一下。

你可以在这里看到包的内容:github.com/illuminate/database

所以,你可能会看到以下内容:

文件夹 描述
Capsule capsule 管理器是一个基本组件。它实例化服务容器并加载一些依赖项。
Connectors 数据库包可以与各种数据库系统通信。例如,SQLite、MySQL 或 PostgreSQL。每种数据库类型都有自己的连接器。这是你将找到它们的文件夹。
Console 数据库包不仅仅是 Eloquent 加上一堆连接器。在这个特定的文件夹中,你可以找到与控制台命令相关的一切,例如 artisan db:seedartisan migrate
Eloquent 每一个 Eloquent 类都放置在这里。
Migrations 不要与 Console 文件夹混淆。与迁移相关的所有类都存储在这里。当你你在终端中输入 artisan migrate 时,你正在调用放置在这里的一个类。
Query 查询构建器放置在这里。
Schema 与 Schema Builder 相关的一切都放置在这里。

在主文件夹中,你还可以找到一些其他文件。但是,不用担心,你不需要知道它们是什么。

如果你打开 composer.json 文件,看看下面的 "require" 部分:

"require": {
  "php": ">=5.4.0",
  "illuminate/container": "5.1.*",
  "illuminate/contracts": "5.1.*",
  "illuminate/support": "5.1.*",
  "nesbot/carbon": "~1.0"
},

如你所见,数据库包有一些先决条件,你无法避免。然而,容器相当小,对于 contracts(只是几个接口)和 "illuminate/support" 也是如此。

注意

Eloquent 使用 Carbon (github.com/briannesbitt/Carbon) 以更智能的方式处理日期。所以,如果你是第一次看到这个,感到困惑,不用担心!一切都很正常。

现在你已经知道了这个包中可以找到什么,让我们看看如何安装它并首次配置它。

安装和配置数据库包

让我们从设置开始。首先,我们将像往常一样使用 composer 安装包。然后,我们将配置胶囊管理器以开始。

安装包

安装 "illuminate/database" 包非常简单。

你所需要做的只是将 "illuminate/database" 添加到你的 composer.json 文件的 "require" 部分,如下所示:

"require": {

  "illuminate/database": "5.0.*",

},

然后在你的终端中输入 composer update,等待几秒钟。

另一种方法是在项目文件夹中使用快捷方式包含它,显然是从终端开始的:

composer require illuminate/database

无论你选择哪种方法,你都已经安装了包。

配置包

是时候使用胶囊管理器了!在你的项目中,你可以使用类似以下内容开始:

use Illuminate\Database\Capsule\Manager as Capsule;

$capsule = new Capsule;

$capsule->addConnection([
  'driver'    => 'mysql',
  'host'      => 'localhost',
  'database'  => 'database',
  'username'  => 'root',
  'password'  => 'password',
  'charset'   => 'utf8',
  'collation' => 'utf8_unicode_ci',
  'prefix'    => '',
]);

// Set the event dispatcher used by Eloquent models... (optional)
use Illuminate\Events\Dispatcher;
use Illuminate\Container\Container;
$capsule->setEventDispatcher(new Dispatcher(new Container));

我使用的配置语法与 config/database.php 配置文件中可以找到的完全相同。唯一的区别是这次你明确地使用胶囊管理器的一个实例来做所有的事情。

在代码的第二部分,我正在设置事件分配器。如果你项目需要事件,你必须这样做。

然而,默认情况下此包不包括事件,所以你将不得不手动将 "illuminate/events" 依赖项添加到你的 composer.json 文件中。

现在,最后一步!

将此代码添加到你的设置文件中:

// Make this Capsule instance available globally via static methods... (optional)
$capsule->setAsGlobal();

// Setup the Eloquent ORM... (optional; unless you've used setEventDispatcher())
$capsule->bootEloquent();

在胶囊管理器上调用 setAsGlobal() 后,你可以将其设置为全局组件,以便使用静态方法。你可能喜欢它,也可能不喜欢;选择权在你。最后一行启动 Eloquent,所以你需要它。

然而,这也是一个可选的指令。在某些情况下,你可能只需要查询构建器。

然后,就没有其他事情要做了!你的应用程序现在已经配置了数据库包(以及 Eloquent)!

使用 ORM

在非 Laravel 应用程序中使用 Eloquent ORM 并不是很大的变化。你所要做的就是像你习惯的那样声明你的模型。然后,你需要调用它并像你习惯的那样使用它。

这就是我所说的完美例子:

use Illuminate\Database\Eloquent\Model;

class Book extends Model {

  ...

  // some attributes here…
  protected $table = 'my_books_table';

  // some scopes here...
  public function scopeNewest()
  {
    // query here...
  }

  ...

}

就像你在 Laravel 中做的那样,你使用的包是相同的。所以,不用担心!如果你想使用你刚刚创建的模型,那么请使用以下内容:

$books = Book::newest()->take(5)->get();

这也适用于关系、观察者等等。一切都是一样的。

注意

为了精确地使用数据库包和 ORM,你会做与 Laravel 中相同的事情;请记住,以遵循 PSR-4 自动加载约定的方式设置项目结构。

使用查询和模式构建器

这不仅仅关于 ORM;有了数据库包,你还可以使用查询和模式构建器。让我们来探索一下!

查询构建器

查询构建器也非常容易使用。这次唯一的区别是,你将通过胶囊管理对象传递,就像这样:

$books = Capsule::table('books')
             ->where('title', '=', "Michael Strogoff")
             ->first();

然而,结果仍然是相同的。

此外,如果你喜欢 Laravel 中的 DB 门面,你可以以相同的方式使用胶囊管理类:

$book = Capsule::select('select title, pages_count from books where id = ?', array(12));

模式构建器

在这本书的开头,我向你展示了模式构建器。你学习了如何使用它与迁移一起,但现在,在没有 Laravel 的情况下,你没有迁移。

然而,你仍然可以使用模式构建器。就像这样:

Capsule::schema()->create('books', function($table)
{ 
    $table->increments(''id'); 
    $table->string(''title'', 30); 
    $table->integer(''pages_count''); 
    $table->decimal(''price'', 5, 2);.
    $table->text(''description''); 
    $table->timestamps(); 
});

之前,你通常调用Schema facadecreate()方法。这次有点不同:你将使用create()方法,将其链接到Capsule类的schema()方法。

显然,你可以以这种方式使用任何 Schema 类方法。例如,你可以调用以下内容:

Capsule::schema()->table('books', function($table)
{
    $table->string('title', 50)->change();
    $table->decimal('special_price', 5, 2);
});

你就可以开始了!

注意

记住,如果你想解锁一些 Schema Builder 特定功能,你需要安装其他依赖项。

例如,你想重命名一个列?你需要doctrine/dbal依赖包。

摘要

嗯,这次,进展相当快。

我决定添加这一章,因为很多人问我如何在没有 Laravel 的情况下使用 Eloquent。主要是因为他们喜欢这个框架,但他们无法将一个已经启动的项目整体迁移。

此外,我认为了解在某种程度上你可以在引擎盖下找到什么,也是很酷的。

这始终只是关于好奇心。好奇心开辟了新的道路,你可以选择以新的、更优雅的方式解决问题。

在这几页中,我只是触及了表面。我想给你一些建议:探索代码。编写好代码的最佳方式是阅读好代码。

现在,让我们进入最后一章!

第八章.还不够!扩展 Eloquent,高级概念

通过这本书,你学习了你可以用 Eloquent 做许多令人惊叹的事情。它是一个出色的活动记录实现;它易于使用,非常灵活,并且提供了许多工具来提高代码库的质量。

开发者通常必须面对两种类型的项目:应用应用,如下所示:

  • 对于应用,我打算做一些你可以做的事情,可能是一种快速的方式,一些在这里和那里的解决方案和技巧。而且,我知道你知道我在说什么。你为朋友做的那个网站,一个小博客,等等。

  • 让我们明确并严肃,你不能用同样的完美关怀来制作每个应用。说实话,可能没有人能做到。那么,你必须处理应用。事情可能会变得非常严重,你必须能够构建一个可维护、出色的应用结构。

这不仅仅是关于一些能工作的事情;在这种情况下,我在想的是一些可以轻松扩展且代码质量良好的事情。这不仅仅是关于从控制器中调用模型。这还不够。你可能会遇到许多问题:可测试性、可维护性,以及遵循某些原则。

在本章中,我们将探讨两种不同的方法来更认真地扩展 Eloquent。

在第一部分,你将学习如何扩展 Eloquent 模型类。实际上,模型做了很多事情,但如果我们需要更多呢?没问题:站在巨人的肩膀上,扩展现有的模型类将会变得容易。

你听说过 Ardent 项目吗?这是一个为 Laravel 4 设计的包,它扩展了模型类,添加了一些超级功能:自我验证的模型和从请求输入数据中自动填充。

你将为 Laravel 5 Eloquent 模型做类似的事情,我会一步步地教你如何做。此外,它受到了菲利普·布朗在他的博客中的工作(culttt.com/2013/08/05/extending-eloquent-in-laravel-4/)的启发。

然而,正如本书前面提到的,Laravel 主要关于自由,尤其是在组织项目方面的自由。现在,一个真正有趣的趋势是仓库模式。这是一种以更好的方式抽象你的代码并分离责任的方法。对于比面包店的博客更大的项目,这是你知识库中必须拥有的。

此外,仓库模式不仅仅与 Laravel 有关。这意味着你将学习一些新东西,你将来可以在其他语言和产品中重用。

好吧,不再闲聊。现在是时候最后一次动手实践了。

来吧,英雄!我们将涵盖以下主题:

  • 扩展模型:Aweloquent !

  • 深入到仓库模式

  • 摘要

扩展模型:Aweloquent!

Eloquent Model 实际上可以非常聪明且容易地完成很多事情。然而,在每次想要执行特定操作时,代码的编写方面可能需要改进。

通常,在创建新的模型实例时,你可能会使用用户之前输入到表单中的某些数据。

向我们的数据库添加新作者可以是一个完美的例子。你所要做的就是将姓名和姓氏输入到表单中,然后按下保存

然后,在专用的路由(或相对控制器方法)中,你将执行以下类似操作:

<?php

public function postAdd(Request $request)
{
  $author = new Author;

  $author->first_name = $request->input('first_name');
  $author->last_name = $request->input('last_name');

  $author->save();
}

这相当不错。然而,你可能还必须验证用户输入。

所以,假设你仍然在控制器中,你可以添加一个控制器验证调用,就像这样:

<?php

public function postAdd(Request $request)
{
  $this->validate($request, [
    'first_name' => 'required',
    'last_name' => 'required'
  ], [
    'first_name.required' => 'You forgot the first name!',
    'last_name.required' => 'You forgot the last name!'
  ]);

  $author = new Author;

  $author->first_name = $request->input('first_name');
  $author->last_name = $request->input('last_name');

  $author->save();
}

再次,救了场!

现在,开发者们经常就单个类的职责进行辩论,讨论这个类应该做什么,不应该做什么。每个人对这个话题都有自己的看法。

单一职责原则,SOLID 原则的一部分,对此非常明确——简单来说,这个原则指出,一个类应该只做一件事情。

另一方面,然而,你经常会发现非常大的类。Eloquent Model 就是其中之一。在撰写本文时,Illuminate\Database\Eloquent\Model 有 3,399 行代码。这绝对不是一个小数字!

显然,Model 并不执行单一操作;它填充自己的属性,处理关系,并序列化自己的属性。是的,它远远超出了你刚才读到的原则。

那么,这到底是怎么回事呢?

好吧,即使它非常大,这样的 Model 也允许你使用一个单独的类执行许多操作。

一个完美的例子是,你可以如何将 Model 用作模型,如下所示:

<?php

$user = new User;

// using magic methods...
$user->first_name = 'Francesco';
$user->last_name = 'Malatesta';

...

你还可以使用它作为工厂(一个用于以更优雅和更好的方式创建实例的类)使用create()方法:

<?php

$user = User::create([
  'first_name' => 'Francesco',
  'last_name' => 'Malatesta',
]);

如果这还不够,Model 还处理与实例持久化相关的一切:

<?php

$user = new User;

// some assignments...
$user->first_name = 'Francesco';
// ...

// and then save!
$user->save();

所有这些都可以使用一个单独的类——这是主要优势。

你可能正在问自己,所有这些话他到底想说什么。答案其实很简单:并不总是只有一个正确的解决方案。有些人讨厌 Eloquent Model,有些人则非常喜欢它。

所以,在这种情况下,我将向现有的 Model 类添加新功能,创建一个新的 Eloquent Model… Aweloquent

注意

在我们继续前进之前,这里有一个澄清。我会再次重复,但我也想现在就说明。在本章的后续部分,我们将扩展 Model 类,添加一个在 Laravel 中由Validator类处理的功能。我之所以教你们这个,并不是因为我想要你们这样做,而是因为我想要展示如何扩展 Model 类。

例如,你可能觉得智能密码哈希功能很愚蠢,但这只是一个例子。扩展模型和使用仓库是两种完全不同的技术,它们是完全分开的。我只是给你提供知识,然后你可以选择做什么,我相信你会做出正确的选择,英雄!

Aweloquent 模型

正如我之前提到的,我们的增强 Eloquent 模型将非常类似于 Ardent Laravel 4 包改进的模型。我还会从 Philip Brown 的 Magniloquent 项目中借鉴一些想法。

更精确地说,我们的改进模型将具有以下特性:

  • 自动填充

  • 模型自我验证

  • 智能密码哈希

  • 确认字段的自动清除

自动填充

而不是逐个分配属性,或者将它们作为一个数组传递,Aweloquent 模型将能够读取当前请求并自动填充其属性,而无需任何其他代码行。

这意味着你将能够使用以下内容:

<?php

$user = new User;
$user->save();

而不是更经典的:

<?php

$user = new User;

$user->first_name = 'Francesco';
$user->last_name = 'Malatesta';
// other assignments here...

$user->save();

模型自我验证

你将能够指定验证规则和消息作为模型的静态属性。然后,模型将自动执行你需要的验证操作,而无需使用任何外部类或控制器验证器。你还将能够将特定的规则分配给特定的操作('create''update'操作,或者两者都分配)。

所以,在你的模型中,你将会有类似以下的内容:

<?php namespace App;

use App\Aweloquent\AweloquentModel;

class Author extends AweloquentModel {

  protected $fillable = [
    'first_name', 'last_name', 'bio'
  ];

  protected static $rules = [
    'everytime' => [
      'first_name' => 'required'
    ],

     'create' => [
      'last_name' => 'required'
    ],

    'update' => [
      'bio' => 'required'
    ],
  ];

  protected static $messages = [
    'first_name.required' => 'You forgot the first name!',
    'last_name.required' => 'You forgot the last name!',
    'bio.required' => 'You forgot the biography!'
  ];

}

智能密码哈希

另一件你经常需要做的事情是哈希密码。通常,你会从password属性中获取值。因此,Aweloquent 模型会自动在存在password字段的情况下执行哈希操作。

确认字段的自动清除

Laravel 验证器有一个基于x_confirmation属性(其中x是字段的名称)的确认规则。你可能已经用它来为密码确认字段使用过了。Aweloquent 模型的自动清除功能会在验证后(当然)自动删除每个_confirmation字段。

然而,这还没有结束!Aweloquent 模型将自动排除由跨站请求伪造CSRF)保护中间件使用的'_token'字段。

好的,这就结束了!现在你可以编写一些代码了。

扩展类

你首先必须做的是创建一个新的类,即扩展现有 Eloquent 模型的AweloquentModel类。

在我的特定情况下,我做了件非常简单的事情:我在app文件夹中创建了一个名为Aweloquent的新文件夹,然后在其中创建了一个AweloquentModel.php文件。

这是你必须放入此文件的代码:

<?php

namespace App\Aweloquent;

use Illuminate\Database\Eloquent\Model;

class AweloquentModel extends Model {}

太好了!作为一个开始,我们有了新的AweloquentModel类。

如果你愿意,你可以将其用作未来模型的基础。这里没有变化,只是简单的扩展。

让我们添加第一个功能:自动填充。

自动填充功能

在我们实现这个第一个功能之前,让我们先思考一下我们想要的结果。

实际上,当你创建一个新模型时,你可以快速通过构造函数传递其属性:

<?php

$user = new User([
  'first_name' => 'Francesco',
  'last_name' => 'Malatesta'
]);

这些参数是从构造函数传递到另一个名为 fill() 的方法:

/**
   * Create a new Eloquent model instance.
   *
   * @param  array  $attributes
   * @return void
   */

  public function __construct(array $attributes = array())
  {
    $this->bootIfNotBooted();

    $this->syncOriginal();

    $this->fill($attributes);
  }

作为逻辑上的后果,如果我们想实现这个自动填充功能,我们必须编写一个新的构造函数来处理那里的自动填充并调用父类。所以,让我们回到我们的 AweloquentModel 类。这是第一个实现:

<?php

namespace App\Aweloquent;

use Illuminate\Database\Eloquent\Model;

class AweloquentModel extends Model {

  public function __construct(array $attributes = [])
  {
    $attributes = $this->autoHydrate($attributes);

    parent::__construct($attributes);
  }

  private function autoHydrate(array $attributes)
  {
    // getting the request instance using the service container
    $request = app('Illuminate\Http\Request');

    // getting the request form data, except the token
    $requestData = $request->except('_token');

    foreach($requestData as $name => $value)
    {
      // manually specified attribute has priority over auto- 
	  hydrated one.
      if(!isset($attributes[$name]))
      $attributes[$name] = $value;
    }

    return $attributes;
  }
}

autoHydrate 方法创建了以下内容:

  • 当前请求的一个实例以获取所需数据

  • 之后,并且对于每个循环,它将请求数据数组中的每个元素(排除 CSRF 的 '_token')添加到属性数组中

注意,显式指定的属性(你可以在模型构造函数中放入的属性)具有优先级,高于请求数据数组。因此,你仍然可以自由地处理模型并决定定义什么,不定义什么,也许可以添加一些你从表单中未获取到的额外数据。

如果你尝试通过设置基本表单来创建新用户,自动填充功能已经起作用了。

让我们继续前进!

Aweloquent 模型自验证功能 – 基本版本

是时候实现我们 Aweloquent 模型的自验证功能了。这个想法很简单:对于每个模型,你将能够声明(作为属性)规则和相关消息。所以,它应该看起来像这样:

<?php namespace App;

use App\Aweloquent\AweloquentModel;

class Author extends AweloquentModel {

  protected $fillable = [
    'first_name', 'last_name', 'bio'
  ];

  protected static $rules = [
    'first_name' => 'required',
    'last_name' => 'required'
  ];

  protected static $messages = [
    'first_name.required' => 'You forgot the first name!',
    'last_name.required' => 'You forgot the last name!'
  ];

}

这些规则和消息将被 validate() 专用方法自动使用。我想实现的是类似这样的效果:

<?php

$user = new User;

if(!$user->validate())
{
  dd($user->errors);
}

所以,让我们打开 AweloquentModel.php 文件并添加一些代码:

<?php

namespace App\Aweloquent;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Validator;

class AweloquentModel extends Model {

  protected static $rules = [];
  protected static $messages = [];

  public $errors;

  public function __construct(array $attributes = [])
  {
    $attributes = $this->autoHydrate($attributes);

    parent::__construct($attributes);
  }

  public function validate()
  {
    $validator = Validator::make($this->attributes, static::$rules, static::$messages);

    if($validator->fails())
    {
      $this->errors = $validator->messages();
      return false;
    }

    return true;
  }

  private function autoHydrate(array $attributes)
  {
    // auto hydrate method here...
  }

}

太好了!Validator Facadec 用于实例化一个新的验证器。静态 $rules$message 属性用于 make() 方法。

然后,$validator->fails() 调用确定给定的模型是否有效。如果不有效,则使用验证错误 MessageBag 对象填充 $errors 属性。

显然,这是一个非常基础的验证系统。然而,我们可以做更多。例如,基于操作的验证实现会很好。

如果你想尝试,那就继续吧!它已经起作用了!

Aweloquent 模型自验证功能 – 操作版本

为了实现自验证系统的先进版本,我们必须为每个模型定义规则格式。

在这个特定的版本中,我选择了类似这样的方法:

<?php namespace App;

use App\Aweloquent\AweloquentModel;

class Author extends AweloquentModel {

  protected $fillable = [
    'first_name', 'last_name', 'bio'
  ];

  protected static $rules = [
    'everytime' => [
      'first_name' => 'required'
    ],

    'create' => [
      'last_name' => 'required'
    ],

    'update' => [
      'bio' => 'required'
    ],
  ];

  protected static $messages = [
    'first_name.required' => 'You forgot the first name!',
    'last_name.required' => 'You forgot the last name!',
    'bio.required' => 'You forgot the biography!'
  ];

}

$message 属性保持不变。唯一需要修改的是 $rules,正如你可以想象的那样。

在这个 $rules 的新版本中,你可以为单个操作 'create' 或两者都定义规则。如果你想在这两者中都使用规则,有一个专门的 'everytime' 项来避免规则重复。

当然,我们必须再次编辑我们的 AweloquentModel。这次,我们必须定义一个方法,该方法必须与现有的验证方法一起工作,并理解我们是创建还是更新它。

然后,将正确的规则合并到一个数组中,并验证模型是否符合这些规则。

让我们看看我们能做什么!考虑以下代码:

<?php

class AweloquentModel extends Model {

  ...

  public function __construct(array $attributes = [])
  {
    // constructor remains the same…
  }

  public function validate()
  {
    static::$rules = $this->mergeValidationRules();

    $validator = Validator::make($this->attributes, static::$rules, static::$messages);

    if($validator->fails())
    {
      $this->errors = $validator->messages();
      return false;
    }

    return true;
  }

  private function mergeValidationRules()
  {
    // if updating, use "update" rules, "create" otherwise.
    if($this->exists)
      $mergedRules = array_merge_recursive(static::$rules['everytime'], static::$rules
      ['update']);
  else
    $mergedRules = array_merge_recursive(static::$rules['everytime'], static::$rules

    ['create']);

  $finalRules = [];

  foreach($mergedRules as $field => $rules){
    if(is_array($rules))
      $finalRules[$field] = implode("|", $rules);
    else
      $finalRules[$field] = $rules;
    }

    return $finalRules;
  }

}

太好了,我们做到了!

validate()方法变化不大。唯一大的不同在于新的一行:

static::$rules = $this->mergeValidationRules();

基本上,我们说的是:“好的,现在将$rules属性分配给这个mergeValidationRules()方法的返回结果。”

然后,在mergeValidationRules()方法中,首先使用的指令是:

if($this->exists)

这用于确定当前操作是插入还是更新。从这个值开始,我们可以获取正确的规则数组,并将它们与everytime规则合并。

你新的复杂自验证模型几乎可以使用了。

智能密码哈希和确认字段自动清除方法

我们必须实现的最后两个功能是智能密码哈希和确认字段自动清除方法。

第一件事非常简单直观:

private function smartPasswordHashing()
{
  if($this->attributes['password'])
    $this->attributes['password'] = Hash::make($this- >attributes['password']);
}

如果存在'password'字段,则对其进行哈希处理。仅此而已!

即使它稍微长一点,purgeConfirmationFields()也不难理解:

private function purgeConfirmationFields()
{
  foreach($this->attributes as $name => $value)
  {
    if(Str::endsWith($name, '_confirmation'))
      unset($this->attributes[$name]);
  }
}

这次,我使用了Str字符串实用类来使用endsWith()方法,该方法用于确定字符串是否以某个字符序列结束。每个'_confirmation'字段都被移除。

修复 save()模型方法

现在,我们需要修复的最后一件事情是save()方法。实际上,save()方法完全忽略了验证过程,这是不行的。所以,这是AweloquentModel类的最终版本:

<?php

namespace App\Aweloquent;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;

class AweloquentModel extends Model {

  protected static $rules = [];
  protected static $messages = [];

  public $errors;

  public function __construct(array $attributes = [])
  {
    $attributes = $this->autoHydrate($attributes);

    parent::__construct($attributes);
  }

  public function save(array $options = [])
  {
    if($this->validate())
    {
      $this->smartPasswordHashing();
      $this->purgeConfirmationFields();

      return parent::save($options);
    }
    else
      return false;
  }

  public function validate()
  {
    static::$rules = $this->mergeValidationRules();

    $validator = Validator::make($this->attributes, static::$rules, static::$messages);

    if($validator->fails())
    {
      $this->errors = $validator->messages();
      return false;
    }

    return true;
  }

  private function autoHydrate(array $attributes)
  {
    // getting the request instance using the service container
    $request = app('Illuminate\Http\Request');

    // getting the request form data, except the token
    $requestData = $request->except('_token');

    foreach($requestData as $name => $value)
    {
      // manually specified attribute has priority over auto- 
	  hydrated one.
      if(!isset($attributes[$name]))
        $attributes[$name] = $value;
    }

    return $attributes;
  }

  private function mergeValidationRules()
  {
    // if updating, use "update" rules, "create" otherwise.
    if($this->exists)
      $mergedRules = array_merge_recursive(static::$rules['everytime'], static::$rules

      ['update']);
    else
      $mergedRules = array_merge_recursive(static::$rules['everytime'], static::$rules

    ['create']);

    $finalRules = [];

    foreach($mergedRules as $field => $rules){
      if(is_array($rules))
        $finalRules[$field] = implode("|", $rules);
      else
        $finalRules[$field] = $rules;
    }

    return $finalRules;
  }

  private function smartPasswordHashing()
  {
    if($this->attributes['password'])
      $this->attributes['password'] = Hash::make($this- >attributes['password']);
  }

  private function purgeConfirmationFields()
  {
    foreach($this->attributes as $name => $value)
    {
      if(Str::endsWith($name, '_confirmation'))
        unset($this->attributes[$name]);
    }
  }
}

让我们详细分析save()方法:

public function save(array $options = [])
{
  if($this->validate())
  {
    $this->smartPasswordHashing();
    $this->purgeConfirmationFields();

    return parent::save($options);
  }
  else
    return false;
}

首先要做的是验证整个输入。之后,如果一切正常,密码将被哈希处理,确认字段将被清除,因为我们不再需要它们了。

最后,调用parent::save()方法,操作完成。

注意

为了与父类保持完美的连续性,我声明了与父类相同的签名(包括$options数组参数)的save()方法。

就这样!AweloquentModel类已经完成,你可以在你的项目中随意使用它。你还学会了如何深入到Model类中并扩展它,以便添加新的方法、行为和功能。

深入了解仓库模式

如果你了解一些关于良好开发和最佳实践的知识,你可能听说过软件设计模式。

你可以将其定义为针对某种问题的有用解决方案模板,或者更准确地说:

"在软件工程中,设计模式是在软件设计给定上下文中对常见问题的一般可重用解决方案。设计模式不是可以直接转换为源代码或机器代码的最终设计。它是对如何解决问题的描述或模板,可以在许多不同情况下使用。模式是程序员在设计和应用或系统时解决常见问题的最佳实践。"

注意

这个摘录来自维基百科上的软件设计模式页面(en.wikipedia.org/wiki/Software_design_pattern)。

现在,让我们专注于第二句话。

设计模式不是可以直接转换为源代码的东西。

这是最重要的部分,因为它解释了许多事情。这不是你为了 Laravel 或可能为了某种特定语言而专门学习的东西。

绝对地,一旦你了解了设计模式,它就会伴随你一生!

在本章的前一部分,你学习了如何创建 Eloquent 模型的改进版本。你通过向现有模型添加内容达到了目标。

然而,许多人不喜欢这种做法。他们坚信单一职责原则,所以每个类必须只做一件事情。没有更多!

让我们明确一点;我不想通过在这个长时间的辩论中添加我的无用观点来让你感到无聊。

在本章的最后部分,我将介绍一种可以真正有助于改进 Laravel 应用的特定设计模式——仓库模式。

嗨,仓库模式!

你知道我喜欢从例子开始解释一个概念。这也不例外。

想象一下,你正在为仓库构建一个应用程序。在这个仓库中,你可以存储任何你想要的东西,你可能有一个像这样的模型:

<?php namespace Warehouse;

use Illuminate\Database\Eloquent\Model;

class Item extends Model {

  // properties and methods here...

}
You are probably using this model in a controller, like this one:
<?php namespace Warehouse\Http\Controllers;

class ItemsController extends Controller {

  public function getIndex()
  {
    $items = \Warehouse\Item::orderBy('created_at', 'DESC')- >paginate(30);

    return view('item.list', compact('items'));
  }

}

没什么好说的;它有效,你知道这一点。

这是一个简单项目的酷解决方案,你可以称之为应用程序。然而,如果我们没有应用程序,会发生什么呢?

想象一下,你正在帮助的业务在增长,管理层决定创建一个必须内部使用的移动应用程序。你可能需要为其他开发者实现一个 API。

如果你不知道如何处理这类事情,你很快就会开始编写重复的代码。在你的 Rest API 中,肯定会有一个 \items 端点,它执行你在控制器方法中执行的操作,例如:

$items = \Warehouse\Item::orderBy('created_at', 'DESC')->paginate(30);

重复相同的代码很多次是不安全的。你知道这一点,对吧?

但别担心,英雄!解决方案被称为仓库模式

这个概念的最佳定义可以在 Martin Fowler 的网站上找到(martinfowler.com/eaaCatalog/repository.html):

"仓库在领域和数据映射层之间进行调解,就像内存中的领域对象集合。客户端对象以声明性方式构建查询规范,并将它们提交给仓库以满足需求。"

对象可以被添加到和从仓库中移除,就像它们可以从一个简单的对象集合中添加和移除一样,由仓库封装的映射代码将在幕后执行适当的操作。"

因此,想象一个仓库就像介于中间,抽象出所有必要的。

回到之前的例子,想象我们有一个包含专用方法getRecent($perPage, $pageNumber)的仓库。

我们将在控制器中使用相同的方法:

 <?php namespace Warehouse\Http\Controllers;

class ItemsController extends Controller {

  public function getIndex(ItemsRepository $itemsRepository)
  {
    // this is an example...
    $items = $itemsRepository->getRecent(30, 1);	

    return view('item.list', compact('items'));
  }

}

在 Rest API 中,使用以下方法:

<?php

Route::get('api/v1/items', function(ItemsRepository $repo){

  return $repo->getRecent(30, 1);

});

同样的代码被两次使用,但只写了一次。然而,还有更多;让我们看看如何在 Laravel 项目中实现仓库。

介绍仓库——一个具体的实现

开始使用仓库的最佳方式是实现一个具体的实现。正如我之前提到的,仓库位于控制器和模型之间。它是中间某个东西。

当你构建一个仓库时,你必须考虑到你将需要从仓库中获取什么。让我们为我们的Author模型想象一个例子。

我可能需要以下方法:

  • getAll($perPage, $pageNumber): 这将返回数据源中每个作者的分页列表

  • find($authorId): 这将返回具有特定主键的特定作者

  • search($firstName, $lastName): 这将返回从第一个名字到最后一个名字的结果数组

足够的搜索和获取记录了!然而,我们还需要其他方法,这些方法专门用于数据持久化:

  • create($authorData): 这将在数据源中保存一个新的作者

  • save($authorData, $authorId): 这将使用特定的主键更新数据源中现有的作者

让我们开始!

首先,在app文件夹中创建一个新的目录。命名为Repositories。在其内部,创建一个名为DbAuthorsRepository.php的新文件。

这里是内容:

<?php

namespace App\Repositories;

use App\Author;

class DbAuthorsRepository {

  private $model;

  public function __construct(Author $model)
  {
    $this->model = $model;
  }

  public function getAll($perPage, $pageNumber)
  {
    $authors = $this->model->skip(($pageNumber - 1) * $perPage)- >take($perPage)->get();
    return $authors->toArray();
  }

  public function find($authorId)
  {
    return $this->model->find($authorId)->toArray();
  }

  public function search($firstName, $lastName)
  {
    return $this->model
    ->where('first_name', 'LIKE', '%'.$firstName.'%')
    ->where('last_name', 'LIKE', '%'.$lastName.'%')
    ->get()
    ->toArray();
  }

  public function create($authorData)
  {
    return $this->model->create($authorData);
  }

  public function update($authorData, $authorId)
  {
    return $this->model->find($authorId)->update($authorData);
  }

}

这就是你可以在一些测试路由中使用它的方法:

<?php

Route::get('authors', function(\App\Repositories\DbAuthorsRepository $repository){

  return $repository->getAll(10, 1);

});

Route::get('create_author', function(\App\Repositories\DbAuthorsRepository $repository){

  $repository->create([
    'first_name' => 'Francesco',
    'last_name' => 'Malatesta',
    'bio' => 'Lorem ipsum...'
  ]);

});

Route::get('update_author', function(\App\Repositories\DbAuthorsRepository $repository){

  $repository->update([
    'first_name' => 'Frank',
    'last_name' => 'Smith',
    'bio' => 'Other ipsum...'
  ], 6);

});

没有更多了!

通过创建一个仓库,你学习了如何改进你的软件架构和解决方案的抽象级别。此外,与本章前面提到的 Aweloquent 相比,这次你可以感受到职责的极大分离。

然而,这还没有结束;仓库模式还没有展示出它的全部力量。

在抽象上编码

我已经在本章的早期介绍了你 SOLID 原则。我提到了单一职责原则,SOLID 的S。现在我们接近尾声,我将介绍D

依赖倒置原则是我最喜欢的之一,因为它真正强调了尽可能抽象你的代码库的重要性。

它的定义是:

“A. 高级模块不应该依赖于低级模块。两者都应该依赖于抽象。”

“B. 抽象不应该依赖于细节。细节应该依赖于抽象。”

“-维基百科 (en.wikipedia.org/wiki/Dependency_inversion_principle)”

简而言之,这个概念是你必须编写抽象,而不应该依赖于具体类。否则,更好的说法是:你必须依赖于抽象,而不是具体类。

在 PHP 中,谈论抽象是指使用接口和契约。

在某种意义上,Laravel 本身广泛使用这个概念。基本的 Laravel 包是Contracts,它由几个接口组成,这些接口指定了每个组件必须做什么以及如何做。

然而,这不仅仅是一个大型框架的问题。你可以在日常开发中应用这个原则。更具体地说,你可以将这个概念应用到仓库中。

我会向你展示如何!

仓库 – 一个完整的实现

在我们深入探讨之前,让我们向我们的软件引入一个小问题。

实际上,我们的情况是这样的:我们的控制器和路由使用DbAuthorsRepository类来获取数据,如下所示:

Route::get('authors', function(\App\Repositories\DbAuthorsRepository $repository){

  return $repository->getAll(10, 1);

});

然后,DbAuthorsRepository类使用作者模型从物理存储中获取所需的数据:

<?php

namespace App\Repositories;

use App\Author;

class DbAuthorsRepository {

  private $model;

  public function __construct(Author $model)
  {
    $this->model = $model;
  }

  public function getAll($perPage, $pageNumber)
  {
    $authors = $this->model->skip(($pageNumber - 1) * $perPage)- >take($perPage)->get();
    return $authors->toArray();
  }

}

现在,让我们假设我们的数据源发生了变化。出于某种原因(我知道,这相当矛盾),管理层想要切换到基于文件的存储。

你有两种处理这个问题的方法:

  • 你尖叫并因恐惧而瘫痪

  • 决定以更好的方式组织你的代码库,在你的仓库工作流程中引入接口

这里是计划:

使用 Laravel 服务容器,你可以决定将某个接口绑定到特定的实现。

因此,如果你为每个仓库创建一个接口,你将能够一次性编写代码,然后编写你需要的每个具体仓库,最后,要从一个仓库切换到另一个仓库,你只需更改一行代码。

然而,让我们一步一步来:

  1. 首先,让我们为我们的作者仓库定义一个标准行为,定义一个AuthorRepository接口。在app/Repositories/Contracts中创建一个新的AuthorRepository.php文件。我将使用Contracts文件夹来存储接口。

    这里是新鲜文件的目录:

    <?php
    
    namespace App\Repositories\Contracts;
    interface AuthorsRepository {
    
      public function getAll($perPage, $pageNumber);
    
      public function find($authorId);
    
      public function search($firstName, $lastName);
    
      public function create($authorData);
    
      public function update($authorData, $authorId);
    
    }
    

    接口是纯粹的抽象。我们在这里所说的就是,“当我构建一个新的作者仓库时,我不关心底层的实现。我不在乎我是使用 NoSQL 数据库还是平面文件驱动器。我只想每个仓库都实现所有这些方法。”

    以这种方式工作意味着我们可以为未来使用的每个组件(或在这个案例中是仓库)定义一个标准格式。

  2. 现在我们可以更新我们的DbAuthorsRepository类以实现我们的接口。考虑以下行:

    class DbAuthorsRepository {
    It now becomes:
    class DbAuthorsRepository implements AuthorsRepository {
    

    好的。现在,让我们看看整个机制在实际中的威力。

  3. 首先,打开app/Providers/AppServiceProvider.php文件,并将此绑定添加到register()方法中:

    public function register()
    {
      $this->app->bind(
        'App\Repositories\Contracts\AuthorsRepository',
        'App\Repositories\DbAuthorsRepository'
      );
    }
    

    Laravel 现在知道,每次你请求AuthorsRepository的实例时,它都必须使用服务容器创建一个DbAuthorsRepository的实例。

  4. 为了测试我们的假设,打开路由文件并添加以下内容:

    <?php
    
    Route::get('authors', function(\App\Repositories\Contracts\AuthorsRepository $repository){
    
        return $repository->getAll(10, 1);
    
    });
    

    结果将正好是我们所期望的。此外,使用方法注入技术,我们不需要显式调用服务容器。

  5. 事实上,这个语法的替代方案可以是以下这样:

    Route::get('authors', function(){
    
        $repository = app('App\Repositories\Contracts\AuthorsRepository');
    
        return $repository->getAll(10, 1);
    
    });
    

添加新的仓库

最后,我们接近了尾声。让我们回到我们的主要问题:我们必须实现一个新的基于文件的作者仓库。

到目前为止,这相当简单:

  1. 首先,在app/Repositories中创建一个新的文件,命名为FileAuthorsRepository。它将是一个新的类,当然。

  2. 它将实现AuthorsRepository接口,这是显而易见的。

    这里是类的内容:

    <?php
    
    namespace App\Repositories;
    
    use App\Repositories\Contracts\AuthorsRepository;
    
    class FileAuthorsRepository implements AuthorsRepository {
    
      public function getAll($perPage, $pageNumber)
      {
        dd('getting all records from flat file driver...');
      }
    
      public function find($authorId)
      {
        dd('searching by id: ' . $authorId);
      }
    
      public function search($firstName, $lastName)
      {
        dd('searching by first and last name...', $firstName, $lastName);
      }
    
      public function create($authorData)
      {
        dd('creating new author ', $authorData);
      }
    
      public function update($authorData, $authorId)
      {
        dd('updating author ' . $authorId, $authorData);
      }
    
    }
    

    如您所容易看到的,我已经从接口实现了所有必需的方法。这意味着我们的应用程序将能够以与 DbAuthorsRepository 相同的方式使用 FileAuthorsRepository。

  3. 我在方法体中添加了一些dd指令,只是为了展示这个概念是如何工作的。为了我们的最后一步,前往AppServiceProvider类并更新之前的绑定到以下内容:

    public function register()
    {
      $this->app->bind(
        'App\Repositories\Contracts\AuthorsRepository',
        'App\Repositories\FileAuthorsRepository'
      );
    }
    
  4. 现在,浏览到/authors路由。是的,输出现在是:

    "从平面文件驱动程序获取所有记录..."

是的,它工作了!

我们的路由文件永远不会知道它正在使用哪个仓库:接口定义了你需要的所有方法。

这真是太棒了,因为例如,如果你想在将来将 NoSQL 仓库添加到你的应用程序中,你只需要创建一个新的NoSQLAuthorsRepository类,该类实现了AuthorsRepository接口。然后,在AppServiceProvider中,你将切换到所需的绑定。

简单、酷炫,而且可测试!

也许我有点重复,但请关注这个具体点:使用提到的结构,你可以将处理数据的方式与访问数据的方式抽象出来。我知道一遍又一遍地读同样的事情很无聊,但我需要你理解这个概念。

很可能,你第一次读到关于仓库的内容时,会想“我在这里做什么?见鬼了?”我也做过同样的事情,所以我完全理解你的疑惑。然而,当你处理更复杂的项目时,你将完全感受到这种差异。

这就是仓库的魔力!

摘要

我们完成了。在本章的最后,你学习了以两种不同且独立的方式增强你的应用程序:一方面,向现有实体添加功能。在这种情况下,是 Eloquent 模型。

在另一方面,你学习了如何使用仓库以不同的方式来结构你的应用程序,以便获得更好的代码可测试性、可维护性,以及目的的分离,而不是将一切委托给单个类。

现在你已经拥有了构建优秀应用程序所需的所有工具,使用 Eloquent 和 Laravel 吧。

你还在等什么?继续吧,让我为你感到骄傲!

posted @ 2025-09-06 13:46  绝不原创的飞龙  阅读(2)  评论(0)    收藏  举报