Php遵从psr0的自动加载实现

自动加载是现代php框架的基石,让php摆脱了“原始社会”的调用模式(笑

我在php的学习路径上只经过简单的语法学习和最基本的记事本练习后,比较粗暴地进入了框架的学习中,框架确实提供了很多很便捷的实现,例如数据库ORM,自动加载;

一个很日常的例子,use一个命名空间类,然后实例化,调用方法,就可以实现我想要的操作,但是问题在于,为什么可以这样呢?我深感受限于基础。

而且在我之后的php路途上,我发现一些很有意思的更为“modern”框架,如slim,号称一个下午可以阅读完源码的一个微框架,确实很有意思,但是我在最开头的部分就看不懂了。很惭愧。所以我准备从头来过。

require / include

在较早的代码中,requrie / include 这两个函数会经常出现。

假设在Bar.php中需要使用到Foo.php中某功能,则需要在Bar.php中引入Foo.php,

// Foo.php
class Foo
{
    public function printIn()
    {
        echo 'className: ' . __CLASS__;
    }
}

// Bar.php
require 'Foo.php';
$foo = new Foo;
$foo->printIn();

输出结果:

className: Foo

如果在Bar.php中引入不存在的Bim.php,则会报一个致命错误,

// Bar.php
require 'Foo.php';
require 'Bim.php';
$foo = new Foo;
$foo->printIn();

// 输出
PHP Fatal error:  require(): Failed opening required 'Bim.php'
    
// 若使用 include
// Bar.php
require 'Foo.php';
include 'Bim.php';
$foo = new Foo;
$foo->printIn();

    
// 输出,报一个Warning
Warning: include(): Failed opening 'Bim.php' for inclusion (include_path='.;C:\php\pear') in D:\Visual-NMP-x64\www\Default\Autoloader\Bar.php on line 4

className: Foo // 代码继续执行,$foo->printIn()输出成功

由上我们可以看到,如果使用require,若引入失败,程序报error,代码会终止执行;而使用include,若引入失败,程序报warning,代码会继续执行

当我们的php代码较为简单的时候(比如练手写个留言板),这样一个个文件require / include 当然是没有问题的,但是代码量上去了,项目结构变得复杂的时候,人力维护这样的引入就会显得十分困难,而且十分容易出错。

命名空间

什么是命名空间呢?简单的说就是现在我们习以为常的在每个php文件开头定义namespace语句

对于命名空间的作用,我们可以简单用一个require的例子来展示一下使用命名空间与不使用命名空间的差别。

在demo目录下,存放test.php, test1.php, test2.php

不使用命名空间

# test1.php
<?php 
function test()
{
    echo __FILE__;
}

# test2.php
function test()
{
    echo __FILE__;
}

# test.php
<?php
require 'test1.php';
require 'test2.php';

在命令行运行test.php,会出现报错Fatal error: Cannot redeclare test(),函数重命名错误;

使用命名空间

其实代码与上面差不多,但是在文件开头加上namespace命名空间

# test1.php
<?php 
namespace Test1;
function test()
{
    echo __FILE__;
}

# test2.php
namespace Test2;
function test()
{
    echo __FILE__;
}

# test.php
<?php
require 'test1.php';
require 'test2.php';

在命令行运行test.php,则终端没有报错输出,证明requeire成功。

如何调用命名空间之间的方法和函数

命名空间后加上方法名,注意的是使用反斜杠’ \ ‘

# test.php
<?php
require 'test1.php';
require 'test2.php';

Test1\test();
echo PHP_EOL; # 换行,让打印好看一点
Test2\test();

运行test.php,则会有相应文件名输出

$ php test.php
/usr/local/var/www/test/demo/test1.php
/usr/local/var/www/test/demo/test2.php% 

总结

早期php是没有命名空间的,在项目越来越大的时候,就难以避免产生命名冲突问题;另一方面,也难以对类与方法进行管理。而命名空间可以帮助解决命名冲突的问题,而且可以类似包的概念,对代码层次进行划分。并且,随着命名空间对代码结构的规范,php能很好地实现对类的自动加载

什么是自动加载

在php manual中,类的自动加载章节中有这样的描述:

在编写面向对象(OOP) 程序时,很多开发者为每个类新建一个 PHP 文件。 这会带来一个烦恼:每个脚本的开头,都需要包含(include)一个长长的列表(每个类都有个文件)。

在 PHP 5 中,已经不再需要这样了。 spl_autoload_register() 函数可以注册任意数量的自动加载器,当使用尚未被定义的类(class)和接口(interface)时自动去加载。通过注册自动加载器,脚本引擎在 PHP 出错失败前有了最后一个机会加载所需的类。

尽管 __autoload() 函数也能自动加载类和接口,但更建议使用 spl_autoload_register() 函数。spl_autoload_register() 提供了一种更加灵活的方式来实现类的自动加载(同一个应用中,可以支持任意数量的加载器,比如第三方库中的)。因此,不再建议使用 __autoload() 函数,在以后的版本中它可能被弃用。

在以往的项目中,如果一个类需要依赖其他几十个类的话,就需要在这个类前面写几十行requeire/include,明显很不人性吧。在我个人的理解中,自动加载就是在开发者对类或者接口进行调用的时候,能提供自动引入的功能,而不需要开发者人力把文件一个个引入需要的文件中,从而完成对需要的类或者接口的调用。

又上面引用的段落,我们知道php 提供两个函数可以完成自动载入功能。下面我们来先看__autoload的实现。

使用__autoload实现自动加载

在php manual中 __autoload魔术方法的定义:__autoload 尝试加载未定义的类

void __autoload ( string $class )

手册上写着,可以通过定义这个函数来启用类的自动加载

我们还是先来看一下这个方法。

// Bar.php
$foo = new Foo; 

直接运行Bar.php,就会报一个error错误。

使用__autoload,加载Foo.php,不过我们先来看看这个函数的参数$class代表什么意思

// Bar.php
function __autoload($className)
{
    echo $className;
    exit;
}

$foo = new Foo();

输出:

Foo% // emmmmm 目前还不知道为什么后面会出现这个%符号

可见,这个参数$className,就是自动获取到的需要加载的”类的名称”。这样,简单修改一下__autoload,我们就能让函数自动引入需要的类了。

// Foo.php
class Foo
{
    public function printIn()
    {
        echo 'className: ' . __class__;
    }
}

// Bar.php
function __autoload($className)
{
    require './' . $className . '.php';
}

$foo = new Foo();
$foo->printIn();

输出:

className: Foo

不过就是简单的引入一个文件可能并不能看出这个__autoload函数的作用,我们来引入多几个类看看。

// Foo.php
class Foo
{
    public function printIn()
    {
        echo 'className: ' . __class__ . PHP_EOL;
    }
}

/* Bim.php, Cook.php, 均与Foo.php相似代码 */

// Bar.php
function __autoload($className)
{
    require './' . $className . '.php';
}

$foo = new Foo();
$bim = new Bim();
$cook = new Cook();

$foo->printIn();
$bim->printIn();
$cook->printIn();

输出:

className: Foo
className: Bim
className: Cook

由上可见,__autoload函数实现的自动加载,在调用未知的类时,可以自动获取需要的引入的类名,在其内部使用一个requeire或者其他相似功能的方法,就可以实现多个类的引入。

该方法在PHP 7.2.0 中已被弃用。

This feature has been DEPRECATED as of PHP 7.2.0. Relying on this feature is highly discouraged.

PSR-0 与 PSR-4

PSR 是 PHP Standard Recommendations 的简写,由 PHP FIG 组织制定的 PHP 规范,是 PHP 开发的实践标准。

PSR 标准规范中文版,可以在laravel-china社区中搜索到。里面的描述更加仔细,本文此段只是简单引述

这里我们来简单引述一下 PSR-0 与 PSR-4。

PSR-0 自动加载规范(已弃用)

  1. 命名空间必须与绝对路径一致
  2. 类名首字母大写
  3. 除入口文件外,其他.php必须只有一个类

PSR-4 自动加载规范

PSR-4规范是PSR-0的升级版本,是改进的自动加载规范,是对PSR-0的补充

  1. 全限定类名必须拥有一个顶级命名空间名称,也称为供应商命名空间(vendor namespace)。
  2. 全限定类名可以有一个或者多个子命名空间名称。
  3. 全限定类名必须有一个最终的类名(我想意思应该是你不能这样 \<NamespaceName>(\<SubNamespaceNames>)*\ 来表示一个完整的类)。
  4. 下划线在全限定类名中没有任何特殊含义(在 PSR-0 中下划是有含义的)。
  5. 全限定类名可以是任意大小写字母的组合。
  6. 所有类名的引用必须区分大小写。

对于两者

或者可以这样简单理解:

  1. PSR-0,类 - 文件自动加载
  2. PSR-4,面向包的自动加载

面对composer的发展,PSR-4 是为了给可交互的 PHP 自动加载器指定一个将命名空间映射到文件系统的规则,并且可以与其他 SPL 注册的自动加载器共存。PSR-4 不是 PSR-0 的替代品,而是对它的补充。

spl_autoload_register()

spl_autoload_register() 是目前推荐的自动加载实现函数,对比__autoload,而__autoload实现的自动加载器只能有一个,spl_autoload_register的优势在于可以实现多个自动加载器。

使用spl_autoload_register()实现自动加载

spl_autoload_register的自动加载实现与__autoload相似。

# test1.php
class Test1
{
    static function test()
    {
        echo __METHOD__;
    }
}

# test2.php
class Test2
{
    static function test()
    {
        echo __METHOD__;
    }
}

# test.php
<?php
spl_autoload_register('autoload1');
# 能注册多个自动加载器
# spl_autoload_register('autoload2');
Test1::test();
echo PHP_EOL;
Test2::test();

function autoload1($class)
{
    require __DIR__ . '/' . $class . '.php';
}

# function autoload2($class){}

能获得输出:

Test1::test
Test2::test

遵从PSR-0的自动加载简单实现

基本要求

  1. 全部使用命名空间
  2. 所有php文件自动载入,不能有include/require
  3. 实现单一入口

目录结构

.
├── App						# 应用程序
│   ├── Action
│   │   └── Index.php
│   └── Model
├── Core					# 核心类
│   ├── Loader.php
│   └── Test.php
└── index.php				# 入口文件

然后我们从头开始理解遵从PSR-0的自动加载简单实现

基础实现

描述所希望实现的

我们的业务代码存放在App目录下,Action存放逻辑的具体实现。

# Index.php
<?php
namespace App\Action;
class Index
{
    static function test()
    {
        echo __METHOD__ . PHP_EOL;     
    }
}

这里注意的是,遵从PSR-0

  1. 命名空间必须与绝对路径一致,Index.php在项目中的路径是 App/Action,故这里的命名空间为 namespace App\Action;
  2. 类名与文件名一致,且首字母大写,故class Index{}
# 入口文件 index.php

<?php
// 具体目标
App\Action\Index::test();
Core\Test::test();

查看入口文件index.php,引用了两个静态方法,我们希望访问入口文件的时候,能成功调用这两个方法,且不需要专门require/include这两个文件/类。

创建自动加载器

Core目录,存放项目核心类。自动加载器类也在这里实现。

# Loader.php 自动加载器类
<?php
/**
 * 自动加载器的实现
 */
namespace Core;

class Loader
{
    static function autoload($class)
    {
        require BASEDIR.'/'.str_replace('\\', '/', $class) . '.php';
        
    }
}

Loader::autoload方法中,$class参数,是自动获取到的需要加载的”类的名称”,结果类似Core\Test,是带有命名空间的类名。因为,Core\Test中的’\‘,故使用str_replace()函数,把符号替换为路径使用的 ‘/‘。

入口文件引入自动加载器

# index.php
<?php
// 定义常量
define('BASEDIR', __DIR__); 

// 引入自动加载类
include BASEDIR . '/Core/Loader.php';
spl_autoload_register('\\Core\\Loader::autoload');

// 具体目标
App\Action\Index::test();
Core\Test::test();

访问与结果

$ php index.php
App\Action\Index::test
Core\Test::test

实现代码

https://github.com/LucienVen/php_demo/tree/master/autoload_demo

使用composer自动加载

暂略

总结

自动加载是现代php框架的很重要的一部分,虽然已经平常到大家可能都有所忽略,对于疲于编写业务代码的人(唉人生)来说,框架已经能帮大家搞掂这些内容了。对我而言,因为我接触的slim框架(一个专注于接口开发的php微框架),并没有像ThinkPHP这般,按照官方文档给好的代码组织结构,controller, model, validate,就可以好好搬砖了2333,

但是我希望Slim也能好好搬砖鸭,我想所以这也是我重头回望自动加载的原因吧。

参考链接

Tags// , ,
More Reading
Older// About