六、突出的面向对象特性

术语面向对象OO)从 70 年代开始就存在了,当时它是由计算机科学家艾伦·凯(Alan Kay)发明的。该术语代表基于对象概念的编程范式。当时,Simula 是第一种展示 OO 特性的语言,如对象、类、继承、子类型等。1977 年标准化为 Simula 67,成为后来语言的灵感来源。其中一种受启发的语言是 Smalltalk,它是施乐公司艾伦·凯(Alan Kay)领导的研究成果。与 Simula 相比,Smalltalk 大大改进了总体 OO 概念。随着时间的推移,Smalltalk 成为最有影响力的 OO 编程语言之一

关于这些早期,我们还有很多要说的,但有一点是 OOP 是出于特定的需要而诞生的。其中 Simula 使用静态对象来建模真实世界的实体,Simultalk 使用可以创建、更改或删除的动态对象作为计算的基础。

The MVC pattern, one of the most common object-oriented software design patterns, was introduced in Smalltalk.

将物理实体映射到由类描述的对象的容易程度无疑影响了 OO 范式在开发人员中的总体流行程度。然而,对象不仅仅是各种属性的映射实例,它们还涉及消息和责任。虽然我们可能会基于第一个前提接受 OOP,但我们肯定会开始欣赏后一个前提,因为制造大型可扩展系统的关键在于易于对象通信。

PHP 语言体现了几个范例,最显著的是:命令式、函数式、面向对象、过程式和反射式。然而,PHP 中的 OOP 支持直到 PHP5 发布才完全启动。PHP7 的最新版本对现在被认为是稳定和成熟的 PHPOOP 模型进行了一些微小但值得注意的改进。

在本章中,我们将探讨面向对象 PHP 的一些突出特性:

  • 对象继承
  • 对象和引用
  • 对象迭代
  • 对象比较
  • 特点
  • 反射

对象继承

OOP 范例将对象放在应用程序设计的核心,对象可以被视为包含各种属性和方法的单元。这些属性和方法之间的交互定义了对象的内部状态。每个对象都是从称为类的蓝图构建的。没有类的对象是不存在的,至少在基于类的 OOP 中是不存在的。

We differentiate class-based OOP (PHP, Java, C#, ...) and prototype-based OOP (ECMAScript / JavaScript, Lua, ...). In class-based OOP, objects are created from classes; in prototype-based OOP, objects are created from other objects.

构建或创建新对象的过程称为实例化。在 PHP 中,与许多其他语言一样,我们使用new关键字来实例化给定类中的对象。让我们来看看下面的例子:

<?php

class JsonOutput
{
  protected $content;

  public function setContent($content)
  {
    $this->content = $content;
  }

  public function render()
  {
    return json_encode($this->content);
  }
}

class SerializedOutput
{
  protected $content;

  public function setContent($content)
  {
    $this->content = $content;
  }

  public function render()
  {
    return serialize($this->content);
  }
}

$users = [
  ['user' => 'John', 'age' => 34],
  ['user' => 'Alice', 'age' => 33],
];

$json = new JsonOutput();
$json->setContent($users);
echo $json->render();

$ser = new SerializedOutput();
$ser->setContent($users);
echo $ser->render();

在这里,我们定义了两个简单的类,JsonOutputSerializedOutput。我们说 simple 仅仅是因为它们有一个属性和两个方法。这两个类几乎是相同的——它们只在render()方法中的一行代码中有所不同。一个类将给定内容转换为 JSON,而另一个类将其转换为序列化字符串。在类声明之后,我们定义了一个伪$users 数组,然后将其馈送到JsonOutputSerializedOutput类的实例,即$json$ser对象。

虽然这远远不是一个理想的类设计,但它是对继承的一个很好的介绍。

允许类和对象继承另一个类的属性和方法。诸如s超类、基类或父类等术语用于标记用作继承基础的类。子类、派生类或子类等术语用于标记继承类。

PHPextends关键字用于启用继承。继承有其局限性。我们一次只能从一个类进行扩展,因为 PHP 不支持多重继承。但是,拥有继承链是完全有效的:

// valid
class A {}
class B extends A {}
class C extends B {}

// invalid
class A {}
class B {}
class C extends A, B {}

有效示例中显示的C类最终将继承类BA的所有允许的属性和方法。当我们说 allowed 时,我们指的是属性和方法可见性,即访问修饰符:

<?php

error_reporting(E_ALL);

class A
{
    public $x = 10;
    protected $y = 20;
    private $z = 30;

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

    protected function y()
    {
        return $this->y;
    }

    private function z()
    {
        return $this->z;
    }
}

class B extends A
{

}

$obj = new B();
var_dump($obj->x); // 10
var_dump($obj->y); // Uncaught Error: Cannot access protected property B::$y
var_dump($obj->z); // Notice: Undefined property: B::$z
var_dump($obj->x()); // 10
var_dump($obj->y()); // Uncaught Error: Call to protected method A::y() from context
var_dump($obj->z()); // Uncaught Error: Call to private method A::z() from context

在对象上下文中,访问修饰符的行为与前面的示例相同,这与我们所期望的非常接近。无论是类A还是类B的实例,对象都会表现出相同的行为。让我们观察访问修饰符在子类内部工作中的行为:

class B extends A
{
  public function test()
  {
    var_dump($this->x); // 10
    var_dump($this->y); // 20
    var_dump($this->z); // Notice: Undefined property: B::$z
    var_dump($this->x()); // 10
    var_dump($this->y()); // 20
    var_dump($this->z()); // Uncaught Error: Call to private method 
      A::z() from context 'B'
  }
}

$obj = new B();
$obj->test();

我们可以看到,publicprotected成员(属性或方法)可以从子类访问,而私有成员不能——它们只能从定义它们的类访问。

extends关键字也适用于接口:

<?php

interface User {}
interface Employee extends User {}

能够继承类和接口属性及方法,就形成了一个强大的整体对象继承机制。

了解这些简单的继承规则,让我们看看如何使用继承将JsonOutputSerializedOutput类重写为更方便的形式:

<?php

class Output
{
    protected $content;

    public function setContent($content)
    {
        $this->content = $content;
    }

    public function render()
    {
        return $this->content;
    }
}

class JsonOutput extends Output
{
    public function render()
    {
        return json_encode($this->content);
    }
}

class SerializedOutput extends Output
{
    public function render()
    {
        return serialize($this->content);
    }
}

我们首先定义了一个内容与前面的JsonOutputSerializedOutput类几乎相同的Output类,只是将其render()方法更改为只按原样返回内容。然后我们重写了JsonOutputSerializedOutput类,使它们都扩展了Output类。在此设置中,Output类成为父类,而JsonOutputSerializedOutput成为子类。子类重新定义render()方法,从而覆盖父类实现。$this关键字可以访问所有公共和受保护的修饰符,这使得访问$content属性变得很容易。

继承可能是一种快速而强大的方法,可以将代码构造成方便的父/子关系链,我们应该避免误用或过度使用它的危险。对于较大的系统,这可能会特别棘手,因为我们可能会花费更多的时间来处理大型类层次结构,而不是实际维护子系统接口。因此,我们应该谨慎使用它。

对象和引用

在代码中传递参数有两种方法:

  • 通过引用:这是调用者和被调用者使用相同变量作为参数的地方。
  • 按值:调用者和被调用者都有自己的变量副本作为参数。如果被调用方决定更改所传递参数的值,则调用方不会注意到它。

按值是默认的 PHP 行为,如以下示例所示:

<?php

class Util
{
  function hello($msg)
  {
    $msg = "<p>Welcome $msg</p>";
    return $msg;
  }
}

$str = 'John';

$obj = new Util();
echo $obj->hello($str); // Welcome John

echo $str; // John

查看hello()方法的内部,我们可以看到它正在将$msg参数值重置为 HTML 标记中包装的另一个字符串值。通过值传递的默认 PHP 行为防止此更改传播到方法范围之外。在函数定义中,使用参数名称前的&运算符,我们可以强制通过引用传递行为:

<?php

class Util
{
  function hello(&$msg)
  {
    $msg = "<p>Welcome $msg</p>";
    return $msg;
  }
}

$str = 'John';

$obj = new Util();
echo $obj->hello($str); // Welcome John

echo $str; // Welcome John

能够做某事并不一定意味着我们应该做。只有在有充分理由这样做的情况下,才应谨慎使用引用传递行为。前面的示例清楚地显示了内部hello()方法对外部范围内的简单标量类型值的副作用。对象实例方法,甚至普通函数,都不应该对外部作用域产生这些类型的副作用。

Several PHP functions, such as sort(), use the & operator to force the pass by reference behavior on a given array argument.

说了这么多,物体放在哪里?PHP 中的对象倾向于按引用传递的行为。当对象作为参数传递时,它仍然作为值传递,但传递的值不是对象本身,而是对象标识符。因此,将对象作为参数传递的行为更像是通过引用传递:

<?php

class User
{
  public $salary = 4200;
}

function bonus(User $u)
{
  $u->salary = $u->salary + 500;
}

$user = new User();
echo $user->salary; // 4200
bonus($user);
echo $user->salary; // 4700 

Since objects are bigger structures than scalar values, passing by reference greatly minimizes the memory and CPU footprint.

对象迭代

PHP 数组是 PHP 中使用最频繁的集合结构。我们可以将几乎任何内容压缩到数组中,从标量值到对象。使用foreach语句遍历这样一个结构的元素非常容易。但是,数组并不是唯一的可 iterable 类型,因为对象本身是可 iterable 的。

让我们来看看下面的基于数组的例子:

<?php

$user = [
  'name' => 'John',
  'age' => 34,
  'salary' => 4200.00
];

foreach ($user as $k => $v) {
  echo "key: $k, value: $v" . PHP_EOL;
}

现在让我们来看看下面的基于对象的例子:

<?php

class User
{
  public $name = 'John';
  public $age = 34;
  public $salary = 4200.00;
}

$user = new User();

foreach ($user as $k => $v) {
  echo "key: $k, value: $v" . PHP_EOL;
}

在控制台上执行,这两个示例将产生相同的输出:

key: name, value: John 
key: age, value: 34 
key: salary, value: 4200

默认情况下,迭代仅适用于公共属性,不包括列表中的任何私有或受保护属性。

PHP 提供了一个Iterator接口,可以指定哪些值可用于迭代。

Iterator extends Traversable {
  abstract public mixed current(void)
  abstract public scalar key(void)
  abstract public void next(void)
  abstract public void rewind(void)
  abstract public boolean valid(void)
} 

以下示例演示了一个简单的Iterator接口实现:

<?php

class User implements \Iterator
{
  public $name = 'John';
  private $age = 34;
  protected $salary = 4200.00;

  private $info = [];

  public function __construct()
  {
    $this->info = [
      'name' => $this->name,
      'age' => $this->age,
      'salary' => $this->salary
    ];
  }

  public function current()
  {
    return current($this->info);
  }

  public function next()
  {
    return next($this->info);
  }

  public function key()
  {
    return key($this->info);
  }

  public function valid()
  {
    $key = key($this->info);
    return ($key !== null && $key !== false);
  }

  public function rewind()
  {
    return reset($this->info);
  }
}

通过这个实现,我们现在似乎能够迭代用户类私有和受保护的属性。虽然事实并非如此。所发生的事情是,通过构造函数,类正在用我们希望迭代的所有其他属性的数据填充$info参数。然后,接口强制的方法确保我们的类能够很好地处理foreach构造。

当涉及到日常开发时,对象迭代是 PHP 的一个整洁的特性,尽管经常被忽略。

对象比较

PHP 语言提供了几个比较运算符,允许我们比较两个不同的值,结果是truefalse

  • ==:相等
  • ===:相同
  • !=:不相等
  • <>:不相等
  • !==:不完全相同
  • <:小于
  • >:大于
  • <=:小于或等于
  • >=:大于或等于

虽然所有这些操作符都同等重要,但让我们仔细研究对象上下文中的等值(Po.T0})和相同的(AuthT1)算子的行为。

让我们来看看下面的例子:

<?php

class User {
  public $name = 'N/A';
  public $age = 0;
}

$user = new User();
$employee = new User();

var_dump($user == $employee); // true
var_dump($user === $employee); // false

这里,我们有一个简单的User类,其中两个属性设置为一些默认值。然后我们有两个相同类的不同实例,$user$employee。假设这两个对象具有相同的属性,具有相同的值,则 equal(==运算符返回true。另一方面,相同的(===运算符返回 false。即使对象属于同一类,并且在这些属性中具有相同的属性和值,相同的操作符也会将它们视为不同的对象。

让我们来看看下面的例子:

<?php

class User {
  public $name = 'N/A';
  public $age = 0;
}

$user = new User();
$employee = $user;

var_dump($user == $employee); // true
var_dump($user === $employee); // true

相同(===运算符仅当两个对象引用同一类的同一实例时,才认为它们是相同的。相同的运算符行为适用于对应运算符,即不相等(<>!=运算符)和不相同(!==运算符。

除对象外,相同运算符适用于任何其他类型:

<?php

var_dump(2 == 2); // true
var_dump(2 == "2"); // true
var_dump(2 == "2ABC"); // true

var_dump(2 === 2); // true
var_dump(2 === "2"); // false
var_dump(2 === "2ABC"); // false

查看前面的示例可以清楚地揭示相同运算符的重要性。2 == "2ABC"评估为真的表达让人难以置信。我们甚至可能认为它是 PHP 语言本身的一个 bug。虽然依靠 PHP 自动类型转换基本上是好的,但有时意外的错误会挤进并破坏我们的应用程序逻辑。同一算子的使用重申了比较,确保我们不仅考虑值,而且考虑类型。

特点

我们前面提到过 PHP 是一种单一的继承语言。我们不能使用extends关键字来扩展 PHP 中的多个类。这一特性实际上是一种稀有的商品,只有少数的编程语言支持,如 C++。不管是好是坏,多重继承允许对代码结构进行一些有趣的修补。

PHP 特性提供了一种机制,通过这种机制,我们可以在代码重用或功能分组的上下文中实现这些结构。trait关键字用于声明特征,如下所示:

<?php

trait Formatter
{
  // Trait body
}

特质的主体几乎可以是我们在一个类中放入的任何东西。虽然它们类似于类,但我们不能实例化特征本身。我们只能使用另一个类的特征。为此,我们在类主体中使用了use关键字,如下例所示:

class Ups
{
  use Formatter;

  // Class body (properties & methods)
}

为了更好地理解特征是如何有帮助的,我们来看看下面的例子:

<?php

trait Formatter
{
   public function formatPrice($price)
   {
       return sprintf('%.2F', $price);
   }
}

class Ups
{
   use Formatter;

private $price = 4.4999; // Base shipping price

public function getShippingPrice($formatted = false)

   {
      // Shipping cost calc... $this->price = XXX

      if ($formatted) {
        return $this->formatPrice($this->price);
      }

        return $this->price;
    }
}

class Dhl
{
    use Formatter;

    private $price = 9.4999; // Base shipping price

    public function getShippingPrice($formatted = false)
    {
        // Shipping cost calc... $this->price = XXX

        if ($formatted) {
            return $this->formatPrice($this->price);
        }

        return $this->price;
    }
}

$ups = new Ups();
echo $ups->getShippingPrice(true); // 4.50

$dhl = new Dhl();
echo $dhl->getShippingPrice(true); // 9.50

前面的示例演示了 trait 在代码重用上下文中的使用,其中两个不同的装运类UpsDhl使用相同的 trait。trait 本身包装了一个很好的小formatPrice()助手方法,将给定的价格格式化为两个十进制字段。

与类一样,trait 可以访问$this,1 这意味着我们可以很容易地重写Formattertrait 之前的formatPrice()方法,如下所示:

<?php

trait Formatter
{
  public function formatPrice()
  {
     return sprintf('%.2F', $this->price);
  }
}

然而,这严重限制了我们对 trait 的使用,因为它的formatPrice()方法现在需要一个$price成员,而使用Formattertrait 的一些类可能没有。

让我们看看另一个例子,我们在函数上下文的分组中使用特性:

<?php

trait SalesOrderCustomer
{
  public function getCustomerFirstname()
  { /* body */
  }

  public function getCustomerEmail()
  { /* body */
  }

  public function getCustomerGender()
  { /* body */
  }
}

trait SalesOrderActions
{
  public function cancel()
  { /* body */
  }

  public function complete()
  { /* body */
  }

  public function hold()
  { /* body */
  }
}

class SalesOrder
{
  use SalesOrderCustomer;
  use SalesOrderActions;

  /* body */
}

我们在这里所做的只是将我们的类代码剪切粘贴到两个不同的特性中。我们将所有与可能的订单操作相关的方法分组为一个SalesOrderActions特征,所有与订单客户相关的方法分组为SalesOrderCustomer特征。这让我们回到可能并不一定意味着更可取的哲学。

使用多个 trait 有时会导致冲突,在多个 trait 中可以找到相同的方法名称。我们可以使用insteadofas关键字来缓解这些类型的冲突,如下例所示:

<?php

trait CsvHandler
{
   public function import()
   {
      echo 'CsvHandler > import' . PHP_EOL;
   }

public function export()
   {
      echo 'CsvHandler > export' . PHP_EOL;
    }
}

trait XmlHandler
{
   public function import()
   {
      echo 'XmlHandler > import' . PHP_EOL;
   }

   public function export()
   {
      echo 'XmlHandler > export' . PHP_EOL;
   }
}

class SalesOrder
{
   use CsvHandler, XmlHandler {
      XmlHandler::import insteadof CsvHandler;
      CsvHandler::export insteadof XmlHandler;
      XmlHandler::export as exp;
   }

   public function initImport()
   {
      $this->import();
   }

   public function initExport()
   {
      $this->export();
      $this->exp();
   }
}

$order = new SalesOrder();
$order->initImport();
$order->initExport();

//XmlHandler > import
//CsvHandler > export
//XmlHandler > export

as关键字也可以与publicprotectedprivate关键字一起使用,以改变方法的可见性:

<?php

trait Message
{
  private function hello()
  {
     return 'Hello!';
  }
}

class User
{
  use Message {
    hello as public;
  }
}

$user = new User();
echo $user->hello(); // Hello!

为了让事情变得更有趣,trait 可以进一步由其他 trait 组成,甚至支持abstractstatic成员,如下例所示:

<?php

trait A
{
  public static $counter = 0;

  public function theA()
  {
    return self::$counter;
  }
}

trait B
{
  use A;

  abstract public function theB();
}

class C
{
  use B;

  public function theB()
  {
    return self::$counter;
  }
}

$c = new C();
$c::$counter++;
echo $c->theA(); // 1
$c::$counter++;
$c::$counter++;
echo $c->theB(); // 3

除了不可实例化之外,trait 还与类共享许多特性。虽然它们为我们提供了一些有趣的代码结构工具,但它们也很容易违反单一责任原则。trait 用法的总体印象通常是扩展常规类,这使得很难找到正确的用例。我们可以用它们来描述许多通用但不必要的特性。例如,喷气发动机不是每架飞机上都必需的,但很多飞机都有,而其他人可能有螺旋桨。

反射

反射是每个开发人员都应该警惕的一个非常重要的概念。它表示程序在运行时检查自身的能力,从而允许类、接口、函数、方法和扩展的简单反向工程。

我们可以从控制台快速体验 PHP 反射功能。PHP CLI 支持多个基于反射的命令:

  • --rf <*function name*>:显示函数的相关信息
  • --rc <*class name*>:显示一个类的信息
  • --re <*extension name*>:显示扩展信息
  • --rz <*extension name*>:显示 Zend 扩展的相关信息
  • --ri <*extension name*>:显示扩展的配置

以下输出演示了php --rf str_replace命令的结果:

Function [ <internal:standard> function str_replace ] {
  - Parameters [4] {
     Parameter #0 [ <required> $search ]
     Parameter #1 [ <required> $replace ]
     Parameter #2 [ <required> $subject ]
     Parameter #3 [ <optional> &$replace_count ]
  }
}

输出反映在str_replace()函数上,这是一个标准的 PHP 函数。它清楚地描述了参数的总数,以及它们的名称和必需或可选的赋值。

开发人员可以利用的反射的真正力量来自反射 API。让我们来看看下面的例子:

<?php

class User
{
  public $name = 'John';
  protected $ssn = 'AAA-GG-SSSS';
  private $salary = 4200.00;
}

$user = new User();

echo $user->name = 'Marc'; // Marc

//echo $user->ssn = 'BBB-GG-SSSS';
// Uncaught Error: Cannot access protected property User::$ssn

//echo $user->salary = 5600.00;
// Uncaught Error: Cannot access private property User::$salary

var_dump($user);
//object(User)[1]
// public 'name' => string 'Marc' (length=4)
// protected 'ssn' => string 'AAA-GG-SSSS' (length=11)
// private 'salary' => float 4200

我们首先定义了一个具有三个属性的User类,每个属性都具有不同的可见性。然后我们实例化了User类的一个对象,并尝试更改所有三个属性的值。通常,定义为protectedprivate的成员不能在对象外部访问。尝试以读或写模式访问它们将引发无法访问。。。错误这就是我们认为的正常行为。

使用 PHP 反射 API,我们可以绕过这种正常行为,从而可以访问私有和受保护的成员。反射 API 本身提供了几个类供我们使用:

  • 反射
  • 映射类
  • 反射 zendex 张力
  • 反射延伸
  • 映射函数
  • 反射函数摘要
  • 反射法
  • 反射对象
  • 反射参数
  • 反射特性
  • 反射型
  • 反射发生器
  • 反射器(接口)
  • ReflectionException(异常)

这些类中的每一个都公开了一组不同的功能,允许我们修补其他类、接口、函数、方法和扩展的内部。假设我们的目标是改变上例中protectedprivate属性的值,我们可以使用ReflectionClassReflectionProperty,如下例所示:

<?php

// ...

$user = new User();

$reflector = new ReflectionClass('User');

foreach ($reflector->getProperties() as $prop) {
  $prop->setAccessible(true);
  if ($prop->getName() == 'name') $prop->setValue($user, 'Alice');
  if ($prop->getName() == 'ssn') $prop->setValue($user, 'CCC-GG-SSSS');
  if ($prop->getName() == 'salary') $prop->setValue($user, 2600.00);
}

var_dump($user);

//object(User)[1]
// public 'name' => string 'Alice' (length=5)
// protected 'ssn' => string 'CCC-GG-SSSS' (length=11)
// private 'salary' => float 2600

我们从实例化一个User类的对象开始,就像我们在前面的示例中所做的那样。然后我们创建了一个ReflectionClass实例,将User类的名称传递给它的构造函数。新创建的$reflector实例允许我们通过其getProperties()方法获取所有User类属性的列表。一个接一个地遍历属性,我们开启了反射 API 的真正魔力。每个属性($prop都是ReflectionProperty类的一个实例。ReflectionProperty方法中的两种setAccessible()setValue()为我们实现目标提供了恰到好处的功能。使用这些方法,我们可以设置其他无法访问的对象属性的值。

另一个简单但有趣的反射示例是文档注释提取:

<?php

class Calc
{
  /**
  * @param $x The number x
  * @param $y The number y
  * @return mixed The number z
  */
  public function sum($x, $y)
  {
    return $x + $y;
  }
}

$calc = new Calc();

$reflector = new ReflectionClass('Calc');
$comment = $reflector->getMethod('sum')->getDocComment();

echo $comment;

只需两行代码,我们就可以对Calc类进行反思,并从其sum()方法中提取 doc 注释。虽然反射 API 的实际用途一开始可能并不明显,但正是这些功能使我们能够构建功能强大、动态的库和平台。

The phpDocumentor tool uses the PHP reflection features to automatically generate documentation from the source code. The popular Magento v2.x eCommerce platform extensively uses the PHP reflection features to automatically instantiate objects that are type-hinted as __construct() arguments.

总结

在本章中,我们了解了 PHP OOP 的一些最基本但鲜为人知的功能,这些功能有时在我们的日常开发中没有得到足够的重视。如今,大多数主流工作都集中在使用框架和平台上,它们往往会从我们这里抽象出其中的一些概念。理解对象的内部工作对成功开发和调试更大的系统至关重要。反射 API 在处理对象时提供了强大的功能。结合我们在第 4 章魔法背后的魔法中提到的魔术方法的力量,PHP OOP 模型似乎功能非常丰富。

接下来,我们将假设我们有一个可用的应用程序,并将重点放在优化它以获得高性能上。