胡乱探讨了包括autoload,异常处理,多进程,反射使用,trait代码复用,生成器等php语言的高级特性
1.autoload自动加载
- spl_autoload_register() 函数可以注册任意数量的自动加载器,当使用尚未被定义的类(class)和接口(interface)时自动去加载。通过注册自动加载器,脚本引擎在 php 出错失败前有了最后一个机会加载所需的类
- 尽管 __autoload() 函数也能自动加载类和接口,但更建议使用 spl_autoload_register() 函数。 spl_autoload_register() 提供了一种更加灵活的方式来实现类的自动加载(同一个应用中,可以支持任意数量的加载器,比如第三方库中的)。因此,不再建议使用 __autoload() 函数,在以后的版本中它可能被弃用。
- 自动加载不可用于 php 的 CLI 交互模式
自动加载简单实例
<?php
spl_autoload_register(function ($class_name) {
require_once $class_name . '.php';
});
$obj = new MyClass1();
$obj2 = new MyClass2();
?>
自动加载抛出异常
<?php
spl_autoload_register(function ($name) {
echo "Want to load $name.\n";
throw new Exception("Unable to load $name.");
});
try {
$obj = new NonLoadableClass();
} catch (Exception $e) {
echo $e->getMessage(), "\n";
}
?>
spl_autoload_register
-
将函数注册到SPL __autoload函数队列中。如果该队列中的函数尚未激活,则激活它们。
-
如果在你的程序中已经实现了__autoload()函数,它必须显式注册到__autoload()队列中。因为 spl_autoload_register()函数会将Zend Engine中的__autoload()函数取代为spl_autoload()或spl_autoload_call()。
-
如果需要多条 autoload 函数,spl_autoload_register() 满足了此类需求。 它实际上创建了 autoload 函数的队列,按定义时的顺序逐个执行。相比之下, __autoload() 只可以定义一次。
-
参数:autoload_function /*欲注册的自动装载函数。如果没有提供任何参数,则自动注册 autoload 的默认实现函数spl_autoload()。
-
参数:throw /*此参数设置了 autoload_function 无法成功注册时, spl_autoload_register()是否抛出异常。
-
参数:prepend /*如果是 true,spl_autoload_register() 会添加函数到队列之首,而不是队列尾部。
composer的自动加载流程
- index.php文件 在config/loader.php文件中会引入vendor.php文件
- autoloader.php文件 1.会引入autoload_real.php文件 2.会返回一个自动加载器
<?php
// autoload.php @generated by Composer
require_once __DIR__ . '/composer' . '/autoload_real.php'; //会返回一个加载类
return ComposerAutoloaderInite572cdf763ad8e4d6770ce15b9328edb::getLoader();
- autoload_real.php文件
<?php
// autoload_real.php @generated by Composer
class ComposerAutoloaderInit85b4dbf6b714d62ec745fcf3dd1a5041
{
private static $loader;
// 在实例化ClassLoader时调用该函数
public static function loadClassLoader($class)
{
// 该函数在getLoader方法里被注册为__autoload的实现,在实例化类时,如果类不存在,会自动调用该方法
// 如果实例化的函数是Composer\Autoload\ClassLoader 则引入该类
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
// 注册 loadClassLoader函数作为 __autoload 的实现
spl_autoload_register(array('ComposerAutoloaderInit85b4dbf6b714d62ec745fcf3dd1a5041', 'loadClassLoader'), true, true);
// 实例化该方法时会自动调用上述方法注册的函数,
// loadClassLoader函数里引入 ClassLoader类
self::$loader = $loader = new \Composer\Autoload\ClassLoader();
spl_autoload_unregister(array('ComposerAutoloaderInit85b4dbf6b714d62ec745fcf3dd1a5041', 'loadClassLoader'));
// PSR-0 的规则
$map = require __DIR__ . '/autoload_namespaces.php';
foreach ($map as $namespace => $path) {
$loader->set($namespace, $path);
}
// PSR-4 的规则
$map = require __DIR__ . '/autoload_psr4.php';
foreach ($map as $namespace => $path) {
$loader->setPsr4($namespace, $path);
}
$classMap = require __DIR__ . '/autoload_classmap.php';
if ($classMap) {
$loader->addClassMap($classMap);
}
// 注册给定的函数作为 __autoload 的实现,具体参考下文
// Class-Map部分
$loader->register(true);
// Files方式 直接加载需要访问的文件
$includeFiles = require __DIR__ . '/autoload_files.php';
foreach ($includeFiles as $fileIdentifier => $file) {
composerRequire85b4dbf6b714d62ec745fcf3dd1a5041($fileIdentifier, $file);
}
return $loader;
}
}
function composerRequire85b4dbf6b714d62ec745fcf3dd1a5041($fileIdentifier, $file)
{
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
// 加载需要调用的文件
require $file;
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
}
}
- 启动过程 index.php引入loader.php,loader.php引入了composer的autoloader.php,在这个文件中,会调用getLoader方法,按照PSR-0与PSR-4标准按照相应的标准进行文件的引入。
2.异常处理
- 抛出异常但是不catch是,会提示
Fatal error: Uncaught exception 'Exception'
with message 'Value must be 1 or below' in C:\webfolder\test.php:6
Stack trace: #0 C:\webfolder\test.php(12):
checkNum(28) #1 {main} thrown in C:\webfolder\test.php on line 6
- catch" 代码块会捕获异常,并创建一个包含异常信息的对象
- 设置顶层异常处理器,设置用户自定义的异常处理函数,用于没有用 try/catch 块来捕获的异常 set_exception_handler($ex)
- 需要进行异常处理的代码应该放入 try 代码块内,以便捕获潜在的异常。
- 每个 try 或 throw 代码块必须至少拥有一个对应的 catch 代码块。
- 使用多个 catch 代码块可以捕获不同种类的异常。
- 可以在 try 代码块内的 catch 代码块中再次抛出(re-thrown)异常。
3.多进程
- php处理多并发主要是依赖服务器或php-FPM的多进程及它们进程的复用,但php实现多进程也意义重大,尤其是在后台Cli模式下处理大量数据或运行后台DEMON守护进程时,多进程的优势不用多说
- php的多线程也曾被人提及,但进程内多线程资源共享和分配的问题难以解决。php也有多线程想关的扩展 pthreads ,但据说不太稳定
- 需要两个扩展 pcntl和 posix
- 创建子进程: pcntl_fork()-在当前进程当前位置产生子进程,子进程会继承父进程的上下文,并且和父进程一样从pcntl_fork()函数的位置继续向下执行,pcntl_fork()函数会返回不同的值,以便于区分各个分支,并执行不同的任务
- 父进程的返回值为1(pid),后续的子进程的pid都比这个值大
- 子进程中,此函数的返回值会是固定值0,我们也可以通过判断pcntl_fork()的返回值为0来确定子进程
- 简单的例子
<?php
$ppid = posix_getpid();
$pid = pcntl_fork();
if ($pid == -1) {
throw new Exception('fork子进程失败!');
} elseif ($pid > 0) {
cli_set_process_title("我是父进程,我的进程id是{$ppid}.");
sleep(30); // 保持30秒,确保能被ps查到
} else {
$cpid = posix_getpid();
cli_set_process_title("我是{$ppid}的子进程,我的进程id是{$cpid}.");
sleep(30);
}
管理子进程
使用信号来管理子进程
分发信号处理器
- 我们通过在父进程接收子进程传来的信号,判断子进程状态,来对子进程进行管理。
- 我们需要在父进程里使用pcntl_signal()函数和pcntl_signal_dispatch()函数来给各个子进程安装信号处理器。
<?php
pcntl_signal (int $signo , callback $handler) 安装一个信号处理器;
#$signo是待处理的信号常量,callback是其处理函数
pcntl_signal_dispatch () 调用每个等待信号通过pcntl_signal()安装的处理器
- php内常见的信号常量有:
SIGCHLD 子进程退出成为僵尸进程会向父进程发送此信号
SIGHUP 进程挂起
SIGTEM 进程终止
... // 其他请在手册中查看
- 安装并调用信号处理器后,一旦子进程有相应的信号返回给父进程,父进程就可以调用相应的callback函数对子进程处理;
处理子进程
- posix_kill():此函数并不能顾名思义,它通过向子进程发送一个信号来操作子进程,在需要要时可以选择给子进程发送进程终止信号来终止子进程;
- pcntl_waitpid():等待或返回fork的子进程状态,如果指定的子进程在此函数调用时已经退出(俗称僵尸进程),此函数将立刻返回,并释放子进程的所有系统资源,此进程可以避免子进程变成僵尸进程,造成系统资源浪费;
总结
1.使用多进程, 子进程结束以后, 内核会负责回收资源
2.使用多进程,子进程异常退出不会导致整个进程Thread退出. 父进程还有机会重建流程.
3.一个常驻主进程, 只负责任务分发, 逻辑更清楚.
4.反射的使用方法 获取类成员及其属性 ReflectionClass
- 面向对象编辑中对象被赋予了自省的能力,而这个自省的过程就是反射.
- 给你一个光秃秃的对象,我可以仅仅通过这个对象就能知道它所属的类,拥有哪些方法.
- 反射指在php运行状态中,扩展分析php程序,导出或提取出关于类,方法,属性,参数等详细信息,包括注释.这种动态获取信息以及动态调用对象方法的功能称为反射API
使用反射
<?php
class HandsonBoy
{
public $name = 'chenqionghe';
public $age = 18;
public function __set($name,$value)
{
echo '您正在设置私有属性'.$name.'<br >值为'.$value.'<br>';
$this->$name = $value;
}
public function __get($name)
{
if(!isset($this->$name))
{
echo '未设置'.$name;
$this->$name = "正在为你设置默认值".'<br>';
}
return $this->$name;
}
}
$boy = new HandsonBoy();
echo $boy->name.'<br />';
$boy->hair = 'short';
获取对象的方法和属性
<?php
$reflect = new ReflectionObject($boy);
$props = $reflect->getProperties();
//获取属性的名字
foreach($props as $prop)
{
print $prop->getName().php_EOL;
}
//获取对象方法列表
$methos = $reflect->getMethods();
foreach($methos as $method)
{
print $method->getName().php_EOL;
}
不用反射API,使用class函数(只针对公开的属性和方法)
<?php
//返回对象属性的关联数组
var_dump(get_object_vars($boy));
//类属性
var_dump(get_class_vars(get_class($boy)));
//返回由类的属性的方法名组成的数组
var_dump(get_class_methods(get_class($boy)));
打印类的代码的实例
<?php
/**
* @param $classObject 对象或者类名
*/
function getClass($classObject)
{
$object = new ReflectionClass($classObject);
$className = $object->getName();
$Methods = $Properties = array();
foreach($object->getProperties() as $v)
{
$Properties[$v->getName()] = $v;
}
foreach($object->getMethods() as $v)
{
$Methods[$v->getName()] = $v;
}
echo "class {$className}\n{\n";
is_array($Properties) && ksort($Properties);
foreach($Properties as $k=>$v)
{
echo "\t";
echo $v->isPublic() ? 'public' : '',$v->isPrivate() ? 'private' :'',$v->isProtected() ? 'protected' : '';
$v->isStatic() ? 'static' : '';
echo "\t{$k}\n";
}
echo "\n";
if(is_array($Methods)) ksort($Methods);
foreach($Methods as $k=>$v)
{
echo "\tfunction {$k}()\n";
}
echo "}\n";
}
反射的作用
- 反射可以用于文档生成,因此可以用它对文件里的类进行扫描,逐个生成描述文档.
- 做hook实现插件功能
- 动态代理 作为动态代理的例子:
<?php
class mysql
{
function connect($db)
{
# echo "连接到数据库{$db[0]}" . php_EOL;
}
}
class sqlproxy
{
private $target;
public function __construct($tar)
{
$this->target[] = new $tar;
}
public function __call($name,$args)
{
foreach($this->target as $obj)
{
$r = new ReflectionClass($obj);
if($method = $r->getMethod($name))
{
if($method->isPublic() && !$method->isAbstract())
{
echo "方法前拦截记录LOG".php_EOL;
$method->invoke($obj,$args);
echo "方法后拦截".php_EOL;
}
}
}
}
}
$obj = new sqlproxy('mysql');
$obj->connect('chenqionghe');
弊端
- 反射也会破坏类的封装性,因为反射可以使本不应该暴露的方法或属性被强制暴露了出来,这既是优点也是缺点.
5.trait 代码复用
- 自 php 5.4.0 起,php 实现了一种代码复用的方法,称为 trait。
- Trait 是为类似 php 的单继承语言而准备的一种代码复用机制。Trait 为了减少单继承语言的限制,使开发人员能够自由地在不同层次结构内独立的类中复用 method
- Trait 是 php 多重继承的一种解决方案。例如,需要同时继承两个 Abstract Class, 这将会是件很麻烦的事情,Trait 就是为了解决这个问题。它为传统继承增加了水平特性的组合
用法
简单使用
<?php
trait first_trait{
public function hello(){
return 'hello';
}
}
- 在Class里使用trait,要使用use关键字,使用多个trait时用英文逗号隔开
<?php
trait first_trait{
public function hello(){
return 'hello';
}
}
trait second_trait{
public function world(){
return 'world';
}
}
class first_class{
use first_trait,second_trait;
}
$obj=new first_class();
echo $obj->hello();
echo $obj->world();
-
优先级 从基类继承的成员会被 trait 插入的成员所覆盖。优先顺序是来自当前类的成员覆盖了 trait 的方法,而 trait 则覆盖了被继承的方法。
-
trait之间的嵌套
<?php
trait first_trait{
public function hello(){
echo 'hello';
}
}
trait second_trait{
//trait之间的嵌套
use first_trait;
public function world(){
echo 'world';
}
}
class first_class{
use second_trait;
}
$obj=new first_class();
echo $obj->hello();
echo $obj->world();
- 可以在trait中声明抽象方法,使用它的Class或trait必须实现抽象方法
冲突的解决
- 如果两个 trait 都插入了一个同名的方法,如果没有明确解决冲突将会产生一个致命错误。
- 为了解决多个 trait 在同一个类中的命名冲突,需要使用 insteadof 操作符来明确指定使用冲突方法中的哪一个。
- 以上方式仅允许排除掉其它方法,as 操作符可以将其中一个冲突的方法以另一个名称来引入,相当于方法的别名。
<?php
trait A {
public function smallTalk() {
echo 'a';
}
public function bigTalk() {
echo 'A';
}
}
trait B {
public function smallTalk() {
echo 'b';
}
public function bigTalk() {
echo 'B';
}
}
class Talker {
use A, B {
B::smallTalk insteadof A; //trait B 的smallTalk方法会代替 trait A 的smallTalk方法
A::bigTalk insteadof B; //trait A 的bigTalk方法会代替 trait B 的bigTalk方法
}
}
class Aliased_Talker {
use A, B {
B::smallTalk insteadof A;//trait B 的smallTalk方法会代替 trait A 的smallTalk方法
A::bigTalk insteadof B;//trait A 的bigTalk方法会代替 trait B 的bigTalk方法
B::bigTalk as talk; //使用 as 操作符来定义了 talk方法 来作为 B 的 bigTalk方法 的别名
}
}
$obj=new Talker();
$obj->smallTalk();
$obj->bigTalk();
//结果会输出 bA
$obj2=new Aliased_Talker();
$obj2->talk();//会输出B
修改访问控制
<?php
trait HelloWorld {
public function sayHello() {
echo 'Hello World!';
}
}
// 修改 sayHello 的访问控制
class MyClass1 {
use HelloWorld { sayHello as protected; }
}
// 给方法一个改变了访问控制的别名
// 原版 sayHello 的访问控制则没有发生变化
class MyClass2 {
use HelloWorld { sayHello as private myPrivateHello; }
}
Trait 同样可以定义属性
- 如果 trait 定义了一个属性,那类将不能定义同样名称的属性,否则会产生一个错误。如果该属性在类中的定义与在 trait 中的定义兼容(同样的可见性和初始值)则错误的级别是 E_STRICT,否则是一个致命错误。
6.php的生成器、yield和协程
迭代生成器
(迭代)生成器也是一个函数,不同的是这个函数的返回值是依次返回,而不是只返回一个单独的值.或者,换句话说,生成器使你能更方便的实现了迭代器接口.下面通过实现一个xrange函数来简单说明:
<?php
function xrange($start, $end, $step = 1) {
for ($i = $start; $i <= $end; $i += $step) {
yield $i;
}
}
foreach (xrange(1, 1000000) as $num) {
echo $num, "\n";
}
- range()函数返回从1到100万0的数组
- xrange() 返回的是依次输出这些值的迭代器
优点
- 可以让你在处理大数据集合的时候不用一次性的加载到内存中.甚至你可以处理无限大的数据流.
- 也可以不同通过生成器来实现这个功能,而是可以通过继承Iterator接口实现.但通过使用生成器实现起来会更方便,不用再去实现iterator接口中的5个方法了
生成器为可中断的函数
- 生成器是一种可中断的函数, 在它里面的yield构成了中断点.
- 调用xrange(1,1000000)的时候, xrange()函数里代码其实并没有真正地运行. 它只是返回了一个迭代器
- 调用迭代器的方法一次, 其中的代码运行一次.例如, 如果你调用$range->rewind(), 那么xrange()里的代码就会运行到控制流第一次出现yield的地方. 而函数内传递给yield语句的返回值可以通过$range->current()获取.
- 为了继续执行生成器中yield后的代码, 你就需要调用$range->next()方法. 这将再次启动生成器, 直到下一次yield语句出现. 因此,连续调用next()和current()方法, 你就能从生成器里获得所有的值, 直到再没有yield语句出现.
- 对xrange()来说, 这种情形出现在$i超过$end时. 在这中情况下, 控制流将到达函数的终点,因此将不执行任何代码.一旦这种情况发生,vaild()方法将返回假, 这时迭代结束.
协程
- 协程的支持是在迭代生成器的基础上, 增加了可以回送数据给生成器的功能(调用者发送数据给被调用的生成器函数). 这就把生成器到调用者的单向通信转变为两者之间的双向通信.
- 传递数据的功能是通过迭代器的send()方法实现的. 下面的logger()协程是这种通信如何运行的例子:
<?php
function logger($fileName) {
$fileHandle = fopen($fileName, 'a');
while (true) {
fwrite($fileHandle, yield . "\n");
}
}
$logger = logger(__DIR__ . '/log');
$logger->send('Foo');
$logger->send('Bar')
?>
yield没有作为一个语句来使用, 而是用作一个表达式, 即它能被演化成一个值. 这个值就是调用者传递给send()方法的值. 在这个例子里, yield表达式将首先被”Foo”替代写入Log, 然后被”Bar”替代写入Log.
- 进行接收和发送的例子
<?php
function gen() {
$ret = (yield 'yield1');
var_dump($ret);
$ret = (yield 'yield2');
var_dump($ret);
}
$gen = gen();
var_dump($gen->current()); // string(6) "yield1"
var_dump($gen->send('ret1')); // string(4) "ret1" (the first var_dump in gen)
// string(6) "yield2" (the var_dump of the ->send() return value)
var_dump($gen->send('ret2')); // string(4) "ret2" (again from within gen)
// NULL (the return value of ->send())
?>
多任务协作
-
多任务协作,单核cpu,只能运行某个程序一会儿,这就需要运行一个程序后中断,然后执行其他程序,一段时间后,再回来执行后面的流程。
-
因此yield指令则提供了任务中断自身的一种方法。把控制交回任务调度器,yield还可以用来在任务和调度器之间进行通信
-
一个简单的包装协程函数