八、附录 A:PHP5 中 OOP 介绍

在本书中,我们主要使用过程代码来构建示例应用程序。然而,PDOAPI 是完全面向对象的,在最后一章中,我们使用类模拟了数据库中的真实实体。本附录适用于不熟悉 PHP5 面向对象扩展的程序员。我们将向您介绍 OOP 的基础知识,因为许多来自早期 PHP 版本的开发人员都没有这种编程经验。然而,这只是一个简短的介绍;如果你想掌握 OOP,你应该参考一些关于这个主题的书。

什么是面向对象编程?

面向对象编程(OOP)是一个相对较新的概念,尽管其根源可以追溯到 20 世纪 60 年代。在面向对象编程中,软件与模拟现实实体的对象(如第 7 章中的书籍和作者)一起工作。过程编程涉及一系列指令,而 OOP 中的应用程序涉及一组相互交互的对象。

声明对象的语法

可以将对象视为多个变量(称为属性)和作用于这些变量的函数的容器。这些函数称为方法。每个对象都属于一个类。在 PHP 中,每个对象只能属于一个类(尽管其他一些 OOP 语言允许多重继承),但可以有许多对象或实例属于单个类。类是一种语法结构,允许您描述属于该类的对象将具有哪些属性和方法。

例如,一只狗(一个物种或一个类)是所有活着的狗的总称。一般化的狗具有体重和年龄等属性,以及树皮等方法,而现实生活中的狗,如莱西,属于狗种,可以被描述为 Dog类的一个实例。

让我们看看如何在 PHP5 中对此进行建模:

class Dog
{
public $weight;
public $age;
function bark()
{
print "woof!";
}
}
$lessie = new Dog();
$lessie->weight = 15;
$lessie->age = 3;
$lessie->bark();

在这段代码中,我们定义了一个名为 Dog的类。在 PHP5 中,类定义以保留字 class开头,后跟类名称(类名称可以包含与函数名称相同的字符)。所有类的属性和方法,统称为成员,都在 {…}块中定义。

正如您所看到的,当我们声明属性和方法时,我们正在使用关键字 public。在 PHP4 中,我们本应使用 var关键字,但在 PHP5 中不推荐使用此关键字。除了 public关键字外,我们还可以使用 protected关键字或 private关键字,但稍后会详细介绍。

正如您在代码的第二部分中所看到的,我们使用 new关键字创建对象:

$lessie = new Dog();

此行创建一个属于 Dog类的新对象,并将其分配给 $lessie变量。这是一个非常重要的步骤,因为这是创建对象的唯一方法。PHP 处理后, $lessie变量被初始化,我们可以访问 Dog类中声明的属性和方法,以便对名为 Lessie的对象进行操作。我们现在希望在我们的申请中有两只狗,第二只将被称为 K9。为了实现这一目标,我们必须写下如下内容:

$k9 = new Dog();
$k9->age = 5;
$k9->weight = 18;

现在,我们可以访问 $k9$lessie变量,如果我们想与我们的每只狗进行交互。

换句话说,在我们与实例通信之前,首先必须使用 new关键字创建实例。

初始化变量后,我们可以访问它的属性和方法。正如您在代码中看到的,这是通过 ->构造实现的,它与属性和方法一起使用。请注意,在访问类的属性时,我们不必在->之后写入美元符号(但在类定义中声明属性时必须使用美元符号)。

方法声明时使用 function关键字,后跟方法名称和参数列表。事实上,类的方法的声明方式与普通函数类似,但有一个主要区别。在方法的声明中,始终存在一个名为 $this的隐式变量,它允许您访问对象的属性。让我们看看如何创建一个 getInfo()方法来返回关于我们的狗的一些附加信息:

<?php
class Dog
{
public $weight;
public $age;
function bark()
{
print "woof!";
}
function getInfo()
{
return 'Weight: ' . $this->weight . ' kg, age: ' . $this->age .
' years';
}
}
$lessie = new Dog();
$lessie->weight = 15;
$lessie->age = 3;
$k9 = new Dog();
$k9->age = 5;
$k9->weight = 18;
echo 'Lessie: ', $lessie->getInfo(), "\n";
echo 'K9: ', $k9->getInfo(), "\n";

此代码将显示以下输出:

Lessie: Weight: 15 kg, age: 3 years
K9: Weight: 18 kg, age: 5 years

施工人员

每个类还具有一个称为构造函数的特殊函数(可以隐式或显式声明)。当 PHP 遇到 new关键字时,总是调用构造函数,其目的是执行一些初始化任务。让我们扩展 Dog类,使其具有 $name属性。我们还将更改代码,以便在构造函数中而不是在主应用程序中初始化 name, weightage属性:

<?php
class Dog
{
public $weight;
public $age;
public $name;
function __construct($name, $age, $weight)
{
$this->name = $name;
$this->weight = $weight;
$this->age = $age;
}
function bark()
{
print "woof!";
}
function getInfo()
{
return
'Name: ' . $this->name .
', weight: ' . $this->weight .
' kg, age: ' . $this->age .
' years';
}
}
$lessie = new Dog('Lessie', 3, 15);
$k9 = new Dog('K9', 5, 18);
echo $lessie->getInfo(), "\n";
echo $k9->getInfo(), "\n";

此应用程序将显示以下内容:

Name: Lessie, weight: 15 kg, age: 3 years
Name: K9, weight: 18 kg, age: 5 years

下面是我们所做工作的简要总结。我们首先声明了 $name属性,然后声明了 Dog类的构造函数。构造函数被声明为函数,其特殊名称为 __construct(单词 constructor前面有两个下划线(“”)。我们的构造函数接受三个参数—nameageweight,其值被分配给对象的属性。我们将值分配给属性的顺序并不重要。请注意,我们始终必须使用 $this变量来表示对象的属性。通过这样做,我们可以区分局部变量 $name, $age$weight(作为参数传递)来自构造函数内具有相同名称的对象自身属性。

我们还更改了 getInfo()方法,以便它也返回狗的名称。我们现在可以通过将名称、年龄和权重传递给构造函数来实例化对象。因为这些属性是在构造函数中分配的,所以我们不必在代码的主要部分中这样做。

还应注意,可以在类定义中将默认值指定给属性。这将确保该类的每个对象都将自动指定默认值。例如,我们可以执行以下操作:

class Dog
{
public $weight;
public $age;
public $name;
public $hasCollar = true;
function __construct($name, $age, $weight)
{
$this->name = $name;
$this->weight = $weight;
$this->age = $age;
}
function bark()
{
print "woof!";
}
function getInfo()
{
return
'Name: ' . $this->name .
', weight: ' . $this->weight .
' kg, age: ' . $this->age .
' years, has collar: ' . ($this->hasCollar ? 'yes' : 'no');
}
}

如果使用此 Dog类定义运行应用程序,则会看到以下输出:

Name: Lessie, weight: 15 kg, age: 3 years, has collar: yes
Name: K9, weight: 18 kg, age: 5 years, has collar: yes

正如您现在看到的, hasCollar的默认属性值已经传播到每个新创建的实例(当然,以后可以为每个对象更改它)。

析构函数

构造函数有一个相反的概念,称为析构函数。顾名思义,析构函数用于执行清理任务(此类任务的经典示例包括删除临时文件、关闭数据库连接等)。在 PHP5 中,调用对象上的析构函数,当不再有对该对象的引用时(例如,通过将保存对该对象的引用的变量设置为null或当应用程序终止时),则调用析构函数。

析构函数是一种方法: __destruct()。如果将该方法添加到类中,则在释放对象时将调用该方法。让我们将析构函数添加到Dog类:

class Dog {
public $weight;
public $age;
public $name;
public $hasCollar = true;
function __construct($name, $age, $weight) {
$this->name = $name;
$this->weight = $weight;
$this->age = $age;
}
function bark() {
print "woof!";
}
function getInfo() {
return
'Name: ' . $this->name .
', weight: ' . $this->weight .
' kg, age: ' . $this->age .
' years, has collar: ’ . ($this->hasCollar ? 'yes’ : 'no’);
}
function __destruct() {
print "Freeing $this->name\n";
}
}

现在,如果再次运行代码,它将给出以下输出:

Name: Lessie, weight: 15 kg, age: 3 years, has collar: yes
Name: K9, weight: 18 kg, age: 5 years, has collar: yes
Freeing K9
Freeing Lessie

请注意,PHP5 调用析构函数的顺序没有定义。此外,在析构函数中,除非被释放的对象引用了其他对象,否则代码可能无法访问这些对象。换句话说,析构函数应该只清理由该对象创建的资源。

OOP 的优势

OOP 的强大之处在于它的三个主要特征:继承、封装和多态性。

继承

OOP 中的继承允许您创建继承现有类的行为(方法)和属性(属性)的新类。让我们考虑下面的例子。假设我们有一个名为 Fruit的类。它是一个针对不同水果的广义类,其共同属性是颜色和重量。在 OOP 中,我们可以子类化 Fruit来创建新的类 AppleBanana。这两个类(水果的子类)将具有相同的属性: weightcolor。(注意,我们谈论的是属性本身,而不是它们的值)。苹果可以是绿色,而 Banana可以是黄色。但是任何与 AppleBanana类实例交互的代码都不需要知道它正在与哪种类型的结果进行通信。

让我们将此示例转化为代码:

class Fruit
{
public $color;
public $weight;
}
class Apple extends Fruit
{
function __construct()
{
$this->color = 'green';
$this->weight = 200;
}
}
class Banana extends Fruit
{
function __construct()
{
$this->color = 'yellow';
$this->weight = 250;
}
}
$a[] = new Apple();
$a[] = new Banana();
foreach($a as $f)
{
echo $f->color, "\t", $f->weight, "\n";
}

如您所见,在这个小应用程序中,我们有一个 Apple对象和一个 Banana对象。我们在循环中对它们进行迭代,但访问它们的属性时不考虑它们的类型,因为这两个类使用相同的属性名。但这些特性对每种水果都有不同的值。

继承还允许扩展或完全重写父类的行为。让我们假设我们的 Fruit类每千克还有一个特征价格。它还有一种新的方法-getPrice()将重量(以克为单位)乘以价格:

class Fruit
{
public $color;
public $weight;
public $price;
function getPrice()
{
return $this->weight / 1000 * $this->price;
}
}

现在我们可以在子类中使用此方法:

class Apple extends Fruit
{
function __construct()
{
$this->color = 'green';
$this->weight = 200;
$this->price = 2;
}
}
class Banana extends Fruit
{
function __construct()
{
$this->color = 'yellow';
$this->weight = 250;
$this->price = 3;
}
}
$a[] = new Apple();
$a[] = new Banana();
foreach($a as $f)
{
echo $f->getPrice(), "\n";
}

接下来,我们假设 Banana类有另一种计算价格的方法,以便应用折扣:

class Banana extends Fruit
{
function __construct()
{
$this->color = 'yellow';
$this->weight = 250;
$this->price = 3;
}
function getPrice()
{
return $this->weight / 1000 * $this->price * 0.9;
}
}

如您所见,我们更改了 Banana类中的方法,以便调用 Banana类的 getPrice()方法实现的代码将获得折扣价格,而 Apple类的 getPrice()方法返回全额价格。

另一方面,我们可以在 Banana类中重用 Fruit类对 getPrice()方法的实现(这样我们就不必复制基类中包含的代码):

function getPrice()
{
return parent::getPrice() * 0.9;
}

封装

封装(有时称为信息隐藏)是一个更具理论性的概念。它涉及在类中定义方法,这样我们就可以对客户机代码隐藏实现细节。当我们在 Banana类中重新定义价格计算时,我们已经看到了这一点。从应用程序的角度来看,没有任何变化:我们仍然调用 getPrice()方法,但我们不知道如何执行此计算。

换句话说,类可以通过它们的方法访问,这些方法具有相同的名称,因此,即使这些名称背后的代码发生更改,名称本身也不会更改。这确保了使用新版本的方法时不需要更改现有代码。

我们可以在客户端代码中隐藏更多的实现细节,PHP5 和其他面向对象语言一样,支持方法和属性的可见性修饰符。例如,我们可以向 Banana类添加一个私有属性,该属性将对应用程序的其余部分隐藏:

class Banana extends Fruit
{
private $mySecretProperty;
function __construct()
{
$this->color = 'yellow';
$this->weight = 250;
$this->price = 3;
}
function getPrice()
{
return parent::getPrice() * 0.9;
}
}

$mySecretProperty属性只能在 Banana类中访问(或可见);试图从 Banana类的方法之外访问它将触发运行时错误。(在编译语言中,这将导致编译错误。)

在 PHP5 中,还有两个修饰符:public(我们已经使用过),和protected。公共方法或属性可以从所有应用程序访问,而 protected 只能在类及其子类内部访问。

多态性

多态性是 OOP 的一个特性,它允许我们编写代码来处理属于不同类的对象,前提是这些类具有相同的基类。在上面的示例中,我们已经看到了多态性的作用,当我们使用不同对象的名称访问它们的属性和方法,但返回不同的值并采取不同的操作时。

子类实现属于基类的所有属性和方法,并且基类的所有未来子类都保证实现这些属性和方法,以便现有代码甚至可以与尚不存在的子类一起工作。

PHP5 支持接口。接口是描述不同类和类层次结构中某些行为的构造。例如,让我们考虑一个有一个方法的接口 T0 接口。

interface Tradeable
{
public isImported();
}

现在,我们可以在 Fruit类的定义中声明它实现了可交易接口:

class Fruit implements Tradeable
{
public $color;
public $weight;
public $price;
function getPrice()
{
return $this->weight / 1000 * $this->price;
}
function isImported()
{
return false;
}
}

我们已经默认不导入 Fruit对象及其子类 (AppleBanans)的所有对象。现在我们可以进口香蕉,而将苹果留在国内:

class Banana extends Fruit
{
function __construct()
{
$this->color = 'yellow';
$this->weight = 250;
$this->price = 3;
}
function getPrice()
{
return parent::getPrice() * 0.9;
}
function isImported()
{
return true;
}
}

接下来,我们将创建一个虚构的 Car类来实现 Tradeable接口:

class Car implements Tradeable
{
public $year;
public $make;
public $model;
function isImported()
{
return true;
}
}

注意, Car没有扩展 Fruit,但仍然有 isImported()方法。现在我们可以从应用程序中调用此方法:

$a[] = new Apple();
$a[] = new Banana();
$a[] = new Car();
foreach($a as $item)
{
echo $item->isImported();
}

这个小示例演示了如何通过给不同类层次结构的对象一个公共接口,以相同的方式处理它们。通过这样做,通常具有完全不同含义的对象可以以相同的方式进行操作,这使得它们具有多态性。

静态属性、方法和类常量

在本附录中的所有示例中,我们都使用类的实例(对象),这些实例(对象)模拟了现实生活中的实体。然而,在 PHP5 中,可以使用静态属性和方法。静态属性是给定类的所有实例所共有的变量,因此,如果更改了静态属性,则属于该类的所有对象的静态属性都将更改。

静态属性的声明与常规属性一样,但有一个特殊的static关键字:

class DataModel
{
public static $conn = null;
}

即使不创建类的实例,也可以访问静态属性:

if(!DataModel::$conn) {
echo 'Connection not established!';
}

访问静态属性的语法如下:类名,然后是双分号,然后是属性名。请注意,对于静态属性(与常规属性不同),美元符号 $必须存在。

静态方法,就像静态属性一样,可以在不实例化对象的情况下访问。它们是通过以下方式声明和访问的:

class DataModel
{
public static $conn = null;
static function getConn()
{
if(!DataModel::$conn) {
DataModel::$conn = new PDO('sqlite:./my.db', 'user', 'pass');
}
return DataModel::$conn;
}
}
$conn = DataModel::getConn();

静态方法的声明具有 static关键字,后跟常规方法声明。该方法由类名、双分号和方法名访问。

静态属性和方法可以使用快捷键 self:在类声明中访问

class DataModel
{
public static $conn = null;
static function getConn()
{
if(!self::$conn) {
self::$conn = new PDO('sqlite:./my.db', 'user', 'pass');
}
return self::$conn;
}
}
$conn = DataModel::getConn();

静态方法的定义也有一个主要区别。您不能使用 $this变量(因为 $this变量无法引用任何对象)。

类的另一个“静态”特性是类常量。类常量的行为类似于静态属性,但其值不能更改。类常量必须始终在类声明部分指定其值,并且它们前面没有美元符号(因此它们的命名与普通 PHP 常量一样)。类常量主要用于保持全局命名空间更干净(这也是静态方法的用途之一):

class DataModel
{
public static $conn = null;
const ORDER_AZ = 1;
const ORDER_ZA = 2;
static function getConn()
{
if(!self::$conn) {
self::$conn = new PDO('sqlite:./my.db', 'user', 'pass');
}
return self::$conn;
}
static function getItems($sortMode)
{
if($sortMode == self::ORDER_AZ) {
$sql = // SQL for ascending
}
else {
$sql = // SQL for descending
}
}
}
$items = DataModel::getItems(DataModel::ORDER_ZA);

试图为代码中的类常量赋值将导致分析错误。

例外情况

正如我们所看到的,异常是 PHP5 的一个非常重要的补充。异常是一种特殊的对象,在实例化和 thrown时,它会中断正常的执行流并跳转到所谓的 catch块。

异常用于报告错误情况。传统上,函数失败时会返回错误代码。在继续下一个函数调用之前,应用程序必须检查每个函数调用。请记住用于连接 MySQL 数据库的代码:

$dbh = mysql_connect($host, $user, $pass);
if(!$dbh) {
die('Could not connect to the DB!');
}
if(!mysql_select_db('mydb')) {
die('Could not select the DB');
}
$q = mysql_query('SELECT * FROM test');
if(!$q) {
die('Could not execute query');
}
while($r = mysql_fetch_row($q))
{
...
}

如果 mysql_xxx函数可以抛出异常,则此代码可以简化为:

try
exception handlingexception, throwing{
mysql_connect($host, $user, $pass);
mysql_select_db('mydb');
$q = mysql_query('SELECT * FROM test');
while($r = mysql_fetch_row($q))
{
...
}
}
catch(Exception $e)
{
die(e->getMessage());
}

当然,这段代码不起作用,因为这些函数不是为抛出异常而设计的。您必须使用 PDO,在第 3 章中,我们了解了如何处理 PDO 异常。

异常允许您推迟错误检查并维护更干净的代码。导致抛出异常的函数(或方法)被终止,并执行由 catch关键字指定的块中的代码。任何可能引发异常的代码都会被包装到 try块中:

try
{
// do something exceptional
}
catch(Exception $e)
{
// display warnings etc
// $e->getMessage() contains error message
}

异常的真正威力在于能够将它们升级到调用堆栈中。这意味着,如果您设计了一个可以引发异常的函数或类方法,则该函数或方法不必捕获该异常。事实上,许多应用程序库都是这样设计的,这样它们自己就不会处理异常,而是让异常传递给调用代码。

例如,我们在本书中遇到的 PDOPDOStatement类的许多方法都可以抛出异常,您有责任捕获它们并采取适当的行动。

仔细看看上面代码片段中的 catch块。后跟单词 Exception(PHP 中所有异常的基类名称)和变量标识符 $e。我们可以使用 catch块中的 $e变量来检查错误消息和其他调试信息。 Exception类定义了以下方法:

  • getMessage()返回错误消息。
  • getCode()返回错误代码。
  • getFile()返回发生异常的文件名。
  • getLine()返回异常发生的行号。
  • getTrace()getTraceAsString()返回回溯(调用堆栈),对调试有用。

当然,错误消息和错误代码取决于异常发生的位置,因此它们取决于您使用的应用程序库(如 PDO)。

我们在 catch关键字后面指定了 Exception类名,因为这个类和其他类一样,可以扩展来创建子类。例如,从 PDO 方法引发的所有异常都是 PDOException类的实例。

异常处理机制允许我们为不同类别的异常创建不同的处理例程。例如,我们可以执行以下操作:

try
{
$conn = new PDO('sqlite:./mydb', '', '');
$q = $conn->query('SELECT * FROM test');
while($r = $q->fetch())
{
...
}
}
catch(PDOException $pdoe)
{
die('Database error: ' . $pdoe->getMessage());
}
catch(Exception $e)
{
die('Unexpected error: ' . $e->getMessage());
}

这段代码为所有 PDO 错误定义了两个错误处理例程:一个类用于数据库错误,另一个类用于所有其他错误,我们将其标识为意外错误。当然,在实际应用中,错误处理策略会更加复杂,但本例展示了如何对异常进行分类。

总结

在本附录中,我们看到 PHP5 有一些新的 OOP 扩展,与现代编程语言的扩展相当。它们允许我们编写非常大的应用程序,同时保持代码重用和清洁。面向对象编程是大型项目(如涉及 PDO 的内容管理系统或数据库库)的自然解决方案。PHP5 的库现在在编写时考虑到了面向对象编程。

然而,本附录只是简单介绍了 OOP 背后的主要概念,以便您可以按照本书中的代码示例进行操作。如果你想完全掌握面向对象编程,你应该参考一些书,这些书将向你介绍并指导你完成这个富有挑战性的主题。