php 类自动执行方法,php的类自动加载

本文分为两部分:第一部分讲__autoload()函数实现的类自动加载。第二部分讲spl_autoload_register()函数实现的类自动加载。第三部分讲spl_autoload_register()相比较__autoload()的好处。第四部分讲yii的autoload机制。

第一部分: _autoload()函数实现的类自动加载

在面向对象编程中,都是以对象为单位的操作,如果我有两个不同的类,类A和类B,在同一个文件里,实例化对象,就能在这个文件同时调用类A和类B的函数

#a.php

class A{

public function funA()

{

echo 'The class:'.__class__.'
';

}

}

class B{

public function funB()

{

echo 'The class:'.__class__.'
';

}

}

$a = new A();

$b = new B();

$a->funA(); //The class:A

$b->funB(); //The class:B

两个类都在同一个文件,所有能运行成功,但现在大部分应用都分MVC,不同类专门处理特定的事物,比如C(Controller),只执行与事物有关的逻辑操作,这些文件很明显分属于不同目录下:

|—root

|—controller

|—controllerA.php

|—model

|—modelA.php

|—view

|—viewA.php

这时我要在modelA.php里怎么引用controllerA.php的函数呢?这就用到__autoload()了。

#保存在MyClass.php

class MyClass {

public function getNamespace()

{

return get_class($this);

}

}

现在我们在同级目录下引用这个类:

function __autoload($name)

{

$file = realpath(__DIR__).'/'.$name.'.php';

if(file_exists($file))

{

require_once($file);

if(class_exists($name,false))

{

return true;

}

return false;

}

return false;

}

$obj = new MyClass();

echo $obj->getNamespace(); //输出 MyClass;

当你引用不存在的类时,__autoload就会被调用,并且你的类名会被作为参数传送过去(当你同时使用命名空间,包含命名空间部分会一起作为参数传送)。

下面我们把命名空间和自动加载类合并使用:

命名空间,PHP从5.3开始支持命名空间(namespace),这个在C语言里非常常见,刚开始我不理解,直到我完整接触一个项目,需要加载在不同目录下Class时,恍然大悟。OK,下面讲一讲自己的理解吧。

用不同的命名空间区别相同的函数名,同时命名空间也可以作为引入类文件的路径。

(在WEB项目中,当在client输入一个url时,一般会有一个入口文件,在这个文件里解析你要调用的controller,以及传递的参数,然后动态的加载相应类文件,这其实就牵扯到一点路由规则,当然还有另外一种,在项目初始化之初就把所有类文件全部加载在内存中,这样,每次请求响应都不用再加载,常驻内存后响应要快点。)

#首先建立一个MyClass目录,在该目录下新MyClass.php文件,代码如下。

namespace MyClass;

class MyClass {

public function getNamespace()

{

return get_class($this);

}

}

#在跟MyClass目录同级的目录下,新建一个文件,内容如下

function __autoload($name)

{

$class_path = str_replace('\\',DIRECTORY_SEPARATOR,$name); //把表示命名空间的分割符号,转换成表示目录结构的斜线

$file = realpath(__DIR__).'/'.$class_path.'.php';

if(file_exists($file))

{

require_once($file); //引入文件

if(class_exists($name,false)) //带有命名空间的类名

{

return true;

}

return false;

}

return false;

}

echo MyClass\MyClass::getNamespace(); //输出: MyClass\MyClass;

这样非常容易避免了,当一个项目非常大时,不停的require文件。

让我们再思考,有没有更工程化的解决方案?当然有,我们可以建立自己的 Autoload 类,在这个类里面自动加载不同的类(可以包含命名空间),需要引用其他类的文件,只需要显示写入这个 Autoload 类即可。

我们知道PHP可以实现自动加载,避免了繁重的体力活,代码更规范,整洁。那如果我们把这个自动加载再升华一下,变成自动加载类,每次只需要引入这个类,那么其他类就自动加载了,已经开源,仓库地址在这里。同时如果加载后可以使其常驻内存,那么效率又高了。。。

关于自动的基础部分可以参考这里

下面有几点需要注意

1.为了实现通用性,设置一个根目录,在大型项目中,目录结构比较复杂,每次加载都应该有一个起始开始的目录位置,这样才能向后兼容。当然也可以设置多个这种目录,放到数组里。

2.带有命名空间的类,需要把转义符转换成表示目录结构的斜线,加载的类是带有命名空间的,因为,该类是属于该命名空间下。

好了,下面贴上代码:

if(!defined('ROOTDIR'))

{

define('ROOTDIR',realpath(__DIR__.'/../')); //定义更目录

}

class Autoloader {

public static function myAutoload( $name )

{

$class_path = str_replace('\\',DIRECTORY_SEPARATOR,$name);

$file = ROOTDIR.'/'.$class_path.'.php';

if( file_exists( $file ) )

{

require_once( $file );

if( class_exists($name, false) )

{

return true;

}

}

return false;

}

}

spl_autoload_register('Autoloader::myAutoload');

自动加载的使用必须显示加载,它是加载其他类的加载器,我们已经重载了PHP的自动加载没有机制可以加载这个Autoloader,所以要 require_once('Autoloader.php');

很简单吧,其实这个可以做的更强大。

比如,我把这个做成一个中间件,命名为Bootstrap,这个可以用于不同项目,而这些项目可以位于不同或者同一个WEB目录下,这时需要一个静态的属性表示所有可能的应用目录或者不同WEB目录

public static $rootPath = array();

在加载时,就需要一个循环遍历每一个可能的目录,找到即加载。

同时,我们可以设置一个 setRootPath() 方法,用于设置不同项目的目录。

public function setRootPath( $path )

{

//do something

return $this;

}

这样就能实现链式操作的添加,是不是很神奇。。。

第二部分:spl_autoload_register类自动加载函数

接下来让我们要在含有命名空间的情况下去实现自动加载。这里我们使用 spl_autoload_register() 函数来实现,这需要你的 PHP 版本号大于 5.12。

spl_autoload_register 函数的功能就是把传入的函数(其实就是一系列的autoload函数)注册到 SPL __autoload 函数队列中,并移除系统默认的 __autoload() 函数。

一旦调用 spl_autoload_register() 函数,当调用未定义类时,系统就会按顺序调用注册到 spl_autoload_register() 函数的所有函数,而不是自动调用 __autoload() 函数。

现在,我们来创建一个 Linux 类,它使用 os 作为它的命名空间(建议文件名与类名保持一致):

namespace os; // 命名空间

class Linux // 类名

{

function __construct()

{

echo '

' . __CLASS__ . '

';

}

}

接着,在同一个目录下新建一个 PHP 文件,使用 spl_autoload_register 以函数回调的方式实现自动加载:

spl_autoload_register(function ($class) { // class = os\Linux

/* 限定类名路径映射 */

$class_map = array(

// 限定类名 => 文件路径

'os\\Linux' => './Linux.php',

);

/* 根据类名确定文件名 */

$file = $class_map[$class];

/* 引入相关文件 */

if (file_exists($file)) {

include $file;

}

});

new \os\Linux();

这里我们使用了一个数组去保存类名与文件路径的关系,这样当类名传入时,自动加载器就知道该引入哪个文件去加载这个类了。

但是一旦文件多起来的话,映射数组会变得很长,这样的话维护起来会相当麻烦。如果命名能遵守统一的约定,就可以让自动加载器自动解析判断类文件所在的路径。

第三部分:既然有__autoload函数来实现了类的自动加载,那么为什么要有spl_autoload_register的存在??

如果在一个系统的实现中,如果需要使用很多其它的类库,这些类库可能是由不同的开发人员编写的,其类名与实际的磁盘文件的映射规则不尽相同。这时如果要实现类库文件的自动加载,就必须在__autoload()函数中将所有的映射规则全部实现,这样的话__autoload()函数有可能 会非常复杂,甚至无法实现。最后可能会导致__autoload()函数十分臃肿,这时即便能够实现,也会给将来的维护和系统效率带来很大的负面影响。在这种情况下,难道就没有更简单清晰的解决办法了吧?答案当然是:NO!

spl_autoload_register() 满足了此类需求。 它实际上创建了 autoload 函数的队列,按定义时的顺序逐个执行。相比之下, __autoload() 只可以定义一次。

bool spl_autoload_register ([ callable $autoload_function [, bool $throw = true [, bool $prepend = false ]]] )

我们继续改写上面那个例子:

function loader($class){

$file = $class . '.php';

if (is_file($file)) {

require_once($file);

}

}

spl_autoload_register('loader');

$a = new A();

或者直接使用匿名函数:

spl_autoload_register(function($file){

$file = $class . '.php';

if (is_file($file)) {

require_once($file);

}

});

$a = new A();

这样子也是可以正常运行的,这时候php在寻找类的时候就没有调用__autoload而是调用我们自己定义的函数loader了。同样的道理,下面这种写法也是可以的:

class Loader {

public static function loadClass($class){

$file = $class . '.php';

if (is_file($file)) {

require_once($file);

}

}

}

spl_autoload_register(array('Loader', 'loadClass'));

//spl_autoload_register(array(__CLASS__, 'loadClass'));

//spl_autoload_register(array($this, 'loadClass'));

$a = new A();

第四部分:yii的类自动加载机制的实现

Yii的类自动加载,依赖于PHP的 spl_autoload_register() , 注册一个自己的自动加载函数(autoloader),并插入到自动加载函数栈的最前面,确保Yii的autoloader会被最先调用。

类自动加载的这个机制的引入要从入口文件 index.php 开始说起:

defined('YII_DEBUG') or define('YII_DEBUG', false);

defined('YII_ENV') or define('YII_ENV', 'prod');

// 这个是第三方的autoloader

require(__DIR__ . '/../../vendor/autoload.php');

// 这个是Yii的Autoloader,放在最后面,确保其插入的autoloader会放在最前面

require(__DIR__ . '/../../vendor/yiisoft/yii2/Yii.php');

// 后面不应再有autoloader了

require(__DIR__ . '/../../common/config/aliases.php');

$config = yii\helpers\ArrayHelper::merge(

require(__DIR__ . '/../../common/config/main.php'),

require(__DIR__ . '/../../common/config/main-local.php'),

require(__DIR__ . '/../config/main.php'),

require(__DIR__ . '/../config/main-local.php')

);

$application = new yii\web\Application($config);

$application->run();

这个文件主要看点在于第三方autoloader与Yii 实现的autoloader的顺序。不管第三方的代码是如何使用 spl_autoload_register() 来注册自己的autoloader的,只要Yii 的代码在最后面,就可以确保其可以将自己的autoloader插入到整个autoloder 栈的最前面,从而在需要时最先被调用。

接下来,看看Yii是如何调用 spl_autoload_register() 注册autoloader的, 这要看 Yii.php 里发生了些什么:

require(__DIR__ . '/BaseYii.php');

class Yii extends \yii\BaseYii

{

}

// 重点看这个 spl_autoload_register

spl_autoload_register(['Yii', 'autoload'], true, true);

// 下面的语句读取了一个映射表

Yii::$classMap = include(__DIR__ . '/classes.php');

Yii::$container = new yii\di\Container;

这段代码,调用了 spl_autoload_register(['Yii', 'autoload', true, true]) ,将 Yii::autoload() 作为autoloader插入到栈的最前面了。并将 classes.php 读取到 Yii::$classMap 中,保存了一个映射表。

在上面的代码中,Yii类是里面没有任何代码,并未对 BaseYii::autoload() 进行重载,所以,这个 spl_autoload_register() 实际上将 BaseYii::autoload() 注册为autoloader。如果,你要实现自己的autoloader,可以在 Yii 类的代码中,对 autoload() 进行重载。

在调用 spl_autoload_register() 进行autoloader注册之后,Yii将 calsses.php 这个文件作为一个映射表保存到 Yii::$classMap 当中。这个映射表,保存了一系列的类名与其所在PHP文件的映射关系,比如:

return [

'yii\base\Action' => YII2_PATH . '/base/Action.php',

'yii\base\ActionEvent' => YII2_PATH . '/base/ActionEvent.php',

... ...

'yii\widgets\PjaxAsset' => YII2_PATH . '/widgets/PjaxAsset.php',

'yii\widgets\Spaceless' => YII2_PATH . '/widgets/Spaceless.php',

];

这个映射表以类名为键,以实际类文件为值,Yii所有的核心类都已经写入到这个 classes.php 文件中,所以,核心类的加载是最便捷,最快的。现在,来看看这个关键先生 BaseYii::autoload()

public static function autoload($className)

{

if (isset(static::$classMap[$className])) {

$classFile = static::$classMap[$className];

if ($classFile[0] === '@') {

$classFile = static::getAlias($classFile);

}

} elseif (strpos($className, '\\') !== false) {

$classFile = static::getAlias('@' . str_replace('\\', '/',

$className) . '.php', false);

if ($classFile === false || !is_file($classFile)) {

return;

}

} else {

return;

}

include($classFile);

if (YII_DEBUG && !class_exists($className, false) &&

!interface_exists($className, false) && !trait_exists($className,

false)) {

throw new UnknownClassException(

"Unable to find '$className' in file: $classFile. Namespace missing?");

}

}

从这段代码来看Yii类自动加载机制的运作原理:

• 检查 $classMap[$className] 看看是否在映射表中已经有拟加载类的位置信息;

• 如果有,再看看这个位置信息是不是一个路径别名,即是不是以 @ 打头, 是的话,将路径别名解析成实际路径。 如果映射表中的位置信息并非一个路径别名,那么将这个路径作为类文件的所在位置。 类文件的完整路径保存在 $classFile ;

• 如果 $classMap[$className] 没有该类的信息, 那么,看看这个类名中是否含有 \ , 如果没有,说明这是一个不符合规范要求的类名,autoloader直接返回。 PHP会尝试使用其他已经注册的autoloader进行加载。 如果有 \ ,认为这个类名符合规范,将其转换成路径形式。 即所有的 \ 用 /替换,并加上 .php 的后缀。

• 将替换后的类名,加上 @ 前缀,作为一个路径别名,进行解析。 从别名的解析过程我们知道,如果根别名不存在,将会抛出异常。 所以,类的命名,必须以有效的根别名打头:

// 有效的类名,因为@yii是一个已经预定义好的别名

use yii\base\Application;

// 无效的类名,因为没有 @foo 或 @foo/bar 的根别名,要提前定义好

use foo\bar\SomeClass;

• 使用PHP的 include() 将类文件加载进来,实现类的加载。

从其运作原理看,最快找到类的方式是使用映射表。 其次,Yii中所有的类名,除了符合规范外,还需要提前注册有效的根别名。

运用自动加载机制

在入口脚本中,除了Yii自己的autoloader,还有一个第三方的autoloader:

require(__DIR__ . '/../../vendor/autoload.php');

这个其实是Composer提供的autoloader。Yii使用Composer来作为包依赖管理器,因此,建议保留Composer的autoloader,尽管Yii的autoloader也能自动加载使用Composer安装的第三方库、扩展等,而且更为高效。但考虑到毕竟是人家安装的,人家还有一套自己专门的规则,从维护性、兼容性、扩展性来考虑,建议保留Composer的autoloader。

如果还有其他的autoloader,一定要在Yii的autoloader注册之前完成注册,以保证Yii的autoloader总是最先被调用。

如果你有自己的autoloader,也可以不安装Yii的autoloaer,只是这样未必能有Yii的高效,且还需要遵循一套类似的类命名和加载的规则。就个人的经验而言,Yii的autoloader完全够用,没必要自己重复造轮子。

至于Composer如何自动加载类文件,这里就不过多的占用篇幅了。可以看看 Composer的文档 。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值