五、与数据库交互

在本章中,我们将介绍以下主题:

  • 使用 PDO 连接到数据库
  • 构建面向对象的 SQL 查询生成器
  • 处理分页
  • 定义实体以匹配数据库表
  • 将实体类绑定到 RDBMS 查询
  • 在查询结果中嵌入辅助查找
  • 实现 jQuery 数据表 PHP 查找

导言

在本章中,我们将介绍一系列利用PHP 数据对象PDO)扩展的数据库连接方法。将解决常见的编程问题,如结构化查询语言SQL)生成、分页以及将对象绑定到数据库表。最后,我们将展示以嵌入匿名函数的形式处理二次查找的代码,并使用 jquerydatatables 发出 AJAX 请求。

使用 PDO 连接数据库

PDO是一个高性能和主动维护的数据库扩展,与特定于供应商的扩展相比具有独特的优势。它有一个通用的应用程序编程接口API),可以兼容几乎十几个不同的关系数据库管理系统RDBMS)。学习如何使用此扩展将为您节省数小时的时间,您可以尝试掌握等效的单个供应商特定数据库扩展的命令子集。

PDO 细分为四个主要类别,如下表所示:

|

|

功能

| | --- | --- | | PDO | 维护与数据库的实际连接,并处理事务支持等低级功能 | | PDOStatement | 处理结果 | | PDOException | 特定于数据库的异常 | | PDODriver | 与实际供应商特定数据库进行通信 |

怎么做。。。

  1. 通过创建一个PDO实例来建立数据库连接。
  2. 您需要构造一个数据源名称DSN)。DSN 中包含的信息因使用的数据库驱动程序而异。作为一个示例,这里是一个用于连接MySQL数据库的 DSN:

    ```php $params = [ 'host' => 'localhost', 'user' => 'test', 'pwd' => 'password', 'db' => 'php7cookbook' ];

    try { $dsn = sprintf('mysql:host=%s;dbname=%s', $params['host'], $params['db']); $pdo = new PDO($dsn, $params['user'], $params['pwd']); } catch (PDOException $e) { echo $e->getMessage(); } catch (Throwable $e) { echo $e->getMessage(); } ```

  3. 另一方面,SQlite是一个较简单的扩展,只需要以下命令:

    php $params = [ 'db' => __DIR__ . '/../data/db/php7cookbook.db.sqlite' ]; $dsn = sprintf('sqlite:' . $params['db']);

  4. PostgreSQL则直接在 DSN 中包含用户名和密码:

    php $params = [ 'host' => 'localhost', 'user' => 'test', 'pwd' => 'password', 'db' => 'php7cookbook' ]; $dsn = sprintf('pgsql:host=%s;dbname=%s;user=%s;password=%s', $params['host'], $params['db'], $params['user'], $params['pwd']);

  5. The DSN could also include server-specific directives, such as unix_socket, as shown in the following example:

    ```php $params = [ 'host' => 'localhost', 'user' => 'test', 'pwd' => 'password', 'db' => 'php7cookbook', 'sock' => '/var/run/mysqld/mysqld.sock' ];

    try { $dsn = sprintf('mysql:host=%s;dbname=%s;unix_socket=%s', $params['host'], $params['db'], $params['sock']); $opts = [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]; $pdo = new PDO($dsn, $params['user'], $params['pwd'], $opts); } catch (PDOException $e) { echo $e->getMessage(); } catch (Throwable $e) { echo $e->getMessage(); } ```

    最佳实践

    将创建 PDO 实例的语句包装在try {} catch {}块中。在发生故障时,捕捉数据库特定信息的PDOException。捕捉Throwable错误或任何其他异常。将 PDO 错误模式设置为PDO::ERRMODE_EXCEPTION以获得最佳结果。有关错误模式的更多详细信息,请参阅步骤 8。

    在 PHP5 中,如果无法构造 PDO 对象(例如,当使用无效参数时),则为实例分配一个值NULL。在 PHP7 中,抛出一个Exception。如果您将 PDO 对象的构造包装在一个try {} catch {}块中,并且PDO::ATTR_ERRMODE被设置为PDO::ERRMODE_EXCEPTION,则您可以捕获并记录此类错误,而无需测试NULL

  6. Send an SQL command using PDO::query(). A PDOStatement instance is returned, against which you can fetch results. In this example, we are looking for the first 20 customers sorted by ID:

    php $stmt = $pdo->query( 'SELECT * FROM customer ORDER BY id LIMIT 20');

    PDO 还提供了一种方便的方法PDO::exec(),它不返回结果迭代,只返回受影响的行数。此方法最好用于管理操作,如ALTER TABLEDROP TABLE等。

  7. Iterate through the PDOStatement instance to process results. Set the fetch mode to either PDO::FETCH_NUM or PDO::FETCH_ASSOC to return results in the form of a numeric or associative array. In this example we use a while() loop to process results. When the last result has been fetched, the result is a boolean FALSE, ending the loop:

    php while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { printf('%4d | %20s | %5s' . PHP_EOL, $row['id'], $row['name'], $row['level']); }

    PDO fetch 操作涉及一个光标,它定义了迭代的方向(即向前或向后)。PDOStatement::fetch()的第二个参数可以是任何PDO::FETCH_ORI_*常量。光标方向包括先前、第一、最后、绝对和相对。默认光标方向为PDO::FETCH_ORI_NEXT

  8. 将 fetch 模式设置为PDO::FETCH_OBJ以将结果作为stdClass实例返回。这里您将注意到,while()循环利用了获取模式PDO::FETCH_OBJ。请注意,printf()语句引用了对象属性,而前面的示例引用了数组元素:

    php while ($row = $stmt->fetch(PDO::FETCH_OBJ)) { printf('%4d | %20s | %5s' . PHP_EOL, $row->id, $row->name, $row->level); }

  9. 如果要在处理查询时创建特定类的实例,请将 fetch 模式设置为PDO::FETCH_CLASS。您还必须有可用的类定义,PDO::query()应该设置类名。正如您在下面的代码片段中所看到的,我们定义了一个名为Customer的类,其公共属性为$id$name$level。属性需要为public才能使提取注入正常工作:

    ```php class Customer { public $id; public $name; public $level; }

    $stmt = $pdo->query($sql, PDO::FETCH_CLASS, 'Customer'); ```

  10. 获取对象时,步骤 5 中所示技术的一个更简单的替代方法是使用PDOStatement::fetchObject()

    php while ($row = $stmt->fetchObject('Customer')) { printf('%4d | %20s | %5s' . PHP_EOL, $row->id, $row->name, $row->level); }

  11. 您也可以使用PDO::FETCH_INTO,它本质上与PDO::FETCH_CLASS相同,但您需要一个活动对象实例,而不是类引用。循环中的每次迭代都会使用当前信息集重新填充相同的对象实例。本例假设与步骤 5 相同的类Customer,具有步骤 1 中定义的相同数据库参数和 PDO 连接:

    php $cust = new Customer(); while ($stmt->fetch(PDO::FETCH_INTO)) { printf('%4d | %20s | %5s' . PHP_EOL, $cust->id, $cust->name, $cust->level); }

  12. 如果未指定错误模式,则默认 PDO 错误模式为PDO::ERRMODE_SILENT。您可以使用PDO::ATTR_ERRMODE键和PDO::ERRMODE_WARNINGPDO::ERRMODE_EXCEPTION 值设置错误模式。错误模式可以以关联数组的形式指定为 PDO 构造函数的第四个参数。或者,您可以在现有实例上使用PDO::setAttribute()

  13. 让我们假设您拥有以下 DSN 和 SQL(在您开始认为这是一种新的 SQL 形式之前,请确保:此 SQL 语句将不起作用!):

    php $params = [ 'host' => 'localhost', 'user' => 'test', 'pwd' => 'password', 'db' => 'php7cookbook' ]; $dsn = sprintf('mysql:host=%s;dbname=%s', $params['host'], $params['db']); $sql = 'THIS SQL STATEMENT WILL NOT WORK';

  14. 如果您随后使用默认的错误模式建立 PDO 连接,则表明有问题的唯一线索是,PDO::query()将返回布尔值FALSE

    php $pdo1 = new PDO($dsn, $params['user'], $params['pwd']); $stmt = $pdo1->query($sql); $row = ($stmt) ? $stmt->fetch(PDO::FETCH_ASSOC) : 'No Good';

    ,而不是生成PDOStatement实例 15. 下一个示例显示使用构造函数方法将错误模式设置为WARNING

    php $pdo2 = new PDO( $dsn, $params['user'], $params['pwd'], [PDO::ATTR_ERRMODE => PDO::ERRMODE_WARNING]);

  15. 如果需要完全分离准备和执行阶段,请使用PDO::prepare()PDOStatement::execute()。然后将语句发送到数据库服务器进行预编译。然后,您可以根据需要多次执行该语句,很可能是在循环中执行。

  16. The first argument to PDO::prepare() can be an SQL statement with placeholders in place of actual values. An array of values can then be supplied to PDOStatement::execute(). PDO automatically provides database quoting, which helps safeguard against SQL Injection.

    最佳实践

    任何外部输入(即来自表单发布)与 SQL 语句组合的应用程序都会受到 SQL 注入攻击。所有外部输入必须首先进行适当过滤、验证和消毒。不要将外部输入直接放入 SQL 语句中。相反,请使用占位符,并在执行阶段提供实际(净化)值。

  17. 要以反向迭代结果,您可以更改可滚动光标的方向。或者,可能更容易,只需将ORDER BYASC反转为DESC。这行代码设置了一个PDOStatement 对象,请求滚动光标:

    php $dsn = sprintf('pgsql:charset=UTF8;host=%s;dbname=%s', $params['host'], $params['db']); $opts = [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]; $pdo = new PDO($dsn, $params['user'], $params['pwd'], $opts); $sql = 'SELECT * FROM customer ' . 'WHERE balance > :min AND balance < :max ' . 'ORDER BY id LIMIT 20'; $stmt = $pdo->prepare($sql, [PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL]);

  18. 您还需要在获取操作期间指定游标指令。本例获取结果集中的最后一行,然后向后滚动:

    php $stmt->execute(['min' => $min, 'max' => $max]); $row = $stmt->fetch(PDO::FETCH_ASSOC, PDO::FETCH_ORI_LAST); do { printf('%4d | %20s | %5s | %8.2f' . PHP_EOL, $row['id'], $row['name'], $row['level'], $row['balance']); } while ($row = $stmt->fetch(PDO::FETCH_ASSOC, PDO::FETCH_ORI_PRIOR));

  19. MySQL 和 SQLite 都不支持滚动游标!为了获得相同的结果,请尝试对前面的代码进行以下修改:

    php $dsn = sprintf('mysql:charset=UTF8;host=%s;dbname=%s', $params['host'], $params['db']); $opts = [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]; $pdo = new PDO($dsn, $params['user'], $params['pwd'], $opts); $sql = 'SELECT * FROM customer ' . 'WHERE balance > :min AND balance < :max ' . 'ORDER BY id DESC . 'LIMIT 20'; $stmt = $pdo->prepare($sql); while ($row = $stmt->fetch(PDO::FETCH_ASSOC)); printf('%4d | %20s | %5s | %8.2f' . PHP_EOL, $row['id'], $row['name'], $row['level'], $row['balance']); }

  20. PDO 为事务提供支持。借用步骤 9 中的代码,我们可以将INSERT系列命令包装成一个事务块:

    php try { $pdo->beginTransaction(); $sql = "INSERT INTO customer ('" . implode("','", $fields) . "') VALUES (?,?,?,?,?,?)"; $stmt = $pdo->prepare($sql); foreach ($data as $row) $stmt->execute($row); $pdo->commit(); } catch (PDOException $e) { error_log($e->getMessage()); $pdo->rollBack(); }

  21. 最后,为了保持所有内容模块化和可重用,我们可以将 PDO 连接封装到一个单独的类Application\Database\Connection。这里,我们通过构造函数构建一个连接。或者,有一个静态的factory()方法,可以让我们生成一系列 PDO 实例:

    ```php namespace Application\Database; use Exception; use PDO; class Connection { const ERROR_UNABLE = 'ERROR: no database connection'; public $pdo; public function construct(array $config) { if (!isset($config['driver'])) { $message = __METHOD . ' : ' . self::ERROR_UNABLE . PHP_EOL; throw new Exception($message); } $dsn = $this->makeDsn($config);
    try { $this->pdo = new PDO( $dsn, $config['user'], $config['password'], [PDO::ATTR_ERRMODE => $config['errmode']]); return TRUE; } catch (PDOException $e) { error_log($e->getMessage()); return FALSE; } }

    public static function factory(
      $driver, $dbname, $host, $user, 
      $pwd, array $options = array())
    {
        $dsn = $this->makeDsn($config);
    
        try {
            return new PDO($dsn, $user, $pwd, $options);
        } catch (PDOException $e) {
            error_log($e->getMessage);
        }
    }
    

    ```

  22. 这个Connection类的一个重要组件是一个可用于构造 DSN 的通用方法。我们需要做的就是将PDODriver作为前缀,后跟:"。之后,我们只需从配置数组中添加键/值对。每个键/值对之间用分号分隔。我们还需要去掉后面的分号,使用带有负数限制的substr()

    php public function makeDsn($config) { $dsn = $config['driver'] . ':'; unset($config['driver']); foreach ($config as $key => $value) { $dsn .= $key . '=' . $value . ';'; } return substr($dsn, 0, -1); } }

它是如何工作的。。。

首先,您可以将步骤 1 中的初始连接代码复制到一个chap_05_pdo_connect_mysql.php文件中。在本例中,我们假设您创建了一个名为php7cookbook的 MySQL 数据库,用户名为 cook,密码为 book。接下来,我们使用PDO::query()方法向数据库发送一条简单的 SQL 语句。最后,我们使用结果语句对象以关联数组的形式获取结果。别忘了将代码包装在try {} catch {}块中:

<?php
$params = [
  'host' => 'localhost',
  'user' => 'test',
  'pwd'  => 'password',
  'db'   => 'php7cookbook'
];
try {
  $dsn  = sprintf('mysql:charset=UTF8;host=%s;dbname=%s',
    $params['host'], $params['db']);
  $pdo  = new PDO($dsn, $params['user'], $params['pwd']);
  $stmt = $pdo->query(
    'SELECT * FROM customer ORDER BY id LIMIT 20');
  printf('%4s | %20s | %5s | %7s' . PHP_EOL, 
    'ID', 'NAME', 'LEVEL', 'BALANCE');
  printf('%4s | %20s | %5s | %7s' . PHP_EOL, 
    '----', str_repeat('-', 20), '-----', '-------');
  while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
    printf('%4d | %20s | %5s | %7.2f' . PHP_EOL, 
    $row['id'], $row['name'], $row['level'], $row['balance']);
  }
} catch (PDOException $e) {
  error_log($e->getMessage());
} catch (Throwable $e) {
  error_log($e->getMessage());
}

以下是结果输出:

How it works...

将该选项添加到 PDO 构造函数,该构造函数将错误模式设置为EXCEPTION。现在更改 SQL 语句并观察产生的错误消息:

$opts = [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION];
$pdo  = new PDO($dsn, $params['user'], $params['pwd'], $opts);
$stmt = $pdo->query('THIS SQL STATEMENT WILL NOT WORK');

你会看到这样的情况:

How it works...

占位符可以是命名的或位置的。在准备好的 SQL 语句中,命名占位符前面有一个冒号(:,是引用,作为提供给execute()的关联数组中的键。位置占位符在准备好的 SQL 语句中表示为问号(?

在下面的示例中,命名占位符用于表示WHERE子句中的值:

try {
  $dsn  = sprintf('mysql:host=%s;dbname=%s', 
                  $params['host'], $params['db']);
  $pdo  = new PDO($dsn, 
                  $params['user'], 
                  $params['pwd'], 
                  [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
  $sql  = 'SELECT * FROM customer '
      . 'WHERE balance < :val AND level = :level '
      . 'ORDER BY id LIMIT 20'; echo $sql . PHP_EOL;
  $stmt = $pdo->prepare($sql);
  $stmt->execute(['val' => 100, 'level' => 'BEG']);
  while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
    printf('%4d | %20s | %5s | %5.2f' . PHP_EOL, 
        $row['id'], $row['name'], $row['level'], $row['balance']);
  }
} catch (PDOException $e) {
  echo $e->getMessage();
} catch (Throwable $e) {
  echo $e->getMessage();
}

此示例显示了在INSERT操作中使用位置占位符。请注意,作为第四个客户插入的数据包含潜在的 SQL 注入攻击。您还将注意到,需要了解所使用数据库的 SQL 语法。在本例中,MySQL 列名使用反勾号(')引用:

$fields = ['name', 'balance', 'email', 
           'password', 'status', 'level'];
$data = [
  ['Saleen',0,'saleen@test.com', 'password',0,'BEG'],
  ['Lada',55.55,'lada@test.com',   'password',0,'INT'],
  ['Tonsoi',999.99,'tongsoi@test.com','password',1,'ADV'],
  ['SQL Injection',0.00,'bad','bad',1,
   'BEG\';DELETE FROM customer;--'],
];

try {
  $dsn  = sprintf('mysql:host=%s;dbname=%s', 
    $params['host'], $params['db']);
  $pdo  = new PDO($dsn, 
                  $params['user'], 
                  $params['pwd'], 
                  [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
  $sql  = "INSERT INTO customer ('" 
   . implode("','", $fields) 
   . "') VALUES (?,?,?,?,?,?)";
  $stmt = $pdo->prepare($sql);
  foreach ($data as $row) $stmt->execute($row);
} catch (PDOException $e) {
  echo $e->getMessage();
} catch (Throwable $e) {
  echo $e->getMessage();
}

要测试具有命名参数的已准备语句的使用情况,请修改 SQL 语句以添加一个WHERE子句,用于检查余额小于一定金额且级别等于BEGINTADV(即开始、中间或高级)的客户。使用PDO::prepare()代替PDO::query()。在获取结果之前,您必须执行PDOStatement::execute(),提供天平和液位的值:

$sql  = 'SELECT * FROM customer '
     . 'WHERE balance < :val AND level = :level '
     . 'ORDER BY id LIMIT 20';
$stmt = $pdo->prepare($sql);
$stmt->execute(['val' => 100, 'level' => 'BEG']);

以下是结果输出:

How it works...

您可以选择绑定参数,而不是在调用PDOStatement::execute()时提供参数。这允许您将变量分配给占位符。在执行时,使用变量的当前值。

在本例中,我们将变量$min$max$level绑定到准备好的语句:

$min   = 0;
$max   = 0;
$level = '';

try {
  $dsn  = sprintf('mysql:host=%s;dbname=%s', $params['host'], $params['db']);
  $opts = [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION];
  $pdo  = new PDO($dsn, $params['user'], $params['pwd'], $opts);
  $sql  = 'SELECT * FROM customer '
      . 'WHERE balance > :min '
      . 'AND balance < :max AND level = :level '
      . 'ORDER BY id LIMIT 20';
  $stmt = $pdo->prepare($sql);
  $stmt->bindParam('min',   $min);
 $stmt->bindParam('max',   $max);
 $stmt->bindParam('level', $level);

  $min   =  5000;
  $max   = 10000;
  $level = 'ADV';
  $stmt->execute();
  showResults($stmt, $min, $max, $level);

  $min   = 0;
  $max   = 100;
  $level = 'BEG';
  $stmt->execute();
  showResults($stmt, $min, $max, $level);

} catch (PDOException $e) {
  echo $e->getMessage();
} catch (Throwable $e) {
  echo $e->getMessage();
}

当这些变量的值发生变化时,下一次执行将反映修改后的标准。

提示

最佳实践

一次性数据库命令使用PDO::query()。当您需要多次处理同一条语句但使用不同的值时,请使用PDO::prepare()PDOStatement::execute()

另见

有关与不同供应商特定 PDO 驱动程序相关的语法和独特行为的信息,请参阅本文章:

有关 PDO 预定义常量的摘要,包括获取模式、光标方向和属性,请参阅以下文章:

构建面向对象的 SQL 查询生成器

PHP7 实现了一种称为上下文敏感词法器的东西。的意思是,如果上下文允许,可以使用通常保留的单词。因此,在构建面向对象的 SQL builder 时,我们可以使用名为andornot等方法。

怎么做。。。

  1. 我们定义了一个Application\Database\Finder类。在类中,我们定义了与我们最喜欢的 SQL 操作相匹配的方法:

    ```php namespace Application\Database; class Finder { public static $sql = ''; public static $instance = NULL; public static $prefix = ''; public static $where = array(); public static $control = ['', ''];

    // $a == name of table
    // $cols = column names
    public static function select($a, $cols = NULL)
    {
      self::$instance  = new Finder();
      if ($cols) {
           self::$prefix = 'SELECT ' . $cols . ' FROM ' . $a;
      } else {
        self::$prefix = 'SELECT * FROM ' . $a;
      }
      return self::$instance;
    }
    
    public static function where($a = NULL)
    {
        self::$where[0] = ' WHERE ' . $a;
        return self::$instance;
    }
    
    public static function like($a, $b)
    {
        self::$where[] = trim($a . ' LIKE ' . $b);
        return self::$instance;
    }
    
    public static function and($a = NULL)
    {
        self::$where[] = trim('AND ' . $a);
        return self::$instance;
    }
    
    public static function or($a = NULL)
    {
        self::$where[] = trim('OR ' . $a);
        return self::$instance;
    }
    
    public static function in(array $a)
    {
        self::$where[] = 'IN ( ' . implode(',', $a) . ' )';
        return self::$instance;
    }
    
    public static function not($a = NULL)
    {
        self::$where[] = trim('NOT ' . $a);
        return self::$instance;
    }
    
    public static function limit($limit)
    {
        self::$control[0] = 'LIMIT ' . $limit;
        return self::$instance;
    }
    
    public static function offset($offset)
    {
        self::$control[1] = 'OFFSET ' . $offset;
        return self::$instance;
    }
    

    public static function getSql() { self::$sql = self::$prefix . implode(' ', self::$where) . ' ' . self::$control[0] . ' ' . self::$control[1]; preg_replace('/ /', ' ', self::$sql); return trim(self::$sql); } } ```

  2. 用于生成 SQL 片段的每个函数都返回相同的属性$instance。这允许我们使用流畅的界面来表示代码,例如:

    php $sql = Finder::select('project')->where('priority > 9') ... etc.

它是如何工作的。。。

将前面定义的代码复制到Application\Database文件夹中的Finder.php文件中。然后,您可以创建一个名为 Ty2 T2 的调用程序,该程序初始化在 AutoT5 中定义的 AutoLooDER,第 1 章 AUTT6T,AUTT7。然后可以运行Finder::select()生成一个对象,从中可以呈现 SQL 字符串:

<?php
require __DIR__ . '/../Application/Autoload/Loader.php';
Application\Autoload\Loader::init(__DIR__ . '/..');
use Application\Database\Finder;

$sql = Finder::select('project')
  ->where()
  ->like('name', '%secret%')
  ->and('priority > 9')
  ->or('code')->in(['4', '5', '7'])
  ->and()->not('created_at')
  ->limit(10)
  ->offset(20);

echo Finder::getSql();

以下是预编码代码的结果:

How it works...

另见

有关上下文敏感 lexer 的更多信息,请参见本文:

https://wiki.php.net/rfc/context_sensitive_lexer

处理分页

分页涉及提供数据库查询结果的有限子集。这通常用于显示目的,但也可以很容易地应用于其他情况。乍一看,LimitIterator类似乎非常适合分页。在潜在结果集可能很大的情况下;然而,LimitIterator并不是一个理想的候选者,因为您需要将整个结果集作为内部迭代器提供,这很可能会超出内存限制。LimitIterator类构造函数的第二个和第三个参数是 offset 和 count。这表明我们将采用的分页解决方案是 SQL 固有的:向给定的 SQL 语句添加LIMITOFFSET子句。

怎么做。。。

  1. 首先,我们创建一个名为Application\Database\Paginate的类来保存分页逻辑。我们添加属性来表示与分页相关的值,$sql$page$linesPerPage

    ```php namespace Application\Database;

    class Paginate {

    const DEFAULT_LIMIT = 20; const DEFAULT_OFFSET = 0;

    protected $sql; protected $page; protected $linesPerPage;

    } ```

  2. 接下来,我们定义一个__construct()方法,该方法接受基本 SQL 语句、当前页码和每页的行数作为参数。然后我们需要重构 SQL 字符串,修改或添加LIMITOFFSET子句。

  3. 在构造函数中,我们需要使用当前页码和每页的行数计算偏移量。我们还需要检查LIMITOFFSET是否已经存在于 SQL 语句中。最后,我们需要使用重新计算的OFFSET

    php public function __construct($sql, $page, $linesPerPage) { $offset = $page * $linesPerPage; switch (TRUE) { case (stripos($sql, 'LIMIT') && strpos($sql, 'OFFSET')) : // no action needed break; case (stripos($sql, 'LIMIT')) : $sql .= ' LIMIT ' . self::DEFAULT_LIMIT; break; case (stripos($sql, 'OFFSET')) : $sql .= ' OFFSET ' . self::DEFAULT_OFFSET; break; default : $sql .= ' LIMIT ' . self::DEFAULT_LIMIT; $sql .= ' OFFSET ' . self::DEFAULT_OFFSET; break; } $this->sql = preg_replace('/LIMIT \d+.*OFFSET \d+/Ui', 'LIMIT ' . $linesPerPage . ' OFFSET ' . $offset, $sql); }

    修改语句,将每页的行数作为我们的LIMIT 4. 现在,我们已经准备好使用第一个配方中讨论的Application\Database\Connection类执行查询。 5. 在新的分页类中,我们添加了一个paginate()方法,该方法将Connection实例作为参数。我们还需要 PDO fetch 模式,以及可选的 prepared 语句参数:

    php use PDOException; public function paginate( Connection $connection, $fetchMode, $params = array()) { try { $stmt = $connection->pdo->prepare($this->sql); if (!$stmt) return FALSE; if ($params) { $stmt->execute($params); } else { $stmt->execute(); } while ($result = $stmt->fetch($fetchMode)) yield $result; } catch (PDOException $e) { error_log($e->getMessage()); return FALSE; } catch (Throwable $e) { error_log($e->getMessage()); return FALSE; } }

  4. 为前面提到的查询生成器类提供支持可能不是一个坏主意。这将使更新LIMITOFFSET更加容易。为Application\Database\Finder提供支持所需要做的就是使用该类并修改__construct()方法来检查传入的 SQL 是否是该类的实例:

    php if ($sql instanceof Finder) { $sql->limit($linesPerPage); $sql->offset($offset); $this->sql = $sql::getSql(); } elseif (is_string($sql)) { switch (TRUE) { case (stripos($sql, 'LIMIT') && strpos($sql, 'OFFSET')) : // remaining code as shown in bullet #3 above } }

  5. 现在需要做的就是添加一个getSql()方法,以防我们需要确认 SQL 语句的格式是否正确:

    php public function getSql() { return $this->sql; }

它是如何工作的。。。

将前面的代码复制到Application/Database 文件夹中的Paginate.php 文件中。然后,您可以创建一个名为 Ty2 T2 的调用程序,该程序初始化 AutoLoad 中定义的第 1 章 AUTT5;

<?php
define('DB_CONFIG_FILE', '/../config/db.config.php');
define('LINES_PER_PAGE', 10);
define('DEFAULT_BALANCE', 1000);
require __DIR__ . '/../Application/Autoload/Loader.php';
Application\Autoload\Loader::init(__DIR__ . '/..');

接下来,使用Application\Database\FinderConnectionPaginate类,创建Application\Database\Connection实例,使用Finder生成 SQL:

use Application\Database\ { Finder, Connection, Paginate};
$conn = new Connection(include __DIR__ . DB_CONFIG_FILE);
$sql = Finder::select('customer')->where('balance < :bal');

我们现在可以从$_GET参数中获取页码和余额,并创建Paginate对象,结束 PHP 块:

$page = (int) ($_GET['page'] ?? 0);
$bal  = (float) ($_GET['balance'] ?? DEFAULT_BALANCE);
$paginate = new Paginate($sql::getSql(), $page, LINES_PER_PAGE);
?>

在脚本的输出部分,我们使用一个简单的foreach()循环简单地迭代分页:

<h3><?= $paginate->getSql(); ?></h3>    
<hr>
<pre>
<?php
printf('%4s | %20s | %5s | %7s' . PHP_EOL, 
  'ID', 'NAME', 'LEVEL', 'BALANCE');
printf('%4s | %20s | %5s | %7s' . PHP_EOL, 
  '----', str_repeat('-', 20), '-----', '-------');
foreach ($paginate->paginate($conn, PDO::FETCH_ASSOC, 
  ['bal' => $bal]) as $row) {
  printf('%4d | %20s | %5s | %7.2f' . PHP_EOL, 
      $row['id'],$row['name'],$row['level'],$row['balance']);
}
printf('%4s | %20s | %5s | %7s' . PHP_EOL, 
  '----', str_repeat('-', 20), '-----', '-------');
?>
<a href="?page=<?= $page - 1; ?>&balance=<?= $bal ?>">
<< Prev </a>&nbsp;&nbsp;
<a href="?page=<?= $page + 1; ?>&balance=<?= $bal ?>">
Next >></a>
</pre>

以下是输出的第 3 页,其中余额小于 1000:

How it works...

另见

关于LimitIterator类的更多信息,请参阅本文:

定义实体以匹配数据库表

PHP 开发人员的一个非常常见的做法是创建表示数据库表的类。这些类通常被称为实体类,构成领域模型软件设计模式的核心。

怎么做。。。

  1. 首先,我们将建立一系列实体类的一些公共特性。这些可能包括公共属性和公共方法。我们将把这些放在Application\Entity\Base类中。所有未来的实体类都将扩展Base
  2. 在本例中,假设所有实体都有两个共同的属性:$mapping(稍后讨论)和$id(及其相应的 getter 和 setter):

    ```php namespace Application\Entity;

    class Base {

    protected $id = 0; protected $mapping = ['id' => 'id'];

    public function getId() : int { return $this->id; }

    public function setId($id) { $this->id = (int) $id; } } ```

  3. 定义一个arrayToEntity()方法并不是一个坏主意,它将数组转换为实体类的实例,反之亦然(entityToArray()。这些方法实施的过程通常称为即水合。因为这些方法应该是泛型的,所以它们最好放在Base类中。

  4. 在下面的方法中,$mapping属性用于在数据库列名和对象属性名之间进行转换。arrayToEntity()从数组中填充此对象实例的值。我们可以将此方法定义为静态,以防需要在活动实例之外调用它:

    php public static function arrayToEntity($data, Base $instance) { if ($data && is_array($data)) { foreach ($instance->mapping as $dbColumn => $propertyName) { $method = 'set' . ucfirst($propertyName); $instance->$method($data[$dbColumn]); } return $instance; } return FALSE; }

  5. entityToArray()从当前实例属性值生成一个数组:

    php public function entityToArray() { $data = array(); foreach ($this->mapping as $dbColumn => $propertyName) { $method = 'get' . ucfirst($propertyName); $data[$dbColumn] = $this->$method() ?? NULL; } return $data; }

  6. 要构建特定的实体,您需要准备好要建模的数据库表的结构。创建映射到数据库列的属性。分配的初始值应反映数据库列的最终数据类型。

  7. 在本例中,我们将使用表customer。下面是 MySQL 数据转储中的CREATE语句,它说明了它的数据结构:

    php CREATE TABLE 'customer' ( 'id' int(11) NOT NULL AUTO_INCREMENT, 'name' varchar(256) CHARACTER SET latin1 COLLATE latin1_general_cs NOT NULL, 'balance' decimal(10,2) NOT NULL, 'email' varchar(250) NOT NULL, 'password' char(16) NOT NULL, 'status' int(10) unsigned NOT NULL DEFAULT '0', 'security_question' varchar(250) DEFAULT NULL, 'confirm_code' varchar(32) DEFAULT NULL, 'profile_id' int(11) DEFAULT NULL, 'level' char(3) NOT NULL, PRIMARY KEY ('id'), UNIQUE KEY 'UNIQ_81398E09E7927C74' ('email') );

  8. 我们现在可以充实类属性了。这也是确定相应表的好地方。在这种情况下,我们将使用一个TABLE_NAME类常量:

    ```php namespace Application\Entity;

    class Customer extends Base { const TABLE_NAME = 'customer'; protected $name = ''; protected $balance = 0.0; protected $email = ''; protected $password = ''; protected $status = ''; protected $securityQuestion = ''; protected $confirmCode = ''; protected $profileId = 0; protected $level = ''; } ```

  9. 将属性定义为protected被认为是最佳实践。为了访问这些属性,您需要设计能够访问这些属性的public方法getset。这里是一个很好的地方,可以使用 PHP7 将数据类型转换为返回值。

  10. In the following block of code, we have defined getters and setters for $name and $balance. You can imagine how the remainder of these methods will be defined:

    php public function getName() : string { return $this->name; } public function setName($name) { $this->name = $name; } public function getBalance() : float { return $this->balance; } public function setBalance($balance) { $this->balance = (float) $balance; } }

    提示

    在 setter 上检查传入值的数据类型不是一个好主意。原因是 RDBMS 数据库查询的返回值都是string数据类型。

  11. 如果属性名称与相应的数据库列不完全匹配,则应考虑创建一个阿纳尔 T0 属性,一个键/值对数组,其中键表示数据库列名称和值属性名称。

  12. 您将注意到三个属性$securityQuestion$confirmCode$profileId与其对应的列名security_questionconfirm_codeprofile_id不对应。$mapping 属性将确保进行适当的翻译:

    php protected $mapping = [ 'id' => 'id', 'name' => 'name', 'balance' => 'balance', 'email' => 'email', 'password' => 'password', 'status' => 'status', 'security_question' => 'securityQuestion', 'confirm_code' => 'confirmCode', 'profile_id' => 'profileId', 'level' => 'level' ];

它是如何工作的。。。

将步骤 2、4 和 5 中的代码复制到Application/Entity文件夹中的Base.php文件中。将步骤 8 到步骤 12 中的代码复制到Customer.php文件中,也复制到Application/Entity文件夹中。然后,您需要为步骤 10 中未显示的剩余属性创建 getter 和 setter:emailpasswordstatussecurityQuestionconfirmCodeprofileIdlevel

然后,您可以创建一个名为“t0”的调用程序,该程序初始化了在 Tyt T4 中定义的 AutoT3 AutoLoad,第 1 章 AUTT5,AUTT6.构建了一个基础 TUR7 T7,使用了 AUT1 T1,以及新创建的 ORT T2 类:

<?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\Entity\Customer;

接下来,获取数据库连接,并使用该连接随机获取一个客户的关联数据数组:

$conn = new Connection(include __DIR__ . DB_CONFIG_FILE);
$id = rand(1,79);
$stmt = $conn->pdo->prepare(
  'SELECT * FROM customer WHERE id = :id');
$stmt->execute(['id' => $id]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);

最后,您可以从数组中创建一个新的Customer实体实例,并使用var_dump()查看结果:

$cust = Customer::arrayToEntity($result, new Customer());
var_dump($cust);

下面是前面代码的输出:

How it works...

另见

有许多很好的作品描述了领域模型。可能最有影响力的是 Martin Fowler 的企业应用程序架构模式(参见http://martinfowler.com/books/eaa.html )。还有一个不错的研究,也可以免费下载,名为 InfoQ 的域驱动设计快速(参见http://www.infoq.com/minibooks/domain-driven-design-quickly

将实体类绑定到 RDBMS 查询

大多数商业上可行的 RDBMS 系统是在程序编程处于领先地位的时候发展起来的。将 RDBMS 世界想象成二维、正方形和面向过程的世界。相反,实体可以被认为是圆形、三维和面向对象的。通过将 RDBMS 查询的结果绑定到实体实例的迭代中,您可以了解我们想要完成的任务。

关系模型是现代关系数据库管理系统所基于的,由数学家 Edgar F.Codd 于 1969 年首次描述。第一批商业上可行的系统是在 20 世纪 70 年代中后期发展起来的。换句话说,RDBMS 技术已经有 40 多年的历史了!

怎么做。。。

  1. 首先,我们需要设计一个类来容纳我们的查询逻辑。如果您遵循域模型,这个类可能被称为存储库。或者,为了保持简单和通用,我们可以简单地将新类称为Application\Database\CustomerService。该类将接受一个Application\Database\Connection 实例作为参数:

    ```php namespace Application\Database;

    use Application\Entity\Customer;

    class CustomerService {

    protected $connection;
    
    public function __construct(Connection $connection)
    {
      $this->connection = $connection;
    }
    

    } ```

  2. Now we will define a fetchById() method, which takes a customer ID as an argument, and returns a single Application\Entity\Customer instance or boolean FALSE on failure. At first glance, it would seem a no-brainer to simply use PDOStatement::fetchObject() and specify the entity class as an argument:

    php public function fetchById($id) { $stmt = $this->connection->pdo ->prepare(Finder::select('customer') ->where('id = :id')::getSql()); $stmt->execute(['id' => (int) $id]); return $stmt->fetchObject('Application\Entity\Customer'); }

    然而,这里的危险在于fetchObject()实际上是在调用构造函数之前填充属性(即使它们受到保护)!因此,存在构造函数可能意外覆盖值的危险。如果你不定义构造函数,或者你能忍受这种危险,我们就完蛋了。否则,要正确实现 RDBMS 查询和 OOP 结果之间的联系就变得更加困难。

  3. fetchById()方法的另一种方法是首先创建对象实例,从而运行其构造函数,并将获取模式设置为PDO::FETCH_INTO,如下例所示:

    php public function fetchById($id) { $stmt = $this->connection->pdo ->prepare(Finder::select('customer') ->where('id = :id')::getSql()); $stmt->execute(['id' => (int) $id]); $stmt->setFetchMode(PDO::FETCH_INTO, new Customer()); return $stmt->fetch(); }

  4. Here again, however, we encounter a problem: fetch(), unlike fetchObject(), is not able to overwrite protected properties; the following error message is generated if it tries. This means we will either have to define all properties as public, or consider another approach.

    How to do it...

  5. 最后一个方法,我们将考虑以数组的形式获取结果,并手动地将阿纳尔 T4 水合物 OT5 T5 实体。尽管这种方法在性能上稍微昂贵一些,但它允许任何潜在的实体构造函数正常运行,并将属性安全地定义为privateprotected

    php public function fetchById($id) { $stmt = $this->connection->pdo ->prepare(Finder::select('customer') ->where('id = :id')::getSql()); $stmt->execute(['id' => (int) $id]); return Customer::arrayToEntity( $stmt->fetch(PDO::FETCH_ASSOC)); }

  6. 要处理产生多个结果的查询,我们需要做的就是生成填充实体对象的迭代。在本例中,我们实现了一个fetchByLevel()方法,该方法以Application\Entity\Customer实例的形式返回给定级别的所有客户:

    php public function fetchByLevel($level) { $stmt = $this->connection->pdo->prepare( Finder::select('customer') ->where('level = :level')::getSql()); $stmt->execute(['level' => $level]); while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { yield Customer::arrayToEntity($row, new Customer()); } }

  7. 我们希望实现的下一个方法是save()。然而,在我们继续之前,必须考虑如果发生INSERT将返回什么值。

  8. 通常,我们会在INSERT之后返回新完成的实体类。有一种方便的PDO::lastInsertId()方法,乍一看,似乎能起到作用。然而,进一步阅读文档会发现,并非所有的数据库扩展都支持此功能,而且不支持此功能的扩展在实现上并不一致。因此,最好使用除$id之外的唯一列来唯一标识新客户。
  9. 在本例中,我们选择了email列,因此需要实现一个fetchByEmail()服务方法:

    php public function fetchByEmail($email) { $stmt = $this->connection->pdo->prepare( Finder::select('customer') ->where('email = :email')::getSql()); $stmt->execute(['email' => $email]); return Customer::arrayToEntity( $stmt->fetch(PDO::FETCH_ASSOC), new Customer()); }

  10. 现在我们准备好定义save()方法。我们不会区分INSERTUPDATE,而是构建此方法来更新 ID 是否已经存在,否则执行插入。

  11. 首先,我们定义一个基本的save()方法,该方法接受Customer实体作为参数,并使用fetchById()确定该条目是否已经存在。如果存在,我们调用doUpdate()更新方法;否则,我们调用一个doInsert()插入方法:

    php public function save(Customer $cust) { // check to see if customer ID > 0 and exists if ($cust->getId() && $this->fetchById($cust->getId())) { return $this->doUpdate($cust); } else { return $this->doInsert($cust); } }

  12. 接下来,我们定义doUpdate(),它将Customer实体对象属性拉入数组,构建初始 SQL 语句,并调用flush()方法,将数据推送到数据库。我们不想更新 ID 字段,因为它是主键。我们还需要指定要更新的行,这意味着附加了一个WHERE子句:

    php protected function doUpdate($cust) { // get properties in the form of an array $values = $cust->entityToArray(); // build the SQL statement $update = 'UPDATE ' . $cust::TABLE_NAME; $where = ' WHERE id = ' . $cust->getId(); // unset ID as we want do not want this to be updated unset($values['id']); return $this->flush($update, $values, $where); }

  13. doInsert()方法与类似,只是初始 SQL 需要以INSERT INTO ...开头,id数组元素需要取消设置。后者的原因是我们希望这个属性由数据库自动生成。如果成功,我们将使用新定义的fetchByEmail()方法查找新客户并返回一个已完成的实例:

    php protected function doInsert($cust) { $values = $cust->entityToArray(); $email = $cust->getEmail(); unset($values['id']); $insert = 'INSERT INTO ' . $cust::TABLE_NAME . ' '; if ($this->flush($insert, $values)) { return $this->fetchByEmail($email); } else { return FALSE; } }

  14. 最后,我们可以定义flush(),它负责实际的准备和执行:

    php protected function flush($sql, $values, $where = '') { $sql .= ' SET '; foreach ($values as $column => $value) { $sql .= $column . ' = :' . $column . ','; } // get rid of trailing ',' $sql = substr($sql, 0, -1) . $where; $success = FALSE; try { $stmt = $this->connection->pdo->prepare($sql); $stmt->execute($values); $success = TRUE; } catch (PDOException $e) { error_log(__METHOD__ . ':' . __LINE__ . ':' . $e->getMessage()); $success = FALSE; } catch (Throwable $e) { error_log(__METHOD__ . ':' . __LINE__ . ':' . $e->getMessage()); $success = FALSE; } return $success; }

  15. 为了结束讨论,我们需要定义一个remove()方法,从数据库中删除客户。同样,与前面定义的save()方法一样,我们使用fetchById()来确保操作成功:

    php public function remove(Customer $cust) { $sql = 'DELETE FROM ' . $cust::TABLE_NAME . ' WHERE id = :id'; $stmt = $this->connection->pdo->prepare($sql); $stmt->execute(['id' => $cust->getId()]); return ($this->fetchById($cust->getId())) ? FALSE : TRUE; }

它是如何工作的。。。

将步骤 1 至 5 中描述的代码复制到Application/Database文件夹中的CustomerService.php文件中。定义一个chap_05_entity_to_query.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\CustomerService;

您现在可以创建一个服务实例,并随机获取一个客户。然后,服务将返回一个客户实体,结果是:

// get service instance
$service = new CustomerService(new Connection(include __DIR__ . DB_CONFIG_FILE));

echo "\nSingle Result\n";
var_dump($service->fetchById(rand(1,79)));

以下是输出:

How it works...

现在将步骤 6 到 15 中显示的代码复制到服务类中。将要插入的数据添加到chap_05_entity_to_query.php调用程序中。然后,我们使用以下数据生成一个Customer 实体实例:

// sample data
$data = [
  'name'              => 'Doug Bierer',
  'balance'           => 326.33,
  'email'             => 'doug' . rand(0,999) . '@test.com',
  'password'          => 'password',
  'status'            => 1,
  'security_question' => 'Who\'s on first?',
  'confirm_code'      => 12345,
  'level'             => 'ADV'
];

// create new Customer
$cust = Customer::arrayToEntity($data, new Customer());

然后我们可以检查调用save()前后的 ID:

echo "\nCustomer ID BEFORE Insert: {$cust->getId()}\n";
$cust = $service->save($cust);
echo "Customer ID AFTER Insert: {$cust->getId()}\n";

最后修改余额,再次调用save(),查看结果:

echo "Customer Balance BEFORE Update: {$cust->getBalance()}\n";
$cust->setBalance(999.99);
$service->save($cust);
echo "Customer Balance AFTER Update: {$cust->getBalance()}\n";
var_dump($cust);

以下是调用程序的输出:

How it works...

还有更多。。。

关于关系模型的更多信息请参考https://en.wikipedia.org/wiki/Relational_model 。有关关系数据库管理系统的更多信息,请参考https://en.wikipedia.org/wiki/Relational_database_management_system 。有关PDOStatement::fetchObject()如何在构造函数之前插入属性值的信息,请查看 php.net 文档参考中关于fetchObject()的“rasmus at mindplay dot dk”的注释 http://php.net/manual/en/pdostatement.fetchobject.php

在查询结果中嵌入二次查找

在实现实体类之间关系的过程中,让我们首先看看如何嵌入执行二次查找所需的代码。这种查找的一个示例是,在显示客户信息时,让视图逻辑执行第二次查找,以获取该客户的购买列表。

这种方法的优点是处理延迟到执行实际的视图逻辑。这将最终平滑性能曲线,使工作负载在最初的客户信息查询和以后的采购信息查询之间更均匀地分布。另一个好处是通过其固有的冗余数据避免了大量的JOIN

怎么做。。。

  1. 首先,定义一个函数,该函数根据客户的 ID 查找客户。在本例中,我们将使用 fetch 模式PDO::FETCH_ASSOC简单地获取一个数组。我们还将继续使用在 To.T5 中讨论的 OrthT1 类,第 1 章 AUTT6T,AUTT7。
  2. 接下来,我们分析 purchases 表,看看customerproduct表是如何链接的。从这个表的CREATE语句中可以看到,customer_idproduct_id外键构成了关系:

    php 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') );

  3. 我们现在扩展原始的findCustomerById()函数,以匿名函数的形式定义二次查找,然后可以在视图脚本中执行。匿名函数被分配给$results['purchases']元素:

    php function findCustomerById($id, Connection $conn) { $stmt = $conn->pdo->query( 'SELECT * FROM customer WHERE id = ' . (int) $id); $results = $stmt->fetch(PDO::FETCH_ASSOC); if ($results) { $results['purchases'] = // define secondary lookup function ($id, $conn) { $sql = 'SELECT * FROM purchases AS u ' . 'JOIN products AS r ' . 'ON u.product_id = r.id ' . 'WHERE u.customer_id = :id ' . 'ORDER BY u.date'; $stmt = $conn->pdo->prepare($sql); $stmt->execute(['id' => $id]); while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { yield $row; } }; } return $results; }

  4. 假设我们已经成功地将客户信息检索到一个$results数组中,在视图逻辑中,我们所需要做的就是循环遍历匿名函数的返回值。在本例中,我们随机检索客户信息:

    php $result = findCustomerById(rand(1,79), $conn);

  5. 在视图逻辑中,我们通过二次查找返回的结果循环。对嵌入式匿名函数的调用在以下代码中突出显示:

    php <table> <tr> <th>Transaction</th><th>Date</th><th>Qty</th> <th>Price</th><th>Product</th> </tr> <?php foreach ($result['purchases']($result['id'], $conn) as $purchase) : ?> <tr> <td><?= $purchase['transaction'] ?></td> <td><?= $purchase['date'] ?></td> <td><?= $purchase['quantity'] ?></td> <td><?= $purchase['sale_price'] ?></td> <td><?= $purchase['title'] ?></td> </tr> <?php endforeach; ?> </table>

它是如何工作的。。。

创建一个chap_05_secondary_lookups.php调用程序,插入创建Application\Database\Connection的实例所需的代码:

<?php
define('DB_CONFIG_FILE', '/../config/db.config.php');
include __DIR__ . '/../Application/Database/Connection.php';
use Application\Database\Connection;
$conn = new Connection(include __DIR__ . DB_CONFIG_FILE);

接下来,添加步骤 3 所示的findCustomerById()功能。然后,您可以提取随机客户的信息,结束调用程序的 PHP 部分:

function findCustomerById($id, Connection $conn)
{
  // code shown in bullet #3 above
}
$result = findCustomerById(rand(1,79), $conn);
?>

对于视图逻辑,您可以显示核心客户信息,如前面几个配方中所示:

<h1><?= $result['name'] ?></h1>
<div class="row">
<div class="left">Balance</div>
<div class="right"><?= $result['balance']; ?></div>
</div>
<!-- etc.l -->

您可以这样显示有关购买的信息:

<table>
<tr><th>Transaction</th><th>Date</th><th>Qty</th>
<th>Price</th><th>Product</th></tr>
  <?php 
  foreach ($result['purchases']($result['id'], $conn) as $purchase) : ?>
  <tr>
    <td><?= $purchase['transaction'] ?></td>
    <td><?= $purchase['date'] ?></td>
    <td><?= $purchase['quantity'] ?></td>
    <td><?= $purchase['sale_price'] ?></td>
    <td><?= $purchase['title'] ?></td>
  </tr>
<?php endforeach; ?>
</table>

关键的部分是,通过调用嵌入的匿名函数$result['purchases']($result['id'], $conn),二次查找作为视图逻辑的一部分执行。以下是输出:

How it works...

实现 jQuery 数据表 PHP 查找

二次查找的另一种方法是让前端生成请求。在这个配方中,我们将对前面配方中提供的二次查找代码稍作修改,将二次查找嵌入到 QueryResults 中。在前面的配方中,即使视图逻辑正在执行查找,所有的处理仍然在服务器上完成。然而,当使用jQuery DataTables时,二次查找实际上是由客户端直接执行的,以浏览器发出的异步 JavaScript 和 XMLAJAX请求的形式。

怎么做。。。

  1. 首先,我们需要将二次查找逻辑(在上面的配方中讨论)剥离到一个单独的 PHP 文件中。这个新脚本的目的是执行二次查找并返回 JSON 数组。
  2. The new script we will call chap_05_jquery_datatables_php_lookups_ajax.php. It looks for a $_GET parameter, id. Notice that the SELECT statement is very specific as to which columns are delivered. You will also note that the fetch mode has been changed to PDO::FETCH_NUM. You might also notice that the last line takes the results and assigns it to a data key in a JSON-encoded array.

    提示

    在处理零配置 jQuery 数据表时,只返回与标头匹配的精确列数是非常重要的。

    php $id = $_GET['id'] ?? 0; sql = 'SELECT u.transaction,u.date, u.quantity,u.sale_price,r.title ' . 'FROM purchases AS u ' . 'JOIN products AS r ' . 'ON u.product_id = r.id ' . 'WHERE u.customer_id = :id'; $stmt = $conn->pdo->prepare($sql); $stmt->execute(['id' => (int) $id]); $results = array(); while ($row = $stmt->fetch(PDO::FETCH_NUM)) { $results[] = $row; } echo json_encode(['data' => $results]);

  3. 接下来,我们需要修改通过 ID 检索客户信息的函数,删除之前配方中嵌入的二次查找:

    php function findCustomerById($id, Connection $conn) { $stmt = $conn->pdo->query( 'SELECT * FROM customer WHERE id = ' . (int) $id); $results = $stmt->fetch(PDO::FETCH_ASSOC); return $results; }

  4. 之后,在视图逻辑中,我们为零配置实现导入最小 jQuery、DataTables 和样式表。至少,您需要 jQuery 本身(在本例中为jquery-1.12.0.min.js)和数据表(jquery.dataTables.js)。我们还添加了一个与数据表相关联的方便样式表jquery.dataTables.css

    php <!DOCTYPE html> <head> <script src="https://code.jquery.com/jquery-1.12.0.min.js"> </script> <script type="text/javascript" charset="utf8" src="//cdn.datatables.net/1.10.11/js/jquery.dataTables.js"> </script> <link rel="stylesheet" type="text/css" href="//cdn.datatables.net/1.10.11/css/jquery.dataTables.css"> </head>

  5. 然后我们定义一个 jQuery 文档ready函数,该函数将一个表与 DataTables 相关联。在本例中,我们为将分配给 DataTables 的 table 元素分配一个 id 属性customerTable。您还会注意到,我们将 AJAX 数据源指定为步骤 1 中定义的脚本,chap_05_jquery_datatables_php_lookups_ajax.php。由于我们有可用的$id,这将附加到数据源 URL:

    php <script> $(document).ready(function() { $('#customerTable').DataTable( { "ajax": '/chap_05_jquery_datatables_php_lookups_ajax.php?id=<?= $id ?>' }); } ); </script>

  6. 在视图逻辑的主体中,我们定义表,确保id属性与前面代码中指定的属性匹配。我们还需要定义与响应 AJAX 请求时显示的数据相匹配的头文件:

    php <table id="customerTable" class="display" cellspacing="0" width="100%"> <thead> <tr> <th>Transaction</th> <th>Date</th> <th>Qty</th> <th>Price</th> <th>Product</th> </tr> </thead> </table>

  7. 现在,剩下要做的就是加载页面,选择客户 ID(在本例中是随机的),然后让 jQuery 请求二次查找。

它是如何工作的。。。

创建一个chap_05_jquery_datatables_php_lookups_ajax.php脚本,它将响应 AJAX 请求。在里面放置初始化自动加载的代码并创建Connection实例。然后,您可以附加上述配方步骤 2 中所示的代码:

<?php
define('DB_CONFIG_FILE', '/../config/db.config.php');
include __DIR__ . '/../Application/Database/Connection.php';
use Application\Database\Connection;
$conn = new Connection(include __DIR__ . DB_CONFIG_FILE);

接下来,创建一个chap_05_jquery_datatables_php_lookups.php调用程序,该程序将获取随机客户的信息。添加上述代码步骤 3 中描述的功能:

<?php
define('DB_CONFIG_FILE', '/../config/db.config.php');
include __DIR__ . '/../Application/Database/Connection.php';
use Application\Database\Connection;
$conn = new Connection(include __DIR__ . DB_CONFIG_FILE);
// add function findCustomerById() here
$id     = random_int(1,79);
$result = findCustomerById($id, $conn);
?>

调用程序还将包含导入最小 JavaScript 以实现 jQuery 数据表的视图逻辑。您可以添加前面代码的步骤 3 中所示的代码。然后,添加文档ready功能和步骤 5、6 所示的显示逻辑。以下是输出:

How it works...

还有更多。。。

有关 jQuery 的更多信息,请访问网站https://jquery.com/ 。要了解 jQuery 的 DataTables 插件,请参阅中的本文 https://www.datatables.net/ 。零配置数据表在中讨论 https://datatables.net/examples/basic_init/zero_configuration.html 。有关 AJAX 源数据的更多信息,请参阅https://datatables.net/examples/data_sources/ajax.html