PHP 7: 真实世界的应用开发
  • 前言
  • 模块一
    • 第一章、建立基础
      • PHP 7 安装注意事项
      • 使用内置的 PHP web 服务器
      • 创建一个 MySQL 测试数据库
      • 安装 PHPUnit
      • 实现类的自动加载
      • 抓取一个网站
      • 建立一个深度网络扫描器
      • 创建一个 PHP 5 到 PHP 7 代码转换器
    • 第二章、使用 PHP 7 高性能特性
      • 了解抽象语法树
      • 理解句法分析中的差异
      • 理解 foreach() 处理中的差异
      • 使用 PHP 7 增强功能提高性能
      • 遍历海量文件
      • 将电子表格上传到数据库
      • 递归目录迭代器
    • 第三章、使用 PHP 函数
      • 函数开发
      • 数据类型提示
      • 使用返回值数据类型
      • 使用迭代器
      • 使用生成器编写自己的迭代器
    • 第四章、使用 PHP 面向对象程序设计
      • 类的开发
      • 类的扩展
      • 使用静态属性和方法
      • 使用命名空间
      • 定义可见性
      • 使用接口
      • 使用特性
      • 实现匿名类
    • 第五章、与数据库的交互
      • 使用PDO连接数据库
      • 构建一个 OOP SQL 查询生成器
      • 处理分页
      • 定义实体以匹配数据库表
      • 将实体类与RDBMS查询绑定
      • 将二次查找嵌入到查询结果中
      • 实现jQuery DataTables的PHP查找
    • 第六章、建立可扩展的网站
      • 创建通用表单元素生成器
      • 创建一个HTML单选元素生成器
      • 创建一个HTML选择元素生成器
      • 实现表单工厂
      • 链式 $_POST 过滤器
      • 链式 $_POST 验证器
      • 将验证绑定到表单
    • 第七章、访问Web服务
      • 在PHP和XML之间转换
      • 创建一个简单的REST客户端
      • 创建一个简单的REST服务器
      • 创建一个简单的SOAP客户端
      • 创建一个简单的SOAP服务器
    • 第八章、处理日期/时间和国际化方面
      • 在视图脚本中使用 emoji
      • 转换复杂字符
      • 从浏览器数据获取语言环境
      • 按地区设置数字格式
      • 按地区处理货币
      • 按地区设置日期/时间格式
      • 创建一个HTML国际日历生成器
      • 构建一个周期性事件生成器
      • 不使用gettext处理翻译
    • 第九章、开发中间件
      • 使用中间件进行认证
      • 使用中间件实现访问控制
      • 使用高速缓存提高性能
      • 实施路由选择
      • 进行框架间的系统调用
      • 使用中间件来跨语言
    • 第十章、高级算法
      • 使用 getter 和 setter
      • 实现一个链表
      • 建立冒泡排序
      • 实现一个堆栈
      • 构建一个二分法查找类
      • 实现一个搜索引擎
      • 显示多维数组并累计总数
    • 第十一章、软件设计模式的实现
      • 创建数组到对象的转化器
      • 构建对象到数组到转化器
      • 实施策略模式
      • 定义一个映射器
      • 实现对象关系映射
      • 实施发布/订阅设计模式
    • 第十二章、提高网站安全
      • 过滤$_POST数据
      • 验证$_POST数据
      • 保护PHP session
      • 用令牌保护表格的安全
      • 建立一个安全的密码生成器
      • 带有验证码的安全保护表格
      • 不使用mcrypt进行加密/解密
    • 第十三章、最佳实践、测试和调试
      • 使用特征和接口
      • 通用异常处理程序
      • 通用错误处理程序
      • 编写一个简单的测试
      • 编写测试套件
      • 生成虚假的测试数据
      • 使用session_start参数自定义会话
    • PSR-7
  • 模块二
  • 模块三
    • GoF 设计模式
      • 结构型
      • 行为型
      • 小结
    • SOLID 设计原则
      • 开闭原则
      • 里氏替换原则
      • 接口隔离原则
      • 依赖反转原则
      • 小结
    • 模块化网店应用的需求规范
      • 线框设计
      • 定义技术栈
      • 小结
    • Symfony 概述
      • 创建一个空白项目
      • 使用 Symfony 控制台
      • 控制器
      • 路由
      • 模板
      • 表单
      • 配置 Symfony
      • bundle 系统
      • 数据库和 Doctrine
      • 测试
      • 验证
      • 小结
    • 构建核心模块
    • 构建目录模块
    • 构建客户模块
    • 构建支付模块
    • 构建发货模块
    • 构建销售模块
    • 总结
由 GitBook 提供支持
在本页
  • 如何做...
  • 技巧#1——预加载所有子信息
  • 技术方法#2——嵌入二级查找功能
  • 如何运行...
  • 更多...
  1. 模块一
  2. 第十一章、软件设计模式的实现

实现对象关系映射

有两种主要技术可以实现对象之间的关系映射。第一种技术是将相关的子对象预先加载到父对象中。这种方法的优点是容易实现,所有的父子信息都可以立即获得。缺点是可能会消耗大量的内存,而且性能曲线是倾斜的。

第二种技术是将二次查找嵌入到父对象中。在后一种方法中,当你需要访问子对象时,你会运行一个执行二次查找的getter。这种方法的优点是,性能需求被分散在整个请求周期中,而且内存的使用(或可以)更容易管理。这种方法的缺点是,会产生更多的查询,这意味着数据库服务器的工作更多。

不过,请注意,我们将说明如何利用准备好的报表来大大抵消这一不利因素。

如何做...

我们来看一下实现对象关系映射的两种技术。

技巧#1——预加载所有子信息

首先,我们将讨论如何通过将所有子类信息预加载到父类中来实现对象关系映射。在这个例子中,我们将使用三个相关的数据库表,即customer、purchases和product。

1.我们将使用现有的Application\Entity\Customer类(在第5章与数据库的交互中,在定义实体类以匹配数据库表的示例中定义)作为模型来开发Application\Entity\Purchase类。与之前一样,我们将使用数据库定义作为实体类定义的基础。下面是 purchases 的数据库定义。

CREATE TABLE `purchases` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `transaction` varchar(8) NOT NULL,
  `date` datetime NOT NULL,
  `quantity` int(10) unsigned NOT NULL,
  `sale_price` decimal(8,2) NOT NULL,
  `customer_id` int(11) DEFAULT NULL,
  `product_id` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `IDX_C3F3` (`customer_id`),
  KEY `IDX_665A` (`product_id`),
  CONSTRAINT `FK_665A` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`),
  CONSTRAINT `FK_C3F3` FOREIGN KEY (`customer_id`) REFERENCES `customer` (`id`)
);

2.基于customer实体类,以下是Application\Entity\Purchase的样子。请注意,并不是所有的getter和setter都被显示出来。

namespace Application\Entity;

class Purchase extends Base
{

  const TABLE_NAME = 'purchases';
  protected $transaction = '';
  protected $date = NULL;
  protected $quantity = 0;
  protected $salePrice = 0.0;
  protected $customerId = 0;
  protected $productId = 0;

  protected $mapping = [
    'id'            => 'id',
    'transaction'   => 'transaction',
    'date'          => 'date',
    'quantity'      => 'quantity',
    'sale_price'    => 'salePrice',
    'customer_id'   => 'customerId',
    'product_id'    => 'productId',
  ];

  public function getTransaction() : string
  {
    return $this->transaction;
  }
  public function setTransaction($transaction)
  {
    $this->transaction = $transaction;
  }
  // NOTE: other getters / setters are not shown here
}

3.现在我们准备定义Application\Entity\Product。这里是product的数据库定义。

CREATE TABLE `products` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `sku` varchar(16) DEFAULT NULL,
  `title` varchar(255) NOT NULL,
  `description` varchar(4096) DEFAULT NULL,
  `price` decimal(10,2) NOT NULL,
  `special` int(11) NOT NULL,
  `link` varchar(128) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `UNIQ_38C4` (`sku`)
);

4.基于customer实体类,下面是Application\Entity\Product的样子。

namespace Application\Entity;

class Product extends Base
{

  const TABLE_NAME = 'products';
  protected $sku = '';
  protected $title = '';
  protected $description = '';
  protected $price = 0.0;
  protected $special = 0;
  protected $link = '';

  protected $mapping = [
    'id'          => 'id',
    'sku'         => 'sku',
    'title'       => 'title',
    'description' => 'description',
    'price'       => 'price',
    'special'     => 'special',
    'link'        => 'link',
  ];

  public function getSku() : string
  {
    return $this->sku;
  }
  public function setSku($sku)
  {
    $this->sku = $sku;
  }
  // NOTE: other getters / setters are not shown here
}

5.接下来,我们需要实现一种嵌入相关对象的方法。我们将从Application\Entity\Customer父类开始。在本节中,我们将假设以下关系,如下图所示。

  • 一个顾客,多次购买

  • 一次购买,一个产品

6. 相应地,我们定义了一个getter和setter,以对象数组的形式处理购买。

protected $purchases = array();
public function addPurchase($purchase)
{
  $this->purchases[] = $purchase;
}
public function getPurchases()
{
  return $this->purchases;
}

7.现在我们把注意力转移到Application\Entity\Purchase上。在这种情况下,购买和产品之间是1:1的关系,所以不需要处理这个数组。

protected $product = NULL;
public function getProduct()
{
  return $this->product;
}
public function setProduct(Product $product)
{
  $this->product = $product;
}

注意,在这两个实体类中,我们并没有改变$mapping数组。这是因为实现对象关系映射对实体属性名和数据库列名之间的映射没有影响。

8. 由于仍然需要获取客户基本信息的核心功能,所以我们需要做的就是扩展第5章“与数据库的交互“中实体类与RDBMS查询配方中描述的Application\Database\CustomerService类。我们可以新建一个Application\Database\CustomerOrmService_1类,该类扩展Application\Database\CustomerService。

namespace Application\Database;
use PDO;
use PDOException;
use Application\Entity\Customer;
use Application\Entity\Product;
use Application\Entity\Purchase;
class CustomerOrmService_1 extends CustomerService
{
  // add methods here
}

9. 然后,我们在新的服务类中添加一个方法,执行查找并将结果以产品和采购实体的形式嵌入到核心客户实体中。这个方法以JOIN的形式执行查找。之所以能够做到这一点,是因为购买和产品之间存在1:1的关系。由于id列在两个表中的名称相同,我们需要添加购买ID列作为别名。然后我们在结果中循环,创建Product和Purchase实体。覆盖ID后,我们就可以将Product实体嵌入到Purchase实体中,然后将Purchase实体添加到Customer实体的数组中。

protected function fetchPurchasesForCustomer(Customer $cust)
{
  $sql = 'SELECT u.*,r.*,u.id AS purch_id '
    . 'FROM purchases AS u '
    . 'JOIN products AS r '
    . 'ON r.id = u.product_id '
    . 'WHERE u.customer_id = :id '
    . 'ORDER BY u.date';
  $stmt = $this->connection->pdo->prepare($sql);
  $stmt->execute(['id' => $cust->getId()]);
  while ($result = $stmt->fetch(PDO::FETCH_ASSOC)) {
    $product = Product::arrayToEntity($result, new Product());
    $product->setId($result['product_id']);
    $purch = Purchase::arrayToEntity($result, new Purchase());
    $purch->setId($result['purch_id']);
    $purch->setProduct($product);
    $cust->addPurchase($purch);
  }
  return $cust;
}

10. 接下来,我们为原来的fetchById()方法提供一个封装器。这块代码不仅需要获取原始的Customer实体,还需要查找并嵌入Product和Purchases实体。我们可以调用新的fetchByIdAndEmbedPurchases()方法,并接受一个客户ID作为参数。

public function fetchByIdAndEmbedPurchases($id)
{
  return $this->fetchPurchasesForCustomer(
    $this->fetchById($id));
}

技术方法#2——嵌入二级查找功能

现在我们将介绍将二次查找嵌入到相关实体类中。我们将继续使用与上面相同的图示,使用定义的实体类对应三个相关的数据库表,即customer、purchases和product。

1.这种方法的机制与上一节中描述的很相似。主要的区别是,我们不做数据库查询,也不立即生成实体类,而是嵌入一系列匿名函数,这些函数将做同样的事情,但从视图逻辑中调用。

2. 我们需要在Application\Entity\Customer类中添加一个新的方法,为购买属性添加一个条目。我们将提供一个匿名函数,而不是一个采购实体数组。

public function setPurchases(Closure $purchaseLookup)
{
  $this->purchases = $purchaseLookup;
}

3. 接下来,我们将Application\Database\CustomerOrmService_1类复制一份,并调用Application\Database\CustomerOrmService_2。

namespace Application\Database;
use PDO;
use PDOException;
use Application\Entity\Customer;
use Application\Entity\Product;
use Application\Entity\Purchase;
class CustomerOrmService_2 extends CustomerService
{
  // code
}

4. 然后,我们定义一个fetchPurchaseById()方法,该方法根据其ID查找单个购买并生成一个Purchase实体。 因为我们最终将以这种方式对单次购买提出一系列重复请求,所以我们可以通过处理相同的准备语句(在本例中为$purchPreparedStmt属性)来重新获得数据库效率。

public function fetchPurchaseById($purchId)
{
  if (!$this->purchPreparedStmt) {
      $sql = 'SELECT * FROM purchases WHERE id = :id';
      $this->purchPreparedStmt = 
      $this->connection->pdo->prepare($sql);
  }
  $this->purchPreparedStmt->execute(['id' => $purchId]);
  $result = $this->purchPreparedStmt->fetch(PDO::FETCH_ASSOC);
  return Purchase::arrayToEntity($result, new Purchase());
}

5. 之后,我们需要一个fetchProductById()方法,该方法根据其ID查找单个产品并产生一个Product实体。 假设客户可能多次购买相同的产品,我们可以通过将获得的产品实体存储在$products数组中来提高效率。 另外,与购买一样,我们可以在相同的准备好的语句上执行查找。

public function fetchProductById($prodId)
{
  if (!isset($this->products[$prodId])) {
      if (!$this->prodPreparedStmt) {
          $sql = 'SELECT * FROM products WHERE id = :id';
          $this->prodPreparedStmt = 
          $this->connection->pdo->prepare($sql);
      }
      $this->prodPreparedStmt->execute(['id' => $prodId]);
      $result = $this->prodPreparedStmt
      ->fetch(PDO::FETCH_ASSOC);
      $this->products[$prodId] = 
        Product::arrayToEntity($result, new Product());
  }
  return $this->products[$prodId];
}

6. 现在我们可以重构fetchPurchasesForCustomer()方法,让它嵌入一个匿名函数,同时调用fetchPurchaseById()和fetchProductById(),然后将得到的产品实体分配给新找到的购买实体。在这个例子中,我们做一个初始查找,只是返回这个客户的所有购买的ID。然后,我们在Customer::$purchases属性中嵌入一系列匿名函数,将购买ID作为数组键,匿名函数作为其值。

public function fetchPurchasesForCustomer(Customer $cust)
{
  $sql = 'SELECT id '
    . 'FROM purchases AS u '
    . 'WHERE u.customer_id = :id '
    . 'ORDER BY u.date';
  $stmt = $this->connection->pdo->prepare($sql);
  $stmt->execute(['id' => $cust->getId()]);
  while ($result = $stmt->fetch(PDO::FETCH_ASSOC)) {
    $cust->addPurchaseLookup(
    $result['id'],
    function ($purchId, $service) { 
      $purchase = $service->fetchPurchaseById($purchId);
      $product  = $service->fetchProductById(
                  $purchase->getProductId());
      $purchase->setProduct($product);
      return $purchase; }
    );
  }
  return $cust;
}

如何运行...

根据本示例的步骤定义以下类,如下。

Class

技巧 #1 步骤

Application\Entity\Purchase

1 - 2, 7

Application\Entity\Product

3 - 4

Application\Entity\Customer

6, 16, + 在第5章,与数据库的交互中

Application\Database\CustomerOrmService_1

8 - 10

第二种方法如下:

Class

技巧 #2 步骤

Application\Entity\Customer

2

Application\Database\CustomerOrmService_2

3 - 6

为了实现方法#1,在实体被嵌入的情况下,定义一个名为chap_11_orm_embedded.php的调用程序,它设置自动加载并使用适当的类。

<?php
define('DB_CONFIG_FILE', '/../config/db.config.php');
require __DIR__ . '/../Application/Autoload/Loader.php';
Application\Autoload\Loader::init(__DIR__ . '/..');
use Application\Database\Connection;
use Application\Database\CustomerOrmService_1;

接下来,创建一个服务的实例,用随机的ID查找一个客户。

$service = new CustomerOrmService_1(new Connection(include __DIR__ . DB_CONFIG_FILE));
$id   = rand(1,79);
$cust = $service->fetchByIdAndEmbedPurchases($id);

在视图逻辑中,你将通过fetchByIdAndEmbedPurchases()方法获得一个完全填充的Customer实体。现在你需要做的就是调用正确的getter来显示信息。

 <!-- Customer Info -->
  <h1><?= $cust->getname() ?></h1>
  <div class="row">
    <div class="left">Balance</div><div class="right">
      <?= $cust->getBalance(); ?></div>
  </div>
    <!-- etc. -->

显示购买信息所需的逻辑就会像下面的HTML一样。注意Customer::getPurchases()会返回一个Purchase实体的数组。要从Purchase实体中获取产品信息,在循环中调用Purchase::getProduct(),它将生成一个Product实体。然后你可以调用任何一个 Product 获取器,在这个例子中,Product::getTitle()。

  <!-- Purchases Info -->
  <table>
  <?php foreach ($cust->getPurchases() as $purchase) : ?>
  <tr>
  <td><?= $purchase->getTransaction() ?></td>
  <td><?= $purchase->getDate() ?></td>
  <td><?= $purchase->getQuantity() ?></td>
  <td><?= $purchase->getSalePrice() ?></td>
  <td><?= $purchase->getProduct()->getTitle() ?></td>
  </tr>
  <?php endforeach; ?>
</table>

将目光转向第二种方法,即使用二次查找,定义一个名为chap_11_orm_secondary_lookups.php的调用程序,该程序设置自动加载并使用相应的类。

<?php
define('DB_CONFIG_FILE', '/../config/db.config.php');
require __DIR__ . '/../Application/Autoload/Loader.php';
Application\Autoload\Loader::init(__DIR__ . '/..');
use Application\Database\Connection;
use Application\Database\CustomerOrmService_2;

接下来,创建一个服务的实例,用随机的ID查找一个客户。

$service = new CustomerOrmService_2(new Connection(include __DIR__ . DB_CONFIG_FILE));
$id   = rand(1,79);

现在你可以检索一个Application\Entity\Customer实例,并为这个客户调用fetchPurchasesForCustomer(),它嵌入了匿名函数序列。

$cust = $service->fetchById($id);
$cust = $service->fetchPurchasesForCustomer($cust);

显示核心客户信息的视图逻辑与之前描述的相同。显示购买信息所需的逻辑就像下面的HTML代码片段一样。请注意,Customer::getPurchases()返回一个匿名函数数组。每个函数调用都会返回一个特定的购买和相关产品。

<table>
  <?php foreach($cust->getPurchases() as $purchId => $function) : ?>
  <tr>
  <?php $purchase = $function($purchId, $service); ?>
  <td><?= $purchase->getTransaction() ?></td>
  <td><?= $purchase->getDate() ?></td>
  <td><?= $purchase->getQuantity() ?></td>
  <td><?= $purchase->getSalePrice() ?></td>
  <td><?= $purchase->getProduct()->getTitle() ?></td>
  </tr>
  <?php endforeach; ?>
</table>

下面是一个输出的例子。

最佳实践

虽然循环的每一次迭代都代表了两个独立的数据库查询(一个查询购买,一个查询产品),但由于使用了准备好的语句,效率得以保留。事先准备了两个语句:一个是查询特定的购买,另一个是查询特定的产品。这些准备好的语句就会被多次执行。同时,每一个产品的检索都独立地存储在一个数组中,从而达到更高的效率。

更多...

上一页定义一个映射器下一页实施发布/订阅设计模式

最后更新于4年前

实现对象关系映射的库的最好例子可能是Doctrine。Doctrine使用了一种嵌入式的方法,它的文档将其称为代理。更多信息,请参考。

http://www.doctrine-project.org/projects/orm.html