PHP基础

基本语法#

常量#

常量与变量相对,变量可以在运行期间修改,而常量一经定义就不能进行变更,因此常量存在的意义就是设置运行期「只读变量」,保护「这些变量」运行期间不被更改。
在 PHP 中定义常量有两种方式,通过 define 函数设置常量,以 const 修饰符的方式定义常量
通过 define 定义的常量全局有效,所以通常在项目初始化期间通过这种方式定义全局常量。
const 定义方式通常用于在类中设置只读属性(类常量),不过也可以在 PHP 文件中使用。
使用 const 修饰符定义的常量命名规则和 define 完全一致,并且不能定义已经在 define 中声明过的常量。

Copy Highlighter-hljs
<?php define("LANGUAGE", "PHP"); define("AUTHOR", "学院君"); const FRAMEWORK = "Laravel"; echo LANGUAGE. '-' . FRAMEWORK . '-' . AUTHOR . PHP_EOL;

单引号与双引号的区别#

单引号字符串中引用变量不会对变量值进行解析,如果是双引号,则会对引用变量值进行解析.

要判断变量数据类型是否是字符串,可以借助 is_string 函数,另外,如果是查看变量类型,还可以通过更简单的方式,那就是通过 var_dump 函数打印变量

整型#

在 PHP 中,整型类型没有位数之分,所有的整型都统归 int/integer 类型,并且不支持无符号整型。整型也可以像字符串那样通过 is_int/is_integer 来判断。

PHP 中默认的浮点型是 float,也可以通过 is_float/is_double 这种函数进行类型判断

自动类型转化#

在 PHP 中,还可以自动对数据类型进行转化:

Copy Highlighter-hljs
$n5 = '1'; $n6 = 2; $s3 = add($n5, $n6); echo "$n5 + $n6 = $s3" . PHP_EOL; 上述代码的执行结果是: 1 + 2 = 3

当然这种类型转化也不是总能奏效,比如我们将调用代码改写如下:

Copy Highlighter-hljs
$n5 = '学院君'; $n6 = 2; $s3 = add($n5, $n6); echo "$n5 + $n6 = $s3" . PHP_EOL;

此时通过命令行执行代码就会报错,因为 $n5 不能转化为有效的数值

声明参数和返回值类型#

从 PHP 7 开始,支持对传入参数和返回值声明数据类型:

Copy Highlighter-hljs
/** * 计算两数相加之和 * @param int $a * @param int $b * @return int */ function add(int $a, int $b): int { $sum = $a + $b; return $sum; }

上述代码限定只能传入整型参数,并且函数返回值也是整型(通过 : + int 设置,其他类型依此类推)。

这样一来,就只能传入整型数据到 add 函数了,如果传入浮点型或字符串类型数据都会报错

不提供返回值#

最后,我们也可以在函数中不提供返回值,比如可以将上述 add 函数改写如下:

Copy Highlighter-hljs
/** * 计算两数相加之和 * @param int $a * @param int $b */ function add(int $a, int $b) { $sum = $a + $b; echo "$a + $b = $sum" . PHP_EOL; }

这完全取决于业务的需要。

值传递和引用传递#

函数参数默认以值传递方式进行传递,也就是说,我们传递到函数内部的实际上是变量值的拷贝,而不是变量本身,还是以 add 函数为例,如果我们要实现类似 $a += $b 这种方式的求和,可以这么做:

Copy Highlighter-hljs
function add(int $a, int $b): int { $a += $b; return $a; }

在这段代码中,看似我们在函数体中运行 $a += $b 修改了 $a 的值,但是由于参数传递默认是值拷贝,这个赋值作用域仅限于函数体内部,在函数外部并没有真正修改 $a 的值,所以需要通过return语句返回$a才能在外部获取求和后 $a 的值.
从另一个角度解释,那就是形参(形式参数)和实参(实际参数),函数签名中的 $a、$b 仅仅是形参而已,外面定义的变量 $a$b 才是实参

如果要实现引用传递,需要显式通过 &$a 进行声明,这样一来,就不需要设置返回值,对变量 $a 的修改会直接同步到外部传入的实参上。

内置函数#

字符串函数

PHP 所有内置的字符串函数都可以在这里查询:https://www.php.net/manual/zh/ref.strings.php。通过这些函数可以满足所有日常对字符串的操作需求

数组函数

PHP 所有内置的数组函数都可以在这里查询:https://www.php.net/manual/zh/ref.array.php。通过这些函数可以满足所有日常对数组的操作需求

数学函数

PHP 还内置了很多数学计算函数,你可以在这里查询所有这些函数列表:https://www.php.net/manual/zh/ref.math.php

文件系统函数

在 PHP 中,我们可以通过内置的文件系统函数与本地操作系统的文件系统进行交互,比如文件的创建、写入、读取、关闭、删除等,下面是一些基本示例:

Copy Highlighter-hljs
// 文件系统函数 file_put_contents('test1.txt', '你好,学院君'); // 快速写入内容到文件 test.txt(不存在则自动创建) $content = file_get_contents('test1.txt'); // 从文件 test.txt 中读取内容 var_dump($content); $file = fopen('test2.txt', 'w'); // 以写入模式打开文件 test2.txt,不存在则创建自动创建 fwrite($file, '你好,'); // 通过 fwrite 写入内容到文件 fwrite($file, '学院君!'); // 继续写入 fclose($file); // 关闭这个文件句柄 $file = fopen('test2.txt', 'r'); // 只读模式打开 test2.txt 文件 $content = ''; while (!feof($file)) { // 还没有到文件末尾,则继续读取 $content .= fread($file, 1024); // 通过 fread 读取指定字节内容 } fclose($file); var_dump($content); // 删除上述文件 unlink('test1.txt'); unlink('test2.txt');

匿名函数#

从 PHP 5.3 开始,引入了对匿名函数的支持,所谓匿名函数就是在函数定义中没有显式声明函数名,在 PHP 中,匿名函数也被称作闭包函数(Closure)。

编写匿名函数

我们在 php_learning/function 目录下创建 closure.php 来存放本篇教程编写的代码。 以上篇教程演示的自定义函数 add 为例,如果通过匿名函数进行定义,就是这样的:

上面第一个红色方框里面是匿名函数的定义部分,可以看到在 function 之后没有声明函数名,而是将整个函数赋值给了 $add 变量(不要漏掉赋值语句最后的分号),这样,$add 就变成了函数类型,也因此,函数在 PHP 中也可以看作是一等公民(first class),可以赋值给变量进行调用,此时,如果我们试图通过 var_dump($add) 打印 $add,结果如下:

可以看到它的类型是用于代表匿名函数的 Closure 类,并且该匿名函数支持两个必填参数 $a 和 $b。

回到 closure.php,在上述截图的第二个红色方框区域是匿名函数的调用部分,我们可以直接将 $add 作为一个函数名进行调用,打印结果是:

Copy Highlighter-hljs
1 + 2 = 3

此外,还可以通过 PHP 内置的 call_user_func 函数调用该函数,第一个参数是函数名,后面的参数是函数参数(非匿名函数亦可通过 call_user_func 函数调用):

Copy Highlighter-hljs
$sum = call_user_func($add, $a, $b);

返回结果和上面的 $add($a, $b) 完全一致。

可变数量的参数列表#

如果感兴趣的话,看 call_user_func 函数的声明:

Copy Highlighter-hljs
function call_user_func ($function, ...$parameter)

可以看到代表参数列表的 $parameter 前面有一个 ... 前缀,其作用是标识该参数是一个可变数量的参数列表,也就是支持传入任意多个参数,从 0~N 个不等,比如我们这里传入的就是 $a 和 $b 两个参数,如果待调用函数 $function 不需要传递参数,则 $parameter 部分留空,如果只需要传入一个参数,则传入一个参数,依此类推。

默认参数#

说到这里,我们还可以为函数设置默认参数,即为指定参数设置默认值,需要注意的是默认参数需要放到参数列表最后

Copy Highlighter-hljs
$add = function (int $a, int $b = 2): int { return $a + $b; };

这个时候,调用 $add 函数就可以不传入第二个参数了,该参数会使用默认参数值,当然,你可以可以传入第二个参数覆盖默认值

可变函数#

最后,由于 $add 是一个函数类型变量,并且 PHP 是动态类型语言,所以我们还可以像操作基本类型变量那样将其他函数类型值赋值给 $add,这些函数类型值包括匿名函数和非匿名函数,比如我们新增一个两数相乘函数 multi,然后在运行时将其赋值给 $add

我们在运行时将 multi 函数赋值给 $add,再调用 $add($n1, $n2) 则等同于调用 multi($n1, $n2),当然如果通过匿名函数定义 multi 也是可以的,对应的实现代码如下:

Copy Highlighter-hljs
<?php /** * 通过匿名函数定义两数相加函数 add * @param int $a * @param int $b * @return int */ $add = function (int $a, int $b = 2): int { return $a + $b; }; /** * 两数相乘函数 multi * @param int $a * @param int $b * @return int */ $multi = function (int $a, int $b): int { return $a * $b; }; // 调用匿名函数 $n1 = 1; $n2 = 3; $sum = $add($n1, $n2); echo "$n1 + $n2 = $sum" . PHP_EOL; //4 // 将 multi 赋值给 $add $add = $multi; $product = $add($n1, $n2); echo "$n1 x $n2 = $product" . PHP_EOL; //3

这种在运行时动态设置函数类型值给变量的功能,在 PHP 中称之为可变函数。

继承父作用域变量#

匿名函数(或者叫闭包函数)的一个强大功能是支持在函数体中直接引用上下文变量(继承父作用域的变量),比如在上述代码中,我们可以这样编写匿名函数实现代码:

Copy Highlighter-hljs
<?php $n1 = 1; $n2 = 3; // 计算两数相加 $add = function () use ($n1, $n2) { return $n1 + $n2; }; // 计算两数相乘 $multi = function () use ($n1, $n2){ return $n1 * $n2; }; // 调用匿名函数 $sum = $add(); echo "$n1 + $n2 = $sum" . PHP_EOL; $product = $multi(); echo "$n1 x $n2 = $product" . PHP_EOL;

只需要通过 use 关键字传递当前上下文中的变量,它们就可以在闭包函数体中直接使用,而不需要通过参数形式传入,这样一来,其他引用该文件的代码就可以间接引用当前父作用域下的变量,如果是在类方法中定义的匿名函数,则可以直接引用相应类实例的属性

面向对象#

基本概念#

面向对象编程中最核心的概念就是类(Class)和对象(Object),类是对象的抽象模板,而对象是类的具体实例。对象包含的数据称之为类属性(Property),操作数据的函数称之为类方法(Method)。

在 PhpStorm 中,可以通过如下方式快速为其生成设置(Setters)和获取(Getters)方法:在 类的花括号中,右键->从下拉菜单选择 Generate(或者通过对应快捷键呼出窗口)。

类常量值不可更改,只能访问,在类外面访问类常量,需要通过类名 + :: + 常量名的方式,
由于常量是类级别的,无需实例化即可访问。而对于对象级别的类属性(变量类型),需要通过实例化后的对象才能访问,并且访问之前,需要先设置。

当然,如果提供了 Setters/Getters 方法,可以通过这些方法进行设置/获取,从而屏蔽实现细节:

Copy Highlighter-hljs
$car->setBrand("奔驰"); var_dump($car->getBrand());

在 PHP 中,对象级别的属性和方法,都是通过箭头符 -> 进行访问的。

构造函数的用途是在对象实例化过程中调用,用于对该对象进行一些初始化操作。

继承#

继承,指的是子类可以通过继承的方式访问父类的属性和方法(protected 或者 public 方式定义),在 PHP 中,继承通过 extends 关键字实现。

通过 parent::__construct 调用父类的构造函数进行初始化(调用父类的同名方法需要通过 parent:: 进行调用,否则 PHP 会不知道调用父类还是子类的方法)

在子类中可以通过 $this 对象直接访问父类定义的属性和方法,前提是该属性或方法的可见性是 protected 或者 public 级别。

另外,我们也可以通过子类对象访问父类方法(在子类函数体中访问父类方法,通过 $this 即可)

Copy Highlighter-hljs
$benz = new Benz(); $benz->drive();

PHP 遵循单继承机制,即一个子类只能继承自一个父类。

封装#

封装一方面指的是调用者无需关心对象方法实现细节,比如我们要开车,就调用 $car->drive() 方法即可,不用编写具体的实现逻辑,也不用去关心(调用了那些属性、那些方法、不管是私有的还是公开的、当前类的还是其他类的,统统不用关心),就像我们在真实世界中开车一样,只需要按照流程来操作就好了,不用关心汽车引擎内部是如何工作的。

另一方面是通过访问控制限定属性和方法的可见性,比如 public 修饰的属性和方法所有地方可见,不管是当前类、子类还是类之外,protected 修饰的属性和方法在当前类和子类中可见,而 private 修饰的属性和方法仅在当前类可见,你可以根据自己的业务需要合理的设置属性和方法的可见性。

反射#

不过,饶是如此,依然可以通过反射的方式将 protected 或者 private 级别的属性和方法变成类以外可以访问,比如我们将 Benz 类中的 customMethod 方法设置为私有的:

Copy Highlighter-hljs
class Benz extends Car { private $customProp = '自定义属性'; ... private function customMethod() { echo "Call custom prop \$customProp: " . $this->customProp . PHP_EOL; echo "This is a custom method in Benz Class" . PHP_EOL; } }

在类外直接调用会报错.

我们通过反射来调用这个方法,可以这么做:

Copy Highlighter-hljs
// 通过反射调用非 public 方法 $method = new ReflectionMethod(Benz::class, 'customMethod'); $method->setAccessible(true); $benz = new Benz(); $method->invoke($benz);

打印结果和调用声明为 public 的 customMethod 方法完全一样,如果将 private 修改为 protected 效果也一样,通过反射,我们可以在运行时以逆向工程的方式对 PHP 类进行实例化,并对类中的属性和方法进行动态调用,不管这些属性和方法是否对外公开,所以这是一个黑科技,更多反射的细节可以参考 PHP 官方文档:https://www.php.net/manual/zh/book.reflection.php

多态#

方法重写
所谓多态,指的是在 PHP 继承体系中,子类可以重写父类的同名方法,这样,在子类对象中调用该方法,就会自动转发到子类方法调用.

类型转化
PHP 不像 Java 那样支持同一个类中定义多个同名方法(参数数量或类型不同,这种叫做方法重载),另外,由于子类一定包含了父类的公开方法,所以当类作为参数类型声明时,如果声明类型为父类,则可以传入子类对象,反过来,如果声明类型为子类,则不能传入父类对象。

比如我们定义一个测试汽车类启动功能的测试类和方法如下,并编写一段测试代码:

Copy Highlighter-hljs
class TestCarDrive { public function testDrive(Car $car) { $car->drive(); } public function testBenzDrive(Benz $benz) { $benz->drive(); } } // 初始化类对象 $bmw = new Car('宝马'); $benz = new Benz(); $test = new TestCarDrive(); // 测试子类转父类 $test->testDrive($benz); // 测试父类转子类 $test->testBenzDrive($bmw);

错误提示时不能将父类对象转化为子类对象,因为存在方法不兼容。

抽象类与接口#

上面介绍了父子类之间的继承与方法重写,并且提到类作为参数类型声明时,子类实例可以转化为父类,但父类实例不能转化为子类,这是因为,子类必然包含了父类方法,反之则不一定。

但是在实际面向对象编程实践中,并不推荐使用具体的类作为类型声明,因为当我们在声明这个类型约束时,更多考虑的是可以在对应方法中调用这个类型提供的某些方法,然后在调用该方法的地方传入的对象实例只要实现了这些方法即可,这样,该方法就不会和具体的类绑定,从而提高了代码的扩展性和复用性。

要实现类似这样的功能,就需要设计出一种新的模式,在这种模式下,定义方法参数时设定一个类型约束,然后调用该方法时,支持传入不同类的对象实例,并且需要通过某种机制保证这些类都实现了方法参数设定类型约束需要实现的方法,就好像它们之间达成了某种「契约」一样,不同的类都按照契约履行合同,而履行的方式就是实现类型约束要求提供的方法,这样一来,传入的对象实例就可以正常在方法体中使用而不会出错。

在 PHP 中,有两种方式实现这种模式,一种是抽象类,一种是接口。

抽象类

抽象类指的是包含抽象方法的类,而抽象方法是通过 abstract 关键字修饰的方法,抽象方法只是一个方法声明,不能有具体实现:

Copy Highlighter-hljs
abstract public function drive();

只要某个类包含了至少一个抽象方法,它就是抽象类,抽象类也需要通过 abstract 关键字修饰

Copy Highlighter-hljs
<?php abstract class Car { abstract public function drive(); }

提示,包含了抽象方法的类必须声明为抽象类。

抽象类本身不能被实例化,只能被子类继承,继承了抽象类的子类必须实现父类中的抽象方法,否则会报错.

这样一来,我们就可以基于 PHP 语法层面的约束顺利达成「契约」:将方法/函数的类型约束设置为某个抽象类,这样,传入该抽象类的子类对象就可以保证约束类型的方法被实现。

在 PhpStorm 中,可以点击错误提示下的「Add method stubs」按照向导快速添加抽象方法实现模板,或者点击左上角的小红灯,下拉菜单也有「Add method stubs」选项

注:可以看到上图还有一个「Make LynkCo abstract」选项,抽象类的子类如果没有实现父类的抽象方法,可以将该子类也声明为抽象类规避语法错误。

在弹出窗口选择要实现的抽象方法,点击「OK」即可生成对应的模板代码

接口

和很多其他语言面向对象编程实现一样,在 PHP 中,接口也是通过 interface 关键字声明的,接口中可以定义多个方法声明,这些方法声明不能有任何实现,并且这些方法的可见性都应该是 public,因为接口中的方法都要被其他类实现。例如,我们可以通过接口方式定义 Car

Copy Highlighter-hljs
<?php interface Car { public function drive(); }

和抽象类的抽象方法一样,实现了某个接口的类必须实现接口声明的所有方法,否则会报错.

另外,标识一个类实现某个接口通过关键字 implements 完成,在 PhpStorm 中,要快速编写接口方法实现模板代码,和抽象方法实现模板一样,可以点击上图中「Add method stubs」,在弹出窗口选择要实现的方法,点击「OK」就可以生成对应的方法模板了。

接口和抽象类一样,也不能被实例化,只能被其他类实现,但是和抽象类不同,接口中不包含任何具体的属性和方法,完全是待实现的「契约」,实现接口的类就相当于和它签了契约,必须要通过实现接口中声明的所有方法来履行契约。所以我们完全可以通过接口类型定义方法中的参数类型约束,这样,就可以传入实现该接口的对象实例进行实际的方法调用,和父子类型转化原理类似,实现该接口的对象实例会被认为是该接口的实例,因为基于 PHP 的语法约束,对象所属类肯定实现了该接口的所有方法。

类型运算符 instanceof

在 PHP 中,还提供了一个类型运算符 instanceof,用于判断某个对象实例是否实现了某个接口,或者是某个父类/抽象类的子类实例:

Copy Highlighter-hljs
var_dump($lynkCo01 instanceof CarContract); var_dump($lynkco03 instanceof BaseCar);

通过对象组合水平扩展 PHP 类功能#

基本实现

所谓对象组合,简而言之,就是在一个类中组合(或者说依赖)另一个类而不是继承另一个类来扩展它的功能,如果说类继承是垂直(纵向)扩展类功能,那么对象组合则是水平(横向)扩展类功能,从某种角度说,这也是对单继承机制缺陷的一种补充,使得类具备水平扩展功能的能力。

耳听为虚,下面我们通过一个具体的示例来演示。还是以汽车为例,传统的汽车多是通过汽油作为动力来源,随着新能源的发展和对环境保护的要求越来越高,目前行业普遍认为未来的趋势是通过电力作为汽车动力来源,并且随着特斯拉的横空出世,现在纯电动和混动汽车也越来越多,以电动汽车为主的新能源汽车行业正如火如荼地发展起来。

但是目前制约电动汽车发展的因素还很多,最核心的就是充电问题,一方面由于充电站的匮乏,充电不如加油方便,长途存在续航里程焦虑,另一方面,充电耗时数小时之久,远不及加油只需要几分钟便捷,所以作为汽车厂商,就需要做好两手准备,针对热门车型,一方面要生产油车,保证主流需求,另一方面也要生产电车,为未来做好技术储备,还可以油电混合,兼顾两者的优势,因此,体现在汽车类里面,在基本结构一致的情况下,需要支持切换不同的动力引擎,来扩展汽车的功能。

在 php_learning/oop 目录下新建一个 compose.php 来存放本篇教程代码,我们先将上篇教程中的 LynkCo01 类及其父类、实现接口都拷贝过来,如果要通过类继承的方式实现动力功能的扩展,需要在父类中新增相关的方法,或者让父类继承自一个包含不同动力方法的类,不管怎样都很难维护,而且代码非常臃肿,后续添加新功能或者新增动力来源代码扩展性都很差。

如果是通过对象组合的方式则非常方便和灵活,以汽油作为动力源为例,我们在 CarContract 接口中新增一个动力来源方法声明 power,然后在实现类和子类中实现这个方法:

Copy Highlighter-hljs
<?php interface CarContract { public function drive(); public function power(Gas $gas); } abstract class BaseCar implements CarContract { protected $brand; public function __construct($brand) { $this->brand = $brand; } abstract public function drive(); abstract public function power(Gas $gas); } class LynkCo01 extends BaseCar { public function __construct() { $this->brand = '领克01'; parent::__construct($this->brand); } public function drive() { echo "启动{$this->brand}汽车" . PHP_EOL; } public function power(Gas $gas) { echo "动力来源: " . $gas . PHP_EOL; } }

我们通过对象组合的方式传入一个 Gas 类对象实例,就可以在目标类方法中调用该对象实例的方法组合出自己需要的功能,这里,我们只是简单打印对象实例,最后,还需要定义这个 Gas 类以及对应的魔术方法 __toString(该方法会在打印对象时调用,这样就可以将其返回结果作为打印字符串输出):

Copy Highlighter-hljs
class Gas { public function __toString() { return "汽油"; } }

编写一段测试代码:

Copy Highlighter-hljs
$lynk01 = new LynkCo01(); $gas = new Gas(); $lynk01->power($gas);

通过接口解除对具体类的依赖

当然,有了之前面向接口编程的经验之后,很显然上述实现与具体的实现类绑定,代码耦合度高,扩展性低,不利于后续运行时自由切换动力来源,我们通过接口类型声明来重构上述代码,先编写一组动力接口和具体的动力来源类:

Copy Highlighter-hljs
interface Power { public function power(); } class Gas implements Power { public function power() { return "汽油"; } } class Battery implements Power { public function power() { return "电池"; } }

然后在 LynkCo01 及其父类 BaseCar 和接口 CarContract 中移除 power 方法声明及实现,这一次,我们改为在类的构造函数参数中声明对 Power 接口实现类的依赖从而完成对象组合:

Copy Highlighter-hljs
<?php interface CarContract { public function drive(); } abstract class BaseCar implements CarContract { protected $brand; /** * @var Power */ protected $power; public function __construct(Power $power, $brand) { $this->power = $power; $this->brand = $brand; } abstract public function drive(); } class LynkCo01 extends BaseCar { public function __construct(Power $power) { parent::__construct($power, '领克01'); } public function drive() { echo "启动{$this->brand}汽车" . PHP_EOL; echo "动力来源: " . $this->power->power() . PHP_EOL; } }

我们在汽车父类中新增了一个 $power 属性来持有组合对象,并且在 LynkCo01 类的构造函数中调用父类构造函数时传入 $power 对象完成 $power 属性的初始化,这样,我们就可以在当前 LynkCo01 类的其他方法中调用 $this->power 访问 $power 对象的对外可访问方法了,这里,我们在 drive() 方法中调用 $this->power->power() 打印动力来源。

编写测试代码如下:

Copy Highlighter-hljs
$battery = new Battery(); $lynk01 = new LynkCo01($battery); $lynk01->drive(); echo "电力不足,自动切换为使用汽油作为动力来源..." . PHP_EOL; $gas = new Gas(); $lynk01 = new LynkCo01($gas); $lynk01->drive();

通过 Trait 水平扩展 PHP 类功能#

基本使用

从 PHP 5.4 开始,引入了一种新的代码复用方式 —— Trait,Trait 其实也是一种通过组合水平扩展类功能的机制,我们在 php_learning/oop 目录下新建一个 trait.php 来存放本篇教程的代码,然后基于 Trait 定义动力源,Trait 结构通过关键字 trait 定义:

Copy Highlighter-hljs
<?php trait Power { protected function gas() { return '汽油'; } protected function battery() { return '电池'; } }

Trait 和类相似,支持定义方法和属性,但不是类,不支持定义构造函数,因而不能实例化,只能被其他类使用,要在一个类中使用 Trait,可以通过 use 关键字引入,然后就可以在类方法中直接使用 trait 中定义的方法了:

Copy Highlighter-hljs
class Car { use Power; public function drive() { echo "动力来源:" . $this->gas() . PHP_EOL; echo "汽车启动..." . PHP_EOL; } }

我们编写一段简单的测试代码:

Copy Highlighter-hljs
$car = new Car(); $car->drive();

由此可见,我们可以轻松通过 Trait + 类的组合扩展类的功能,在某个类中使用了 Trait 之后,就好像把它的所有代码合并到这个类中一样,可以自由调用,并且同一个 Trait 可以被多个类复用,从而突破 PHP 单继承机制的限制,有效提升代码复用性和扩展性。

可见性

Trait 和类一样,支持属性和方法以及可见性设置(private、protected、public),并且即使是 private 级别的方法和属性,依然可以在使用类中调用:

Copy Highlighter-hljs
<?php trait Power { protected function gas() { return '汽油'; } public function battery() { return '电池'; } private function water() { return '水'; } } class Car { use Power; public function drive() { echo "动力来源:" . $this->water() . PHP_EOL; echo "切换动力来源:" . $this->battery() . PHP_EOL; echo "切换动力来源:" . $this->gas() . PHP_EOL; echo "汽车启动..." . PHP_EOL; } } $car = new Car(); $car->drive();

上述代码的打印结果是:

所以不同于类继承,这完全是把 Trait 的所有代码组合到使用类,变成了使用类的一部分。从另一个角度来印证,就是 Trait 中定义的属性不能再使用类中重复定义。

我们在 Power Trait 中定义一个属性 $power,并重构所有代码如下:

Copy Highlighter-hljs
<?php trait Power { protected $power; protected function gas() { $this->power = '汽油'; } public function battery() { $this->power = '电池'; } private function water() { $this->power = '水'; } } class Car { use Power; public function drive() { // 设置动力来源 $this->gas(); echo "动力来源:" . $this->power . PHP_EOL; echo "汽车启动..." . PHP_EOL; } } $car = new Car(); $car->drive();

可以看到,我们在 Trait 中可以使用 $this 指向当前 Trait 定义的属性和方法,因为 Trait 最终会被类使用,$this 也就最终对应着被使用类的对象实例。然后我们在使用类 Car 中可以通过 $this->power 调用 Trait 属性,就好像调用自己的属性一样。

如果我们试图在 Car 中调用同名属性,会报错,提示不能定义和 Trait 同名的属性

方法重写与优先级

属性如此,那方法呢,如果我们尝试在使用了 Trait 的类中定义和 Trait 内同名的方法,会发生什么呢?

在 Car 中定义一个和 Power 同名的方法 gas:

Copy Highlighter-hljs
class Car { use Power; public function drive() { // 设置动力来源 $this->gas(); echo "动力来源:" . $this->power . PHP_EOL; echo "汽车启动..." . PHP_EOL; } protected function gas() { $this->power = '柴油'; } }

然后在命令行执行代码,可以看到,动力来源变成 Car 中定义的 gas 方法设置的 柴油,也就是说,Car 中定义的 gas 方法覆盖了 Trait 中定义的 gas 方法!

那如果 Car 还继承自父类 BaseCar,并且 BaseCar 中也定义了和 Trait 中同名的方法,又会如何呢?

Copy Highlighter-hljs
abstract class BaseCar { abstract public function drive(); protected function gas() { echo "动力来源:柴油" . PHP_EOL; } abstract function battery(); } class Car extends BaseCar { use Power; public function drive() { // 设置动力来源 $this->gas(); echo "动力来源:" . $this->power . PHP_EOL; echo "汽车启动..." . PHP_EOL; } }

这一次,我们从 Car 中移除 gas 方法,改为在 BaseCar 中定义,在命令行执行代码,打印结果
这一次变成了 Trait 覆盖了父类中定义的同名方法,并且 Trait 中包含了对抽象方法 battery 的实现,所以无需在 Car 中实现该方法。

综上,我们可以看到,同名方法重写的优先级依次是:使用 Trait 的类 > Trait > 父类。并且 Trait 除了不能实例化和可见性上的差异之外,和类继承有着非常多的相似之处,它是介于类继承和标准对象组合之间的一种存在,就像抽象类是不完全的面向接口编程一样。

使用多个 Trait

引用多个 Trait 通过逗号分隔即可.
我们还要引入一个新的问题,之前讨论了类中包含了和 Trait 同名的方法会存在覆盖优先级,如果引入多个 Trait 中包含同名方法会发生什么呢?
此时就不存在同名方法覆盖了,而是直接报冲突错误,PHP 提供了如下方式解决这个问题 —— 指定使用多个 Trait 同名方法中的哪一个来替代其他的,这样会导致其他未选择方法被覆盖。

如果你仍然想调用其他 Trait 中的同名方法,PHP 还提供了别名方案,我们可以通过 as 关键字为同名方法设置不同别名,再通过别名来调用对应方法,不过这种方式还是要先通过 insteadof 解决方法名冲突问题:

···
class Car
{
use Power, Engine {
Engine::printText insteadof Power;
Power::printText as printPower;
Engine::printText as printEngine;
}

Copy Highlighter-hljs
public function drive() { // 设置动力来源 $this->gas(); $this->four(); $this->printPower(); $this->printText(); $this->printEngine(); echo "汽车启动..." . PHP_EOL; }

}
···
在上述代码中,调用 printPower 等同于调用 Power 定义的 printText 方法,调用 printText 和 printEngine 则都将调用 Engine 定义的 printText 方法。

静态属性和方法的定义和调用#

在 PHP 中,我们通过 static 关键字来修饰静态属性和方法。
由于静态属性和方法可以直接通过类引用,所以又被称作类属性和类方法(相应的,非静态属性和非静态方法需要实例化后通过对象引用,因此被称作对象属性和对象方法),静态属性和方法可以通过 类名::属性/方法 的方式调用。
如果是在类内部方法中,需要通过 self:: 引用当前类的静态属性和方法,就像常量一样,因为静态属性和方法无需实例化类即可使用,而没有实例化的情况下,$this 指针指向的是空对象,所以不能动过它引用静态属性和方法
同理,我们也不能在静态方法中通过 $this 引用对象属性和方法。
不能在静态方法中通过 $this 调用非静态属性/方法,但是在非静态方法中可以通过 self:: 调用静态属性/方法

后期静态绑定#

后期静态绑定(Late Static Bindings)针对的是静态方法的调用,使用该特性时不再通过 self:: 引用静态方法,而是通过 static::,如果是在定义它的类中调用,则指向当前类,此时和 self 功能一样,如果是在子类或者其他类中调用,则指向调用该方法所在的类,我们通过后期静态绑定改写上述代码:

Copy Highlighter-hljs
class Car { ... public static function getClassName() { return __CLASS__; } public static function who() { echo static::getClassName() . PHP_EOL; } } class LynkCo01 extends Car { public static function getClassName() { return __CLASS__; } } ... Car::who(); LynkCo01::who();

再次执行,打印结果如下:

Copy Highlighter-hljs
Car LynkCo01

表明后期静态绑定生效,即 static 指向的是调用它的方法所在的类,而不是定义时,所以称之为后期静态绑定。

此外,还可以通过 static::class 来指向当前调用类的类名,例如我们可以通过它来替代 __CLASS__,这样上述子类就没有必要重写 getClassName 方法了:

Copy Highlighter-hljs
class Car { ... public static function getClassName() { return static::class; } public static function who() { echo static::getClassName() . PHP_EOL; } } class LynkCo01 extends Car { }

代码执行结果和之前一样。

同理,self::class 则始终指向的是定义它的类。

魔术方法#

__call() 和 __callStatic()

当在指定对象上调用一个不存在的成员方法时,如果该对象包含 __call 魔术方法,则尝试调用该方法作为兜底,与之类似的,当在指定类上调用一个不存在的静态方法,如果该类包含 __callStatic 方法,则尝试调用该方法作为兜底。

__set()、__get()、__isset() 和 __unset()

这是一组相关的魔术方法,__set() 方法会在给不可访问属性赋值时调用;__get() 方法会在读取不可访问属性值时调用;当对不可访问属性调用 isset() 或 empty() 时,__isset() 会被调用;当对不可访问属性调用 unset() 时,__unset() 会被调用。

不可访问有两层意思,一层是属性的可见性不是 public,另一层是对应属性压根不存在

__invoke()

__invoke 魔术方法会在以函数方式调用对象时执行

Copy Highlighter-hljs
<?php class Car { protected $brand; ... public function __invoke($brand) { $this->brand = $brand; echo "蓝天白云,定会如期而至 -- " . $this->brand . PHP_EOL; } }

当我们试图以函数方式调用该对象时:

Copy Highlighter-hljs
$car = new Car(); $car('宝马'); 打印结果如下: 蓝天白云,定会如期而至 -- 宝马

错误和异常处理#

在 PHP 5 中,程序错误会被划分为多种级别:https://www.php.net/manual/zh/errorfunc.constants.php,然后可以通过 error_reporting 函数设置报告的错误级别:

Copy Highlighter-hljs
error_reporting(E_ALL); // 报告所有 PHP 错误 error_reporting(0); // 关闭所有 PHP 错误报告 error_reporting(-1); // 与上面👆操作相反,也是报告所有 PHP 错误

当然,更常见的是通过位运算 报告特定级别的错误:

Copy Highlighter-hljs
error_reporting(E_ERROR | E_WARNING | E_PARSE | E_NOTICE);

要排除对 E_NOTICE 级别的错误报告可以这么做:

Copy Highlighter-hljs
error_reporting(E_ALL ^ E_NOTICE);

如果没有在 PHP 应用程序中调用 error_reporting 设置错误报告级别,则会应用 PHP 全局配置文件 php.ini 中默认的错误报告级别。我们可以在命令行通过 php -i | grep error_reporting 查看本地环境下这个默认配置值,32767 对应的错误级别是 E_ALL

可以通过设置 display_errors 选项决定是否向用户显示错误报告和 Error 异常,该配置默认在 PHP 配置文件中全局设置,你也可以通过 ini_set 在运行时设置:

Copy Highlighter-hljs
ini_set('display_errors', 0);

该值默认为 1,表示显示用户级错误,设置为 0 则表示不显示用户级错误

在 PHP 7 中,所有错误都归属于 Error 类,所有异常都归属于 Exception 类,两者是并列关系,而 Error 和 Exception 类又都实现了 Throwable 接口。

捕获异常#

try...catch... 语句块捕获异常
通过 throw 关键字即可抛出异常,抛出异常后会终止后续代码的执行,其原理是当 try 语句块中遇到异常后,会通过 catch 语句进行捕获,如果抛出的异常和声明异常类型匹配,则执行 catch 语句块中的内容。
如果你不知道抛出的异常类型是什么,可以通过 Exception 基类捕获(或者其他父级异常类),也就是说,此处也符合父子类型的转化逻辑.
可以通过多个 catch 语句进行捕获:

Copy Highlighter-hljs
try { $val = getItemFromBook($book, 'desc'); } catch (InvalidArgumentException $exception) { echo $exception->getMessage(); exit(); } catch (OutOfBoundsException $exception) { echo $exception->getMessage(); exit(); } var_dump($val);

抛出异常#

我们也可以在捕获到异常后不进行处理,直接抛出,交给上一层调用代码进行进一步处理:

Copy Highlighter-hljs
try { $val = getItemFromBook([], null); $val = getItemFromBook($book, 'desc'); } catch (InvalidArgumentException $exception) { throw $exception; } catch (OutOfBoundsException $exception) { throw $exception; } finally { var_dump($val); }

上一层的处理逻辑也无非是进行 try...catch... 捕获后进行处理或者继续抛出。

全局异常处理器#

在进行系统框架设计时,考虑到系统的稳健型,总会有一些异常的「漏网之鱼」没有被捕获和处理,这个时候就要通过 set_exception_handler 函数注册全局的异常处理器来处理这些未被捕获和处理的异常:

Copy Highlighter-hljs
<?php ... function myExceptionHandler(Exception $exception) { echo 'Uncaught Exception [' . get_class($exception) . ']: ' . $exception->getMessage() . PHP_EOL; echo 'Thrown in ' . $exception->getFile() . ' on line ' . $exception->getLine() . PHP_EOL; } set_exception_handler('myExceptionHandler'); try { $val = getItemFromBook($book, 'desc'); } catch (InvalidArgumentException $exception) { throw $exception; } catch (OutOfBoundsException $exception) { throw $exception; } finally { if (isset($val)) { var_dump($val); } else { echo '异常将通过全局异常处理器处理...' . PHP_EOL; } }

我们首先需要定义一个自定义的 myExceptionHandler 函数作为全局异常处理器,在这个函数中,我们需要传入异常对象作为参数,然后输出该异常类名、消息、出现异常的文件和行号,最后通过 set_exception_handler 函数将其注册为全局异常处理器。

在后续调用 getItemFromBook 时,由于捕获的异常抛给了上一层,但目前没有上一层调用代码,也就变成了未处理异常,最终这些异常会通过全局异常处理器进行兜底处理

自定义异常类#

上面所有的异常都是 PHP 内置的异常类,除此之外,我们也可以根据需要创建自定义的异常类,只需要继承自 Exception 基类或者其子类即可,比如我们为索引不存在定义一个独立的异常类,并且继承自 LogicException 父类:

Copy Highlighter-hljs
<?php class IndexNotExistsException extends LogicException { }

暂时不需要编写任何方法,它可以继承祖先类 Exception 的所有 protected/public 方法和属性:

需要注意的是,Exception 类中的很多方法定义前面都有一个 final 关键字,通过该关键字修饰的方法不能被子类重写

命名空间与类自动加载实现#

include 和 require 都可以通过指定路径引入一个 PHP 脚本,区别是 include 没有找到对应路径脚本时发出警告(E_WARNING),而 require 会抛出致命错误(E_COMPILE_ERROR),include_once/require_once 也是用于引入指定路径 PHP 脚本,与 include/require 的区别是如果指定路径已经包含过,不会再次包含,换言之,只会包含一次同一路径脚本

自动加载类文件
对于类文件的引入,如果你觉得反复编写 require_once/include_once 语句太麻烦,还可以借助 spl_auto_register 函数注册自动加载器,实现系统未定义类或接口的自动加载。

Copy Highlighter-hljs
spl_autoload_register(function ($className) { require_once 'core/' . $className. '.php'; });

通过 Composer 管理命名空间#

实际项目开发时,手动编写这段 spl_autoload_register 代码有点麻烦,尤其是项目除了自己编写的代码外,还要引入各种第三方库,我们可以借助 PHP 的包管理工具 Composer 帮我们管理这种命名空间与目录路径的映射.

posted @   caibaotimes  阅读(125)  评论(0编辑  收藏  举报
编辑推荐:
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
点击右上角即可分享
微信分享提示
CONTENTS