四、使用 PHP 面向对象编程

在本章中,我们将介绍:

  • 发展中阶级
  • 扩展类
  • 使用静态属性和方法
  • 使用名称空间
  • 定义可见性
  • 使用接口
  • 使用特征
  • 实现匿名类

导言

在本章中,我们将考虑利用 PoT T0 面向对象编程 To1 T1(Po.T2SooOut-T3)能力,在 PHP 7、7.1 和以上中可用的能力 To4 T4。PHP7.x 中提供的大部分 OOP 功能在 PHP5.6 中也可用。PHP7 中引入的一个新特性是支持匿名类。在 PHP7.1 中,可以修改类常量的可见性。

另一个全新的特性是能够捕捉某些类型的错误。这将在第 13 章最佳实践、测试和调试中详细讨论。

发展类

传统的开发方法是将类放入自己的文件中。通常,类包含实现单一目的的逻辑。类被进一步分解为自包含的函数,这些函数被称为方法。在类中定义的变量称为作为属性。建议同时开发一个测试类,该主题在第 13 章最佳实践、测试和调试中有详细讨论。

怎么做。。。

  1. Create a file to contain the class definition. For the purposes of autoloading it is recommended that the filename match the classname. At the top of the file, before the keyword class, add a DocBlock. You can then define properties and methods. In this example, we define a class Test. It has a property $test, and a method getTest():

    ```php <?php declare(strict_types=1); /* * This is a demonstration class. * * The purpose of this class is to get and set * a protected property $test * / class Test {

    protected $test = 'TEST';

    /* * This method returns the current value of $test * * @return string $test / public function getTest() : string { return $this->test; }

    /* * This method sets the value of $test * * @param string $test * @return Test $this / public function setTest(string $test) { $this->test = $test; return $this; } } ```

    提示

    最佳实践

    将文件命名为类名称被认为是最佳实践。尽管 PHP 中的类名不区分大小写,但使用大写字母作为类名被认为是最佳实践。不应将可执行代码放入类定义文件中。

    每个类在关键字class前应包含一个DocBlock。在 DocBlock 中,您应该包括对类用途的简短描述。跳过一行,然后包括更详细的描述。您还可以包括@标记,如@author@license等。同样,每个方法前面都应该有一个 DocBlock,用于标识该方法的用途,以及它的传入参数和返回值。

  2. It's possible to define more than one class per file, but is not considered best practice. In this example we create a file, NameAddress.php, which defines two classes, Name and Address:

    ```php <?php declare(strict_types=1); class Name {

    protected $name = '';

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

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

    return $this;
    

    } }

    class Address {

    protected $address = '';

    public function getAddress() : string { return $this->address; }

    public function setAddress(string $address) { $this->address = $address; return $this; } } ```

    提示

    尽管您可以在单个文件中定义多个类,如前面的代码段所示,但这并不是最佳实践。这不仅否定了文件的逻辑纯度,而且使自动加载更加困难。

  3. 类名不区分大小写。重复将被标记为错误。在本例中,在文件TwoClass.php中,我们定义了两个类TwoClasstwoclass

    ```php <?php class TwoClass { public function showOne() { return 'ONE'; } }

    // a fatal error will occur when the second class definition is parsed class twoclass { public function showTwo() { return 'TWO'; } } ```

  4. PHP7.1 解决了使用关键字$this时出现的不一致行为。尽管 PHP7.0 和 PHP5.x 中允许使用,$this的以下任何一种用法都将在 PHP7.1 中产生错误,如果$this用作:

    • 参数
    • 一个static变量
    • 一个global变量
    • try...catch块中使用的变量
    • foreach()中使用的变量
    • 作为unset()的论据
    • 作为变量(即,$a = 'this'; echo $$a
    • 间接引用
  5. 如果您需要创建一个对象实例,但不想定义一个离散类,那么可以使用内置于 PHP 中的泛型stdClassstdClass允许您动态定义属性*,而无需定义扩展stdClass

    php $obj = new stdClass();

    的谨慎类 这个工具在 PHP 中的许多不同地方都使用。例如,当您使用PHP 数据对象*PDO进行数据库查询时,其中一种获取模式是PDO::FETCH_OBJ。此模式返回stdClass实例,其中属性表示数据库表列:

    php $stmt = $connection->pdo->query($sql); $row = $stmt->fetch(PDO::FETCH_OBJ);*

*## 它是如何工作的。。。

以前面代码片段中显示的Test类为例,将代码放在名为Test.php的文件中。创建另一个名为chap_04_oop_defining_class_test.php的文件。添加以下代码:

require __DIR__ . '/Test.php';

$test = new Test();
echo $test->getTest();
echo PHP_EOL;

$test->setTest('ABC');
echo $test->getTest();
echo PHP_EOL;

输出将显示$test属性的初始值,然后是通过调用setTest()修改的新值:

How it works...

下一个例子有您在一个文件NameAddress.php中定义了两个类NameAddress。您可以通过以下代码调用和使用这两个类:

require __DIR__ . '/NameAddress.php';

$name = new Name();
$name->setName('TEST');
$addr = new Address();
$addr->setAddress('123 Main Street');

echo $name->getName() . ' lives at ' . $addr->getAddress();

虽然 PHP 解释器不会生成错误,但通过定义多个类,文件的逻辑纯度会受到影响。此外,文件名与类名不匹配,这可能会影响自动加载的能力。

本示例的输出如下所示:

How it works...

步骤 3 还显示了一个文件中的两个类定义。然而,在本例中,目的是证明 PHP 中的类名不区分大小写。将代码放入文件TwoClass.php中。尝试包含该文件时,会生成一个错误:

How it works...

要演示stdClass的直接使用,请创建一个实例,为属性赋值,然后使用var_dump()显示结果。要查看如何在内部使用stdClass,请使用var_dump()显示PDO查询的结果,其中获取模式设置为FETCH_OBJ

输入以下代码:

$obj = new stdClass();
$obj->test = 'TEST';
echo $obj->test;
echo PHP_EOL;

include (__DIR__ . '/../Application/Database/Connection.php');
$connection = new Application\Database\Connection(
  include __DIR__ . DB_CONFIG_FILE);

$sql  = 'SELECT * FROM iso_country_codes';
$stmt = $connection->pdo->query($sql);
$row  = $stmt->fetch(PDO::FETCH_OBJ);
var_dump($row);

以下是输出:

How it works...

另见。。。

有关 PHP7.1 中对关键字$this的改进的更多信息,请参见https://wiki.php.net/rfc/this_var

扩展类

开发人员使用 OOP 的主要原因之一是它能够重用现有代码,但同时可以添加或覆盖功能。在 PHP 中,关键字extends用于在类之间建立父/子关系。

怎么做。。。

  1. child类中,使用关键字extends设置继承。在下面的示例中,Customer类扩展了Base类。Customer的任何实例都将继承可见的方法和属性,在本例中为$idgetId()setId()

    ```php class Base { protected $id; public function getId() { return $this->id; } public function setId($id) { $this->id = $id; } }

    class Customer extends Base { protected $name; public function getName() { return $this->name; } public function setName($name) { $this->name = $name; } } ```

  2. You can force any developer using your class to define a method by marking it abstract. In this example, the Base class defines as abstract the validate() method. The reason why it must be abstract is because it would be impossible to determine exactly how a child class would be validated from the perspective of the parent Base class:

    php abstract class Base { protected $id; public function getId() { return $this->id; } public function setId($id) { $this->id = $id; } public function validate(); }

    如果类包含抽象方法,则类本身必须声明为abstract

  3. PHP 只支持一行继承。下一个示例显示了一个类Member,它继承自CustomerCustomer继承自Base

    ```php class Base { protected $id; public function getId() { return $this->id; } public function setId($id) { $this->id = $id; } }

    class Customer extends Base { protected $name; public function getName() { return $this->name; } public function setName($name) { $this->name = $name; } }

    class Member extends Customer { protected $membership; public function getMembership() { return $this->membership; } public function setMembership($memberId) { $this->membership = $memberId; } } ```

  4. 为了满足类型提示,可以使用目标类的任何子类。下面的代码片段中显示的test()函数需要Base类的实例作为参数。继承行中的任何类都可以接受为参数。传递到test()的任何其他内容都会抛出一个TypeError

    php function test(Base $object) { return $object->getId(); }

它是如何工作的。。。

在第一个项目符号点中,定义了Base类和Customer类。为了便于演示,将这两个类定义放在一个文件chap_04_oop_extends.php中,并添加以下代码:

$customer = new Customer();
$customer->setId(100);
$customer->setName('Fred');
var_dump($customer);

请注意,$id属性以及getId()setId()方法是从父Base类继承到子Customer类的:

How it works...

为了说明abstract方法的使用,假设您希望向任何扩展Base的类添加某种验证功能。问题是,没有办法知道继承的类中可以验证什么。唯一可以确定的是,您必须具有验证功能。

以前面解释中提到的Base类为例,添加一个新方法validate()。将方法标记为abstract,不定义任何代码。注意当孩子Customer类扩展Base时会发生什么。

How it works...

如果您随后将Base类标记为abstract,但在子类中未定义validate()方法,则会生成相同错误。最后,继续在儿童Customer类中实现validate()方法:

class Customer extends Base
{
  protected $name;
  public function getName()
  {
    return $this->name;
  }
  public function setName($name)
  {
    $this->name = $name;
  }
  public function validate()
  {
    $valid = 0;
    $count = count(get_object_vars($this));
    if (!empty($this->id) &&is_int($this->id)) $valid++;
    if (!empty($this->name) 
    &&preg_match('/[a-z0-9 ]/i', $this->name)) $valid++;
    return ($valid == $count);
  }
}

然后可以添加以下过程代码来测试结果:

$customer = new Customer();

$customer->setId(100);
$customer->setName('Fred');
echo "Customer [id]: {$customer->getName()}" .
     . "[{$customer->getId()}]\n";
echo ($customer->validate()) ? 'VALID' : 'NOT VALID';
$customer->setId('XXX');
$customer->setName('$%£&*()');
echo "Customer [id]: {$customer->getName()}"
  . "[{$customer->getId()}]\n";
echo ($customer->validate()) ? 'VALID' : 'NOT VALID';

以下是输出:

How it works...

要显示一行继承,请将新的Member类添加到前面步骤 1 中所示的BaseCustomer的第一个示例中:

class Member extends Customer
{
  protected $membership;
  public function getMembership()
  {
    return $this->membership;
  }
  public function setMembership($memberId)
  {
    $this->membership = $memberId;
  }
}

创建一个Member的实例,注意,在下面的代码中,所有属性和方法都可以从每个继承的类中获得,即使不是直接继承的:

$member = new Member();
$member->setId(100);
$member->setName('Fred');
$member->setMembership('A299F322');
var_dump($member);

以下是输出:

How it works...

现在定义一个函数test(),它将Base的一个实例作为参数:

function test(Base $object)
{
  return $object->getId();
}

请注意,BaseCustomerMember的实例都可以作为参数:

$base = new Base();
$base->setId(100);

$customer = new Customer();
$customer->setId(101);

$member = new Member();
$member->setId(102);

// all 3 classes work in test()
echo test($base)     . PHP_EOL;
echo test($customer) . PHP_EOL;
echo test($member)   . PHP_EOL;

以下是输出:

How it works...

但是,如果您尝试使用不在继承行中的对象实例运行test(),则会抛出一个TypeError

class Orphan
{
  protected $id;
  public function getId()
  {
    return $this->id;
  }
  public function setId($id)
  {
    $this->id = $id;
  }
}
try {
    $orphan = new Orphan();
    $orphan->setId(103);
    echo test($orphan) . PHP_EOL;
} catch (TypeError $e) {
    echo 'Does not work!' . PHP_EOL;
    echo $e->getMessage();
}

我们可以在下图中观察到这一点:

How it works...

使用静态特性和方法

PHP 允许您访问属性或方法,而无需创建类的实例。用于此目的的关键字为静态

怎么做。。。

  1. 最简单的方法是,在声明普通属性或方法时,在声明可见性级别之后添加static关键字。使用self关键字在内部引用属性:

    php class Test { public static $test = 'TEST'; public static function getTest() { return self::$test; } }

  2. self关键字将提前绑定,这将导致在访问子类中的静态信息时出现问题。如果您确实需要访问子类中的信息,请使用static关键字代替self。此过程称为为后期静态绑定

  3. 在下面的示例中,如果您回显Child::getEarlyTest(),则输出为测试。另一方面,如果您运行Child::getLateTest(),则输出将是。原因是 PHP 在使用self时会绑定到最早的定义,而static关键字

    ```php class Test2 { public static $test = 'TEST2'; public static function getEarlyTest() { return self::$test; } public static function getLateTest() { return static::$test; } }

    class Child extends Test2 { public static $test = 'CHILD'; } ```

    使用的是最新的绑定 4. 在许多情况下,工厂设计模式与静态方法结合使用,以生成给定不同参数的对象实例。在本例中,定义了一个静态方法factory(),该方法返回一个 PDO 连接:

    php public static function factory( $driver,$dbname,$host,$user,$pwd,array $options = []) { $dsn = sprintf('%s:dbname=%s;host=%s', $driver, $dbname, $host); try { return new PDO($dsn, $user, $pwd, $options); } catch (PDOException $e) { error_log($e->getMessage); } }

它是如何工作的。。。

您可以使用类解析操作符::"引用静态属性和方法。鉴于前面显示的Test类,如果您运行此代码:

echo Test::$test;
echo PHP_EOL;
echo Test::getTest();
echo PHP_EOL;

您将看到以下输出:

How it works...

为了说明后期静态绑定,基于前面显示的类Test2Child,请尝试以下代码:

echo Test2::$test;
echo Child::$test;
echo Child::getEarlyTest();
echo Child::getLateTest();

输出说明了selfstatic之间的差异:

How it works...

最后,为了测试前面显示的factory()方法,将代码保存到Application\Database文件夹中Connection.php文件中的Application\Database\Connection类中。然后,您可以尝试以下操作:

include __DIR__ . '/../Application/Database/Connection.php';
use Application\Database\Connection;
$connection = Connection::factory(
'mysql', 'php7cookbook', 'localhost', 'test', 'password');
$stmt = $connection->query('SELECT name FROM iso_country_codes');
while ($country = $stmt->fetch(PDO::FETCH_COLUMN)) 
echo $country . '';

您将看到从样本数据库中提取的国家列表:

How it works...

另见

有关后期静态绑定的更多信息,请参见 PHP 文档中的说明:

http://php.net/manual/en/language.oop5.late-static-bindings.php

使用名称空间

对高级 PHP 开发至关重要的一个方面是名称空间的使用。任意定义的名称空间成为类名的前缀,从而避免了意外的类复制问题,并允许您非常自由地进行开发。使用命名空间的另一个好处是,假设它与目录结构相匹配,则它有助于自动装页,如在《专利 T1 章第 1 章》中所讨论的,TUR3 T3。

怎么做。。。

  1. To define a class within a namespace, simply add the keyword namespace at the top of the code file:

    php namespace Application\Entity;

    最佳实践

    正如建议每个文件只有一个类一样,同样,每个文件也应该只有一个名称空间。

  2. 关键字namespace前面唯一的 PHP 代码是注释和/或关键字declare

    php <?php declare(strict_types=1); namespace Application\Entity; /** * Address * */ class Address { // some code }

  3. 在 PHP5 中,如果需要访问外部名称空间中的类,可以在前面加上只包含名称空间的use语句。然后,您需要在此命名空间中的任何类引用前面加上命名空间的最后一个组件:

    php use Application\Entity; $name = new Entity\Name(); $addr = new Entity\Address(); $prof = new Entity\Profile();

  4. 或者,您可以清楚地指定所有三个类:

    php use Application\Entity\Name; use Application\Entity\Address; use Application\Entity\Profile; $name = new Name(); $addr = new Address(); $prof = new Profile();

  5. PHP7 引入了一个语法改进,称为组使用,这大大提高了代码的可读性:

    php use Application\Entity\ { Name, Address, Profile }; $name = new Name(); $addr = new Address(); $prof = new Profile();

  6. 正如在第 1 章中提到的,第二章第二章,建立了一个基础 T4,命名空间构成了 AutoToLoT7 过程中的一个组成部分。此示例显示了一个演示自动加载程序,它回显传递的参数,然后尝试基于名称空间和类名包含一个文件。这假设目录结构与名称空间

    php function __autoload($class) { echo "Argument Passed to Autoloader = $class\n"; include __DIR__ . '/../' . str_replace('\\', DIRECTORY_SEPARATOR, $class) . '.php'; }

    匹配

它是如何工作的。。。

为了便于说明,请定义与Application\*命名空间匹配的目录结构。创建基本文件夹Application和子文件夹Entity。您还可以根据需要包括其他章节中使用的任何子文件夹,如DatabaseGeneric

How it works...

接下来,在Application/Entity文件夹下创建三个entity类,每个类位于各自的文件中:Name.phpAddress.phpProfile.php。我们这里只展示Application\Entity\NameApplication\Entity\AddressApplication\Entity\Profile将是相同的,只是Address具有$address属性,Profile具有$profile属性,每个属性都具有适当的getset方法:

<?php
declare(strict_types=1);
namespace Application\Entity;
/**
 * Name
 *
 */
class Name
{

  protected $name = '';

  /**
   * This method returns the current value of $name
   *
   * @return string $name
   */
  public function getName() : string
  {
    return $this->name;
  }

  /**
   * This method sets the value of $name
   *
   * @param string $name
   * @return name $this
   */
  public function setName(string $name)
  {
    $this->name = $name;
    return $this;
  }
}

然后,您可以使用在 OutT5 中定义的 AutoLoad,第 1 章 AutoT6T,AutoT7.构建基础 Ty8 T8,或者使用前面提到的简单自动装填器。将设置自动加载的命令放入文件chap_04_oop_namespace_example_1.php。在这个文件中,您可以指定一个 use 语句,它只引用名称空间,而不引用类名。创建三个实体类NameAddressProfile的实例,在类名前面加上名称空间的最后一部分Entity

use Application\Entity;
$name = new Entity\Name();
$addr = new Entity\Address();
$prof = new Entity\Profile();

var_dump($name);
var_dump($addr);
var_dump($prof);

以下是输出:

How it works...

接下来,使用另存为将文件复制到名为chap_04_oop_namespace_example_2.php的新文件中。将use语句更改为以下内容:

use Application\Entity\Name;
use Application\Entity\Address;
use Application\Entity\Profile;

现在,您可以仅使用类名创建类实例:

$name = new Name();
$addr = new Address();
$prof = new Profile();

运行此脚本时,以下是输出:

How it works...

最后,再次运行另存为并创建一个新文件chap_04_oop_namespace_example_3.php。您现在可以测试 PHP 7 中引入的组使用功能:

use Application\Entity\ {
  Name,
  Address,
  Profile
};
$name = new Name();
$addr = new Address();
$prof = new Profile();

同样,当您运行此代码块时,输出将与前面的输出相同:

How it works...

定义可见性

欺骗性地说,可见性一词与应用程序安全无关!相反,它只是一种控制代码使用的机制。它可以用来引导没有经验的开发人员远离公共方法的使用,这些方法只应该在类定义内部调用。

怎么做。。。

  1. 通过在任何属性或方法定义前面加上publicprotectedprivate关键字来指示可见性级别。您可以将属性标记为protectedprivate,以强制仅通过公共getterssetters进行访问。
  2. In this example, a Base class is defined with a protected property $id. In order to access this property, the getId() and setId() public methods are defined. The protected method generateRandId() can be used internally, and is inherited in the Customer child class. This method cannot be called directly outside of class definitions. Note the use of the new PHP 7 random_bytes() function to create a random ID.

    ```php class Base { protected $id; private $key = 12345; public function getId() { return $this->id; } public function setId() { $this->id = $this->generateRandId(); } protected function generateRandId() { return unpack('H*', random_bytes(8))[1]; } }

    class Customer extends Base { protected $name; public function getName() { return $this->name; } public function setName($name) { $this->name = $name; } } ```

    最佳实践

    将属性标记为protected,定义publicgetNameOfProperty()setNameOfProperty()方法控制对该属性的访问。这些方法被称为getterssetters

  3. 将属性或方法标记为private,以防止其从类定义之外的继承或可见。这是将类创建为单例的好方法。

  4. The next code example shows a class Registry, of which there can only be one instance. Because the constructor is marked as private, the only way an instance can be created is through the static method getInstance():

    php class Registry { protected static $instance = NULL; protected $registry = array(); private function __construct() { // nobody can create an instance of this class } public static function getInstance() { if (!self::$instance) { self::$instance = new self(); } return self::$instance; } public function __get($key) { return $this->registry[$key] ?? NULL; } public function __set($key, $value) { $this->registry[$key] = $value; } }

    您可以将方法标记为final,以防止其被重写。将一个类标记为final以防止其被扩展。

  5. 通常,类常量被认为具有public的可见性级别。从 PHP7.1 开始,您可以将类常量声明为protectedprivate。在下面的示例中,TEST_WHOLE_WORLD类常量的行为与 PHP5 中的行为完全相同。接下来的两个常量TEST_INHERITEDTEST_LOCAL遵循与任何protectedprivate属性或方法相同的规则:

    ```php class Test {

    public const TEST_WHOLE_WORLD = 'visible.everywhere';

    // NOTE: only works in PHP 7.1 and above protected const TEST_INHERITED = 'visible.in.child.classes';

    // NOTE: only works in PHP 7.1 and above private const TEST_LOCAL= 'local.to.class.Test.only';

    public static function getTestInherited() { return static::TEST_INHERITED; }

    public static function getTestLocal() { return static::TEST_LOCAL; }

    } ```

它是如何工作的。。。

创建一个文件chap_04_basic_visibility.php并定义两个类:BaseCustomer。接下来,编写代码以创建每个的实例:

$base     = new Base();
$customer = new Customer();

请注意,以下代码工作正常,实际上被认为是最佳实践:

$customer->setId();
$customer->setName('Test');
echo 'Welcome ' . $customer->getName() . PHP_EOL;
echo 'Your new ID number is: ' . $customer->getId() . PHP_EOL;

尽管$idprotected,但相应的方法getId()setId()都是public,因此可以从类定义之外访问。以下是输出:

How it works...

但是,以下代码行将不起作用,因为无法从类定义之外访问privateprotected属性:

echo 'Key (does not work): ' . $base->key;
echo 'Key (does not work): ' . $customer->key;
echo 'Name (does not work): ' . $customer->name;
echo 'Random ID (does not work): ' . $customer->generateRandId();

以下输出显示了预期的错误:

How it works...

另见

有关getterssetters的更多信息,请参阅本章中题为使用吸气剂和设定剂的配方。有关 PHP7.1 类恒定可见性设置的更多信息,请参见https://wiki.php.net/rfc/class_const_visibility

使用接口

接口是系统架构师的有用工具,通常用于应用程序编程接口API的原型。接口不包含实际代码,但可以包含方法名称以及方法签名。

Interface中确定的所有方法的可见性级别均为public

怎么做。。。

  1. 接口标识的方法不能包含实际的代码实现。但是,您可以指定方法参数的数据类型。
  2. 在本例中,ConnectionAwareInterface标识了一个方法setConnection(),该方法需要Connection的实例作为参数:

    php interface ConnectionAwareInterface { public function setConnection(Connection $connection); }

  3. 要使用该接口,请在定义类的开放行之后添加关键字implements。我们定义了两个类CountryListCustomerList,它们都需要通过setConnection()方法访问Connection类。为了识别这种依赖关系,两个类都实现了ConnectionAwareInterface

    ```php class CountryList implements ConnectionAwareInterface { protected $connection; public function setConnection(Connection $connection) { $this->connection = $connection; } public function list() { $list = []; $stmt = $this->connection->pdo->query( 'SELECT iso3, name FROM iso_country_codes'); while ($country = $stmt->fetch(PDO::FETCH_ASSOC)) { $list[$country['iso3']] = $country['name']; } return $list; }

    } class CustomerList implements ConnectionAwareInterface { protected $connection; public function setConnection(Connection $connection) { $this->connection = $connection; } public function list() { $list = []; $stmt = $this->connection->pdo->query( 'SELECT id, name FROM customer'); while ($customer = $stmt->fetch(PDO::FETCH_ASSOC)) { $list[$customer['id']] = $customer['name']; } return $list; }

    } ```

  4. 接口可用于满足类型提示。下面的类ListFactory包含一个factory()方法,该方法初始化实现ConnectionAwareInterface的任何类。接口是定义setConnection()方法的保证。将类型提示设置为接口而不是特定的类实例会使factory方法更通用:

    ```php namespace Application\Generic;

    use PDO; use Exception; use Application\Database\Connection; use Application\Database\ConnectionAwareInterface;

    class ListFactory { const ERROR_AWARE = 'Class must be Connection Aware'; public static function factory( ConnectionAwareInterface $class, $dbParams) { if ($class instanceofConnectionAwareInterface) { $class->setConnection(new Connection($dbParams)); return $class; } else { throw new Exception(self::ERROR_AWARE); } return FALSE; } } ```

  5. 如果一个类实现了多个接口,如果方法签名不匹配,则会发生命名冲突。在本例中,有两个接口,DateAwareTimeAware。除了定义setDate()setTime()方法外,它们还定义了setBoth()。有重复的方法名不是问题,尽管它不被认为是最佳实践。问题在于方法签名不同:

    ```php interface DateAware { public function setDate($date); public function setBoth(DateTime $dateTime); }

    interface TimeAware { public function setTime($time); public function setBoth($date, $time); }

    class DateTimeHandler implements DateAware, TimeAware { protected $date; protected $time; public function setDate($date) { $this->date = $date; } public function setTime($time) { $this->time = $time; } public function setBoth(DateTime $dateTime) { $this->date = $date; } } ```

  6. As the code block stands, a fatal error will be generated (which cannot be caught!). To resolve the problem, the preferred approach would be to remove the definition of setBoth() from one or the other interface. Alternatively, you could adjust the method signatures to match.

    最佳实践

    不要使用重复或重叠的方法定义定义接口。

它是如何工作的。。。

Application/Database文件夹中,创建一个文件ConnectionAwareInterface.php。插入前面步骤 2 中讨论的代码。

接下来,在Application/Generic文件夹中,创建两个文件CountryList.phpCustomerList.php。插入步骤 3 中讨论的代码。

接下来,在与Application目录平行的目录中,创建一个源代码文件chap_04_oop_simple_interfaces_example.php,该文件初始化自动加载器并包括数据库参数:

<?php
define('DB_CONFIG_FILE', '/../config/db.config.php');
require __DIR__ . '/../Application/Autoload/Loader.php';
Application\Autoload\Loader::init(__DIR__ . '/..');
$params = include __DIR__ . DB_CONFIG_FILE;

本例中的数据库参数假定位于由DB_CONFIG_FILE常量指示的数据库配置文件中。

您现在处于位置,可以使用ListFactory::factory()生成CountryListCustomerList对象。请注意,如果这些类没有实现ConnectionAwareInterface,将抛出一个错误:

  $list = Application\Generic\ListFactory::factory(
    new Application\Generic\CountryList(), $params);
  foreach ($list->list() as $item) echo $item . '';

以下是国家/地区列表的输出:

How it works...

您也可以使用factory方法生成CustomerList对象并使用它:

  $list = Application\Generic\ListFactory::factory(
    new Application\Generic\CustomerList(), $params);
  foreach ($list->list() as $item) echo $item . '';

以下是CustomerList的输出:

How it works...

如果您想检查在实现多个接口时会发生什么,但方法签名不同,请在文件chap_04_oop_interfaces_collisions.php中输入前面步骤 4 中显示的代码。尝试运行该文件时,会生成一个错误,如下所示:

How it works...

如果在TimeAware界面进行以下调整,则不会产生错误:

interface TimeAware
{
  public function setTime($time);
  // this will cause a problem
  public function setBoth(DateTime $dateTime);
}

使用性状

如果你曾经做过任何 C 编程,你可能对宏很熟悉。宏是一个预定义的代码块,在所示行扩展。以类似的方式,traits 可以包含代码块,这些代码块被复制并粘贴到 PHP 解释器指示行的类中。

怎么做。。。

  1. 特征用关键字trait标识,可以包含属性和/或方法。您可能已经注意到,在检查前面的具有CountryListCustomerList类的配方时,代码重复。在本例中,我们将重新考虑这两个类,并将list()方法的功能移到Trait中。请注意,list()方法在两个类中是相同的。
  2. Traits 用于类之间存在代码重复的情况。但是,请注意,创建抽象类并扩展它的传统方法可能比使用 traits 具有某些优势。Traits 不能用于识别继承线,而抽象父类可以用于此目的。
  3. 现在我们将把list()复制到一个名为ListTrait

    php trait ListTrait { public function list() { $list = []; $sql = sprintf('SELECT %s, %s FROM %s', $this->key, $this->value, $this->table); $stmt = $this->connection->pdo->query($sql); while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) { $list[$item[$this->key]] = $item[$this->value]; } return $list; } }

    的特征中 4. We can then insert the code from ListTrait into a new class, CountryListUsingTrait, as shown in the following code snippet. The entire list() method can now be removed from this class:

    ```php class CountryListUsingTrait implements ConnectionAwareInterface {

    use ListTrait;

    protected $connection; protected $key = 'iso3'; protected $value = 'name'; protected $table = 'iso_country_codes';

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

    } ```

    任何时候,当您需要进行更改时,都会出现代码重复的潜在问题。您可能会发现自己不得不执行太多全局搜索和替换操作,或者剪切和粘贴代码,结果往往是灾难性的。特性是避免这种维护噩梦的好方法。

  4. 特性受名称空间的影响。在步骤 1 所示的示例中,如果我们的新CountryListUsingTrait类被放置在名称空间Application\Generic中,我们还需要将ListTrait移动到该名称空间中:

    ```php namespace Application\Generic;

    use PDO;

    trait ListTrait { public function list() { // code as shown above } } ```

  5. traits 中的方法覆盖继承的方法。

  6. In the following example, you will notice that the return value for the setId() method differs between the Base parent class and the Test trait. The Customer class inherits from Base, but also uses Test. In this case, the method defined in the trait will override the method defined in the Base parent class:

    ```php trait Test { public function setId($id) { $obj = new stdClass(); $obj->id = $id; $this->id = $obj; } }

    class Base { protected $id; public function getId() { return $this->id; } public function setId($id) { $this->id = $id; } }

    class Customer extends Base { use Test; protected $name; public function getName() { return $this->name; } public function setName($name) { $this->name = $name; } } ```

    在 PHP5 中,traits 也可以覆盖属性。在 PHP7 中,如果 trait 中的属性初始化为与父类中不同的值,则会生成致命错误。

  7. 类中直接定义的方法使用 trait 中定义的 trait 覆盖重复方法。

  8. 在本例中,Test特征定义了一个属性$id以及getId()方法和setId()。trait 还定义了setName(),它与Customer类中定义的相同方法冲突。在这种情况下,Customer中直接定义的setName()方法将覆盖 trait:

    ```php trait Test { protected $id; public function getId() { return $this->id; } public function setId($id) { $this->id = $id; } public function setName($name) { $obj = new stdClass(); $obj->name = $name; $this->name = $obj; } }

    class Customer { use Test; protected $name; public function getName() { return $this->name; } public function setName($name) { $this->name = $name; } } ```

    中定义的setName() 10. 使用多个 trait 时,使用insteadof关键字解决方法名称冲突。结合使用as关键字来别名方法名称。 11. 在这个例子中,有两个特征,IdTraitNameTrait。这两种特性都定义了一种setKey()方法,但表达方式不同。Test类同时使用这两种特征。注意insteadof关键字,它允许我们区分冲突的方法。因此,当从Test类调用setKey()时,源代码将从NameTrait中提取。此外,IdTrait中的setKey()仍将可用,但别名为setKeyDate()

    ```php trait IdTrait { protected $id; public $key; public function setId($id) { $this->id = $id; } public function setKey() { $this->key = date('YmdHis') . sprintf('%04d', rand(0,9999)); } }

    trait NameTrait { protected $name; public $key; public function setName($name) { $this->name = $name; } public function setKey() { $this->key = unpack('H*', random_bytes(18))[1]; } }

    class Test { use IdTrait, NameTrait { NameTrait::setKeyinsteadofIdTrait; IdTrait::setKey as setKeyDate; } } ```

它是如何工作的。。。

从步骤 1 中,您了解到特征用于代码重复的情况。您需要评估是否可以简单地定义基类并扩展它,或者使用 trait 是否更符合您的目的。当在逻辑上不相关的类中发现代码重复时,trait 尤其有用。

为了说明 trait 方法如何覆盖继承的方法,请将步骤 7 中提到的代码块复制到单独的文件chap_04_oop_traits_override_inherited.php中。添加以下代码行:

$customer = new Customer();
$customer->setId(100);
$customer->setName('Fred');
var_dump($customer);

从输出中可以看到(如下所示),属性$id存储为stdClass()的实例,这是 trait 中定义的行为:

How it works...

为了说明直接定义的类方法如何覆盖 trait 方法,请将步骤 9 中提到的代码块复制到单独的文件chap_04_oop_trait_methods_do_not_override_class_methods.php中。添加以下代码行:

$customer = new Customer();
$customer->setId(100);
$customer->setName('Fred');
var_dump($customer);

从以下输出中可以看到,$id属性存储为整数,如Customer类中所定义,而 trait 将$id定义为stdClass的实例:

How it works...

在步骤 10 中,您学习了如何在使用多个 trait 时解决重复的方法名称冲突。将步骤 11 中显示的代码块复制到单独的文件chap_04_oop_trait_multiple.php中。添加以下代码:

$a = new Test();
$a->setId(100);
$a->setName('Fred');
$a->setKey();
var_dump($a);

$a->setKeyDate();
var_dump($a);

请注意,在以下输出中,setKey()产生了新的 PHP 7 函数random_bytes()(在NameTrait中定义)产生的输出,而setKeyDate()产生了使用date()rand()函数(在IdTrait中定义)的键:

How it works...

实现匿名类

PHP7 引入了一个新的特性匿名类。与匿名函数非常相似,匿名类可以定义为表达式的一部分,从而创建一个没有名称的类。匿名类用于需要动态创建对象的情况,该对象被使用,然后被丢弃。

*## 怎么做。。。

  1. An alternative to stdClass is to define an anonymous class.

    在定义中,可以定义任何属性和方法(包括魔术方法)。在本例中,我们定义了一个具有两个属性和一个神奇方法的匿名类,__construct()

    php $a = new class (123.45, 'TEST') { public $total = 0; public $test = ''; public function __construct($total, $test) { $this->total = $total; $this->test = $test; } };

  2. An anonymous class can extend any class.

    在本例中,一个匿名类扩展了FilterIterator,并重写了__construct()accept()方法。作为参数,它接受ArrayIterator``$b,它以 10 为增量表示 10 到 100 的数组。第二个参数用作对输出的限制:

    php $b = new ArrayIterator(range(10,100,10)); $f = new class ($b, 50) extends FilterIterator { public $limit = 0; public function __construct($iterator, $limit) { $this->limit = $limit; parent::__construct($iterator); } public function accept() { return ($this->current() <= $this->limit); } };

  3. An anonymous class can implement an interface.

    在本例中,使用匿名类生成 HTML 颜色代码图表。类实现了内置的 PHPCountable接口。定义了一个count()方法,当此类与需要Countable的方法或函数一起使用时调用该方法:

    ```php define('MAX_COLORS', 256 ** 3);

    $d = new class () implements Countable { public $current = 0; public $maxRows = 16; public $maxCols = 64; public function cycle() { $row = ''; $max = $this->maxRows * $this->maxCols; for ($x = 0; $x < $this->maxRows; $x++) { $row .= '

    '; for ($y = 0; $y < $this->maxCols; $y++) { $row .= sprintf( 'current); $row .= sprintf( 'title="#%06X"> ', $this->current); $this->current++; $this->current = ($this->current >MAX_COLORS) ? 0 : $this->current; } $row .= ''; } return $row; } public function count() { return MAX_COLORS; } }; ```
  4. 匿名类可以使用 traits。

  5. 最后一个例子是对前面一个例子的修改。我们没有定义类Test,而是定义了一个匿名类:

    php $a = new class() { use IdTrait, NameTrait { NameTrait::setKeyinsteadofIdTrait; IdTrait::setKey as setKeyDate; } };

它是如何工作的。。。

在匿名类中,可以定义任何属性或方法。使用前面的示例,您可以定义一个接受构造函数参数的匿名类,并且可以在其中访问属性。将步骤 2 中描述的代码放入测试脚本chap_04_oop_anonymous_class.php。添加以下echo语句:

echo "\nAnonymous Class\n";
echo $a->total .PHP_EOL;
echo $a->test . PHP_EOL;

以下是匿名类的输出:

How it works...

为了使用FilterIterator必须覆盖accept()方法。在该方法中,您定义了将迭代的哪些元素包括为输出的条件。现在继续并将步骤 4 中显示的代码添加到测试脚本中。然后可以添加这些echo语句来测试匿名类:

echo "\nAnonymous Class Extends FilterIterator\n";
foreach ($f as $item) echo $item . '';
echo PHP_EOL;

在此示例中,确定了 50 的限制。原始的ArrayIterator包含一个 10 到 100 的值数组,增量为 10,如以下输出所示:

How it works...

若要在实现接口的匿名类中查看 OUTT1,请考虑步骤 5 和 6 中所示的示例。将此代码放在文件chap_04_oop_anonymous_class_interfaces.php中。

接下来,添加代码,让您可以在 HTML 颜色图表中分页:

$d->current = $_GET['current'] ?? 0;
$d->current = hexdec($d->current);
$factor = ($d->maxRows * $d->maxCols);
$next = $d->current + $factor;
$prev = $d->current - $factor;
$next = ($next <MAX_COLORS) ? $next : MAX_COLORS - $factor;
$prev = ($prev>= 0) ? $prev : 0;
$next = sprintf('%06X', $next);
$prev = sprintf('%06X', $prev);
?>

最后,继续并将 HTML 颜色图表显示为网页:

<h1>Total Possible Color Combinations: <?= count($d); ?></h1>
<hr>
<table>
<?= $d->cycle(); ?>
</table>    
<a href="?current=<?= $prev ?>"><<PREV</a>
<a href="?current=<?= $next ?>">NEXT >></a>

注意,您可以通过将匿名类的实例传递到count()函数(显示在<H1>标记之间)来利用Countable接口。以下是浏览器窗口中显示的输出:

How it works...

最后,为了说明 traits 在匿名类中的用法,将前面配方中提到的chap_04_oop_trait_multiple.php文件复制到一个新文件chap_04_oop_trait_anonymous_class.php。删除Test类的定义,并将其替换为匿名类:

$a = new class() {
  use IdTrait, NameTrait {
    NameTrait::setKeyinsteadofIdTrait;
    IdTrait::setKey as setKeyDate;
  }
};

删除此行:

$a = new Test();

运行代码时,您将看到与前面屏幕截图所示完全相同的输出,只是类引用是匿名的:

How it works...**