OOP 功能

PHP 7 引入了一些新的 OOP 功能,使开发人员能够编写干净有效的代码。在本节中,我们将讨论这些功能。

类型提示

在 PHP 7 之前,没有必要声明传递给函数或类方法的参数的数据类型。同样,也无需提及返回数据类型。任何数据类型都可以传递给函数或方法,也可以从函数或方法返回。这是 PHP 中的一个大问题,因为人们并不总是清楚应该从函数或方法中传递或接收哪些数据类型。为了解决这个问题,PHP 7 引入了类型提示。目前,引入了两种类型提示:标量和返回类型提示。下文将讨论这两种类型。

类型提示在 OOP 和过程式 PHP 中都是一项功能,因为它既可用于过程式函数,也可用于对象方法。

标量类型提示

PHP 7 使在函数和方法中使用整数、浮点数、字符串和布尔类型的标量类型提示成为可能。让我们看看下面的示例:

class Person
{
    public function age(int $age)
    {
        return $age;
    }

    public function name(string $name)
    {
        return $name;
    }

    public function isAlive(bool $alive)
    {
        return $alive;
    }
}

$person = new Person();
echo $person->name('Altaf Hussain');
echo $person->age(30);
echo $person->isAlive(TRUE);

在前面的代码中,我们创建了一个 Person 类。我们有三个方法,每个方法接收不同的参数,这些参数的数据类型已在前面的代码中进行了定义。如果运行前面的代码,它将正常工作,因为我们将为每个方法传递所需的数据类型。

年龄可以是浮点数,例如 30.5 岁;因此,如果我们向年龄方法传递一个浮点数,它仍然可以工作,如下所示:

echo $person->age(30.5);

这是为什么呢?因为在默认情况下,标量类型提示是非限制性的。这就意味着,我们可以将浮点数传递给预期为整数的方法。

为了提高限制性,可以在文件顶部添加以下单行代码:

declare(strict_types = 1);

现在,如果我们向 age 函数传递一个浮点数,我们将得到一个 未捕获类型错误(Uncaught Type Error),这是一个致命的错误,它告诉我们 Person::age 必须是 int 类型,因为它给定的是浮点数。如果我们向一个非字符串类型的方法传递字符串,也会产生类似的错误。请看下面的示例:

echo $person->isAlive('true');

当字符串传递给前面的代码时,它会生成致命错误。

返回类型提示

PHP 7 的另一个重要特性是可以定义函数或方法的返回数据类型。它的行为与标量类型提示的行为相同。让我们稍微修改一下 Person 类,以理解返回类型提示,如下所示:

class Person
{
    public function age(float $age) : string
    {
        return 'Age is '.$age;
    }

    public function name(string $name) : string
    {
        return $name;
    }

    public function isAlive(bool $alive) : string
    {
        return ($alive) ? 'Yes' : 'No';
    }
}

类中的变化会突出显示。返回类型使用 : data-type 语法定义。返回类型是否与标量类型相同并不重要。只要与各自的数据类型相匹配,它们就可以是不同的。

现在,让我们以对象返回类型为例。考虑之前的 Person 类,并为其添加一个 getAddress 方法。同时,我们将在同一文件中添加一个新类 Address,如下代码所示:

class Address
{
    public function getAddress()
    {
        return ['street' => 'Street 1', 'country' => 'Pak'];
    }
}

class Person
{
    public function age(float $age) : string
    {
        return 'Age is '.$age;
    }

    public function name(string $name) : string
    {
        return $name;
    }

    public function isAlive(bool $alive) : string
    {
        return ($alive) ? 'Yes' : 'No';
    }

    public function getAddress() : Address
    {
        return new Address();
    }
}

Person 类和新的 Address 类中添加的代码高亮显示。现在,如果我们调用 Person 类的 getAddress 方法,该方法将完美运行,不会出错。但是,假设我们修改了返回语句,如下所示:

public function getAddress() : Address
{
    return ['street' => 'Street 1', 'country' => 'Pak'];
}

在这种情况下,前面的方法将抛出类似于以下内容的未捕获异常:

Fatal error: Uncaught TypeError: Return value of Person::getAddress() must be an instance of Address, array returned

这是因为我们返回的是数组而不是地址对象。现在,问题来了:为什么要使用类型提示?使用类型提示的最大好处是,它总是可以避免意外地向方法或函数传递或返回错误的、意想不到的数据。

从前面的示例中可以看出,这使得代码清晰明了,通过查看方法的声明,我们可以通过查看每个方法的代码或注释(如果有的话),准确地知道应该向每个方法传递哪些数据类型,以及返回什么样的数据。

命名空间和组使用声明

在一个非常大的代码库中,类被划分到命名空间中,这使得它们易于管理和使用。但是,如果一个命名空间中有太多的类,而我们需要使用其中的 10 个,那么我们就必须为所有这些类键入完整的使用声明。

在 PHP 中,不需要像其他编程语言那样根据命名空间将类划分到子文件夹中。命名空间只是为类提供了一个逻辑分隔。不过,我们并不局限于根据命名空间将类放在子文件夹中。

例如,我们有一个 Publishers/Packt 命名空间以及图书、电子书、视频和演示文稿类。此外,我们还有一个 functions.php 文件,该文件包含我们的常规函数,并位于相同的 Publishers/Packt 命名空间中。另一个文件 constants.php 包含应用程序所需的常量值,也在同一命名空间中。每个类以及 functions.php 和 constants.php 文件的代码如下:

//book.php
namespace Publishers\Packt;

class Book
{
    public function get() : string
    {
        return get_class();
    }
}

现在,Ebook 类的代码如下:

//ebook.php
namespace Publishers\Packt;

class Ebook
{
    public function get() : string
    {
        return get_class();
    }
}

Video 类的代码如下:

//presentation.php
namespace Publishers\Packt;

class Video
{
    public function get() : string
    {
        return get_class();
    }
}

同样,presentation 类的代码如下:

//presentation.php
namespace Publishers\Packt;

class Presentation
{
    public function get() : string
    {
        return get_class();
    }
}

这四个类都有相同的方法,使用 PHP 内置的 get_class() 函数返回类的名称。

现在,在 functions.php 文件中添加以下两个函数:

//functions.php
namespace Publishers\Packt;

function getBook() : string
{
    return 'PHP 7';
}

function saveBook(string $book) : string
{
    return $book.' is saved';
}

现在,我们将以下代码添加到 constants.php 文件中:

//constants.php
namespace Publishers\Packt;

const COUNT = 10;
const KEY = '123DGHtiop09847';
const URL = 'https://www.Packtpub.com/';

functions.phpconstants.php 中的代码不言自明。请注意,每个文件的顶部都有一行命名空间 Publishers\Packt,这使得这些类、函数和常量都属于这个命名空间。

现在,有三种方法可以使用类、函数和常量。让我们逐一考虑。

请看下面的代码:

//Instantiate objects for each class in namespace
$book = new Publishers\Packt\Book();
$ebook = new Publishers\Packt\Ebook();
$video = new Publishers\Packt\Video();
$presentation = new Publishers\Packt\Presentation();

//Use functions in namespace
echo Publishers/Packt/getBook();
echo Publishers/Packt/saveBook('PHP 7 High Performance');

//Use constants
echo Publishers\Packt\COUNT;
echo Publishers\Packt\KEY;

在前面的代码中,我们在创建对象或使用函数和常量时直接使用了空间名。代码看起来不错,但却很杂乱。命名空间无处不在,如果我们有很多命名空间,代码看起来就会非常难看,可读性也会受到影响。

在前面的代码中,我们没有包含类文件。可以使用 include 语句或 PHP 的 __autoload 函数来包含所有文件。

现在,让我们重写前面的代码以使其更具可读性,如下所示:

use Publishers\Packt\Book;
use Publishers\Packt\Ebook;
use Publishers\Packt\Video;
use Publishers\Packt\Presentation;
use function Publishers\Packt\getBook;
use function Publishers\Packt\saveBook;
use const Publishers\Packt\COUNT;
use const Publishers\Packt\KEY;

$book = new Book();
$ebook = new Ebook();

$video = new Video();
$pres = new Presentation();

echo getBook();
echo saveBook('PHP 7 High Performance');

echo COUNT;
echo KEY;

在前面的代码顶端,我们对命名空间中的特定类、函数和常量使用了 PHP 语句。但是,我们仍然为每个类、函数和/或常量编写了重复的代码行。这可能会导致我们在文件顶端有大量的使用语句,整体的冗长程度并不好。

为了解决这个问题,PHP 7 引入了组使用声明。组使用声明有三种类型:

  • 非混合使用声明

  • 混合使用声明

  • 组合使用声明

非混合组使用声明

在命名空间中,我们有不同类型的功能,就像命名空间中有类、函数和常量一样。在非混合组使用声明中,我们使用 use 语句分别声明它们。为了更好地理解,请看下面的代码:

use Publishers\Packt\{ Book, Ebook, Video, Presentation };
use function Publishers\Packt\{ getBook, saveBook };
use const Publishers\Packt\{ COUNT, KEY };

命名空间中有三类功能:类、函数和常量。因此,我们使用单独的组使用声明语句来使用它们。现在的代码看起来更加简洁、有条理、可读性强,而且不需要太多的重复键入。

混合组使用声明

在此声明中,我们将所有类型组合到一个 use 语句中。看一下下面的代码:

use Publishers\Packt\{
    Book,
    Ebook,
    Video,
    Presentation,
    function getBook,
    function saveBook,
    const COUNT,
    const KEY
};

复合命名空间声明

为了理解复合命名空间声明,我们将考虑以下标准。

假设我们在 Publishers\Packt\Paper 命名空间中有一个 Book 类。同时,我们在 Publishers\Packt\Electronic 命名空间中有一个电子书类。视频和演示类属于 Publishers\Packt\Media 命名空间。因此,要使用这些类,我们将使用如下代码:

use Publishers\Packt\Paper\Book;
use Publishers\Packt\Electronic\Ebook;
use Publishers\Packt\Media\{Video,Presentation};

在复合命名空间声明中,我们可以如下使用前面的命名空间:

use Publishers\Packt\{
    Paper\Book,
    Electronic\Ebook,
    Media\Video,
    Media\Presentation
};

它更优雅、更清晰,如果命名空间名称较长,也不需要额外键入。

匿名类

匿名类是一个同时声明和实例化的类。它没有名称,可以拥有普通类的全部功能。当需要执行一个一次性的小任务,而不需要编写一个完整的类时,这些类就会派上用场。

在创建匿名类时,它不会被命名,但 PHP 会根据它在内存块中的地址,用唯一的引用对它进行内部命名。例如,匿名类的内部名称可能是 class@0x4f6a8d124。

该类的语法与已命名类的语法相同,只是缺少了类名,如下面的语法所示:

new class(argument) { definition };

让我们看一个基本且非常简单的匿名类示例,如下所示:

$name = new class() {
    public function __construct()
    {
        echo 'Altaf Hussain';
    }
};

前面的代码只会将输出显示为 Altaf Hussain。

也可以向 匿名类构造函数传递参数,如下代码所示:

$name = new class('Altaf Hussain') {
    public function __construct(string $name)
    {
        echo $name;
    }
};

这将为我们提供与第一个示例相同的输出。

匿名类可以扩展其他类,并具有与普通命名类相同的父子类功能。让我们再举一个例子,看看下面的内容:

class Packt
{
    protected $number;

    public function __construct()
    {
        echo 'I am parent constructor';
    }

    public function getNumber() : float
    {
        return $this->number;
    }
}

$number = new class(5) extends packt
{
    public function __construct(float $number)
    {
        parent::__construct();
        $this->number = $number;
    }
};

echo $number->getNumber();

前面的代码将显示 I am parent constructor 和 5。可以看出,我们以扩展命名类的方式扩展了 Packt 类。此外,我们还可以访问匿名类中的公共和受保护属性和方法,并使用匿名类对象访问公共属性和方法。

与命名类一样,匿名类也可以实现接口。让我们先创建一个接口。运行以下程序:

interface Publishers
{
    public function __construct(string $name, string $address);
    public function getName();
    public function getAddress();
}

现在,让我们按如下方式修改 Packt 类。我们添加了突出显示的代码:

class Packt
{
    protected $number;
    protected $name;
    protected $address;
    public function …
}

其余代码与第一个 Packt 类相同。现在,让我们创建匿名类,它将实现前面代码中创建的 Publishers 接口,并扩展新的 Packt 类,如下所示:

$info = new class('Altaf Hussain', 'Islamabad, Pakistan') extends packt implements Publishers
{
    public function __construct(string $name, string $address)
    {
        $this->name = $name;
        $this->address = $address;
    }

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

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

echo $info->getName(). ' ' . $info->getAddress();

前面的代码不言自明,它将输出 Altaf Hussain 以及地址。

可以在另一个类中使用匿名类,如图所示:

class Math
{
    public $first_number = 10;
    public $second_number = 20;

    public function add() : float
    {
        return $this->first_number + $this->second_number;
    }

    public function multiply_sum()
    {
        return new class() extends Math
        {
            public function multiply(float $third_number) : float
            {
                return $this->add() * $third_number;
            }
        };
    }
}

$math = new Math();
echo $math->multiply_sum()->multiply(2);

前面的代码将返回 60。这是如何发生的呢?Math 类有一个 multiply_sum 方法,该方法返回一个匿名类的对象。这个匿名类是从数学类扩展而来的,也有一个 multiply 方法。因此,我们的 echo 语句可以分为两部分:第一部分是 $math->multiply_sum(),它返回匿名类的对象;第二部分是 ->multiply(2),我们在其中链入这个对象,调用匿名类的 multiply 方法和一个值为 2 的参数。

在前面的例子中,Math 类可称为外层类,匿名类可称为内层类。但请记住,内层类并不需要扩展外层类。在上例中,我们扩展外层类只是为了确保内层类可以通过扩展外层类来访问外层类的属性和方法。

旧式构造函数弃用

在 PHP 4 中,类的构造函数与类的名称方法相同。这种方法在 PHP 5.6 版本之前仍然有效。但现在,在 PHP 7 中,它已被弃用。让我们举个例子,如图所示:

class Packt
{
    public function packt()
    {
        echo 'I am an old style constructor';
    }
}

$packt = new Packt();

前面的代码将显示输出 I am a old style constructor with a deprecated 消息,如下所示:

Deprecated: Methods with the same name as their class will not be constructors in a future version of PHP; Packt has a deprecated constructor in…

但是,旧式的构造函数仍然会被调用。现在,让我们在类中添加 PHP __construct 方法,如下所示:

class Packt
{
    public function __construct()
    {
        echo 'I am default constructor';
    }

    public function packt()
    {
        echo 'I am just a normal class method';
    }
}

$packt = new Packt();
$packt->packt();

在前面的代码中,当我们实例化类对象时,调用了正常的 __construct 构造函数。packt() 方法并不被视为普通的类方法。

旧式构造函数已被弃用,这意味着它们仍然可以在 PHP 7 中工作,并且会显示一条已弃用的消息,但它将在即将推出的版本中删除。最佳做法是不要使用它们。

可抛出的接口

PHP 7 引入了一个基础接口,它可以作为每个可以使用 throw 语句的对象的基础。在 PHP 中,异常和错误都可能发生。以前,异常可以处理,但错误无法处理,因此,任何致命错误都会导致整个应用程序或应用程序的一部分停止运行。为了使错误(最致命的错误)也能被捕获,PHP 7 引入了 throwable 接口,异常和错误都实现了该接口。

我们创建的 PHP 类不能实现 throwable 接口。如果需要,这些类必须扩展异常。

我们都知道异常,因此在本主题中,我们将只讨论错误,它可以处理丑陋的致命错误。

错误

几乎所有致命错误现在都可以抛出错误实例,并且与异常类似,可以使用 try/catch 块捕获错误实例。让我们举一个简单的例子:

function iHaveError($object)
{
    return $object->iDontExist();
    {

//Call the function
iHaveError(null);
echo "I am still running";

如果执行了前面的代码,将显示致命错误,应用程序将停止运行,并且最终不会执行 echo 语句。

现在,让我们把函数调用放在 try/catch 块中,如下所示:

try {
    iHaveError(null);
} catch(Error $e) {
//Either display the error message or log the error message
    echo $e->getMessage();
}

echo 'I am still running';

现在,如果执行了前面的代码,catch body 将被执行,之后应用程序的其他部分将继续运行。在前一种情况下,echo 语句将被执行。

在大多数情况下,最致命的错误都会抛出错误实例,但对于某些错误,则会抛出错误的子实例,如 TypeErrorDivisionByZeroErrorParseError 等。

现在,让我们看看下面示例中的除零(DivisionByZeroError)异常:

try {
    $a = 20;
    $division = $a / 20;
} catch(DivisionByZeroError $e) {
    echo $e->getMessage();
}

在 PHP 7 之前,前面的代码会对除以零发出警告。但现在在 PHP 7 中,它将抛出一个 DivisionByZeroError(除以零错误),而这个错误是可以处理的。