PHP-MySQL-入门教程-三-

64 阅读58分钟

PHP MySQL 入门教程(三)

原文:Beginning PHP and MySQL

协议:CC BY-NC-SA 4.0

六、面向对象的 PHP

尽管 PHP 最初并不是一种面向对象的语言,但是多年来,人们已经投入了大量的精力来添加其他语言中的许多面向对象的特性。本章及以下内容旨在介绍这些特性。在这样做之前,让我们考虑一下面向对象编程(OOP)开发模型的优点。

注意

虽然这一章和下一章为您提供了 PHP 的 OOP 特性的广泛介绍,但是对 PHP 开发人员来说,它们的分支的彻底处理实际上值得一整本书。Matt Zandstra 的 PHP Objects,Patterns,and Practice ,第五版(Apress,2016)很方便地详细介绍了这个主题,并附有用 PHP 实现设计模式的精彩介绍以及 Phing、PEAR 和 phpDocumentor 等关键开发工具的概述。

面向对象的好处

面向对象编程强调应用的对象及其交互。对象可以被认为是现实世界中某个实体的虚拟表示,例如整数、电子表格或表单文本字段,将实体的属性和行为捆绑到一个独立的结构中。当采用面向对象的方法开发应用时,您将以这样一种方式创建这些对象,当一起使用时,它们形成了您的应用想要表示的“世界”。这种方法有很多优点,包括增强的代码可重用性、可测试性和可伸缩性。随着你不仅阅读本章和下一章,而且阅读本书的大部分内容,面向对象的方法将会在任何可行的时候被接受,OOP 赋予这些优势背后的原因将会变得更加明显。

本节考察了 OOP 的三个基本概念:封装、继承多态。这三个理念共同构成了迄今为止最强大的编程模型的基础。

包装

程序员通常喜欢把东西拆开,学习所有的小部件如何一起工作。虽然令人满意,但获得如此深入的项目内部工作知识并不是编程熟练的先决条件。例如,数百万人每天都在使用电脑,但很少有人知道它实际上是如何工作的。同样的想法也适用于汽车、微波炉和许多其他物品。我们可以通过使用接口来摆脱这种无知。例如,你知道转动收音机的调谐盘或使用扫描按钮可以改变电台;不要介意你实际做的是告诉无线电收听以特定频率传输的信号,这是使用解调器完成的壮举。不理解这个过程并不妨碍你使用无线电,因为界面优雅地隐藏了这些细节。通过众所周知的接口将用户与应用的真正内部工作分离的实践被称为封装。

面向对象编程通过发布定义良好的接口(从这些接口可以访问某些对象属性和行为),促进了隐藏应用内部工作的相同概念。具有面向对象思想的开发人员设计每个应用组件,使其独立于其他组件,这不仅鼓励重用,而且使开发人员能够像拼图一样组装各个部分,而不是将它们紧紧捆绑在一起。这些部分被称为对象,对象是从一个被称为的模板中创建的,该模板指定了从其类模板中生成的典型对象(一个被称为实例化的过程)中可以预期的数据和行为。这种策略有几个优点:

  • 开发人员可以更有效地维护和改进类实现,而不会影响应用中与对象交互的部分,因为用户与对象的唯一交互是通过其定义良好的接口。

  • 由于对用户与应用的交互进行了控制,因此减少了用户出错的可能性。例如,用于表示网站用户的典型类可能包含保存电子邮件地址的行为。如果该行为包含确保电子邮件地址在语法上有效的逻辑,那么用户就不可能错误地分配一个空的或无效的电子邮件地址,比如carli# example.com

遗产

构成您的环境的许多对象可以使用一组明确定义的需求来建模。例如,所有雇员都有一组共同的特征:姓名、雇员 ID 和工资。然而,有许多不同类型的雇员:职员、主管、出纳员、首席执行官等等,他们中的每一个都可能拥有由这个通用雇员定义所定义的那些特征的一些超集。用面向对象的术语来说,每个专门化的雇员类型可以继承一般的雇员定义,并进一步扩展定义以适应每种类型的特定需求。例如,CEO(首席执行官)类型可能另外标识关于授予的股票期权的信息。基于这个想法,您可以稍后创建一个Human类,然后使Employee类成为Human的子类。其结果是,Employee类及其所有的派生类(ClerkCashierExecutive等)。)将立即继承由Human定义的所有特征和行为。

面向对象的开发方法非常重视继承的概念。这种策略促进了代码的可重用性,因为它假设人们能够在许多应用中使用设计良好的类(即,足够抽象以允许重用的类)。

我将在下一章正式探讨继承的主题;然而,在这一章中,我将不可避免地偶尔提到父类和子类。如果这些偶然的引用没有意义,不要担心,因为到下一章结束时,一切都会变得非常清楚。

多态性

多态,一个源于希腊语的术语,意思是“有多种形式”,定义了 OOP 重新定义或变形一个类的特性或行为的能力,这取决于它被使用的上下文。

回到这个例子,假设在雇员定义中包含了一个与签到有关的行为。对于类型(或类别)Clerk的雇员,这种行为可能涉及到实际使用一个时钟来给一个卡加时间戳。例如,对于其他类型的员工,Programmer登录可能需要登录到公司网络。尽管这两个类都是从Employee类中派生出这种行为,但是每个类的实际实现都依赖于实现“登录”的上下文。这就是多态性的力量。在 PHP 中,这个概念是通过定义一个或多个方法的名称和参数列表的接口类来实现的。这些方法的实际实现由每个实现接口的类来处理。

关键 OOP 概念

本节介绍关键的面向对象实现概念,包括 PHP 特定的示例。

班级

我们的日常环境由无数实体组成:植物、人、车辆、食物...我可以连续几个小时列举它们。每个实体都由一组特定的特征和行为来定义,这些特征和行为最终用来定义实体。例如,车辆可能被定义为具有诸如颜色、轮胎数量、品牌、型号和座位容量等特征,并且具有诸如停止、行驶、转弯和鸣喇叭等行为。在 OOP 的词汇表中,这样一个实体定义属性和行为的实例被称为

类旨在表示那些您希望在应用中操作的真实项目。例如,如果您想要创建一个管理公共库的应用,您可能想要包含表示书籍、杂志、雇员、特殊事件、顾客以及参与库管理过程的任何其他事物的类。这些实体中的每一个都包含了一组特定的特征和行为,在 OOP 中分别称为属性和方法,它们定义了实体的本质。PHP 的通用类创建语法如下:

class Class_Name
{
    // Property declarations defined here
    // Method declarations defined here
}

清单 6-1 描述了一个代表库雇员的类。

class Employee
{

    private $name;
    private $title;

    public function getName() {
        return $this->name;
    }

    public function setName($name) {
        $this->name = $name;
    }

    public function sayHello() {
        echo "Hi, my name is {$this->getName()}.";
    }

}

Listing 6-1Class Creation

这个名为Employee的类定义了两个属性:nametitle,以及三个方法:getName(), setName()sayHello()。如果您不熟悉某些或任何语法,也不用担心;这一点在本章后面会变得清楚。

注意

虽然 PHP 没有提供编码标准,但社区中有许多标准可用。第一个来自 PEAR ( https://pear.php.net/manual/en/standards.php ),但是后来的一些越来越受欢迎,因为它们被许多不同的框架所采用。这些由 PHP-FIG ( https://www.php-fig.org/ )管理和记录,PHP-FIG 是一个提供编码和使用编程语言的许多其他方面的标准的组织。

目标

一个类提供了一个基础,从这个基础上你可以创建类模型的实体的特定实例,更好的说法是对象。例如,员工管理应用可能包含一个Employee类。然后,您可以调用这个类来创建和维护特定的实例,如SallyJim

注意

基于预定义类创建对象的实践通常被称为类实例化

使用关键字new创建对象,如下所示:

$employee = new Employee;

一旦创建了对象,该类中定义的所有特征和行为都可供新实例化的对象使用。这是如何实现的将在下面的章节中揭示。

性能

属性是描述特定值的属性,如名称、颜色或年龄。它们与标准的 PHP 变量非常相似,除了几个关键的区别,这将在本节中介绍。您还将了解如何声明和调用属性,以及如何使用属性范围来限制访问。

声明属性

关于属性声明的规则与变量声明的规则非常相似;基本上没有。因为 PHP 是一种松散类型的语言,属性甚至不一定需要声明;它们可以简单地由一个类对象同时创建和赋值,尽管你很少想这么做,因为这会降低代码的可读性。相反,通常的做法是在类的开头声明属性。或者,您可以在此时为它们分配初始值。下面是一个例子:

class Employee
{
    public $name = "John";
    private $wage;
}

在这个例子中,namewage这两个属性前面都有一个范围描述符(publicprivate),这是声明属性时的一种常见做法。一旦声明,每个属性都可以在作用域描述符赋予它的条件下使用。如果你不知道作用域在类属性中扮演什么角色,不要担心,这个主题将在本章后面讨论。

调用属性

使用->操作符引用属性,与变量不同,属性前面没有美元符号。此外,因为属性值通常特定于给定的对象,所以它与该对象的关联如下:

$object->property

例如,Employee类包括属性nametitlewage。如果你创建了一个名为$employeeEmployee类型的对象,你可以像这样引用它的公共属性:

$employee->name
$employee->title
$employee->wage

当您从定义它的类中引用一个属性时,它仍然以->操作符开头,尽管您没有将它与类名相关联,而是使用了$this关键字。$this暗示你所指的属性与被访问或操作的属性位于同一个类中。因此,如果您要在Employee类中创建一个设置 name 属性的方法,它可能看起来像这样:

function setName($name)
{
    $this->name = $name;
}

管理属性范围

PHP 支持三个类属性范围: *public,private,*和 protected

公共财产

通过在属性前加上关键字public,可以在公共范围内声明属性。下面是一个例子:

class Employee
{
    public $name;
    // Other property and method declarations follow...
}

此示例定义了一个具有单个公共属性的简单类。为了使用该类,必须将其实例化为一个对象。这是通过使用new操作符完成的。$employee = new Employee();类名后面的括号用来给构造函数提供参数。在这种情况下,没有定义构造函数,所以没有参数。

然后,可以通过相应的对象直接访问和操作公共属性,如下所示:

$employee = new Employee();
$employee->name = "Mary Swanson";
$name = $employee->name;
echo "New employee: $name";

执行这段代码会产生以下结果:

New employee: Mary Swanson

尽管这看起来像是维护类属性的逻辑手段,但公共属性实际上通常被认为是禁忌,这是有充分理由的。避免这种实现的原因是,这种直接访问剥夺了类实施任何类型的数据验证的便利手段。例如,没有什么可以阻止用户像这样分配一个name:

$employee->name = "12345";

这肯定不是您所期望的那种输入。为了防止这种情况发生,有两种解决方案。一种解决方案是将数据封装在对象中,使其只能通过一系列接口获得,这就是所谓的公共方法。以这种方式封装的数据通常在范围上是私有的。第二个推荐的解决方案涉及到属性的使用,实际上与第一个解决方案非常相似,尽管在大多数情况下更方便一些。接下来介绍私有范围,随后是关于属性的部分。

私有财产

私有属性只能从定义它们的类中访问。下面是一个例子:

class Employee
{
    private $name;
    private $telephone;
}

被指定为私有的属性只能被从类实例化的对象直接访问,但是它们不能被从子类实例化的对象访问(子类的概念将在下一章介绍)。如果您想让这些属性对子类可用,可以考虑使用 protected 作用域,这将在下面介绍。注意私有属性必须通过公开暴露的接口来访问,这满足了本章开始时介绍的 OOP 的主要原则之一:封装。考虑下面的例子,其中私有属性由公共方法操作:

   class Employee
   {
      private $name;
      public function setName($name) {
         $this->name = $name;
      }
   }

   $employee = new Employee;
   $employee->setName("Mary");

将此类属性的管理封装在一个方法中,使开发人员能够保持对如何设置该属性的严格控制。例如,您可以增强setName()方法的功能,以验证名称是否被设置为只包含字母字符,并确保它不为空。这种策略比让最终用户提供有效信息要实用得多。

受保护的属性

就像函数通常需要仅供函数内部使用的变量一样,类可以包含仅供内部使用的属性。这些财产被视为受保护,并相应地被加上前缀。下面是一个例子:

class Employee
{
     protected $wage;
}

受保护的属性也可供继承的类访问和操作,这是私有属性所没有的特性。因此,如果计划扩展该类,应该使用受保护的属性来代替私有属性。

下面的示例显示了一个类如何扩展另一个类,并从父类获得对所有受保护属性的访问权,就好像这些属性是在子类中定义的一样。

class Programmer extends Employee
{
     public function bonus($percent) {
        echo "Bonud = " . $this->wage * $percent / 100;
    }
}

属性重载

属性重载通过公共方法强制访问和操作来继续保护属性,同时允许像访问公共属性一样访问数据。这些方法被称为访问器变异器,或者更通俗地说,更广为人知的名称是获取器设置器,它们分别在属性被访问或操作时被自动触发。

不幸的是,PHP 不提供属性重载特性,如果您熟悉 C++和 Java 等其他 OOP 语言,您可能会习惯这种特性。因此,您需要使用公共方法来模仿这样的功能。例如,您可以为属性名创建 getter 和 setter 方法,方法是分别声明两个函数getName()setName(),并在每个函数中嵌入适当的语法。本节的结尾给出了这种策略的一个例子。

PHP 5 引入了一些对属性重载的支持,通过重载__set__get方法来实现。如果试图引用不存在于类定义中的成员变量,将调用这些方法。属性可用于各种目的,如调用错误信息,甚至通过动态创建新变量来扩展类。本节将介绍__get__set

使用 __set()方法设置属性

赋值器设置器方法负责隐藏属性赋值实现,并在将类数据赋给类属性之前验证类数据。其原型如下:

public void __set([string name], [mixed value])

__set()方法将属性名和相应的值作为输入。下面是一个例子:

class Employee
{
    public $name;
    function __set($propName, $propValue)
    {
        echo "Nonexistent variable: \$$propName!";
    }
}

$employee = new Employee;
$employee->name = "Mario";
$employee->title = "Executive Chef";

这会产生以下输出:

Nonexistent variable: $title!

您可以使用这个方法用新的属性来扩展这个类,就像这样:

class Employee
{
    public $name;
    public function __set($propName, $propValue)
    {
        $this->$propName = $propValue;
    }
}

$employee = new Employee;
$employee->name = "Mario";
$employee->title = "Executive Chef";
echo "Name: {$employee->name}<br />";
echo "Title: {$employee->title}";

这会产生以下结果:

Name: Mario
Title: Executive Chef

使用 __get()方法获取属性

访问器,或赋值器方法,负责封装检索类变量所需的代码。其原型如下:

public mixed __get([string name])

它接受一个参数作为输入,即您想要检索其值的属性的名称。它应该在成功执行时返回值TRUE,否则返回值FALSE。下面是一个例子:

class Employee
{
    public $name;
    public $city;
    protected $wage;

    public function __get($propName)
    {
        echo "__get called!<br />";
        $vars = array("name", "city");
        if (in_array($propName, $vars))
        {
           return $this->$propName;
        } else {
           return "No such variable!";
        }
    }

}

$employee = new Employee();
$employee->name = "Mario";

echo "{$employee->name}<br />";
echo $employee->age;

这将返回以下内容:

Mario
__get called!
No such variable!

创建自定义 Getters 和 Setters

坦率地说,尽管__set()__get()方法有一些好处,但它们对于管理复杂的面向对象应用中的属性来说确实不够,主要是因为大多数属性都需要它们自己特定的验证逻辑。因为 PHP 不像 Java 或 C#那样支持属性的创建,所以您需要实现自己的解决方案。考虑为每个私有属性创建两个方法,如下所示:

<?php

    class Employee
    {

        private $name;

        // Getter
        public function getName() {
            return $this->name;
        }

        // Setter
        public function setName($name) {
            $this->name = $name;
        }

    }

?>

尽管这种策略不能提供与使用属性相同的便利,但它确实使用标准化的命名约定封装了管理和检索任务。当然,您应该向 setter 添加额外的验证功能;然而,这个简单的例子应该足以说明问题。

常数

您可以在一个类中定义*常量、*或不想改变的值。这些值将在从该类实例化的任何对象的整个生命周期中保持不变。类常量是这样创建的:

const NAME = 'VALUE';

例如,假设您创建了一个与数学相关的类,其中包含许多定义数学函数的方法,以及许多常量:

class mathFunctions
{
    const PI = '3.14159265';
    const E = '2.7182818284';
    const EULER = '0.5772156649';
    // Define other constants and methods here...
}

类常量被定义为类定义的一部分,其值不能像属性一样在运行时改变,也不能像用define()函数定义的其他常量那样改变。类常量被认为是类的静态成员,因此可以使用::而不是- >来访问它们。稍后将详细介绍静态属性和方法。然后可以这样调用类常量:

echo mathFunctions::PI;

方法

一个方法非常类似于一个函数,除了它打算定义一个特定类的行为。在前面的例子中,您已经使用了许多方法,其中许多都与对象属性的设置和获取有关。像函数一样,方法可以接受参数作为输入,并可以向调用者返回值。方法也像函数一样被调用,除了方法前面有调用方法的对象的名称,就像这样:

$object->methodName();

在这一节中,您将学习所有关于方法的知识,包括方法声明、方法调用和作用域。

声明方法

方法的创建方式与函数完全相同,使用相同的语法。方法和普通函数之间的唯一区别是方法声明通常以范围描述符开头。通用语法如下:

scope function functionName()
{
    // Function body goes here
}

例如,名为calculateSalary()的公共方法可能如下所示:

public function calculateSalary()
{
    return $this->wage * $this->hours;
}

在这个例子中,该方法使用关键字$this直接调用两个类属性wagehours。它通过将两个属性值相乘来计算薪水,并像函数一样返回结果。但是,请注意,方法并不仅限于处理类属性;传递参数完全有效,就像传递函数一样。

有许多保留的方法名用于具有特殊用途的方法。这些被称为魔术的方法和名字是:__construct(), __destruct(), __call(), __callStatic(), __get(), __set(), __isset(), __unset(), __sleep(), __wakeup(), __toString(), __invoke(), __set_state(), __clone(), and __debugInfo()。这些方法将在以后定义,它们都不需要创建一个类。

调用方法

方法的调用方式与函数几乎完全相同。继续前面的例子,calculateSalary()方法将像这样被调用:

$employee = new Employee("Janie");
$salary = $employee->calculateSalary();

方法范围

PHP 支持三种方法作用域: *public、private、*和 protected。

公共方法

可以随时随地访问公共方法。您通过在公共方法前面加上关键字public来声明它。下面的示例演示了这两种声明方法,还演示了如何从类外部调用公共方法:

<?php
    class Visitor
    {
        public function greetVisitor()
        {
            echo "Hello!";
        }
}

    $visitor = new Visitor();
    $visitor->greetVisitor();
?>

以下是结果:

Hello!

私有方法

标记为私有的方法只能在同一个类中定义的方法中使用,而不能用于子类中定义的方法。仅用于帮助类中其他方法的方法应该标记为私有。例如,考虑一个名为validateCardNumber()的方法,它用于确定顾客的图书证号码的语法有效性。虽然这种方法对于满足许多任务(比如创建顾客和自助结账)肯定是有用的,但是当单独执行时,这种功能没有任何用处。因此,validateCardNumber()应该被标记为私有,然后在例如setCardNumber()方法中使用,如下面的清单 6-2 所示:

{
    if $this->validateCardNumber($number) {
        $this->cardNumber = $number;
        return TRUE;
    }
    return FALSE;
}

private function validateCardNumber($number)
{
    if (!preg_match('/^([0-9]{4})-([0-9]{3})-([0-9]{2})/', $number) ) return FALSE;
        else return TRUE;
}

Listing 6-2public function setCardNumber($number)

试图从实例化对象外部调用validateCardNumber()方法会导致致命错误。

保护

标记为 protected 的类方法仅对起源类及其子类可用。这种方法可能用于帮助类或子类执行内部计算。例如,在检索特定职员的信息之前,您可能希望验证作为参数传入类构造函数的雇员标识号(EIN)。然后使用verifyEIN()方法验证这个 EIN 的语法正确性。因为该方法只供类中的其他方法使用,并且可能对从Employee派生的类有用,所以应该声明为 protected,如下所示:

<?php
    class Employee
    {
        private $ein;
        function __construct($ein)
        {
            if ($this->verifyEIN($ein)) {

                echo "EIN verified. Finish";
            }

        }
        protected function verifyEIN($ein)
        {
            return TRUE;
        }
    }
    $employee = new Employee("123-45-6789");
?>

由于其受保护的作用域状态,试图从类外部或任何子类调用verifyEIN()将导致致命错误。

摘要

方法的特殊之处在于,它们只在父类中声明,而在子类中实现。只有声明为抽象的类才能包含抽象方法,抽象类不能被实例化。它们充当子类或子类的基本定义。如果您想要定义一个应用编程接口(API ),以便以后用作实现的模型,您可以声明一个抽象方法。开发人员应该知道,只要该方法满足抽象方法定义的所有需求,他的特定实现就应该工作。抽象方法是这样声明的:

abstract function methodName();

假设您想要创建一个抽象的Employee类,它将作为各种雇员类型(经理、职员、出纳员等)的基类。):

abstract class Employee
{
    abstract function hire();
    abstract function fire();
    abstract function promote();
    abstract function demote();
}

这个类可以由各自的雇员类扩展,比如ManagerClerkCashier。第七章详细阐述了这个概念,并且更深入地研究了抽象类。

最后的

将方法标记为 final 可以防止它被子类覆盖。一个最终的方法是这样声明的:

class Employee
{

    final function getName() {
    ...
    }
}

以后试图重写已完成的方法会导致致命错误。

注意

下一章将讨论类继承以及方法和属性重写的主题。

构造函数和析构函数

通常,在创建和销毁对象时,您会想要执行许多任务。例如,您可能希望立即为新实例化的对象分配几个属性。但是,如果您必须手动执行,您几乎肯定会忘记执行所有必需的任务。面向对象编程通过提供被称为构造函数析构函数的特殊方法来自动化对象的创建和销毁过程,从而大大减少了出现这种错误的可能性。

构造器

您通常希望初始化某些属性,甚至在新实例化一个对象时触发方法的执行。在实例化之后立即这样做没有错,但是如果自动完成的话会更容易。这种机制存在于 OOP 中,称为构造函数。很简单,构造函数被定义为在对象实例化时自动执行的代码块。OOP 构造函数提供了许多优势:

  • 构造函数可以接受参数,这些参数可以在创建时分配给特定的对象属性。

  • 构造函数可以调用类方法或其他函数。

  • 类构造函数可以调用其他构造函数,包括来自父类的构造函数。

PHP 通过名字__construct识别构造函数(一个双下划线在构造函数关键字前面)。构造函数声明的一般语法如下:

function __construct([argument1, argument2, ..., argumentN])
{
     // Class initialization code
}

例如,假设您想在创建一个新的Book对象时立即设置一本书的 ISBN。通过使用构造函数,可以省去创建对象后执行setIsbn()方法的麻烦。代码可能如下所示:

<?php

    class Book
    {

        private $title;
        private $isbn;
        private $copies;

        function __construct($isbn)
        {
            $this->setIsbn($isbn);
        }

        public function setIsbn($isbn)
        {
            $this->isbn = $isbn;
        }

    }

    $book = new Book("0615303889");

?>

定义了构造函数后,实例化 book 对象会导致构造函数的自动调用,进而调用setIsbn方法。如果您知道每当实例化一个新对象时都应该调用这样的方法,那么通过构造函数自动调用要比自己手动调用好得多。

此外,如果您想确保这些方法只通过构造函数调用,您应该将它们的范围设置为 private,以确保它们不能被对象或子类直接调用。

遗产

正如多次提到的,创建扩展到其他类的类是可能的。这就是通常所说的继承。这意味着新类继承了另一个类的所有属性和方法。

调用父构造函数

PHP 不会自动调用父构造函数;您必须使用parent关键字以及范围解析操作符(::)显式调用它。这不同于调用在使用了->操作符的对象或任何父对象上定义的其他方法。下面是一个例子:

<?php

    class Employee
    {

        protected $name;
        protected $title;

        function __construct()

        {
            echo "Employee constructor called! ";
        }
    }

    class Manager extends Employee
    {
        function __construct()
        {
            parent::__construct();
            echo "Manager constructor called!";
        }
    }

    $employee = new Manager();
?>

这将导致以下结果:

Employee constructor called!Manager constructor called!

忽略对parent::__construct()的调用会导致只调用Manager构造函数,如下所示:

Manager constructor called!

析构函数

正如您可以使用构造函数来定制对象创建过程一样,您也可以使用析构函数来修改对象销毁过程。析构函数像其他方法一样创建,但是必须命名为__destruct()。下面是一个例子:

<?php

    class Book
    {

        private $title;
        private $isbn;
        private $copies;

        function __construct($isbn)
        {
            echo "Book class instance created. ";
        }

        function __destruct()
        {
            echo "Book class instance destroyed.";
        }

    }

    $book = new Book("0615303889");

?>

结果如下:

Book class instance created.Book class instance destroyed.

尽管这个析构函数不是由脚本直接调用的,但是当脚本结束并且 PHP 正在释放对象使用的内存时,它会被调用。

当脚本完成时,PHP 将销毁所有驻留在内存中的对象。因此,如果实例化的类和作为实例化结果创建的任何信息都驻留在内存中,则不需要显式声明析构函数。但是,如果由于实例化而创建了不太稳定的数据(比如说,存储在数据库中),并且应该在对象销毁时销毁,那么就需要创建一个自定义的析构函数。在脚本结束后调用的析构函数(也称为请求关闭)不会以任何特定的顺序调用,如果脚本由于致命错误而终止,析构函数可能不会被调用。

类型提示

类型提示是 PHP 5 中引入的一个特性,在 PHP 7 中被重命名为类型声明。类型声明确保传递给方法的对象确实是预期类的成员或者变量是特定类型的。例如,只有类Employee的对象应该被传递给takeLunchbreak()方法是有意义的。因此,您可以在方法定义的唯一输入参数$employee前面加上Employee,强制执行这条规则。下面是一个例子:

private function takeLunchbreak(Employee $employee)
{
    ...
}

尽管在 PHP 5 中实现的类型声明只适用于对象和数组,但是这个特性后来扩展到了标量类型(PHP 7)和可迭代类型(PHP 7.1)。类型声明特性仅在参数被传递给函数/方法时起作用。可以在函数/方法内部分配其他类型的变量。

静态类成员

有时,创建不被任何特定对象调用,而是与所有类实例相关并被所有类实例共享的属性和方法是很有用的。例如,假设您正在编写一个跟踪网页访问者数量的类。您不希望每次实例化类时访问者计数都重置为零,所以您应该将属性设置为静态范围,如下所示:

<?php

    class Visitor
    {

        private static $visitors = 0;

        function __construct()
        {
            self::$visitors++;
        }

        static function getVisitors()
        {
            return self::$visitors;
        }

    }

    // Instantiate the Visitor class.
    $visits = new Visitor();

    echo Visitor::getVisitors()."<br />";

    // Instantiate another Visitor class.
    $visits2 = new Visitor();

    echo Visitor::getVisitors()."<br />";

?>

结果如下:

1
2

因为$visitors属性被声明为静态的,所以对其值的任何更改(在本例中是通过类构造函数)都会反映到所有实例化的对象中。还要注意,静态属性和方法是通过使用self关键字、sope 解析操作符(::)和类名来引用的,而不是通过$this和箭头操作符。这是因为使用“常规”同级所允许的方法来引用静态属性是不可能的,如果尝试这样做,将会导致语法错误。

注意

你不能在一个类中使用$this来引用一个声明为静态的属性。

关键字 instanceof

关键字instanceof帮助你确定一个对象是一个类的实例,还是一个类的子类,或者实现了一个特定的接口(见第六章),并做相应的事情。例如,假设您想知道$manager是否是从Employee类派生出来的:

$manager = new Employee();
...
if ($manager instanceof Employee) echo "Yes";

请注意,类名没有被任何分隔符(引号)包围。包含它们将导致语法错误。当你同时处理多个对象时,instanceof关键字特别有用。例如,您可能会重复调用一个特定的函数,但希望根据给定类型的对象调整该函数的行为。你可以使用一个case语句和instanceof关键字来以这种方式管理行为。

助手函数

有许多函数可以帮助您管理和使用类库。本节将介绍一些更常用的函数。

确定类是否存在

如果由class_name指定的类存在于当前执行的脚本上下文中,则class_exists()函数返回TRUE,否则返回FALSE。其原型如下:

boolean class_exists(string class_name)

确定对象上下文

get_class()函数返回object所属的类名,如果object不是对象,则返回FALSE。其原型如下:

string get_class(object object)

了解类方法

get_class_methods()函数返回一个数组,其中包含由类class_name定义的方法名(可以通过类名或传入一个对象来标识)。名称列表取决于调用函数的范围。如果从类范围之外调用该函数,该函数将返回在该类或任何父类中定义的所有公共方法的列表。如果在对象的方法内部调用它(作为参数传入$this ),它将返回来自任何父类的公共或受保护方法的列表以及来自类本身的所有方法。其原型如下:

array get_class_methods(mixed class_name)

了解类属性

get_class_vars()函数返回一个关联数组,其中包含所有属性的名称及其在class_name指定的类中定义的相应值。返回的属性名列表遵循与上述方法相同的模式。其原型如下:

array get_class_vars(string class_name)

了解声明的类

函数get_declared_classes()返回一个数组,包含当前正在执行的脚本中定义的所有类的名称,包括 PHP 定义的任何标准类和加载的任何扩展。这个函数的输出会根据 PHP 发行版的配置而有所不同。例如,在测试服务器上执行get_declared_classes()会产生 134 个类的列表。其原型如下:

array get_declared_classes(void)

了解对象属性

函数get_object_vars()返回一个关联数组,其中包含受作用域限制的对象可用的非静态属性及其相应的值。那些没有值的属性将在关联数组中被赋值NULL。其原型如下:

array get_object_vars(object object)
Casting the object to an array or using the print_r() or var_dump() functions will make it possible to see/access private properties and their values.

确定对象的父类

get_parent_class()函数返回对象所属类的父类的名称。如果对象的类是基类,将返回该类名。其原型如下:

string get_parent_class(mixed object)

确定对象类型

如果对象属于class_name类型的类,或者如果它属于class_name的子类,则is_a()函数返回TRUE。如果对象与class_name类型无关,则返回FALSE。其原型如下:

boolean is_a(object object, string class_name)

确定对象子类类型

如果object(可以作为类型字符串或对象传入)属于从class_name继承的类,则is_subclass_of()函数返回TRUE,否则返回FALSE。其原型如下:

boolean is_subclass_of(mixed object, string class_name)

确定方法存在

如果名为method_name的方法对object可用,则method_exists()函数返回TRUE,否则返回FALSE。其原型如下:

boolean method_exists(object object, string method_name)

自动加载对象

出于组织的原因,通常的做法是将每个类放在一个单独的文件中。回到库场景,假设管理应用调用表示书籍、员工、事件和顾客的类。对于这个项目,您可能会创建一个名为classes的目录,并在其中放置以下文件:Books.class.phpEmployees.class.phpEvents.class.phpPatrons.class.php。虽然这确实方便了类管理,但它也要求每个单独的文件对任何需要它的脚本都可用,通常是通过require_once()语句。因此,需要所有四个类的脚本需要在开头插入以下语句:

require_once("classes/Books.class.php");
require_once("classes/Employees.class.php");
require_once("classes/Events.class.php");
require_once("classes/Patrons.class.php");

以这种方式管理类包含可能会变得相当乏味,并且给已经非常复杂的开发过程增加了额外的步骤。为了消除这个额外的任务,PHP 引入了自动加载对象的概念。自动加载允许你定义一个特殊的__autoload函数,当引用一个还没有在脚本中定义的类时,这个函数会被自动调用。通过定义以下函数,可以消除手动包含每个类文件的需要:

function __autoload($class) {
    require_once("classes/$class.class.php");
}

定义这个函数消除了对require_once()语句的需要,因为当一个类第一次被调用时,__autoload()将被调用,根据__autoload()中定义的命令加载该类。这个函数可以放在一个全局应用配置文件中,这意味着脚本只需要使用这个函数。

注意

第三章中介绍了require_once()函数及其兄弟函数。

特征

PHP 5.4 的一个伟大的新增功能是 traits 的实现。

特征是实现代码重用的一种方式,其中多个类实现相同的功能。而不是一遍又一遍地写同样的代码,可以定义为一个 trait,并“包含”在多个类定义中。该实现在编译时以复制和粘贴的方式工作。如果有必要改变实现,可以在一个地方完成,特征的定义,它将在每个使用它的地方生效。

特征的定义方式类似于类,但是使用关键字trait而不是class。它们可以包含属性和方法,但不能实例化为对象。通过语句use <trait name>;可以将一个特征包含在一个类中,通过将每个特征作为逗号分隔的列表添加为use <trait1>, <trait2>,可以在每个类中包含多个特征;。

<?php
trait Log {
    function writeLog($message) {
        file_put_contents("log.txt", $message . "\n", FILE_APPEND);
    }
}
class A {
    function __construct() {
        $this->WriteLog("Constructor A called");
    }
    use Log;
}
class B {
    function __construct() {
        $this->WriteLog("Constructor B called");
    }
    use Log;
}

特征中定义的属性或方法将覆盖从父类和属性继承的同名属性或方法,并且特征中定义的方法可以在使用该特征的类中被覆盖。

使用特征,部分是为了解决 PHP 中存在的单一继承的限制。

摘要

本章介绍了面向对象编程的基础知识,接着概述了 PHP 的基本面向对象特性,特别关注了 PHP 5 版本中的增强和增加。

下一章将详细介绍这些介绍性信息,包括继承、接口、抽象类等主题。

七、高级 OOP 特性

第六章介绍了面向对象编程(OOP)的基础知识。本章通过介绍 PHP 的几个更高级的 OOP 特性建立在这个基础上。具体来说,本章介绍了以下五个功能:

  • 对象克隆: PHP 将所有对象视为引用,可以使用new操作符创建它们。考虑到这一点,如果所有对象都被当作引用,那么如何创建对象的副本呢?通过克隆对象。

  • 继承:正如在第六章中所讨论的,通过继承建立类层次的能力是一个基本的 OOP 概念。这一章介绍了 PHP 的继承特性和语法,并且包括了几个展示这一关键 OOP 特性的例子。

  • **接口:**一个接口是一个未实现的方法定义和常量的集合,作为一个类蓝图。接口确切地定义了可以用类做什么,而不会被特定于实现的细节所困扰。本章介绍了 PHP 的接口支持,并提供了几个例子来展示这个强大的 OOP 特性。

  • **抽象类:**抽象类是不能实例化的类。抽象类旨在由可以实例化的类继承,更好的说法是一个具体类。抽象类可以完全实现,部分实现,或者根本不实现。本章介绍了抽象类的一般概念,并介绍了 PHP 的类抽象能力。

  • 名称空间:名称空间通过根据上下文划分不同的库和类,帮助您更有效地管理代码库。在这一章中,我将向你介绍 PHP 的名称空间特性。

PHP 不支持的高级 OOP 特性

如果您有其他面向对象语言的经验,您可能会感到困惑,为什么前面的特性列表没有包括其他编程语言支持的某些 OOP 特性。原因很可能是 PHP 不支持这些特性。为了避免您进一步的困惑,下面的列表列举了 PHP 不支持的高级 OOP 特性,因此不在本章中讨论:

  • 方法重载**:**PHP 不支持通过方法重载实现多态性的能力,可能永远也不会支持。然而,有可能以类似的方式实现某些功能。这是用魔法方法__set()__get()__call()等完成的。( http://php.net/manual/en/language.oop5.overloading.php )

  • 操作符重载**:**PHP 目前不支持根据你试图修改的数据类型给操作符赋予额外含义的能力。根据 PHP 开发人员邮件列表中的讨论和实现的 RFC(https://wiki.php.net/rfc/operator-overloading),它可能有一天会实现。

  • 多重继承 : PHP 不支持多重继承。支持多个接口的实现。和特征提供了一种实现类似功能的方法。

只有时间才能证明这些特性中的任何一个或全部是否会在 PHP 的未来版本中得到支持。

对象克隆

在 PHP 中,对象被视为引用。将一个对象赋给另一个变量只是创建了对同一对象的第二个引用。操作任何属性都会对这两个变量引用的对象产生影响。这使得将对象传递给函数和方法成为可能。但是,因为所有对象都被视为引用而不是值,所以复制对象更加困难。如果你试图复制一个被引用的对象,为了解决复制的问题,PHP 提供了一个明确的方法来克隆一个对象。

让我们首先看一个例子,清单 7-1 ,其中一个对象被赋给第二个变量。

<?php
class Employee {
  private $name;
  function setName($name) {
    $this->name = $name;
  }
  function getName() {
    return $this->name;
  }
}

$emp1 = new Employee();
$emp1->setName('John Smith');
$emp2 = $emp1;
$emp2->setName('Jane Smith');

echo "Employee 1 = {$emp1->getName()}\n";
echo "Employee 2 = {$emp2->getName()}\n";

Listing 7-1Copying an Object

这个例子的输出表明,尽管$emp1$emp2看起来像两个不同的变量,但它们都引用同一个对象。它看起来像这样:

Employee 1 = Jane Smith
Employee 2 = Jane Smith

克隆示例

您可以通过在对象前面加上关键字clone来克隆对象,就像这样:

$destinationObject = clone $targetObject;

清单 7-2 给出了一个对象克隆的例子。这个例子使用了一个名为Employee的示例类,它包含两个属性(employeeidtiecolor)以及这些属性对应的 getters 和 setters。示例代码实例化了一个Employee对象,并将其用作演示克隆操作效果的基础。

<?php
    class Employee {
        private $employeeid;
        private $tiecolor;
        // Define a setter and getter for $employeeid
        function setEmployeeID($employeeid) {
            $this->employeeid = $employeeid;
        }

        function getEmployeeID() {
            return $this->employeeid;
        }

        // Define a setter and getter for $tiecolor
        function setTieColor($tiecolor) {
            $this->tiecolor = $tiecolor;
        }

        function getTieColor() {
            return $this->tiecolor;
        }
    }

    // Create new Employee object
    $employee1 = new Employee();

    // Set the $employee1 employeeid property

    $employee1->setEmployeeID("12345");

    // Set the $employee1 tiecolor property
    $employee1->setTieColor("red");

    // Clone the $employee1 object
    $employee2= clone $employee1;

    // Set the $employee2 employeeid property
    $employee2->setEmployeeID("67890");

    // Output the $employee1and $employee2employeeid properties

   printf("Employee 1 employeeID: %d <br />", $employee1->getEmployeeID());
   printf("Employee 1 tie color: %s <br />", $employee1->getTieColor());

   printf("Employee 2 employeeID: %d <br />", $employee2->getEmployeeID());
   printf("Employee 2 tie color: %s <br />", $employee2->getTieColor());

?>

Listing 7-2Cloning an Object with the clone Keyword

执行此代码会返回以下输出:

Employee1 employeeID: 12345
Employee1 tie color: red
Employee2 employeeID: 67890
Employee2 tie color: red

如您所见,$雇员2变成了类型为Employee的对象,并继承了$employee1的属性值。为了进一步证明$Employee2确实属于类型Employee,它的employeeid属性也被重新分配。

__clone()方法

您可以通过在对象类中定义一个__clone()方法来调整对象的克隆行为。该方法中的任何代码都将直接按照 PHP 的本地克隆行为执行。让我们修改Employee类,添加以下方法:

function __clone() {
   $this->tiecolor = "blue";
}

准备就绪后,让我们创建一个新的Employee对象,添加employeeid属性值,克隆它,然后输出一些数据来表明克隆对象的tiecolor确实是通过__clone()方法设置的。清单 7-3 提供了一个例子。

// Create new Employee object
$employee1 = new Employee();

// Set the $employee1 employeeid property
$employee1->setEmployeeID("12345");

// Clone the $employee1 object
$employee2 = clone $employee1;

// Set the $employee2 employeeid property
$employee2->setEmployeeID("67890");

// Output the $employee1 and $employee2 employeeid properties
printf("Employee1 employeeID: %d <br />", $employee1->getEmployeeID());
printf("Employee1 tie color: %s <br />", $employee1->getTieColor());
printf("Employee2 employeeID: %d <br />", $employee2->getEmployeeID());
printf("Employee2 tie color: %s <br />", $ employee2->getTieColor());

Listing 7-3Extending clone’s Capabilities with the __clone() Method

执行此代码会返回以下输出:

Employee1 employeeID: 12345
Employee1 tie color: red
Employee2 employeeID: 67890
Employee2 tie color: blue

遗产

人们善于根据组织层级进行思考;我们广泛使用这种概念观点来管理我们日常生活的许多方面。公司管理结构,杜威十进制系统,以及我们对动植物王国的看法,只是严重依赖于等级观念的系统的几个例子。因为 OOP 的前提是允许人类对我们试图用代码实现的真实世界环境的属性和行为进行近似建模,所以能够表示这些层次关系是有意义的。

例如,假设您的应用调用了一个名为Employee的类,该类旨在表示一个公司员工可能具有的特征和行为。一些代表特征的类属性可能包括以下内容:

  • name:员工姓名

  • age:员工的年龄

  • salary:员工的工资

  • 员工在公司工作的年数

一些Employee类方法可能包括以下内容:

  • 执行一些与工作相关的任务

  • 午休时间

  • 充分利用这宝贵的两周时间

这些特征和行为与所有类型的员工都相关,无论员工在组织中的目的或地位如何。不过,很明显,员工之间也存在差异;例如,高管可能持有股票期权,能够掠夺公司,而其他员工却享受不到这种奢侈。助理必须会做备忘录,办公室经理需要做供应品清单。尽管有这些差异,如果您必须为所有类共享的那些属性创建和维护冗余的类结构,这将是非常低效的。OOP 开发范式考虑到了这一点,允许您继承现有的类并在其上构建。

类继承

PHP 中的类继承是通过使用extends关键字来完成的。清单 7-4 展示了这种能力,首先创建一个Employee类,然后创建一个从Employee继承的Executive类。

注意

从另一个类继承的类被称为子类,或者子类。子类继承的类被称为父类,或者基类

<?php
   // Define a base Employee class
   class Employee {

      private $name;

      // Define a setter for the private $name property.
      function setName($name) {
         if ($name == "") echo "Name cannot be blank!";
         else $this->name = $name;
      }

      // Define a getter for the private $name property

      function getName() {
         return "My name is ".$this->name."<br />";
      }
   } // end Employee class

   // Define an Executive class that inherits from Employee

   class Executive extends Employee {

      // Define a method unique to Employee
      function pillageCompany() {
         echo "I'm selling company assets to finance my yacht!";
      }

   } // end Executive class

   // Create a new Executive object
   $exec = new Executive();

   // Call the setName() method, defined in the Employee class
   $exec->setName("Richard");

   // Call the getName() method
   echo $exec->getName();

   // Call the pillageCompany() method
   $exec->pillageCompany();
?> 

Listing 7-4Inheriting from a Base Class

这将返回以下内容:

My name is Richard.
I'm selling company assets to finance my yacht!

因为所有的雇员都有名字,Executive类继承自Employee类,省去了重新创建name属性以及相应的 getter 和 setter 的麻烦。然后,您可以只关注那些特定于高管的特征,在本例中是一个名为pillageCompany()的方法。这个方法只适用于类型为Executive的对象,而不适用于Employee类或任何其他类——除非你创建一个从Executive继承的类。下面的例子演示了这个概念,产生了一个名为CEO的类,它继承自Executive:

<?php

class Employee {
 private $name;
 private $salary; 

 function setName($name) {
   $this->name = $name;
 }

 function setSalary($salary) {
   $this->salary = $salary;
 }

 function getSalary() {
   return $this->salary;
 }
}

class Executive extends Employee {
 function pillageCompany() {
   $this->setSalary($this->getSalary() * 10);
 }
}

class CEO extends Executive {
  function getFacelift() {
     echo "nip nip tuck tuck\n";
  }
}

$ceo = new CEO();
$ceo->setName("Bernie");
$ceo->setSalary(100000);
$ceo->pillageCompany();
$ceo->getFacelift();
echo "Bernie's Salary is: {$ceo->getSalary()}\n";
?>

Listing 7-5Inheritance

输出将如下所示:

nip nip tuck tuck
Bernie's Salary is: 1000000

因为ExecutiveEmployee继承而来,CEO类型的对象拥有Executive可用的所有属性和方法,此外还有getFacelift()方法,该方法只保留给CEO类型的对象。

继承和构造函数

与类继承相关的一个常见问题与构造函数的使用有关。当一个孩子被实例化时,一个父类构造函数会执行吗?如果有,如果子类也有自己的构造函数会怎么样?它是在父构造函数之外执行,还是覆盖父构造函数?此类问题将在本节中回答。

如果父类提供了构造函数,只要子类没有构造函数,它就会在子类实例化时执行。例如,假设Employee类提供了这个构造函数:

function __construct($name) {
    $this->setName($name);
}

然后实例化CEO类并检索name属性:

$ceo = new CEO("Dennis");
echo $ceo->getName();

它将产生以下结果:

My name is Dennis

但是,如果子类也有构造函数,那么无论父类是否也有构造函数,该构造函数都会在子类被实例化时执行。例如,假设除了包含前面描述的构造函数的Employee类之外,CEO类还包含这个构造函数:

function __construct() {
    echo "<p>CEO object created!</p>";
}

然后实例化CEO类:

$ceo = new CEO("Dennis");
echo $ceo->getName();

这次它将产生以下输出,因为CEO构造函数覆盖了Employee构造函数:

CEO object created!
My name is

当需要检索name属性时,您会发现它是空白的,因为在Employee构造函数中执行的setName()方法从未触发。当然,你可能想让那些父构造函数也触发。不要害怕,因为有一个简单的解决办法。像这样修改CEO构造函数:

function __construct($name) {
    parent::__construct($name);
    echo "<p>CEO object created!</p>";
}

同样,实例化CEO类并以与之前相同的方式执行getName(),这一次您将看到不同的结果:

CEO object created!
My name is Dennis

你应该明白,当遇到parent::__construct()时,PHP 开始向上搜索父类,寻找合适的构造函数。因为在Executive中没有找到,所以继续搜索到Employee类,在这一点上找到了合适的构造函数。如果 PHP 在Employee类中找到了一个构造函数,那么它就会被触发。如果您想让EmployeeExecutive构造函数都触发,您需要调用Executive构造函数中的parent::__construct()

您还可以选择以另一种方式引用父构造函数。例如,假设当创建一个新的CEO对象时,EmployeeExecutive构造函数都应该执行。这些构造函数可以在CEO构造函数中显式引用,如下所示:

function __construct($name) {
    Employee::__construct($name);
    Executive::__construct();
    echo "<p>CEO object created!</p>";
}

继承和后期静态绑定

创建类层次结构时,您偶尔会遇到这样的情况:父方法会与可能在子类中被覆盖的静态类属性进行交互。这与关键字self的使用有关。让我们考虑一个涉及修改后的EmployeeExecutive类的例子:

<?php

class Employee {

  public static $favSport = "Football";

  public static function watchTV()
  {
    echo "Watching ".self::$favSport;
  }

}

class Executive extends Employee {
  public static $favSport = "Polo";
}
echo Executive::watchTV();

?>

Listing 7-6Late Static Binding

因为Executive类继承了Employee中的方法,所以人们会认为这个例子的输出是Watching Polo,对吗?实际上,这不会发生,因为self关键字是在编译时而不是运行时确定其范围的。因此,这个例子的输出将总是Watching Football。PHP 解决了这个问题,当您实际上想要在运行时确定静态属性的范围时,可以重新使用static关键字。为此,您可以像这样重写watchTV()方法:

  public static function watchTV()
  {
    echo "Watching ".static::$favSport;
  }

接口

一个接口定义了一个实现特定服务的通用规范,声明了所需的函数和常数,但没有具体说明必须如何实现。没有提供实现细节,因为不同的实体可能需要以不同的方式实现发布的方法定义。接口的本质要求所有接口方法都是公共的。

要点是建立一套通用的指导原则,为了使接口被认为是已实现的,必须执行这些原则。

警告

类属性没有在接口中定义。这是完全留给实现类的事情。

以掠夺一家公司的概念为例。这项任务可以通过多种方式完成,这取决于谁在做脏活。例如,一个典型的员工可能会使用办公室信用卡购买鞋子和电影票,将购买的物品记为“办公室费用”,而一名高管可能会要求他的助理通过在线会计系统将资金重新分配到瑞士银行账户。这两个雇员都想抢劫,但每个人都以不同的方式去做。在这种情况下,该接口的目标是定义一套掠夺公司的准则,然后要求各个类相应地实现该接口。例如,接口可能只包含两个方法:

emptyBankAccount()

burnDocuments()

然后您可以要求EmployeeExecutive类实现这些特性。在本节中,您将了解这是如何实现的。然而,首先花点时间来理解 PHP 5 是如何实现接口的。在 PHP 中,接口是这样创建的:

interface iMyInterface
{
    CONST 1;
    ...
    CONST N;
    function methodName1();
    ...
    function methodNameN();
}

小费

通常的做法是用小写字母i作为接口名称的前缀,以便于识别。

接口是方法定义(名称和参数列表)的集合,当一个类实现一个或多个接口时,它被用作一种契约形式。当类通过 implements 关键字实现接口时,契约就完成了。所有方法必须用接口中定义的相同签名实现,或者实现类必须声明为抽象(下一节介绍的概念);否则,将会出现类似下面的错误:

Fatal error: Class Executive contains 1 abstract methods and must

therefore be declared abstract (pillageCompany::emptyBankAccount) in
/www/htdocs/pmnp/7/executive.php on line 30

下面是实现上述接口的一般语法:

class Class_Name implements iMyInterface
{
    function methodName1()
    {
        // methodName1() implementation
    }

    function methodNameN()
    {
        // methodNameN() implementation
    }
}

实现单一接口

本节通过创建和实现一个名为iPillage的接口来展示 PHP 接口实现的一个工作示例,该接口用于掠夺公司:

interface iPillage
{
    function emptyBankAccount();
    function burnDocuments();
}

然后这个接口被实现以供Executive类使用:

class Executive extends Employee implements iPillage
{
    private $totalStockOptions;
    function emptyBankAccount()
    {
        echo "Call CFO and ask to transfer funds to Swiss bank account.";
    }

    function burnDocuments()
    {
        echo "Torch the office suite."; 

    }
}

因为掠夺应该在公司的各个层面进行,所以你可以通过Assistant类实现同一个接口:

class Assistant extends Employee implements iPillage
{
    function takeMemo() {
        echo "Taking memo...";
    }

    function emptyBankAccount()
    {
        echo "Go on shopping spree with office credit card.";
    }

    function burnDocuments()
    {
        echo "Start small fire in the trash can.";
    }
}

如您所见,接口特别有用,因为尽管它们定义了一些行为发生所需的方法和参数的数量和名称,但它们承认不同的类可能需要不同的方法来实现这些方法。在这个例子中,Assistant类通过在垃圾桶中放火焚烧文档,而Executive类则通过更激进的方式(放火焚烧主管的办公室)来焚烧文档。

实现多个接口

当然,允许外部承包商掠夺公司是不公平的;毕竟,这个组织是在全职员工的支持下建立起来的。也就是说,你如何让员工既能完成自己的工作,又能掠夺公司,同时又能限制承包商只完成要求他们完成的任务?解决方案是将这些任务分解成几个任务,然后根据需要实现多个接口。考虑这个例子:

<?php
    interface iEmployee {...}
    interface iDeveloper {...}
    interface iPillage {...}
    class Employee implements IEmployee, IDeveloper, iPillage {
    ...
    }

    class Contractor implements iEmployee, iDeveloper {
    ...
    }
?>

如您所见,所有三个界面(iEmployeeiDeveloperiPillage)都可供雇员使用,而只有iEmployeeiDeveloper可供承包商使用。

确定接口是否存在

interface_exists()函数确定一个接口是否存在,如果存在则返回TRUE,否则返回FALSE。其原型如下:

boolean interface_exists(string interface_name [, boolean autoload])

抽象类

抽象类是实际上不应该被实例化的类,而是作为基类被其他类继承。例如,考虑一个名为Media的类,旨在体现各种类型的出版材料(如报纸、书籍和 CD)的共同特征。因为Media类不代表现实生活中的实体,而是一系列相似实体的一般化表示,所以您永远不会想要直接实例化它。为了确保这不会发生,这个类被认为是*抽象的。*然后,各种派生的Media类继承这个抽象类,确保子类之间的一致性,因为抽象类中定义的所有方法都必须在子类中实现。

一个类通过在定义前加上抽象这个词来声明抽象,就像这样:

abstract class Media
{
  private $title;
  function setTitle($title) {
    $this->title = $title;
  }
  abstract function setDescription($description)
}

class Newspaper extends Media
{
  function setDescription($description) {
  }

  function setSubscribers($subscribers) {
  }
}

class CD extends Media
{
  function setDescription($description) {
  }

  function setCopiesSold($subscribers) {
  }
}

尝试实例化抽象类会导致以下错误信息:

Fatal error: Cannot instantiate abstract class Employee in
/www/book/chapter07/class.inc.php.

抽象类确保一致性,因为从它们派生的任何类都必须实现该类中派生的所有抽象方法。试图放弃类中定义的任何抽象方法的实现会导致致命错误。

抽象类还是接口?

什么时候应该使用接口而不是抽象类,反之亦然?这可能会非常令人困惑,而且经常是一个争论不休的问题。但是,有几个因素可以帮助您做出这方面的决定:

  • 如果您打算创建一个由许多密切相关的对象构成的模型,请使用抽象类。如果你打算创建一个功能,这个功能随后会被许多不相关的对象所包含,那么使用一个接口。

  • 如果您的对象必须从许多来源继承行为,请使用接口。PHP 类可以实现多个接口,但只能扩展单个(抽象)类。

  • 如果你知道所有的类将共享一个公共的行为实现,那么使用一个抽象类并在那里实现行为。您不能在接口中实现行为。

  • 如果多个类共享完全相同的代码,使用 traits。

名称空间简介

随着您继续创建类库以及使用由其他开发人员创建的第三方类库,您将不可避免地遇到两个库使用相同类名的情况,从而产生意外的应用结果。

为了说明这一挑战,假设你已经创建了一个网站,帮助你组织你的藏书,并允许访问者对你个人库中的任何书籍发表评论。为了管理这些数据,您创建了一个名为Library.inc.php的库,并在其中创建了一个名为Clean的类。这个类实现了多种通用的数据过滤器,您不仅可以将这些过滤器应用于与书籍相关的数据,还可以应用于用户评论。下面是这个类的一个片段,包括一个名为filterTitle()的方法,可以用来清理书名和用户评论:

class Clean {

    function filterTitle($text) {
        // Trim white space and capitalize first word
        return ucfirst(trim($text));
    }

}

因为这是一个 G 级网站,您还希望通过一个亵渎过滤器传递所有用户提供的数据。网上搜索发现了一个名为DataCleaner.inc.php的 PHP 类库,其中有一个名为Clean的类。这个类包括一个名为RemoveProfanity()的函数,它负责用可接受的替代词替换不良词汇。该类如下所示:

class Clean {

    function removeProfanity($text) {
        $badwords = array("idiotic" => "shortsighted",
                          "moronic" => "unreasonable",
                          "insane" => "illogical");

        // Replace bad words
        return strtr($text, $badwords); 

    }

}

急于开始使用脏话过滤器,您在相关脚本的顶部包含了DataCleaner.inc.php文件,后面是对Library.inc.php库的引用:

require "DataCleaner.inc.php";
require "Library.inc.php";

然后,您进行一些修改以利用亵渎过滤器,但是在将应用加载到浏览器中时,您会看到以下致命错误消息:

Fatal error: Cannot redeclare class Clean

您收到这个错误是因为不可能在同一个脚本中使用两个同名的类。这类似于一个文件系统,一个目录中不能有两个同名的文件,但是它们可以存在于两个不同的目录中。

有一种简单的方法可以解决这个问题,那就是使用名称空间。您需要做的就是给每个类分配一个名称空间。为此,您需要对每个文件进行一次修改。打开Library.inc.php并将这条线放在顶部:

namespace Library;

同样,打开DataCleaner.inc.php,将下面一行放在顶部:

namespace DataCleaner;

namespace 语句必须是文件中的第一条语句。

然后您可以开始使用各自的Clean类,而不用担心名称冲突。为此,通过在每个类前面加上命名空间来实例化每个类,如下例所示:

<?php
    require "Library.inc.php";
    require "Data.inc.php";

    use Library;
    use DataCleaner;

    // Instantiate the Library's Clean class

    $filter = new Library\Clean();

    // Instantiate the DataCleaner's Clean class
    $profanity = new DataCleaner\Clean();

    // Create a book title
    $title = "the idiotic sun also rises";

    // Output the title before filtering occurs
    printf("Title before filters: %s <br />", $title);

    // Remove profanity from the title
    $title = $profanity->removeProfanity($title);

    printf("Title after Library\Clean: %s <br />", $title);

    // Remove white space and capitalize title
    $title = $filter->filterTitle($title);

    printf("Title after DataCleaner\Clean: %s <br />", $title);

?>

执行该脚本会产生以下输出:

Title before filters: the idiotic sun also rises
Title after DataCleaner\Clean: the shortsighted sun also rises
Title after Library\Clean: The Shortsighted Sun Also Rises

名称空间可以定义为子名称空间的层次结构。这是通过添加更多由名称空间分隔符(反斜杠)分隔的名称来实现的。如果同一个包或供应商提供了一个类、函数或常数的多个版本,或者提供了多个具有您想要组合在一起的功能的类,这将非常有用。

例如,下面是 Amazon Web Services (AWS) SDK 提供的名称空间列表:

namespace Aws\S3;
namespace Aws\S3\Command;
namespace Aws\S3\Enum;
namespace Aws\S3\Exception;
namespace Aws\S3\Exception\Parser;
namespace Aws\S3\Iterator;
namespace Aws\S3\Model;
namespace Aws\S3\Model\MultipartUpload;
namespace Aws\S3\Sync; 

SDK 包含许多其他名称空间,用于所提供的各种服务。这些例子中的名字都不太长,只使用了两三个层次。在某些情况下,您可能希望为您的命名空间指定一个较短的名称。这将需要更少的输入,并使代码更具可读性。这是通过为名称空间提供别名来实现的。一个简短的例子可以很好地说明这一点。

<php
use Aws\S3\Command;
$cmd = new Aws\S3\Command\S3Command();

在这种情况下,名称空间是按原样导入或使用的,所有的类(以及函数和常数)都必须以完整的名称空间名称作为前缀。

<php
use Aws\S3\Command as Cmd;
$cmd = new Cmd\S3Command();

在第二个例子中,名称空间被重命名为Cmd,,此后所有对类和函数的引用都将以短名称为前缀。

一个特殊的名称空间是全局名称空间。这用反斜杠()引用。所有内置函数和类都放在全局名称空间中。为了从给定的名称空间中访问这些函数,您必须指定该函数或类属于全局名称空间。只有在使用名称空间时才需要这样做。

<?php
namespace MyNamespace; 

/* This function is MyNamespace\getFile() */
function getFile($path) {
     /* ... */
     $content = \file_get_contents($path);
     return $content;
}
?>

在上面的例子中,新函数getFile()被定义在一个名为 MyNamespace 的名称空间中。为了调用全局函数file_get_contents(),必须在它前面加上前缀\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\

摘要

这一章和前一章向您介绍了 PHP 的 OOP 特性的全部。PHP 支持大多数存在于其他编程语言中的 OOP 概念,并且今天可用的许多库和框架都利用了这些概念。如果你是面向对象编程的新手,这些材料应该能帮助你更好地理解许多关键的面向对象编程概念,并启发你进行更多的实验和研究。

下一章介绍了一个强大的解决方案,可以有效地检测和响应在网站运行过程中可能突然出现的意外操作错误,即所谓的异常。

八、错误和异常处理

当涉及到编程时,错误和其他意想不到的事情无疑会蔓延到最琐碎的应用中。其中一些错误是程序员引起的,是开发过程中所犯错误的结果。还有一些是用户引起的,是由于最终用户不愿意或不能遵守应用的限制,例如没有输入语法上有效的电子邮件地址。还有一些是由于你完全无法控制的事件造成的,比如数据库或网络连接暂时无法访问。然而,不管错误的来源是什么,您的应用必须能够以一种优雅的方式对这种意外的错误做出反应,希望这样做不会丢失数据或崩溃。此外,您的应用应该能够为用户提供必要的反馈,以了解此类错误的原因,并相应地调整他们的行为。一些警告或错误也应该通知系统管理员或开发人员,允许他们采取措施并纠正问题。

本章介绍了 PHP 必须提供的处理错误和其他意外事件(称为异常)的几个特性。具体来说,涵盖了以下主题:

  • 配置指令 : PHP 的错误相关配置指令决定了 PHP 对错误检测的敏感程度以及该语言如何响应这些错误。本章介绍了其中的许多指令。

  • 错误记录:保持运行日志是记录纠正重复错误的进展和快速识别新引入问题的最佳方式。在本章中,您将学习如何将消息记录到操作系统的日志守护进程和自定义日志文件中。

  • 异常处理:异常是开发人员预测代码执行时可能发生的错误类型,并在不终止程序执行的情况下建立处理这些错误的机制的一种方式。许多其他编程语言都知道这一点,从版本 5 开始,它就成为 PHP 的一部分,在版本 7 中得到显著改进,允许捕捉异常和错误。

从历史上看,开发社区在实现适当的应用错误处理方面的松懈是出了名的。然而,随着应用变得越来越复杂和笨拙,将适当的错误处理策略融入到您的日常开发程序中的重要性怎么强调都不为过。因此,您应该花些时间熟悉 PHP 在这方面提供的许多特性。

你所有的虫子都属于你

作为一名程序员,你所有的错误都是属于你自己的,我保证你会看到很多错误。如果你是一个开发团队的一员,那么所有的 bug 都属于这个团队,一个团队成员可能必须修复其他团队成员引入的 bug。对于你来说,掌握这样一个事实是非常重要的:作为一名程序员,你的大量时间将被用来扮演 bug 修复者的角色,因为通过认识到甚至接受这一现实,并因此采取必要的步骤来最有效地检测和解决 bug,你将在提高生产力的同时显著地减少你的挫折感。

那么,一个典型的 PHP 错误是什么样子的呢?在尝试目前介绍的示例时,您可能已经被粗鲁地介绍过至少几个,但是让我们借此机会做一个正式的介绍:

Parse error: syntax error, unexpected '}' , expecting end of file in /Applications/first.php on line 7

这个密码实际上是 PHP 最常见的错误之一,报告了一个意外遇到的花括号(})。当然,正如您在前一章中所了解到的,括号是 PHP 语法中非常有效的一部分,用于包含诸如foreach语句之类的块。然而,当没有找到匹配的括号时,您会看到上面的错误。事实上,是打字错误(忘记插入匹配的括号)导致了这个错误。

$array = array(4,5,6,7);
foreach ($array as $arr)
  echo $arr;
}

你看到错误了吗?foreach语句的左括号丢失,意味着位于最后一行的右括号没有匹配。当然,通过使用支持自动完成匹配括号的代码编辑器,可以大大减少这些琐碎而耗时的错误的发生率;然而,仍然存在大量不容易识别和解决的错误。因此,您需要充分利用配置 PHP 的优势来有效地监控和报告错误,这是我接下来要深入探讨的主题。

配置 PHP 的错误报告行为

许多配置指令决定了 PHP 的错误报告行为。本节将介绍其中的许多指令。

设置所需的误差灵敏度水平

error_reporting指令决定了报告的敏感度等级。有 16 个级别可用,这些级别的任何组合都是有效的。这些级别的完整列表见表 8-1 。请注意,每个级别都包含其下的所有级别。例如,E_ALL级别报告表中低于它的 15 个级别的所有消息。

表 8-1

PHP 的错误报告级别

|

误差水平

|

描述

| | --- | --- | | E_ALL | 所有错误和警告 | | E_COMPILE_ERROR | 致命的编译时错误 | | E_COMPILE_WARNING | 编译时警告 | | E_CORE_ERROR | PHP 初始启动时发生的致命错误 | | E_CORE_WARNING | PHP 初始启动时出现的警告 | | E_DEPRECATED | 关于使用计划在未来 PHP 版本中删除的特性的警告(在 PHP 5.3 中引入) | | E_ERROR | 致命的运行时错误 | | E_NOTICE | 运行时通知 | | E_PARSE | 编译时分析错误 | | E_RECOVERABLE_ERROR | 近乎致命的错误 | | E_STRICT | PHP 版本可移植性建议 | | E_USER_DEPRECATED | 关于用户启动使用在未来 PHP 版本中计划删除的特性的警告 | | E_USER_ERROR | 用户产生的错误 | | E_USER_NOTICE | 用户生成的通知 | | E_USER_WARNING | 用户生成的警告 | | E_WARNING | 运行时警告 |

基于核心开发人员对正确编码方法的决定,建议代码变更,旨在确保 PHP 版本间的可移植性。如果您使用了不推荐的函数或语法,错误地使用了引用,对类字段使用了var而不是作用域级别,或者引入了其他的风格差异,E_STRICT会提醒您注意。

注意

error_reporting指令使用波浪号字符(~)来表示逻辑运算符 NOT。

在开发阶段,您可能希望报告所有的错误。因此,考虑在 php.ini 中这样设置指令:

error_reporting = E_ALL

这个指令也可以在 PHP 脚本中设置。这在调试脚本时很有用,并且您不想更改所有脚本的服务器配置。这是通过如下的ini_set()函数完成的:

ini_set('error_reporting', E_ALL);

也可以使用error_reporting()功能。它比一般的ini_set()函数更短,可读性更好。

error_reporting(E_ALL);

在 php.ini 中配置指令时使用的常量也可以作为 php 脚本中的常量。

对于其他报告变化有很多机会,包括抑制某些错误类型,同时监视其他错误类型。然而,在开发阶段,您肯定希望有机会捕捉并解决所有可能的错误,这一点E_ALL做得很好。当然,当您的应用在生产环境中运行时,您绝不会希望向浏览器或 API 客户端输出任何难看的错误,这意味着您希望控制错误显示的方式和位置:这是我接下来将讨论的主题。

在浏览器中显示错误

启用display_errors指令会显示符合error_reporting定义的标准的任何错误。您应该仅在开发期间启用此指令,并确保在站点运行于生产环境中时禁用它,因为显示此类消息不仅可能进一步混淆最终用户,还可能暴露敏感信息,从而增加黑客攻击的可能性。例如,假设您使用一个名为configuration.ini的文本文件来管理一组应用配置设置。由于权限配置错误,应用无法写入文件。然而,您没有捕捉错误并提供用户友好的响应,而是允许 PHP 将问题报告给最终用户。显示的错误如下所示:

Warning: fopen(configuration.ini): failed to open stream: Permission denied in
/home/www/htdocs/www.example.com/configuration.ini on line 3

当然,您将敏感文件放在文档根树中已经违反了一条基本规则,但是现在您将文件的确切位置和名称告知用户,这大大加剧了问题的严重性。除非您已经采取了某些预防措施来防止通过您的 web 服务器访问该文件,否则用户可以简单地输入类似于 http://www.example.com/configuration.ini 的 URL,并检查您所有潜在的敏感配置设置。

在这个过程中,一定要启用display_startup_errors指令,它将显示 PHP 引擎初始化过程中遇到的任何错误。像display_errors一样,您需要确定display_startup_errors在您的生产服务器上是禁用的。

小费

error_get_last()函数返回一个关联数组,由类型、消息、文件和最后出现的错误行组成。

记录错误

从逻辑上讲,当您的应用在生产服务器上运行时,您会希望继续进行错误检测;但是,您不希望在浏览器中显示这些错误,而是希望记录它们。为此,在 php.ini 中启用log_errors指令。

记录这些日志语句的确切位置取决于error_log指令的设置。该值可以为空,在这种情况下,错误将记录到 SAPI 日志中。如果您在 Apache 下运行脚本,SAPI 日志将是 Apache 错误日志文件;如果您在 CLI 下执行,则是stderrerror_log指令也可以设置为特殊的关键字 syslog,这会导致错误被发送到 Linux 上的 syslog 或者 Windows 系统上的 Even log。最后,您可以指定一个文件名。这可以是一个绝对路径,使主机上的所有网站使用相同的文件,或者您可以指定一个相对路径,每个网站一个文件。最好将该文件放在文档根目录之外,并且运行 web 服务器的进程必须能够写入该文件。

如果您不熟悉 syslog,它是一个基于 Linux 的日志记录工具,提供了一个 API 来记录与系统和应用执行相关的消息。这些文件可以在大多数 Linux 系统的/var/log 中找到。Windows 事件日志本质上相当于 Linux 系统日志。通常使用事件查看器来查看这些日志。

如果您决定将错误记录到单独的文本文件中,web 服务器进程所有者必须有足够的权限写入该文件。此外,一定要将该文件放在文档根目录之外,以减少攻击者偶然发现它的可能性,并有可能发现一些对秘密进入您的服务器有用的信息。

在任何情况下,每个日志消息都将包括消息时间戳:

[24-Apr-2014 09:47:59] PHP Parse error: syntax error, unexpected '}' in /Applications/MAMP/htdocs/5thedition/08/first.php on line 7

至于使用哪一个,您应该根据具体环境来决定。如果你使用的是共享虚拟主机服务,那么主机提供商很可能已经配置了一个预定义的登录目的地,这意味着不需要做任何决定。如果您控制服务器,使用 syslog 可能是理想的,因为您将能够利用 syslog 解析实用程序来查看和分析日志。请仔细检查这两种可能性,并选择最适合您的服务器环境配置的策略。

您可以使用许多不同的指令进一步调整 PHP 的错误记录行为,包括log_errors_max_len,它设置每个记录项的最大长度(以字节为单位);ignore_repeated_errors,这使得 PHP 忽略出现在同一文件中同一行的重复错误消息;和ignore_repeated_source,这使得 PHP 忽略来自不同文件或同一文件中不同行的重复错误消息。有关这些指令和所有其他影响错误报告的指令的更多细节,请参见 PHP 手册:

https://php.net/manual/en/errorfunc.configuration.php#ini.error-log

创建和记录自定义消息

当然,您并不局限于依靠 PHP 来检测和记录错误消息。事实上,您可以随意将任何内容记录到日志中,包括状态消息、基准统计数据和其他有用的数据。

要记录定制消息,使用error_log()函数,传递消息、期望的日志目的地和一些额外的定制参数。最简单的用例如下所示:

error_log("New user registered");

在执行时,消息和相关的时间戳将被保存到由error_log指令定义的目的地。该消息将类似于以下内容:

[24-Apr-2014 12:15:07] New user registered

您可以选择覆盖由error_log指令定义的目的地,通过传递一些附加参数来指定一个定制的日志位置:

error_log("New user registered", 3, "/var/log/users.log");

第二个参数设置消息类型(0=PHP’s logging system, 1=Send email, 2=no logger, 3=Append to a file and 4+Use the SAPI logger),而第三个参数(/var/log/users.log)标识新的日志文件。请记住,该文件需要对 web 服务器是可写的,因此请确保相应地设置权限。

异常处理

在本节中,您将学习所有关于异常处理的知识,包括基本概念、语法和最佳实践。因为异常处理对许多读者来说可能是一个全新的概念,所以我将首先提供一个概述。如果您已经熟悉了基本概念,请随意跳到 PHP 特定的材料。

为什么异常处理很方便

在一个完美的世界里,你的程序运行起来就像一台润滑良好的机器,没有内部的和用户发起的会扰乱执行流程的错误。然而,编程就像现实世界一样,经常会遇到不可预见的事情。在程序员的行话中,这些意外事件被称为异常。一些编程语言能够优雅地对异常做出反应,而不是导致应用陷入停顿,这种行为被称为异常处理。当检测到错误时,代码发出,或者抛出异常。反过来,相关的异常处理代码获得问题的所有权,或者捕获异常。这种策略有很多好处。

首先,异常处理通过使用一种通用的策略,不仅识别和报告应用错误,还指定一旦遇到错误程序应该做什么,从而使错误识别和管理过程变得有条不紊。此外,异常处理语法促进了错误处理器与一般应用逻辑的分离,从而使代码更加有组织性和可读性。大多数实现异常处理的语言将这个过程抽象为四个步骤:

  • 应用试图执行一些任务。

  • 如果尝试失败,异常处理功能将引发异常。

  • 分配的处理器捕捉异常并执行任何必要的任务。

  • 异常处理功能会清除尝试过程中消耗的所有资源。

几乎所有的语言都借鉴了 C++的语法,称为try / catch。下面是一个简单的伪代码示例:

try {
    perform some task
    if something goes wrong
        throw exception("Something bad happened")
// Catch the thrown exception
} catch(exception) {
    Execute exception-specific code
}

您还可以创建多个处理器块,这允许您考虑各种错误。然而,这很难管理,而且可能会有问题,因为很容易忽略异常。您可以通过使用各种预定义的处理器或通过扩展其中一个预定义的处理器(本质上是创建您自己的自定义处理器)来实现这一点。为了便于说明,让我们以前面的伪代码示例为基础,使用人为的处理器类来管理 I/O 和与除法相关的错误:

try {
    perform some task
    if something goes wrong
        throw IOexception("Could not open file.")
    if something else goes wrong
        throw Numberexception("Division by zero not allowed.")
// Catch IOexception
} catch(IOexception) {
    output the IOexception message
}
// Catch Numberexception
} catch(Numberexception) {
    output the Numberexception message
}

如果你是例外的新手,这种处理意外结果的标准化方法可能就像一股新鲜空气。下一节通过介绍和演示 PHP 中可用的各种异常处理过程,将这些概念应用于 PHP。

PHP 的异常处理能力

本节介绍 PHP 的异常处理特性。具体来说,我将触及基本异常类的内部结构,并演示如何扩展这个基类、定义多个 catch 块以及引入其他高级处理任务。让我们从基础开始:基本异常类。

扩展基本异常类

PHP 的基本异常类实际上非常简单,提供了一个不包含参数的默认构造函数、一个包含两个可选参数的重载构造函数和六个方法。本节将介绍这些参数和方法。

默认构造函数

调用默认异常构造函数时不带参数。例如,您可以像这样调用异常类:

throw new Exception();

例如,将下面一行代码保存到一个支持 PHP 的文件中,并在您的浏览器中执行它:

throw new Exception("Something bad just happened");

执行时,您会收到一个类似如下的致命错误:

Fatal error: Uncaught exception 'Exception' with message 'Something bad just happened' in /Applications/ /08/first.php:9 Stack trace: #0 {main} thrown in /Applications/uhoh.php on line 9

术语堆栈跟踪指的是在错误发生之前调用的函数列表,它将帮助您识别正确的文件、类和方法。这是调试时的重要信息。

当然,致命错误恰恰是您试图避免的!为此,您需要处理或者说捕获异常。一个示例很好地说明了这是如何实现的,通过确定是否发生了异常,如果发生了异常,则正确处理该异常:

try {
    $fh = fopen("contacts.txt", "r");
    if (! $fh) {
        throw new Exception("Could not open the file!");
    }
} catch (Exception $e) {
    echo "Error (File: ".$e->getFile().", line ".
          $e->getLine()."): ".$e->getMessage();
}

如果引发异常,将会输出如下内容:

Warning: fopen(contacts.txt): failed to open stream: No such file or directory in /Applications/read.php, line 3
Error (File: /Applications/read.php, line 5): Could not open the file!

在这个例子中,引入了 catch 语句,它负责实例化异常对象(存储在这里的$e)。一旦实例化,这个对象的方法就可以用来了解关于异常的更多信息,包括抛出异常的文件的名称(通过getFile()方法)、发生异常的行(通过getLine()方法)以及与抛出的异常相关的消息(通过getMessage()方法)。

一旦实例化了异常,就可以使用本节后面介绍的以下六种方法中的任何一种。然而,只有四个是有用的;另外两个只有在用重载的构造函数实例化类时才有用。

介绍 Finally 块

finally块与 try 和 catch 块一起工作,执行总是在 try 和 catch 块之后执行的代码。无论发生什么,代码都会执行;也就是说finally块不关心异常是否实际发生。

finally块中的代码通常用于恢复系统资源,比如那些用于打开文件或数据库连接的资源。

$fh = fopen("contacts.txt", "r");
try {
    if (! fwrite($fh, "Adding a new contact")) {
        throw new Exception("Could not open the file!");
    }
} catch (Exception $e) {
    echo "Error (File: ".$e->getFile().", line ".
          $e->getLine()."): ".$e->getMessage();
} finally {
    fclose($fh);
}

在本例中,不管fwrite()函数是否成功写入文件,您都需要正确地关闭文件。通过将这段代码包含在 finally 块中,您可以确定这将会发生。

扩展异常类

尽管 PHP 的基本异常类提供了一些漂亮的特性,但在某些情况下,您可能希望扩展该类以允许额外的功能。例如,假设您想要国际化您的应用,以允许翻译错误消息。这些消息可能位于一个单独的文本文件中的数组中。扩展的异常类将从这个平面文件中读取,将传递给构造函数的错误代码映射到适当的消息(可能已经本地化为适当的语言)。下面是一个平面文件示例:

1,Could not connect to the database!
2,Incorrect password. Please try again.
3,Username not found.
4,You do not possess adequate privileges to execute this command.

当用一种语言和一个错误代码实例化MyException时,它将读入适当的语言文件,将每一行解析成一个由错误代码及其相应消息组成的关联数组。在清单 8-1 中可以找到MyException类和一个使用示例。

class MyException extends Exception {
    function __construct($language, $errorcode) {
        $this->language = $language;
        $this->errorcode = $errorcode;
    }
    function getMessageMap() {
        $errors = file("errors/{$this->language}.txt");
        foreach($errors as $error) {
             list($key,$value) = explode(",", $error, 2);
             $errorArray[$key] = $value;
        }
        return $errorArray[$this->errorcode];
    }
}
try {
    throw new MyException("english", 4);
}
catch (MyException $e) {
    echo $e->getMessageMap();
}

Listing 8-1MyExcetion Class

捕捉多个异常

优秀的程序员必须始终确保考虑到所有可能的情况。考虑这样一个场景,您的站点提供了一个 HTML 表单,允许用户通过提交他或她的电子邮件地址来订阅时事通讯。几种结果是可能的。例如,用户可以执行以下操作之一:

  • 请提供有效的电子邮件地址

  • 提供无效的电子邮件地址

  • 完全忽略输入任何电子邮件地址

  • 试图发动攻击,如 SQL 注入

适当的异常处理将考虑所有这样的场景。但是,您需要提供捕捉每个异常的方法。幸运的是,用 PHP 很容易做到这一点。清单 8-2 给出了满足这个需求的代码。

<?php
    /* The InvalidEmailException class notifies the
       administrator if an e-mail is deemed invalid. */
    class InvalidEmailException extends Exception {
        function __construct($message, $email) {
           $this->message = $message;
           $this->notifyAdmin($email);
        }

        private function notifyAdmin($email) {
           mail("admin@example.org","INVALID EMAIL",$email,"From:web@example.com");
        }
    }

    /* The Subscribe class validates an e-mail address
       and adds the e-mail address to the database. */
    class Subscribe {
        function validateEmail($email) {
            try {
                if ($email == "") {
                    throw new Exception("You must enter an e-mail address!");
                } else {
                    list($user,$domain) = explode("@", $email);
                    if (! checkdnsrr($domain, "MX"))
                        throw new InvalidEmailException(
                            "Invalid e-mail address!", $email);
                    else
                        return 1;
                }
            } catch (Exception $e) {
                  echo $e->getMessage();
            } catch (InvalidEmailException $e) {
                  echo $e->getMessage();
                  $e->notifyAdmin($email);
            }
        }
        /* Add the e-mail address to the database */
        function subscribeUser() {
            echo $this->email." added to the database!";
        }
    }

    // Assume that the e-mail address came from a subscription form
    $_POST['email'] = "someuser@example.com";

    /* Attempt to validate and add address to database. */
    if (isset($_POST['email'])) {
        $subscribe = new Subscribe();
        if($subscribe->validateEmail($_POST['email']))
            $subscribe->subscribeUser($_POST['email']);
    }
?>

Listing 8-2Proper Exception Handling

您可以看到有可能触发两个不同的异常:一个从基类派生,另一个从InvalidEmailException类扩展。

一些验证可以由浏览器中的 JavaScript 代码执行。这通常会带来更好的用户体验,但是您仍然需要在 PHP 代码中执行输入验证。这是因为不能保证请求来自浏览器或启用了 JavaScript 的浏览器,或者恶意用户找到了绕过您用 JavaScript 创建的任何客户端检查的方法。永远不要相信 PHP 脚本的输入。

标准 PHP 库异常

标准 PHP 库(SPL)扩展了 PHP,为普通任务提供现成的解决方案,如文件访问、各种类型的迭代以及 PHP 本身不支持的数据结构(如堆栈、队列和堆)的实现。认识到异常的重要性,SPL 还提供了对 13 种预定义异常的访问。这些扩展可以分为逻辑相关的或运行时相关的。所有这些类最终都扩展了原生的Exception类,这意味着你可以访问像getMessage()getLine()这样的方法。每个异常的定义如下:

  • BadFunctionCallException:BadFunctionCallException类应该用于处理调用未定义方法的情况,或者调用方法时参数数量不正确的情况。

  • BadMethodCallException:BadMethodCallException类应该用于处理调用未定义方法的情况,或者调用方法时参数数量不正确的情况。

  • DomainException:DomainException类应该用于处理输入值超出范围的情况。例如,如果一个减肥应用包含一个将用户当前体重保存到数据库的方法,并且提供的值小于零,那么应该抛出一个类型为DomainException的异常。

  • InvalidArgumentException:InvalidArgumentException类应该用于处理不兼容类型的参数被传递给函数或方法的情况。

  • LengthException:应该使用LengthException类来处理字符串长度无效的情况。例如,如果一个应用包含一个处理用户社会保险号的方法,并且传递给该方法的字符串长度不正好是九个字符,那么应该抛出一个类型为LengthException的异常。

  • LogicException:LogicException类是两个基类之一,所有其他 SPL 异常都从这两个基类中扩展而来(另一个基类是RuntimeException类)。您应该使用LogicException类来处理应用编程错误的情况,比如在设置类属性之前试图调用一个方法。

  • OutOfBoundsException:OutOfBoundsException类应该用于处理提供的值与数组定义的任何键都不匹配的情况,或者超出了任何其他数据结构的定义限制并且没有更合适的异常(例如,字符串的长度异常)的情况。

  • OutOfRangeException:应该使用OutOfRangeException类来处理函数的输出值,这些值落在预定义的范围之外。这与DomainException的不同之处在于DomainException应该关注输入而不是输出。

  • OverflowException:应该使用OverflowException类来处理算术或缓冲区溢出的情况。例如,当试图向预定义大小的数组中添加值时,会触发溢出异常。

  • RangeException:在文档中定义为DomainException类的运行时版本,RangeException类应该用于处理与溢出和下溢无关的算术错误。

  • RuntimeException:RuntimeException类是所有其他 SPL 异常扩展的两个基类之一(另一个基类是LogicException类),旨在处理只在运行时出现的错误。

  • UnderflowException:应该使用UnderflowException类来处理发生算术或缓冲区下溢的情况。例如,当试图从空数组中移除一个值时,会触发下溢异常。

  • UnexpectedValueException:UnexpectedValueException类应该用于处理所提供的值与一组预定义的值不匹配的情况。

请记住,这些异常类目前不提供任何与它们要处理的情况相关的特殊功能;相反,它们的目的是通过使用恰当命名的异常处理器,而不是简单地使用一般的Exception类,来帮助您提高代码的可读性。

PHP 7 中的错误处理

在版本 7 之前的 PHP 版本中,许多错误都是由一个简单的错误报告特性来处理的,这使得捕捉许多错误变得困难或不可能。特别是致命的错误可能是一个问题,因为这些错误会导致执行停止。从 PHP 7 开始,这变成了对大多数错误使用一个错误异常。以这种方式抛出的错误必须由一个catch(Error $e) {}语句来处理,而不是由本章前面提到的catch(Exception $e) {}语句来处理。

Error ( https://php.net/manual/en/class.error.php )和Exception ( https://php.net/manual/en/class.exception.php )类都实现了 Throwable 接口。Error 类用于内部错误,exception 用于用户定义的异常。

Error类定义了许多子类来处理特殊情况。这些是ArithmeticErrorDivisionByZeroError``AssertionError``ParseErrorTypeError

摘要

本章涵盖的主题涉及当今编程行业中使用的许多核心错误处理实践。虽然不幸的是,这些特性的实现仍然是偏好而不是策略,但是日志和错误处理等功能的引入极大地提高了程序员检测和响应代码中不可预见的问题的能力。

下一章将深入探讨 PHP 的字符串解析能力,涵盖该语言强大的正则表达式特性,并深入探讨许多强大的字符串操作函数。