现代的PHP语言有很多令人兴奋的新特性,其中很多特性针对从旧版PHP升级过来的程序员来说是全新的。这些新特性会让PHP变成一个强大的平台,为构建Web应用和命令行工具提供了愉快的体验。
命名空间 namespace
命名空间需要PHP5.3.0+
如果只需要知道现代PHP特性中的一个,我想应该是命名空间。命名空间是一个很重要的工具, 其作用是按照一种虚拟的层次结构组织PHP代码,这种层次结构类似于操作系统中文件系统的目录结构。现代的PHP组件和框架都放在各自全局唯一的厂商命名空间中,以免与其他厂商的常见类名发生冲突。
PHP命名空间与操作系统的物理文件系统不同,这是一个虚拟的概念,不必与目录结构完全对应。尽管如此,但是大多数PHP组件为了兼容广泛使用的PSR-4自动加载器标准,会把子命名空间放在文件系统的子目录中。
:::info 从技术层面来看,命名空间只是PHP语言中的一种符号,PHP解释器会将其作为前缀添加到类、接口、函数和常量前面。 :::
为什么使用命名空间
命名空间很重要,因为代码放在沙盒中,可以和其他开发者编写的代码一起使用。这是现代PHP组件生态系统的基础。组件和框架的作者编写了大量代码,供众多的PHP开发者使用,这些作者不可能知道或控制他人在使用自己代码时还使用了什么其他类、接口、函数或常量。在你的私人项目中也会遇到这种问题,在为项目编写PHP组件或类时,要确保这些代码能和项目的第三方依赖一起使用。
你的代码可能和其他开发者的代码使用了相同的类名、接口名、或常量名,如果不适用命名空间,名称就会冲突,从而导致PHP执行报错。而使用命名空间,把代码放在唯一的厂商命名空间中的话,你的代码和其他开发者的代码可以使用相同的类名、接口、函数、常量。
如果只是个人项目开发,只有少量的依赖,命名冲突可能不是问题。但如果是与他人协同开发,或者项目中有许多依赖第三方的大项目,就必须使用命名空间来规避命名冲突的问题,你无法控制项目依赖在全局命名空间中引入的类、接口、函数和常量。这就是为什么一定要在你的代码中使用命名空间的原因。
生成命名空间
每一个PHP类、接口、函数和常量都在命名空间中。命名空间在PHP文件的顶部, <?php
标签之后的第一行声明。命名空间声明语句以 namespace
开头,随后是一个空格,然后是命名空间的名称,最后以 “;”符号结尾。
:::tips 命名空间经常用于设定顶层厂商名,比如下面的示例设定的厂商名称是 Oreilly : :::
<?php
namespace Oreilly;
在这个命名空间声明语句后面的所有PHP类、接口、函数或者常量都在 Oreilly 命名空间中,而且和 O’Reilly Media 有某种关系。如果我们想组织本书用到的PHP代码,可以使用子命名空间。
子命名空间和命名空间的声明方式完全一样,唯一不同的是使用“\”符号隔开:
<?php
namespace Oreilly\ModernPHP;
同一个命名空间或子命名空间没必要所有的类都写在同一个PHP文件中声明。可以在PHP文件顶部指定一个命名空间或子命名空间。此时这个文件中的代码就是该命名空间中的一部分。因此我们可以在不同的文件中编写属于同一个命名空间的多各类。
:::tips 厂家的命名空间是最重要的命名空间,也是最顶层的命名空间。用于识别品牌或所属阻止,必须具有全局唯一性,子命名空间没有那么重要,不过有助于组织代码。 :::
导入和别名
在命名空间出现之前,PHP开发者使用Zend式的类名来解决冲突问题。这种类的命名方式,因为Zend框架而流行。这种命名方式在PHP类名中使用下划线表示文件系统的目录分隔符。这种约定有两个作用:其一,是确保类名是唯一的;其二,原生的自动加载器会把类名中的下划线替换为文件系统的分隔符,从而确定文件的路径。
例如: Zend_cloud_DocumentService_Adapter_WindowsAuzre_Query
类对应的文件是:Zend/cloud/DocumentService/AdapterWindowsAuzre/Query.php
可以看出,Zend式命名有一个缺点,类名特别的长。
现代的PHP命名空间也有类似的问题。幸好我们可以导入命名空间中的代码,并为其创建别名。在文件头部通过use关键字导入,告诉PHP想使用那个命名空间、类、接口、函数和常量。导入后就不需要输入全名了。
:::info 从PHP5.3开始可以导入PHP类、接口和其他命名空间,并为其创建别名。从PHP5.6版本开始可以导入PHP函数和常量,并为其创建别名 :::
/**
不使用命名空间,不创建别名
*/
<?php
$response = new Symfony\Component\HttpFoundation\Response('0ops',400);
$response->send();
?>
/**
使用命名空间和默认别名,通过use关键字告诉PHP引入指定类
我们只需要输入一次完全限定的类名,随后实例化Response类时无需使用完整类名。
*/
<?php
use Symfony\Component\HttpFoundation\Response;
$response = new Response('0ops',400);
$response->send();
?>
/**
使用命名空间并自定义别名
在命名空间后面加上 as Res,告诉PHP使用 Res做Response类的别名。
如果不使用as设置别名,PHP默认使用导入的类名做别名。
*/
<?php
use Symfony\Component\HttpFoundation\Response as Res;
$r = new Res('0ops',400);
$r->send();
?>
:::info
应该在PHP文件的顶部使用use关键字导入代码,而且要放在<?php标签或命名空间声明语句之后。
使用use关键字导入代码是无需在开头加上\符号,因为PHP假定导入的是完全限定的命名空间。
use关键字必须出现在全局作用域中(即不能再类或函数中),因为这个关键字在编译时使用。不过,use关键字可以再命名空间声明语句之后使用,导入其他命名空间中的代码。
:::
导入函数和常量(PHP5.6+)
<?php
// 导入函数
use func Namespace\FunctionName;
functionName();
// 导入常量
use contant Namespace\CONST_NAME;
echo CONST_NAME;
多重导入
如果想在一个PHP文件中导入多个类、接口、函数或常量,要在PHP文件的顶部使用多个use语句。
<?php
/*
PHP允许使用简短的导入语法,把多个use语句写成一行:
*/
use Symfony\Component\HttpFoundation\Request,
Symfony\Component\HttpFoundation\Respinse,
Symfony\Component\HttpFoundation\Cookie;
?>
/*
虽然官方支持写成一行,但不建议这么做,这样写容易让人困惑。
建议一行写一个use语句,这样多输入几个字符,但代码更易于阅读和纠错,
*/
<?php
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Respinse;
use Symfony\Component\HttpFoundation\Cookie;
?>
全局命名空间
如果引用的类、接口、函数或常量没有使用命名空间,PHP假定引用类、接口、函数或常量在当前命名空间中。如果这个假定不存在,PHP会尝试解析类、接口、函数或常量。如果需要在命名空间中引入其他命名空间中的类、接口、函数或常量,必须使用完全限定的PHP类名(命名空间+类名),可以使用完全限定的PHP类名,也可以use导入到当前命名空间中,
有些需要引入的代码可能没有命名空间 我们需要在类名前面加上“\”前缀,这么做的目的是告诉PHP别在当前命名空间中找这个类,要在全局命名空间中查找:
<?php
namespace My\App;
class Foo
{
public function doSomething(){
// 这么用会报错,因为这个方法不存在
// $exception = new Exception();
// 在这个方法前面加上"\"前缀,让PHP从全局命名空间中查找
$exception = new \Exception();
}
}
接口 interface
接口是主要作用是约束
类的方法,接口指定需要统一的方法,然后使用不同的类去实现这个接口,类必须包含接口中指定的方法,否则这个类就会报错。
<?php
// 定义接口必须包含的方法
interface Person
{
// 接口指定方法的修饰符只允许定义成 `public`
// 接口中不能定义
public function name(); // 姓名
public function sex(); // 性别
}
// 'zhangsan' 这个类实现了Person接口
class zhangsan implements Person
{
public function name()
{
echo '张三'.PHP_EOL;
}
public function sex()
{
echo "张三是男的".PHP_EOL;
}
}
// 'lisi' 这个类也实现了Person接口
class lisi implements Person
{
public function name()
{
echo '李四'.PHP_EOL;
}
public function sex()
{
echo '李四是女的'.PHP_EOL;
}
// 可以写接口中没有指定的方法,不会报错可以正常使用,但这样使用就失去了接口的初衷
public function age(){
echo '李四18岁'.PHP_EOL;
}
}
/**
* 使用'zhangsan'和'lisi'类,可以在不知道内容的情况下 放心调用name方法和sex方法
* 因为这些方法都与是在接口中有规定的
*/
$user1=new zhangsan();
$user1->name();
$user1->sex();
$user2=new lisi;
$user2->name();
$user2->sex();
$user2->age();
// 传参限制 使用对象作为参数传递的时候,可以限制必须实现了指定接口的对象才允许传入
// `wangwu` 这个类没有实现Person接口
class wangwu{
public function name()
{
echo '王五'.PHP_EOL;
}
public function sex()
{
echo "王五是男的".PHP_EOL;
}
}
// 定义一个方法
class L
{
// 这里规定了传入类型 必须是实现了Person的方法
public static function factory(Person $obj){
return $obj;
}
}
// 在这里传入 `wangwu` 则会报错 因为 `wangwu` 没有实现Person接口
$l = L::factory(new wangwu);
性状 Trait
Trait需要PHP5.4.0+
PHP是一种典型的集成模型,大多时候都可以良好的运作,可是如果想让两个毫无关联的PHP类拥有相同的功能,继承就不能很好的来完成这个需求。
解决这个问题最简单的方法是声明一个父类,让两个独立的类去继承这个父类。但这种解决办法并不好,因为我们让两个完全不同的类继承同一个祖先,而且很明显,这个祖先不属于各自继承层次结构。
第二种方法是使用接口,定义需要哪些方法,让两个独立的类实现这个接口,这种解决办法能够保有自然的继承层次结构,但我们需要在两个类中重复实现相同的功能代码,这个不符合 DRY原则
(DRY是Don’t Reoeat Yourself “不要自我重复”的简称,这是一个良好的实践,不要在多个地方编写相同的代码,如果需要修改遵循这个原则编写的代码,只需要维护一处,更改就能体现在其他地方)
使用 trait
,开发人员能够在不同层次中复用类方法,在一定的程度上弥补了单继承语言在某些复杂情况下代码不能复用的问题。
Trait与普通类的同异
- 相同
- 能够像普通类一样可以定义属性、方法(包括抽象、静态)
- 引入到父类中,其子类也能访问triat里面的方法和属性
- 不同
- 不需要实例化就能访问定义的普通方法和属性
- 里面不能定义构造函数
<?php
// 定义 Trait
trait TraitOne{
public function sayHello()
{
return 'My trait one';
}
}
// 引入Trait 写在定义体内部
class MyClass{
use TraitOne;
}
// 使用
$MyClass=new MyClass();
echo $MyClass->sayHello(); // 输出:My trait one
还可以在一个类中引入多个Trait
<?php
Trait TraitOne {
public $propertyOne = 'argumentOne';
public function sayHello()
{
return 'My trait one';
}
}
Trait TraitTwo {
public function sayHello()
{
return 'My Trait Two';
}
}
class MyClass {
use TraitOne, TraitTwo {
TraitTwo::sayHello insteadof TraitOne; //指定要使用的trait
Traittwo::sayHello as twoSayHello; //同方法名的trait设置别名
}
public function traitMethodValue()
{
return $this->twoSayHello(); //调用trait别名方法
}
}
class MyClassSon extends MYClass {
}
$myClass = new MyClassSon();
echo $myClass->twoSayHello();
//输出:My Trait Two
Trait的权重
子类 > Trait >继承
很多较为流行的PHP框架基于这个功能来做软删除,当不引入trait的时候是直接删除,引入后重写继承Model中的删除功能,实现软删除
生成器 yield
生成器需要PHP5.5.0+
生成器就是简单的迭代器,仅此而已。与标准的迭代器不同的是PHP生成器不要求实现Iterator接口,从而减轻了类的负担。生成器会根据需求计算并产出要迭代的值。这对应用的性能有重大影响。试想一下,假如标准的PHP迭代器经常在内存中执行迭代操作,这要预先计算出数据集,性能低下;如果要满足特定的计算方式,对性能的影响更甚。此时我们可以使用生成器,即时计算并产出后续值,不占用宝贵的内存资源。
一个简单的生成器
调用生成器函数时,PHP会返回一个属于Generator类的对象。这个对象可以使用foreach()函数携带。每次迭代,PHP都会要求Generator实例计算并提供下一个要迭代的值。生成器的优雅体现在,每产出一个之后,生成器内部都会停顿;向生成器请求下一个值时,内部状态又会恢复。生成器的内部状态会一直在停顿和恢复之间切换,直到抵达函数定义体的尾部或遇到空的 return;
语句为止。使用下列代码可以定义生成器:
<?php
function myGenerator(){
yield 'value1';
yield 'value2';
yield 'value3';
}
foreach(myGenerator() as $yieldedValue){
echo $yieldedValue,PHP_EOL;
}
/*
输出代码如下:
value1
value2
value3
*/
假设我们需要迭代一个大小为4GB的CSV文件,而服务器只允许PHP使用1GB内存,因此不能把整个文件都加载到内存中,下面的代码就演示了如何用生成器完成这个操作。
演示为了方便使用21MB的CSV文件
<?php
// 使用生成器读取
function getRows($file){
$handle = fopen($file,'rb');
if ($handle===false){
throw new Exception();
}
while (feof($handle)===false){
yield fgetcsv($handle);
}
fclose($handle);
}
foreach (getRows('data.csv') as $row){
// 逻辑代码
}
/*
内存消耗:696bytes ≈ 0.679688kb
耗时:4.6920030117035ms
*/
// 使用传统方法读取
$list=file('data.csv',FILE_IGNORE_NEW_LINES);
foreach ($list as $val){
explode(',',$val); // 进行分割处理
}
/*
内存消耗:52330448bytes ≈ 51103.95313kb ≈ 49.9062mb
耗时: 0.3206090927124ms
*/
生成器是功能多样性和间接性之间的折中方案。生成器是只能前进的的迭代器,这意味着不能使用生成器在数据集中执行后退、快进或查找操作,只能让生成器计算并产生下一个值。迭代大型数据集或数列时,最适合使用生成器,因为这样占用的系统内存极少。生成器也能完成迭代器能完成的简单任务,而且使用的代码较少。
生成器没为PHP添加新功能,不用生成器也能做生成器能做的事情。只不过生成器简化了某些任务,而且使用的内存更少。如果需要更多功能,比如在数据集中执行后退、快进、或查找操作,最好自己编写类,实现Iterator接口(https://www.php.net/manual/zh/class.iterator.php),或者使用PHP标准库中的某个原生迭代器(https://www.php.net/manual/zh/spl.iterators.php)。
闭包(匿名函数)
闭包需要PHP5.3.0+
这两个特性非常有用,每个PHP开发者都应该掌握。
闭包 是指在创建时封装周围状态的函数,即使闭包所在的环境不存在了,闭包中封装的状态依然存在。这个概念很难理解,不过一旦掌握了,用处非常大。
匿名函数 其实就是没有名称的函数,匿名函数可以赋值给变量,还能像其他任何PHP对象那样传递,不过匿名函数仍是函数,因此可以调用,还可以传入参数。匿名函数特别适合函数或方法的回调
:::tips 注意: 理论上来讲,闭包和匿名函数是不同的概念。不过,PHP将其是作相同的概念。 :::
下面创建了一个闭包对象,然后将其赋值给 $closure
变量。闭包和普通的PHP函数很像:使用的句法相同,也接受参数,而且能返回值。不过,匿名函数没有名称。
:::info
我们之所以能调用 $closure
变量,我因为这个变量的值是一个闭包,而且闭包对象实现了 __invoke()
魔术方法。只要变量名后有 “()”,PHP就会查找并调用 __invoke()
方法。
:::
<?php
// 创建一个闭包函数
$closure=function ($name){
echo 'hello '.$name;
};
// 输出 hello zhangsan
$closure('zhangsan');
在闭包出现之前,PHP开发者别无选择,只能单独创建“具名函数”,然后使用名称引用那个函数,这么做,代码执行的稍微慢一点,而且要把毁掉的实现使用场景隔离开了。
使用闭包写法更简洁,如果只需要调用一次没必要单独定义“具名函数” incrementNumber()
,把闭包当作回调使用,写出的代码更简洁,更清晰。
<?php
// 闭包写法
$numbersPlusOne = array_map(function ($number) {
return $number + 1;
}, [2, 3, 4]);
print_r($numbersPlusOne);
// 传统写法
function incrementNumber ($number){
return $number+1;
}
$numbersPlusTwo=array_map('incrementNumber',[2,3,4]);
print_r($numbersPlusOne);
PHP不会像真正的JavaScript闭包那样自动封装应用的状态,在PHP中必须调用闭包对象 bindTo()
方法或者使用use关键字,把状态封装到闭包上。
使用use关键字附加到闭包上时,附加的变量会记住附加时赋给他的值。具名函数 enclosePerson()
有一个名为$name参数,这个函数返回一个闭包对象,而这个对象封装了$name参数。即使返回的闭包对象跳出了 enclosePerson()
函数的作用域,它也会记住$name的值,因为$name变量仍在闭包中。
<?php
// 附加状态
function enclosePerson($name){
// 可以使用use传入多个参数,使用逗号分隔
return function($doCommand) use($name){
return 'name:'.$name . ' doCommand:'.$doCommand;
};
}
// 把字符串“Clay”封装在闭包中
$clay= enclosePerson('Clay');
// 传入参数,调用闭包
echo $clay('get me sweet tea!');
// 输出--> "name:Clay doCommand:get me sweet tea!"
PHP闭包就是对象,不过有一个 __invoke()
魔术方法和 bindTo()
方法,仅此而已。
bindTo()方法为闭包增加了一些有趣的潜力,我们可以使用这个方法把Closure对象的内部状态绑定到其他对象上。 bindTo()
方法的第二个参数很重要,其作用是指定绑定闭包的那个对象所属的PHP类。因此闭包可以访问绑定闭包的对象中受保护和私有的成员变量。
PHP框架经常使用bindTo()方法把路由URL映射到匿名回调函数上,框架会把匿名函数绑定到应用对象上,这么做可以在这个匿名函数中使用 $this
关键词引用重要的应用对象。
<?php
class App
{
protected $routes = array();
protected $responsetatus= '200 OK';
protected $responseContentType= 'text/html';
protected $responseBody= 'hello world';
public function addRoute($routePath,$routeCallback)
{
$this->routes[$routePath]= $routeCallback->bindTo($this,__CLASS__);
}
public function dispatch($currentPath)
{
foreach ($this->routes as $routePath => $callback){
if ($routePath == $currentPath){
$callback();
}
}
header('HTTP/1.1: '.$this->responsetatus);
header('Content-type: '.$this->responsetatus);
header('Content-length: '. mb_strlen($this->responseBody));
echo $this->responseBody;
}
}
$app=new App();
// 给这个路由设置一个回调函数
$app->addRoute('/users/josh',function(){
$this->responseContentType=' application/json;charset=utf8';
$this->responseBody='{"name":"Josh"}';
});
// 当执行这里的时候 执行回调函数
$app->dispatch('/users/josh');
字节码缓存 Zend OPcache
Zend OPcache 需要PHP5.5.0+
字节码缓存不是PHP的新特性,很多独立缓存的扩展都可以实现缓存,例如 Alternative PHP Cache APC
eAccelerator
ionCube
和 XCache
。但这些扩展都没有集成到PHP核心中,从PHP5.5.0开始,PHP内置了字节码缓存功能,名为 Zend OPcache。
PHP是解释型语言,PHP解释器执行PHP脚本时会解析PHP脚本代码,把PHP代码编译成一系列Zend操作码,然后执行字节码。每次请求PHP文件都是这样,会消耗很多资源,如果每次HTTP请求PHP都需要不断地解析、编译、执行PHP脚本,消耗的资源更多。如果有一种方式能缓存预先编译好的字节码,减少应用的响应时间,降低系统资源消耗的压力,那该多好啊。
字节码缓存能存储预先编译好的PHGP字节码。这意味着,请求PHP脚本时,PHP解释器不用每次都读取,解析和编译PHP代码。PHP解释器会从内存中读取预先编译好的字节码,然后立即执行。这样能节省很多时间,极大的提升应用的性能。
:::info 默认情况下 “Zend OPcache”没有启用,编译PHP时我们必须明确指定启用ZendOPcache :::
:::danger 如果使用分析器 “Xdebug” 在PHP.ini 文件中务必先加载 “Zend OPcache” 扩展,再加载Xdebug。 :::
Zend OPcache使用起来很简单,因为启动之后它就会自动运行。Zend OPcache会自动在内存中缓存预先编译好的PHP字节码,如果缓存了某个文件的字节码,就执行对应的字节码。
内置HTTP服务器
内置服务器需要 PHP5.4.0+
PHP内置了一个Web服务器,虽然这个Web服务器不应该应用于生产环境中,但对本地开发来说是一个极好的工具。
:::info 记住,PHP内置的是一个Web服务器。这个服务器知道怎么处理HTTP协议,能够处理静态资源和PHP文件,使用它我们无需安装MAMP、WAMP或大型Web服务器,就能在本地编写并预览HTML :::
启动服务器
这个PHP Web服务器启动很容易,打开应用终端 进入项目的根目录,然后执行如下命令即可:php -S locahost:4000
上述命令会启动一个PHP Web服务器,地址是locahost。这个服务器监控的端口是4000。当前工作目录就是这个Web服务器的文档根目录。
使用浏览器访问:”http://locahost:4000",就能预览应用了。在Web浏览器访问应用时,每一个HTTP请求的信息都会在终端的标准输出中,因此,我们可以查看应用是否抛出了400或500相应,
有时,我们需要在同一局域网中的另一台设备中访问这个Web服务器,比如在手机端或者虚拟机中预览应用,这个时候我们可以把 “locahost” 换成 “0.0.0.0”,让服务器监听所有IP的4000端口:php -S 0.0.0.0:4000
需要停止PHP Web服务器时,可以关闭终端应用,直接按 Ctrl+C
键。
配置这个服务器
应用经常需要使用专属的PHP初始化配置文件,尤其是对内存用量、文件上传、分析或字节码缓存有特殊需求时,一定要单独配置。我们可以使用 -c
选项,让PGP内置的服务器使用指定的初始化文件:php -S locahost:8000 -c app/config/php.ini
:::info 最好把自定义的初始化文件放在应用的根目录中。如果需要和团队中的其他开发者分享,还可以把初始化文件纳入版本控制系统(git/svn) :::
设置根目录
如果需要指定服务器的根目录,比如 “public” 可以使用 -t
命令:php -S locahost:8000 -t ./public
路由器脚本**
PHP内置的服务器明显遗漏了一个重要功能:与Apache和Nginx不同,它不支持 .htaccess
文件,因此这个服务器和男使用多数流行PHP框架中常见的前端控制器。
:::tips 前端控制器是一个PHP文件,用于转发所有HTTP请求(通过.htaccess文件或重写规则实现)。前端控制器文件的职责是,分发请求,以及调度适当的PHP代码。Symfony和其他流行的框架都使用了这种模式。 :::
PHP内置的服务器使用了路由器脚本来弥补了这个遗漏的功能。处理每个HTTP请求钱,会先执行这个路由器脚本,如果结果非false,返回当前HTTP中引入的静态资源URL。否则,把路由器脚本的执行结果当作HTTP响应主体返回。换句话说,路由器脚本的作用其实和 .htaccess
文件一样。
使用路由器脚本很简单,只需要在启动这个PHP内置服务器的时候指定这个PHP文件的路径:php -S locahost:8000 router.php
判断是否正在使用内置服务器
有时我们需要所运行的环境是PHP的内置服务器还是传统的Apache、nginx服务器。之所以想知道这一点,是因为一部分设定是为传统服务器设定的,而不想为PHP内置服务器设置。我们可以使用 php_sapi_name()
函数查明使用的是哪一个PHP Web服务器。如果当前脚本使用的是PHP内置服务器,则会返回字符串 “cli-server”。
缺点
PHP内置服务器不能用于生产环境中,只能在本地开发环境中使用。如果在生产设备中使用PHP内置的Web服务器,会让很多用户失望。
- 内置的服务器性能不是最好的,因为一次只能处理一个请求,其他请求会受到阻塞。如果每个PHP文件必须等待慢速的数据库查询得到结果或远程API返回响应,Web应用将会处于停顿状态。
- 内置服务器仅支持少量的媒体类型
- 内置的服务器通过路由器脚本支持少量的url重写规则。如果需要更高级的URL重写规则,还是用Apache或Nginx