// 
//     Licensed to the Apache Software Foundation (ASF) under one
//     or more contributor license agreements.  See the NOTICE file
//     distributed with this work for additional information
//     regarding copyright ownership.  The ASF licenses this file
//     to you under the Apache License, Version 2.0 (the
//     "License"); you may not use this file except in compliance
//     with the License.  You may obtain a copy of the License at
// 
//       http://www.apache.org/licenses/LICENSE-2.0
// 
//     Unless required by applicable law or agreed to in writing,
//     software distributed under the License is distributed on an
//     "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
//     KIND, either express or implied.  See the License for the
//     specific language governing permissions and limitations
//     under the License.
//

= 使用 PHP 创建数据库驱动的应用程序
:jbake-type: tutorial
:jbake-tags: tutorials 
:jbake-status: published
:icons: font
:syntax: true
:source-highlighter: pygments
:toc: left
:toc-title:
:description: 使用 PHP 创建数据库驱动的应用程序 - Apache NetBeans
:keywords: Apache NetBeans, Tutorials, 使用 PHP 创建数据库驱动的应用程序

= 第 4 课：使用类和对象优化代码
:jbake-type: tutorial
:jbake-tags: tutorials 
:jbake-status: published
:icons: font
:syntax: true
:source-highlighter: pygments
:toc: left
:toc-title:
:description: 第 4 课：使用类和对象优化代码 - Apache NetBeans
:keywords: Apache NetBeans, Tutorials, 第 4 课：使用类和对象优化代码


在本课中，将优化代码以便于将来进行维护。这会影响  ``createNewWisher.php``  和  ``wishlist.php``  文件。此外，还会创建一个名为  ``db.php``  的新文件。

应用程序代码包含几个类似的块，其中提供了一些数据库查询。为了便于将来阅读和维护代码，您可以提取这些块，将这些块作为名为  ``WishDB``  的单独类的函数进行实现，然后将  ``WishDB``  放在  ``db.php``  中。接下来，您可以将  ``db.php``  文件包含在任何 PHP 文件中，然后使用 <<includedFunctions,WishDB 中的任何函数>>而无需复制代码。这种方法确保了对查询或函数进行的任何更改是在一个地方完成的，您不必解析整个应用程序代码。

在使用 WishDB 中的函数时，不可更改任何 WishDB 变量的值。而应将 WishDB 类作为模板来创建 WishDB 对象，然后在该对象中更改变量值。在使用完该对象后，将会销毁该对象。由于从未更改 WishDB 类本身的值，您可以不限次数地重复使用该类。在某些情况下，您可能希望同时存在多个类实例，而在其他情况下，您可能希望只存在“单个”类，即，在任何时候都只有一个实例。本教程中的 WishDB 就是单个类。

请注意，创建类对象的术语是“实例化”该类，而有关对象的另一个术语是类“实例”。使用类和对象进行编程的一般术语是面向对象的编程 (OOP)。PHP 5 使用复杂的 OOP 模型。有关详细信息，请参见 link:http://us3.php.net/zend-engine-2.php[+php.net+]。

在本教程中，您将数据库调用功能从单独的 PHP 文件移到 WishDB 类。MySQL 用户还会将过程样式的  ``mysqli``  调用替换为面向对象的调用。这是为了与面向对象的新应用程序设计保持一致。

当前文档是“在适用于 PHP 的 NetBeans IDE 中创建 CRUD 应用程序”教程的一部分。


[[previousLessonSourceCode]]
== 来自上一课的应用程序源代码

MySQL 用户：单击link:https://netbeans.org/files/documents/4/1929/lesson3.zip[+此处+]以下载源代码，该代码反映了在完成上一课之后的项目状态。

Oracle 数据库用户：单击link:https://netbeans.org/projects/www/downloads/download/php%252Foracle-lesson3.zip[+此处+]以下载源代码，该代码反映了在完成上一课之后的项目状态。


[[createDbPhpFile]]
== 创建 db.php 文件

在 "Source Files"（源文件）文件夹中创建一个新子文件夹。将该文件夹命名为 Includes。创建一个名为 db.php 的新文件，并将其放在 Includes 中。然后，您可以在该文件夹中添加更多文件，这些文件将包含在其他 PHP 文件中。

*在新文件夹中创建 db.php：*

1. 在 "Source Files"（源文件）节点上单击鼠标右键，然后从上下文菜单中选择 "New"（新建）> "Folder"（文件夹）。"New Folder"（新建文件夹）对话框打开。
2. 在 "Folder Name"（文件夹名称）字段中，键入 Includes。然后，单击 "Finish"（完成）。
3. 在 "Includes" 节点上单击鼠标右键，然后从上下文菜单中选择 "New"（新建）> "PHP File"（PHP 文件）。"New PHP File"（新建 PHP 文件）对话框打开。
4. 在 "File Name"（文件名）字段中，键入 db。然后，单击 "Finish"（完成）。


[[wishDBClass]]
== 创建 WishDB 类

要创建 WishDB 类，您需要初始化该类的变量，并实现该类的构造函数。MySQL 用户请注意，WishDB 类_扩展_了  ``mysqli`` 。这意味着 WishDB _继承_ PHP mysqli 类的函数和其他特性。在类中添加  ``mysqli `` 函数时，您会看到这种继承的重要性。

打开文件 db.php 并创建 WishDB 类。在该类中，声明数据库配置变量以存储数据库所有者（用户）的名称和口令、数据库名称和数据库主机。所有这些变量声明都是私有的 ("private")，这意味着无法从 WishDB 类外部访问声明中的初始值（请参见 link:http://us3.php.net/manual/en/language.oop5.visibility.php[+php.net+]）。您还可以声明私有的 _static_  ``$instance``  变量以存储 WishDB 实例。"static" 关键字表示，即使没有类实例，类中的函数也可以访问变量。

*对于 MySQL 数据库：*


[source,php]
----

class WishDB extends mysqli {

    // single instance of self shared among all instances
    private static $instance = null;

    // db connection config vars
    private $user = "phpuser";
    private $pass = "phpuserpw";
    private $dbName = "wishlist";
    private $dbHost = "localhost";

}

----

*对于 Oracle 数据库：*


[source,php]
----

class WishDB {

    // single instance of self shared among all instances
    private static $instance = null;

    // db connection config vars
    private $user = "phpuser";
    private $pass = "phpuserpw";
    private $dbName = "wishlist";
    private $dbHost = "localhost/XE";
    private $con = null;

}

----

[[instantiate-wishdb]]
== 实例化 WishDB 类

如果其他 PHP 文件要使用 WishDB 类中的函数，这些 PHP 文件需要调用一个函数以创建 WishDB 类的对象（“实例化”）。WishDB 设计为link:http://www.phpclasses.org/browse/package/1151.html[+单个类+]，这意味着在任何时候都只有一个类实例。因此，这对防止任何外部 WishDB 实例化是非常有用的，这种实例化可能会创建重复的实例。

在 WishDB 类中，键入或粘贴以下代码：


[source,php]
----

// This method must be static, and must return an instance of the object if the object
// does not already exist.

public static function getInstance() {

  if (!self::$instance instanceof self) {
    self::$instance = new self;
  }

  return self::$instance;
}

// The clone and wakeup methods prevents external instantiation of copies of the Singleton class,
// thus eliminating the possibility of duplicate objects.
 
public function __clone() {
  trigger_error('Clone is not allowed.', E_USER_ERROR);
}

public function __wakeup() {
  trigger_error('Deserializing is not allowed.', E_USER_ERROR);
}

----

 ``getInstance``  函数为 "public" 和 "static"。"Public" 表示可以从类外部任意访问该函数。"Static" 表示即使未实例化类，也可以使用该函数。在调用  ``getInstance``  函数以实例化类时，该函数必须是静态的。请注意，该函数访问静态  ``$instance``  变量，并将其值设置为类实例。

称为作用域解析运算符的双冒号 (::) 和  ``self``  关键字用于访问静态函数。 ``Self``  从类定义中使用以引用类本身。从类定义外部使用双冒号时，将使用类名而不是  ``self`` 。请参见 link:http://us3.php.net/manual/en/language.oop5.paamayim-nekudotayim.php[+php.net 上的作用域解析运算符+]。


[[wishdb-constructor]]
== 在 WishDB 类中添加构造函数

类可以包含一个称为“构造函数”的特殊方法，每次创建该类的实例时，都会自动处理该方法。在本教程中，将在 WishDB 中添加一个构造函数；每次实例化 WishDB 时，它都会连接到数据库。

在 WishDB 中添加以下代码：

*对于 MySQL 数据库：*


[source,php]
----

// private constructor
private function __construct() {

  parent::__construct($this->dbHost, $this->user, $this->pass, $this->dbName);
  
  if (mysqli_connect_error()) {
    exit('Connect Error (' . mysqli_connect_errno() . ') '. mysqli_connect_error());
  }

  parent::set_charset('utf-8');
}

----

*对于 Oracle 数据库：*


*For the Oracle database:*

[source,php]
----

// private constructor
private function __construct() {

    $this->con = oci_connect($this->user, $this->pass, $this->dbHost);

    if (!$this->con) {
        $m = oci_error();
        echo $m['message'], "\n";
        exit;
    }
}

----

请注意，使用了伪变量  ``$this`` ，而不是使用  ``$con`` 、 ``$dbHost`` 、 ``$user``  或  ``$pass``  变量。从对象上下文中调用方法时，将使用伪变量  ``$this`` 。它引用该对象中的变量值。

[[includedFunctions]]
== WishDB 类中的函数

在本课中，将实现 WishDB 类的以下函数：

* <<getIDByName,get_wisher_id_by_name>> - 根据许愿者名字检索许愿者的 ID
* <<getWishesByID,get_wishes_by_wisher_id>> - 使用特定 ID 检索许愿者的愿望列表
* <<createWisher,create_wisher>> - 将新许愿者记录添加到 wishers 表中

[[getIDByName]]
=== 函数 get_wisher_id_by_name

该函数要求将许愿者名字作为输入参数，并返回许愿者的 ID。 

在 WishDB 类中，在 WishDB 函数后面键入或粘贴以下函数：

*对于 MySQL 数据库：*


[[source,php]
----

public function get_wisher_id_by_name($name) {
  
  $name = $this->real_escape_string($name);
  $wisher = $this->query("SELECT id FROM wishers WHERE name = '" . $name . "'");

  if ($wisher->num_rows > 0){
    $row = $wisher->fetch_row();
    return $row[0];
  } else {
    return null;
  }
}

----

*对于 Oracle 数据库：*


[source,php]
----

public function get_wisher_id_by_name($name) {
    
    $query = "SELECT id FROM wishers WHERE name = :user_bv";
    $stid = oci_parse($this->con, $query);
    
    oci_bind_by_name($stid, ':user_bv', $name);
    oci_execute($stid);
    
    //Because user is a unique value I only expect one row
    $row = oci_fetch_array($stid, OCI_ASSOC);

    if ($row) {
      return $row["ID"];
    } else {
      return null;
    }
}

----

该代码块执行  ``SELECT ID FROM wishers WHERE name = [variable for name of the wisher]``  查询。查询结果是一个数组，其中包含符合查询条件的记录中的 ID。如果该数组不为空，则自动表示它包含一个元素，这是因为在创建表期间将字段名称指定为 UNIQUE。在本示例中，该函数返回  ``$result``  数组的第一个元素（编号为零的元素）。如果数组为空，该函数将返回空值。

*安全注意事项：*对于 MySQL 数据库，将转义  ``$name``  字符串以防止 SQL 注入攻击。请参见link:http://en.wikipedia.org/wiki/SQL_injection[+有关 SQL 注入的维基百科+]和 link:http://us3.php.net/mysql_real_escape_string[+mysql_real_escape_string 文档+]。虽然在本教程的上下文中，您不会遇到有害 SQL 注入的风险，但最佳做法是转义存在此类攻击风险的 MySQL 查询中的字符串。Oracle 数据库通过使用绑定变量来避免该问题。

[[getWishesByID]]
=== 函数 get_wishes_by_wisher_id

该函数要求将许愿者 ID 作为输入参数，并返回为许愿者注册的愿望。

请输入以下代码块：

*对于 MySQL 数据库：*


[source,php]
----

public function get_wishes_by_wisher_id($wisherID) {
  return $this->query("SELECT id, description, due_date FROM wishes WHERE wisher_id=" . $wisherID);
}

----

*对于 Oracle 数据库：*


[source,php]
----

public function get_wishes_by_wisher_id($wisherID) {
  
  $query = "SELECT id, description, due_date FROM wishes WHERE wisher_id = :id_bv";
  $stid = oci_parse($this->con, $query);
  
  oci_bind_by_name($stid, ":id_bv", $wisherID);
  oci_execute($stid);

  return $stid;
}

----

该代码块执行  ``"SELECT id, description, due_date FROM wishes WHERE wisherID=" . $wisherID``  查询并返回一个结果集，这是一个符合查询条件的记录数组。（出于数据库性能和安全考虑，Oracle 数据库使用绑定变量。）数据选择是按 wisherID 执行的，这是  ``wishes``  表的外键。

*注：*在第 7 课之前，您不需要使用  ``id``  值。

[[createWisher]]
=== 函数 create_wisher

该函数在 wishers 表中创建一个新记录。该函数要求将新许愿者的名字和口令作为输入参数，并且不返回任何数据。

请输入以下代码块：

*对于 MySQL 数据库：*


[source,php]
----

public function create_wisher ($name, $password) {

  $name = $this->real_escape_string($name);
  $password = $this->real_escape_string($password);

  return $this->query("INSERT INTO wishers (name, password) VALUES ('" . $name . "', '" . $password . "')");
}

----

*对于 Oracle 数据库：*

[source,php]
----

public function create_wisher($name, $password) {

  $query = "INSERT INTO wishers (name, password) VALUES (:user_bv, :pwd_bv)";
  $stid = oci_parse($this->con, $query);

  oci_bind_by_name($stid, ':user_bv', $name);
  oci_bind_by_name($stid, ':pwd_bv', $password);
  oci_execute($stid);

  return $stid;
}

----

该代码块执行  ``"INSERT wishers (Name, Password) VALUES ([variables representing name and password of new wisher])``  查询。该查询在 "wishers" 表中添加一个新记录，并分别使用  ``$name``  和  ``$password``  值填充 "name" 和 "password" 字段。

[[refactoring]]
== 重构应用程序代码

现在，您已创建了一个单独的类以使用数据库，接下来便可将重复的块替换为对该类中的相关函数的调用。这有助于避免将来出现拼写错误和不一致的情况。不影响功能的代码优化称为“重构”。

[[refactoringWishlistFile]]
=== 重构 wishlist.php 文件

请从 wishlist.php 文件入手，因为该文件很短，改进更能说明问题。

1. 在 <?php ?> 块的顶部，输入以下行以允许使用  ``db.php``  文件：

[source,php]
----

require_once("Includes/db.php");

----

2. 将连接到数据库并获取许愿者 ID 的代码替换为  ``get_wisher_id_by_name``  函数调用。

对于 *MySQL 数据库*，替换的代码为：

[source,php]
----

// to remove

 $con = mysqli_connect("localhost", "phpuser", "phpuserpw");
if (!$con) {
  exit('Connect Error (' . mysqli_connect_errno() . ') '
          . mysqli_connect_error());
}
//set the default client character set 
mysqli_set_charset($con, 'utf-8');

mysqli_select_db($con, "wishlist");
$user = mysqli_real_escape_string($con, $_GET['user']);
$wisher = mysqli_query($con, "SELECT id FROM wishers WHERE name='" . $user . "'");
if (mysqli_num_rows($wisher) < 1) {
  exit("The person " . $_GET['user'] . " is not found. Please check the spelling and try again");
}
$row = mysqli_fetch_row($wisher);
$wisherID = $row[0];
mysqli_free_result($wisher);

// to replace

$wisherID = WishDB::getInstance()->get_wisher_id_by_name($_GET["user"]);

if (!$wisherID) {
  exit("The person " .$_GET["user"]. " is not found. Please check the spelling and try again" );
}

----

对于 *Oracle 数据库*，替换的代码为：

[source,php]
----

// to remove

$con = oci_connect("phpuser", "phpuserpw", "localhost/XE");
if (!$con) {
  $m = oci_error();
  echo $m['message'], "\n";
  exit;
}        
$query = "SELECT ID FROM wishers WHERE name = :user_bv";
$stid = oci_parse($con, $query);
$user = $_GET['user'];

oci_bind_by_name($stid, ':user_bv', $user);
oci_execute($stid);

//Because user is a unique value I only expect one row
$row = oci_fetch_array($stid, OCI_ASSOC);
if (!$row) {
  echo("The person " . $user . " is not found. Please check the spelling and try again" );
  exit;
}
$wisherID = $row['ID']; 

// to replace

$wisherID = WishDB::getInstance()->get_wisher_id_by_name($_GET["user"]);

if (!$wisherID) {
  exit("The person " .$_GET["user"]. " is not found. Please check the spelling and try again" );
}

----

新代码先调用 WishDB 中的  ``getInstance``  函数。 ``getInstance``  函数返回一个 WishDB 实例，然后代码在该实例中调用  ``get_wisher_id_by_name``  函数。如果在数据库中找不到请求的许愿者，代码将终止该进程，然后显示一条错误消息。

此处不需要用于打开数据库连接的代码。连接是通过 WishDB 类的构造函数打开的。如果名字和/或口令发生变化，您只需要更新 WishDB 类的相关变量即可。

3. 将获取按 ID 标识的许愿者的愿望的代码替换为调用  ``get_wishes_by_wisher_id``  函数的代码。

对于 *MySQL 数据库*，替换的代码为：

[[source,php]
----

// to remove

$result = mysqli_query($con, "SELECT description, due_date FROM wishes WHERE wisher_id=" . $wisherID);

// to replace      
 
$result = WishDB::getInstance()->get_wishes_by_wisher_id($wisherID);

----

对于 *Oracle 数据库*，替换的代码为：

[source,php]
----

// to remove

$query = "SELECT description, due_date FROM wishes WHERE wisher_id = :id_bv";
$stid = oci_parse($con, $query);
oci_bind_by_name($stid, ":id_bv", $wisherID);
oci_execute($stid);

// to replace

$stid = WishDB::getInstance()->get_wishes_by_wisher_id($wisherID);

----

4. 删除关闭数据库连接的行。

[source,php]
----

// For MYSQL database
mysqli_close($con);

// For Oracle database
oci_close($con);

----

不需要该代码，因为在销毁 WishDB 对象时自动关闭数据库连接。不过，保留了释放资源的代码。即使您调用了  ``close``  函数或销毁了使用数据库连接的实例，也需要释放使用连接的所有资源以确保正确关闭连接。


[[refactoringCreateNewWisher]]
=== 重构 createNewWisher.php 文件

重构不影响 HTML 输入窗体或显示相关错误消息的代码。

1. 在 <?php?> 块的顶部，输入以下代码以允许使用  ``db.php``  文件：

[source,php]
----

require_once("Includes/db.php");

----

2. 删除数据库连接凭证（ ``$dbHost``  等）。这些凭证现在包含在  ``db.php``  中。
3. 将连接到数据库并获取许愿者 ID 的代码替换为  ``get_wisher_id_by_name``  函数调用。

对于 *MySQL 数据库*，替换的代码为：

[source,php]
----

// to remove

$con = mysqli_connect("localhost", "phpuser", "phpuserpw");
if (!$con) {
  exit('Connect Error (' . mysqli_connect_errno() . ') '
          . mysqli_connect_error());
}
//set the default client character set 
mysqli_set_charset($con, 'utf-8');

/** Check whether a user whose name matches the "user" field already exists */
mysqli_select_db($con, "wishlist");
$user = mysqli_real_escape_string($con, $_POST['user']);
$wisher = mysqli_query($con, "SELECT id FROM wishers WHERE name='".$user."'");
$wisherIDnum=mysqli_num_rows($wisher);
if ($wisherIDnum) {
  $userNameIsUnique = false;
}

// to replace

$wisherID = WishDB::getInstance()->get_wisher_id_by_name($_POST["user"]);

if ($wisherID) {
  $userNameIsUnique = false;
}

----

对于 *Oracle 数据库*，替换的代码为：

[source,php]
----

// to remove

$con = oci_connect("phpuser", "phpuserpw", "localhost/XE", "AL32UTF8");
if (!$con) {
  $m = oci_error();
  exit('Connect Error ' . $m['message']);
}
$query = "SELECT id FROM wishers WHERE name = :user_bv";
$stid = oci_parse($con, $query);
$user = $_POST['user'];

oci_bind_by_name($stid, ':user_bv', $user);
oci_execute($stid);

//Each user name should be unique. Check if the submitted user already exists.
$row = oci_fetch_array($stid, OCI_ASSOC);
if ($row) {
  $userNameIsUnique = false;
}

// to replace

$wisherID = WishDB::getInstance()->get_wisher_id_by_name($_POST["user"]);
if ($wisherID) {
  $userNameIsUnique = false;
}

----

只要处理当前页面， ``WishDB``  对象就会存在。在处理完成或中断后，将销毁该对象。不需要用于打开数据库连接的代码，因为该操作是由 WishDB 函数完成的。不需要用于关闭连接的代码，因为在销毁  ``WishDB``  对象后，将立即关闭连接。
4. 将在数据库中插入新许愿者的代码替换为调用  ``create_wisher``  函数的代码。

对于 *MySQL 数据库*，替换的代码为：

[source,php]
----

// to remove

if (!$userIsEmpty && $userNameIsUnique && !$passwordIsEmpty && !$password2IsEmpty && $passwordIsValid) {
  $password = mysqli_real_escape_string($con, $_POST['password']);
  mysqli_select_db($con, "wishlist");
  mysqli_query($con, "INSERT wishers (name, password) VALUES ('" . $user . "', '" . $password . "')");
  mysqli_free_result($wisher);
  mysqli_close($con);
  header('Location: editWishList.php');
  exit;
}

// to replace

if (!$userIsEmpty && $userNameIsUnique && !$passwordIsEmpty && !$password2IsEmpty && $passwordIsValid) {

  WishDB::getInstance()->create_wisher($_POST["user"], $_POST["password"]);

  header('Location: editWishList.php' );
  exit;
}

----

对于 *Oracle 数据库*，替换的代码为：

[source,php]
----

// to remove

if (!$userIsEmpty && $userNameIsUnique && !$passwordIsEmpty && !$password2IsEmpty && $passwordIsValid) {

  $query = "INSERT INTO wishers (name, password) VALUES (:user_bv, :pwd_bv)";
  $stid = oci_parse($con, $query);
  $pwd = $_POST['password'];
  oci_bind_by_name($stid, ':user_bv', $user);
  oci_bind_by_name($stid, ':pwd_bv', $pwd);
  oci_execute($stid);
  oci_free_statement($stid);
  oci_close($con);
  header('Location: editWishList.php');
  exit;
}

// to replace

if (!$userIsEmpty && $userNameIsUnique && !$passwordIsEmpty && !$password2IsEmpty && $passwordIsValid) {

  WishDB::getInstance()->create_wisher($_POST["user"], $_POST["password"]);

  header('Location: editWishList.php' );
  exit;
}

----

[[lessonResultSourceCode]]
== 完成当前课程后的应用程序源代码

MySQL 用户：单击link:https://netbeans.org/projects/www/downloads/download/php%252Flesson4.zip[+此处+]以下载源代码，该代码反映了在完成课程后的项目状态。

Oracle 数据库用户：单击link:https://netbeans.org/projects/www/downloads/download/php%252Foracle-lesson4.zip[+此处+]以下载源代码，该代码反映了在完成课程后的项目状态。


== 后续步骤

link:wish-list-lesson3.html[+<< 上一课+]

link:wish-list-lesson5.html[+下一课 >>+]

link:wish-list-tutorial-main-page.html[+返回到教程主页+]


== 有用链接

了解在 PHP 中使用类的详细信息：

* link:http://us3.php.net/manual/en/language.oop5.php[+类和对象+]

了解重构 PHP 代码的详细信息：

* link:http://www.slideshare.net/spriebsch/seven-steps-to-better-php-code-presentation/[+优化 PHP 代码的七个步骤+]
* link:http://www.dokeos.com/wiki/index.php/Refactoring[+PHP 重构+]


link:/about/contact_form.html?to=3&subject=Feedback:%20PHP%20Wish%20List%20CRUD%204:%20Optimizing%20Code[+发送有关此教程的反馈意见+]


要发送意见和建议、获得支持以及随时了解 NetBeans IDE PHP 开发功能的最新开发情况，请link:../../../community/lists/top.html[+加入 users@php.netbeans.org 邮件列表+]。

link:../../trails/php.html[+返回至 PHP 学习资源+]

