PHP 懒加载哪些事儿

PHP自动加载

此篇文章主要介绍加载的php类的方式,以及自动加载的几种模式;
题外话:大家空了可以多看看别人写的框架源码;自然能学到很多高级的东西;

PHPER几个发展阶段

初级阶段

大家在学刚开始学习php的时候常用的加载方式大致如下(大致说下目录结构):

project  项目根目录
├─controller            控制器层
├─model                 模型层
├─view                  视图层
├─config                配置目录
│  ├─config.php         常量定义
│  ├─require_file.php   加载文件   
│  └─...                其他配置
|...                    其他目录

通常启动时需要加载的文件都放在require_file.php中,使用加载函数都是includerequireinclude_once以及require_once;require_file.php就变成下面的样子;

<?php
//引入配置文件
require_once(DIR_CLASS. 'DB.class.php');
require_once(DIR_LIB. 'BussinessLogic.php');
require_once(DIR_LIB. 'Common.php');
require_once(DIR_LIB. 'DBconnectProcessor.php');
require_once(DIR_LIB. 'BusinessLogicTc.php');
require_once(DIR_LIB. 'page/page.php');
require_once(DIR_LIB. 'LogProcessor.php');
require_once(DIR_LIB. 'LogicProcessor.php');
require_once(DIR_CLASS. 'TcApiProcess.class.php');
include_once (DIR_LIB. 'LogProcessor2.php');
require_once(DIR_LANG. 'language.php');
require_once(DIR_CLASS. 'companyAuth.class.php');

include_once(DIR_ROOT. "extend".DS."SDK".DS."RongCloud.php");
include_once(DIR_ROOT. "extend".DS."SDK".DS."SendRequest.php");
include_once(DIR_ROOT. "extend".DS."SDK".DS."methods".DS."Chatroom.php");
include_once(DIR_ROOT. "extend".DS."SDK".DS."methods".DS."Group.php");
include_once(DIR_ROOT. "extend".DS."SDK".DS."methods".DS."Log.php");
include_once(DIR_ROOT. "extend".DS."SDK".DS."methods".DS."Message.php");
include_once(DIR_ROOT. "extend".DS."SDK".DS."methods".DS."Push.php");
include_once(DIR_ROOT. "extend".DS."SDK".DS."methods".DS."SMS.php");
include_once(DIR_ROOT. "extend".DS."SDK".DS."methods".DS."User.php");
include_once(DIR_ROOT. "extend".DS."SDK".DS."methods".DS."Wordfilter.php");

······

这么做的坏处是什么,就是需要的,不需要的都一股脑加载了。当然你可以说,我只加载核心的就可以了,其他的地方需要的时候再去引入加载就行了。但是你不觉得麻烦吗?时间长了,自己在什么地方引入了什么文件,加载了什么文件都不知道了。
如果你从来没有想过要改变这种方式,那么接下来内容,就可以不看了。

中级阶段

此时可能你已经认识到了这种方式的弊端了,并且想要改变什么,这时候你会谷歌php的懒加载,自动加载方面的内容;此时,你可能会接触两个魔术函数__autoload()(官方已不推荐使用该函数)与spl_autoload_register();这时候你可能在spl_autoload_register()函数内写大量的规则去实现自动加载;可是你还是觉得不优雅,此时,你如果还想改变。那么就会进入到下个阶段。
这个阶段你可能大概会写一些,关于自动加载函数,下面这样的:

//第一种方式
function __autoload($classname) {
    if ($classname === 'xxx.php'){
        $filename = "./". $classname .".php";
        include_once($filename);
    } else if ($classname === 'yyy.php'){
        $filename = "./other_library/". $classname .".php";
        include_once($filename);
    } else if ($classname === 'zzz.php'){
        $filename = "./my_library/". $classname .".php";
        include_once($filename);
    }
    // blah
}
//第二种方式
spl_autoload_register(function($className){
    
    $filePath = __DIR__.DIRECTORY_SEPARATOR."...".DIRECTORY_SEPARATOR;
    //完整类名
    $completeClassName = $filePath.$className;
    if(class_exist($filePath.$className)){ //或者file_exist()
        include_once($completeClassName.".php");
    }else{
        throw new Exception("can't find this class(".$class.")!");
    }
})
//第三种方式
spl_autoload_register('my_library_loader');
spl_autoload_register('other_library_loader');
spl_autoload_register('basic_loader');

function my_library_loader($classname) {
    $filename = "./my_library/". $classname .".php";
    include_once($filename);
}
function other_library_loader($classname) {
    $filename = "./other_library/". $classname .".php";
    include_once($filename);
}
function basic_loader($classname) {
    $filename = "./". $classname .".php";
    include_once($filename);
}

在这个阶段,你可能还有个过程就是了解了命名空间PSR-4;此时又会产生比以上稍微高级点的方式,我只是写下,我的代码:

//第一个文件 base.php
<?php
//定义常量
defined("DS") or define("DS",DIRECTORY_SEPARATOR);
defined("NS") or define("NS","\\");
defined("PHP_EXT") or define("PHP_EXT",".php");
//定义路径常量
define("SDK_ROOT_PATH",realpath(__DIR__).DS);
define("SDK_LOG_PATH",SDK_ROOT_PATH."log".DS);
define("SDK_CONF_PATH",SDK_ROOT_PATH."config".DS);
define("SDK_LIB_PATH",SDK_ROOT_PATH."library".DS);

//加载核心文件
require_once SDK_LIB_PATH."Loader.php";

//注册自动加载
\DevicePlatform\library\Loader::register();
?>

//自动注册类
<?php
class Loader
{
    private static $classNameAlias = null;

    /**
     * @var array SDK 允许的根命令空间对应文件夹路径
     */
    private static $nameSpace = [
        //根命名空间对应文件夹名称
        "DevicePlatform" => SDK_ROOT_PATH,
    ];

    public static function auto_load($className)
    {
        //如果class别名不存在;则使用PSR-4尝试查找文件
        $nameSpaceArr = explode("\\",$className);
        //获取根命名空间
        $rootNameSpace = array_shift($nameSpaceArr);
        if(array_key_exists($rootNameSpace,self::$nameSpace)){
            $classFile = self::$nameSpace[$rootNameSpace].implode(DS,$nameSpaceArr).PHP_EXT;
            if(file_exists($classFile)){
                return __include_once_file($classFile);
            }
        }
        return false;
    }

    public static function register($autoload = null){
        // 注册自动加载
        spl_autoload_register(!is_null($autoload)?:"DevicePlatform\\library\\Loader::auto_load",true,true);
    }
}

/**
 * 引入文件
 * @param string $filePath
 * @return mixed
 */
function __include_once_file($filePath){
    return (include_once($filePath));
}

/**
 * 加载文件
 * @param string $filePath
 * @return mixed
 */
function _require_once_file($filePath){
    return (require_once($filePath));
}
?>

高级阶段

达到上面的中级阶段,其实你已经离成功不远了。其实自动加载的原理都是根据:查找文件或者类存不存在,然后加载文件。然而,我们最难跨越的就是究竟如何写出来才是真的。理论再多也不及自己实践一把;
此时,你可能拼命了解PSR-0PSR-4以及classmap这三种懒加载标准以及方便的工具:composer

此时你写出来的代码应该和tp5的Loader.php差不多了,此处就不放全部代码了;这个代码就比较长了,放了就不能说什么内容了就放两个最核心的方法,有兴趣的可以去看看。这个类基本包含了所有的了;

/**
 * 自动加载
 * @access public
 * @param  string $class 类名
 * @return bool
 */
public static function autoload($class)
{
    // 检测命名空间别名
    if (!empty(self::$namespaceAlias)) {
        $namespace = dirname($class);
        if (isset(self::$namespaceAlias[$namespace])) {
            $original = self::$namespaceAlias[$namespace] . '\\' . basename($class);
            if (class_exists($original)) {
                return class_alias($original, $class, false);
            }
        }
    }

    if ($file = self::findFile($class)) {
        // 非 Win 环境不严格区分大小写
        if (!IS_WIN || pathinfo($file, PATHINFO_FILENAME) == pathinfo(realpath($file), PATHINFO_FILENAME)) {
            __include_file($file);
            return true;
        }
    }

    return false;
}

/**
 * 注册自动加载机制
 * @access public
 * @param  callable $autoload 自动加载处理方法
 * @return void
 */
public static function register($autoload = null)
{
    // 注册系统自动加载
    spl_autoload_register($autoload ?: 'think\\Loader::autoload', true, true);

    // Composer 自动加载支持
    if (is_dir(VENDOR_PATH . 'composer')) {
        if (PHP_VERSION_ID >= 50600 && is_file(VENDOR_PATH . 'composer' . DS . 'autoload_static.php')) {
            require VENDOR_PATH . 'composer' . DS . 'autoload_static.php';

            $declaredClass = get_declared_classes();
            $composerClass = array_pop($declaredClass);

            self::$prefixLengthsPsr4 = $composerClass::$prefixLengthsPsr4;

            self::$prefixDirsPsr4 = property_exists($composerClass, 'prefixDirsPsr4') ? $composerClass::$prefixDirsPsr4 : [];

            self::$prefixesPsr0 = property_exists($composerClass, 'prefixesPsr0') ? $composerClass::$prefixesPsr0 : [];
            self::$map          = property_exists($composerClass, 'classMap') ? $composerClass::$classMap : [];
        } else {
            self::registerComposerLoader();
        }
    }

    // 注册命名空间定义
    self::addNamespace([
        'think'    => LIB_PATH . 'think' . DS,
        'behavior' => LIB_PATH . 'behavior' . DS,
        'traits'   => LIB_PATH . 'traits' . DS,
    ]);

    // 加载类库映射文件
    if (is_file(RUNTIME_PATH . 'classmap' . EXT)) {
        self::addClassMap(__include_file(RUNTIME_PATH . 'classmap' . EXT));
    }

    self::loadComposerAutoloadFiles();

    // 自动加载 extend 目录
    self::$fallbackDirsPsr4[] = rtrim(EXTEND_PATH, DS);
}

关于懒加载

其实只要我们遵循了PSR-4的相关规范,我们可以直接使用composer中的相关懒加载逻辑。毕竟别人已经帮我们完全实现了懒加载的目的;只需我们在项目的启动时,引入composer自己的autoload.php即可;并且根据命名空间规范使用即可;其实包括thinkphp5框架,在关于懒加载这块,也是集成了composer自身的autoload.php相关规则。只是我们在开发SDK时,如何让其他开发人员更好的使用SDK,可以将相关懒加载代码写入进去;使其他开发人员(未使用composer工具的开发人员),通过引入一个SDK文件,就可完成整个SDK的运行;

懒加载四种模式

此处就说说懒加载的四种模式:PSR-0、psr-4、classmap、files;这四种模式需要结合下composer的自动加载;

命名空间

命名空间意义

PHP 命名空间(namespace)是在PHP 5.3中加入的,如果你学过C#和Java,那命名空间就不算什么新事物。 不过在PHP当中还是有着相当重要的意义。

PHP 命名空间可以解决以下两类问题:

  1. 用户编写的代码与PHP内部的类/函数/常量或第三方类/函数/常量之间的名字冲突。
  2. 为很长的标识符名称(通常是为了缓解第一类问题而定义的)创建一个别名(或简短)的名称,提高源代码的可读性。[1]

命名空间使用

PHP 命名空间中的类名可以通过三种方式引用:

  • 非限定名称,或不包含前缀的类名称,例如 $a=new foo(); 或 foo::staticmethod();。如果当前命名空间是 currentnamespacefoo 将被解析为 currentnamespace\foo。如果使用 foo 的代码是全局的,不包含在任何命名空间中的代码,则 foo 会被解析为foo。 警告:如果命名空间中的函数或常量未定义,则该非限定的函数名称或常量名称会被解析为全局函数名称或常量名称。

  • 限定名称,或包含前缀的名称,例如 $a = new subnamespace\foo(); 或 subnamespace\foo::staticmethod();。如果当前的命名空间是 currentnamespace,则 foo 会被解析为 currentnamespace\subnamespace\foo。如果使用 foo 的代码是全局的,不包含在任何命名空间中的代码,foo 会被解析为subnamespace\foo

  • 完全限定名称,或包含了全局前缀操作符的名称,例如, $a = new \currentnamespace\foo(); 或 \currentnamespace\foo::staticmethod();。在这种情况下,foo 总是被解析为代码中的文字名(literal name)currentnamespace\foo

psr-0 标准 autoload_namespaces

懒加载,将目标目录作为基目录再进行命名空间和路径的映射后继续向后加载;

//composer.json文件
{
    "autoload": {
        "psr-0": {
            "Psr0\\Lib\\": "psr0/lib/src/"
        }
    }
}

psr-4 标准 autoload_psr4

懒加载,将目标目录直接映射为命名空间对应的目录继续向后加载;

//composer.json文件
{
    "autoload": {
        // php 的 psr-4 规范的自动载入,是将目标目录直接影射为命名空间的
        "psr-4": {
            "Psr4\\Lib\\": "psr4/lib/src/",
            "App\\Controllers\\": "app/controllers/",
            "App\\Models\\": "app/models/"
        }
    }
}

classmap 模式 autoload_classmap

懒加载,扫描目录下的所有类文件,支持递归扫描, 生成对应的类名=>路径的映射,当载入需要的类时直接取出路径,速度最快

//composer.json文件
{
    "autoload": {
        // php 的 psr-4 规范的自动载入,是将目标目录直接影射为命名空间的
        "psr-4": {
            "Psr4\\Lib\\": "psr4/lib/src/",
            "App\\Controllers\\": "app/controllers/",
            "App\\Models\\": "app/models/"
        }
    }
}

files 模式

自动载入的文件,主要用来载入一些没办法懒加载的公共函数

//composer.json文件
// 扫描目录下的所有文件生成 hash => 路径的映射  运行时实时加载
// 主要用来载入工具函数
{
    "autoload": {
        // php 的 psr-4 规范的自动载入,是将目标目录直接影射为命名空间的
        "files": [
            "ext/common/functions.php",
            "ext/system/functions.php"
        ]
    }
}

完整composer.json文件加载

编辑composer.json文件如下,

{
    "autoload": {
        "psr-0": {
            "Psr0\\Lib\\": "psr0/lib/src/"
        },
        "psr-4": {
            "Psr4\\Lib\\": "psr4/lib/src/",
            "App\\Controllers\\": "app/controllers/",
            "App\\Models\\": "app/models/"
        },
        "classmap": [
            "classmap/lib/src/"
        ],
        "files": [
            "ext/common/functions.php",
            "ext/system/functions.php"
        ]
    }
}

刷新 autoload 规则

使用composer dump-autoload重新加载生成对应的文件映射结构即可。

psr-0

vendor/composer/autoload_namespaces.php文件中可以看到:

<?php

// autoload_namespaces.php @generated by Composer

$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);

return array(
    'Psr0\\Lib\\' => array($baseDir . '/psr0/lib/src'),
);

psr-4

vendor/composer/autoload_psr4.php文件中可以看到:

<?php

// autoload_psr4.php @generated by Composer

$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);

return array(
    'Psr4\\Lib\\' => array($baseDir . '/psr4/lib/src'),
    'App\\Models\\' => array($baseDir . '/app/models'),
    'App\\Controllers\\' => array($baseDir . '/app/controllers'),
);

其实 psr-0/psr-4 的自动载入都将命名空间映射为相应的目录。只不过 psr-0 是映射到目标地址作为基目录再解析命名空间,而 psr-4 是直接映射。

classmap

因为它本身就没遵循命名空间和路径映射的半点规范...随便写一个好了,他的模式其实就是扫描指定目录下的所有文件,把类采集出来,做一个映射表,这个类在这个文件里,完了。

files

在运行时就直接载入的一些函数文件(非类文件)

PSR-0与PSR-4的区别

PSR是Proposing a Standards Recommendation(提出标准建议)的缩写,是由PHP Framework Interoperability Group(PHP通用性框架小组,简称PHP-FIG)发起的,通过他们命名就可以看出,这是个主要是针对框架通用性而做努力的开放性小组,他们的在Github上有自己的仓库地址,目前只有一个被接受的标准,那就是PSR-0标准,标准定义了PHP自动加载的命名规范和文件路径规范。 针对PSR-0标准主要提到了以下几点:

  1. 一个完全合格的命名空间和类名必须有以下的结构“<提供者名称>(<命名空间>)*<类名>”
  2. 每个命名空间必须有顶级的命名空间(“提供者”)
  3. 每个命名空间可以有任意多个子命名空间
  4. 每个命名空间在被从文件系统加载时必须被转换为“操作系统路径分隔符”(DIRECTORY_SEPARATOR )
  5. 每个“”字符在“类名”中被转换为DIRECTORY_SEPARATOR 。“”符号在命名空间中没有这个含义
  6. 符合命名标准的命名空间和类名必须以“.php”结尾来加载文件
  7. 提供商名称,命名空间,类名可以由大小写字母组成,其中命名空间和类名是大小写敏感的以保证多系统兼容性
  8. 如果文件不存在需要返回false

而PSR-4与PSR-0的区别:

  1. 在composer中定义的NS,psr4必须以\结尾否则会抛出异常,psr0则不要求

  2. psr0里面最后一个\之后的类名中,如果有下划线,则会转换成路径分隔符,如Name_Space_Test会转换成Name\Space\Test.php。在psr4中下划线不存在实际意义

  3. psr0有更深的目录结构
    比如定义了NS为 Foo\Bar=>vendor\foo\bar\src, use Foo\Bar\Tool\Request调用NS。
    如果以psr0方式加载,实际的目录为vendor\foo\bar\src\Foo\Bar\Tool\Request.php
    如果以psr4方式加载,实际目录为vendor\foo\bar\src\Tool\Request.php

参考博客


  1. 引用于菜鸟教程中《PHP 命名空间(namespace)》文章

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,830评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,992评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,875评论 0 331
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,837评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,734评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,091评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,550评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,217评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,368评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,298评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,350评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,027评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,623评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,706评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,940评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,349评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,936评论 2 341

推荐阅读更多精彩内容

  • 根包:是由composer.json项目根目录定义的包。它是composer.json定义项目需求的主要部分。 首...
    孤独而灿烂的郑金叹阅读 6,203评论 2 1
  • 本文分为两部分:第一部分讲__autoload()函数实现的类自动加载。第二部分讲spl_autoload_reg...
    舒小贱阅读 2,540评论 1 4
  • Composer是一个非常流行的PHP包依赖管理工具,已经取代PEAR包管理器,对于PHP开发者来说掌握Compo...
    dreamer_lk阅读 2,409评论 0 15
  • Welcome 目前网络上充斥着大量的陈旧信息,让PHP新手误入歧途,传播着错误的实践和糟糕的代码,这必须得到纠正...
    layjoy阅读 21,649评论 7 118
  • 是什么 如果你知道yum、apt-get、npm、bower等命令中的一种或者多种,那么,你也能很快知道compo...
    旱魃一样阅读 3,109评论 0 9