Php遵从psr0的自动加载实现
5/Jan 2019
自动加载是现代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 自动加载规范(已弃用)
- 命名空间必须与绝对路径一致
- 类名首字母大写
- 除入口文件外,其他
.php必须只有一个类
PSR-4 自动加载规范
PSR-4规范是PSR-0的升级版本,是改进的自动加载规范,是对PSR-0的补充
- 全限定类名必须拥有一个顶级命名空间名称,也称为供应商命名空间(vendor namespace)。
- 全限定类名可以有一个或者多个子命名空间名称。
- 全限定类名必须有一个最终的类名(我想意思应该是你不能这样
\<NamespaceName>(\<SubNamespaceNames>)*\来表示一个完整的类)。 - 下划线在全限定类名中没有任何特殊含义(在 PSR-0 中下划是有含义的)。
- 全限定类名可以是任意大小写字母的组合。
- 所有类名的引用必须区分大小写。
对于两者
或者可以这样简单理解:
- PSR-0,类 - 文件自动加载
- 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的自动加载简单实现
基本要求
- 全部使用命名空间
- 所有php文件自动载入,不能有include/require
- 实现单一入口
目录结构
.
├── 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
- 命名空间必须与绝对路径一致,Index.php在项目中的路径是 App/Action,故这里的命名空间为
namespace App\Action; - 类名与文件名一致,且首字母大写,故
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也能好好搬砖鸭,我想所以这也是我重头回望自动加载的原因吧。