PHP 理解 autoload 、PSR-0、PSR-4 的因缘并分析 PS0-0 与 PSR-4 的差异到底在哪里

2023-10-24 12:50

本文主要是介绍PHP 理解 autoload 、PSR-0、PSR-4 的因缘并分析 PS0-0 与 PSR-4 的差异到底在哪里,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

1. include & require

我们知道一个 A.php 文件若想引入 B.php 文件里的类,就需要通过 include / require 的方式将 B.php 引入。
这种方式对小项目来说没啥问题,但对大型项目来说,通常会包含很多公共文件,比如:Foo/Bar/Dog.php,按照传统方式我们在每个所需的地方将这个文件引入即可,但这样会造成如下问题:

  1. 每个地方都要引入 Foo/Bar/Dog.php ,操作实在繁琐
  2. 代码量增多
  3. 重复粘贴容易出现残漏情况

那有没有办法解决这个问题呢?有!__autoload 就是用来解放 include / require 的。

2. __autoload

__autoload 是 php 5 以后新增的一个魔法函数,此函数在使用 new xxx 时自动触发,并传递一个 $class 的参数,这个参数就是 new xxx 中的 xxx 部分, 下面是它的用法

function __autoload($class_name) {# $class = Foo\Bar\Dogrequire_once $class_name . '.php';
}
$dog = new Foo\Bar\Dog();
$dog->say();

这个函数帮我们减少了许多的 include/require,但由于 __autoload 只能使用一次,假设我们不止有 Foo/Bar 这个目录,还有 Coo/TooAoo/Boo 等共用目录这可怎么办? 有没有办法能让多次加载呢?有! sql_autoload_register 就是来解决这个问题的。

3. sql_autoload_register

sql_autoload_register 专门用来定义多个 __autoload 的函数,它的用法如下:

function my_autoload_1($class_name) {require_once $class_name . '.php';
}
function my_autoload_2($class_name)  {require_once $class_name . '.php';
}
function my_autoload_3($class_name) {require_once $class_name . '.php';
}
sql_autoload_register('my_autoload_1');
sql_autoload_register('my_autoload_2');
sql_autoload_register('my_autoload_3');
$dog1 = new Foo\Bar\Dog();
$dog1->say();
$dog2 = new Coo\Too\Dog();
$dog2->say();
$dog3 = new Foo\Bar\Dog();
$dog3->say();

现在多个自动加载的问题解决了,由于sql_autoload_register 既能代替 __autoload 也能实现多个 __autoload ,所以 __autoload 自然也就被 PHP 官方淘汰了。
然后这就完了吗?并没有~

每个人都可以用 sql_autoload_register 定义自己的自动加载器,而每个人的写法又是不同的,若第三方插件/框架的作者们都实现自家的自动加载器,当我们使用这些插件/框架时就得熟悉它们的引入语法,对开发者的学习成本增加了许多,后来就有了一群志同道合的人联合起来要搞一个自动加载器的规范,而这个规范就叫做 PSR-0 ,全称是 PHP Standard Recommend,大家需要统一按照这种规范来写出自己的自动加载器才算合格。

4. PSR-0

PSR-0 的规范这里我就不细说,有意者可以参考官方文档 PSR-0
这里我们重点关注实现了 PSR-0 自动加载器后写法是怎么样的?或者说,哪些比较流行的框架帮我们写好了一个 PSR-0 规范的自动加载器? 要怎么使用?这里我们就以 composer 为例子,假设我们的项目结构如下:

srcFooBarDog.phpCooTooDog.php
test.php

接着参考 composer 文档需要在 composer.json 里进行映射配置

{"name": "cookcyq","autoload": {"psr-0": {"Foo\\Bar": "src/","Coo\\Too": "src/"}}
}

配置后需要执行: composer durmp-auto -o 它会自动在 vendor/composer/ 下生成 autoload.php 文件,我们引入这个文件就可以使用愉快的使用自动加载器了。

// test.php
require_once "vendor/composer/autoload.php";
// 提示:
// PSR-0 规范里支持 _ 下划线语法,它最终会被替换成 / ,所以下面是等价的。
$dog1 = new Foo_Bar_Dog();
$dog2 = new Foo\Bar\Dog();$dog3 = new Coo\Too_Dog();
$dog4 = new Coo\Too\Dog();

有的小伙伴可能疑惑了,为什么会支持下划线 _ 这种形式呢?这是为了起到独立作用域作用,避免有重复名字冲突,因为 namespace 在那时还没出现呢。
嗯,到了 PSR-0 就结束了吗?然鹅并没有,这不 namespace 在不久后就出现了。
所以这才有了后来的 PSR-4 新的规范。

5. PSR-4

我认为 PSR-4 完全就是因为有了 namespace 这玩意才诞生出的新规范。
如果你还不知道什么是 namespace 可以参考我前面写过的 PHP & 理解 Namespace (命名空间)

PSR-4 与 PSR-0 有什么不同呢?

  1. PSR-4 不支持 _ 下划线这种写法,因为已经有了 namespace
  2. composer.json 中 key 的结尾必须要带上 \\ ,如下
{"name": "cookcyq","autoload": {"psr-0": {"Foo\\Bar": "src/","Coo\\Too": "src/"},"psr-4": {"Foo\\Bar\\": "src/","Coo\\Too\\": "src/"}}
}

看到这里,相信你已经懂得 autoload 这个自动加载的概念以及如何在 composer 中使用它们的自动加载了,对于时间匆忙的同学也可以不用往下看,我认为这已经够用了。

如果时间充裕的话可以接着往下读。

6. 为什么 composer 不直接拥抱 PSR-4 还要兼容 PSR-0 ?

答案很明显,目前有些古老且有用的插件作者采用的还是 PSR-0 规范,
其中有些作者用的是 _ 下划线语法,所以 composer 不能一刀切。

7. composer 的 PSR-0 和 PSR-4 实现方式有啥不同?

对于新手(包括我)来说,常常找到的 PSR-0 与 PSR-4 的解释很令人疑惑。
在这里插入图片描述

在这里插入图片描述

图中的PSR-0 映射关系我能看懂,但 PSR-4 我是一脸懵逼。假设 Bar.php 文件就放到 src/Acme/Foo/Bar.php 里面,但 PSR-4 的 Acme\Foo => /src/Bar.php/src/Bar.php 的这种映射关系肯定找不到 Bar.php 文件啊?于是本着好奇心便各种搜索,结果还是令人失望,大部分要么都是搬官方的例子要么都是复制别人的过来然后也不说明为什么会这样的关系,至少对我来说,这种解释是行不通的。目前我安慰自己的方式是:底层会自动帮我们找到完整的路径进行引入,然后这个映射关系不是指上面的引入文件关系,这样我心里才舒服些,于是我就在想,与其这么找,倒不如去看看源码到底是怎么帮我们引入最终的文件的,于是就有了接下来的源码分析,放心,我这里仅仅摘取最关键部分,因为其它的我也看不懂。

8. 分析 composer PSR-0 & PSR-4 实现原理

这是本案例的完整目录结构
在这里插入图片描述

假设 composer.json 采用 PSR-4 ,内容如下

{"name": "cookcyq","license": "n","require": {},"autoload": {"psr-4": {"Foo\\Bar\\": "vendor/foo/bar/src"}}
}

使用 comopser durmp-auto -o 生成以下文件:
在这里插入图片描述

我们重点关注里面的 ClassLoader.php 这个文件,里面包含了 PSR-0 和 PSR-4 的几个核心关键实现自动加载的属性和方法

关键属性:

class ClassLoader {// PSR-4 关键属性private $prefixLengthsPsr4 = array();private $prefixDirsPsr4 = array();// PSR-0 关键属性private $prefixesPsr0 = array();private $fallbackDirsPsr0 = array();

关键方法1 add / addPsr4():将 composer.json 里的 "psr-0": {... } 和 psr-4": {... } 内容添加关键属性里面,源码如下

public function add($prefix, $paths, $prepend = false) { ...代码与 addPsr4 差不多 }
public function addPsr4($prefix, $paths, $prepend = false){if (!$prefix) {// ...} elseif (!isset($this->prefixDirsPsr4[$prefix])) {$length = strlen($prefix);// 结尾必须添加 \\ if ('\\' !== $prefix[$length - 1]) {throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");}$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;$this->prefixDirsPsr4[$prefix] = (array) $paths;} elseif ($prepend) {// Prepend directories for an already registered namespace.$this->prefixDirsPsr4[$prefix] = array_merge((array) $paths,$this->prefixDirsPsr4[$prefix]);} else {// Append directories for an already registered namespace.$this->prefixDirsPsr4[$prefix] = array_merge($this->prefixDirsPsr4[$prefix],(array) $paths);}}

你可以不用看上面的代码,只需关心最终存储结构类似为:

// PSR-4==========================
$prefixLengthsPsr4 = ["F" => ["Foo\\Bar\\" => 6,]
]
$prefixDirsPsr4 = ["Foo\\Bar\\" => ["vendor/foo/bar/src"]
]// PSR-0==========================
$prefixesPsr0 = ["F" => ["Foo\\Bar\\" => ["vendor/foo/bar/src"]]
]

关键方法2 findFileWithExtension(): 查找 PSR-0 和 PSR-4 的完整文件路径就是在这里完成的,当找到后就将其 includeFile 引入即可,整个 autoload 基本流程就是这样,关键源码如下:

private function findFileWithExtension($class, $ext){$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;$first = $class[0];// ============================ PSR-4  查询 =============================================if (isset($this->prefixLengthsPsr4[$first])) {$subPath = $class;while (false !== $lastPos = strrpos($subPath, '\\')) {$subPath = substr($subPath, 0, $lastPos);$search = $subPath . '\\';if (isset($this->prefixDirsPsr4[$search])) {$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);foreach ($this->prefixDirsPsr4[$search] as $dir) {if (file_exists($file = $dir . $pathEnd)) {return $file;}}}}}// ============================ PSR-0  查询 =============================================// 支持下划线的条件语句if (false !== $pos = strrpos($class, '\\')) {$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1). strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);} else {$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;}if (isset($this->prefixesPsr0[$first])) {foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {if (0 === strpos($class, $prefix)) {foreach ($dirs as $dir) {if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {return $file;}}}}}return false;}

上面的代码我们只需要找到最关键的两层循环:
PSR-4 采用: while + foreach
PSR-0 采用: foreach + foeach
因为 PSR-4 不需要 _ 下划线,以及结尾必须要带上 \\ ,势必要用另外一种方式来实现找到文件路径结构,才有了这两个属性: $prefixLengthsPsr4 / $prefixDirsPsr4,然后再结合 whre +foreach 来寻找完整路径,而 PSR-0 只定义了 $prefixesPsr0 属性,所以采用了 foreach + foeach 来寻找完整路径,最终这两种方式都成功找出完整路径。

9. 好了,做个总结吧

  1. PSR-0 支持 _ 下划线,PSR-4 不支持
  2. composer.json PSR-0 后面不用加 \\,PSR-4 后面必须加 \\

剩下的在 composer.json 配置用法是完全一致的。只是查找完整文件路径方式采用不同的循环策略。

参考文献:
https://stackoverflow.com/questions/24868586/what-are-the-differences-between-psr-0-and-psr-4#:~:text=The%20summary%20is%20that%20PSR,part%20following%20the%20anchor%20point.
https://my.oschina.net/sallency/blog/893518

这篇关于PHP 理解 autoload 、PSR-0、PSR-4 的因缘并分析 PS0-0 与 PSR-4 的差异到底在哪里的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/275277

相关文章

Spring事务中@Transactional注解不生效的原因分析与解决

《Spring事务中@Transactional注解不生效的原因分析与解决》在Spring框架中,@Transactional注解是管理数据库事务的核心方式,本文将深入分析事务自调用的底层原理,解释为... 目录1. 引言2. 事务自调用问题重现2.1 示例代码2.2 问题现象3. 为什么事务自调用会失效3

找不到Anaconda prompt终端的原因分析及解决方案

《找不到Anacondaprompt终端的原因分析及解决方案》因为anaconda还没有初始化,在安装anaconda的过程中,有一行是否要添加anaconda到菜单目录中,由于没有勾选,导致没有菜... 目录问题原因问http://www.chinasem.cn题解决安装了 Anaconda 却找不到 An

Spring定时任务只执行一次的原因分析与解决方案

《Spring定时任务只执行一次的原因分析与解决方案》在使用Spring的@Scheduled定时任务时,你是否遇到过任务只执行一次,后续不再触发的情况?这种情况可能由多种原因导致,如未启用调度、线程... 目录1. 问题背景2. Spring定时任务的基本用法3. 为什么定时任务只执行一次?3.1 未启用

C++ 各种map特点对比分析

《C++各种map特点对比分析》文章比较了C++中不同类型的map(如std::map,std::unordered_map,std::multimap,std::unordered_multima... 目录特点比较C++ 示例代码 ​​​​​​代码解释特点比较1. std::map底层实现:基于红黑

Spring、Spring Boot、Spring Cloud 的区别与联系分析

《Spring、SpringBoot、SpringCloud的区别与联系分析》Spring、SpringBoot和SpringCloud是Java开发中常用的框架,分别针对企业级应用开发、快速开... 目录1. Spring 框架2. Spring Boot3. Spring Cloud总结1. Sprin

Spring 中 BeanFactoryPostProcessor 的作用和示例源码分析

《Spring中BeanFactoryPostProcessor的作用和示例源码分析》Spring的BeanFactoryPostProcessor是容器初始化的扩展接口,允许在Bean实例化前... 目录一、概览1. 核心定位2. 核心功能详解3. 关键特性二、Spring 内置的 BeanFactory

MyBatis-Plus中Service接口的lambdaUpdate用法及实例分析

《MyBatis-Plus中Service接口的lambdaUpdate用法及实例分析》本文将详细讲解MyBatis-Plus中的lambdaUpdate用法,并提供丰富的案例来帮助读者更好地理解和应... 目录深入探索MyBATis-Plus中Service接口的lambdaUpdate用法及示例案例背景

MyBatis-Plus中静态工具Db的多种用法及实例分析

《MyBatis-Plus中静态工具Db的多种用法及实例分析》本文将详细讲解MyBatis-Plus中静态工具Db的各种用法,并结合具体案例进行演示和说明,具有很好的参考价值,希望对大家有所帮助,如有... 目录MyBATis-Plus中静态工具Db的多种用法及实例案例背景使用静态工具Db进行数据库操作插入

Mysql中InnoDB与MyISAM索引差异详解(最新整理)

《Mysql中InnoDB与MyISAM索引差异详解(最新整理)》InnoDB和MyISAM在索引实现和特性上有差异,包括聚集索引、非聚集索引、事务支持、并发控制、覆盖索引、主键约束、外键支持和物理存... 目录1. 索引类型与数据存储方式InnoDBMyISAM2. 事务与并发控制InnoDBMyISAM

Go使用pprof进行CPU,内存和阻塞情况分析

《Go使用pprof进行CPU,内存和阻塞情况分析》Go语言提供了强大的pprof工具,用于分析CPU、内存、Goroutine阻塞等性能问题,帮助开发者优化程序,提高运行效率,下面我们就来深入了解下... 目录1. pprof 介绍2. 快速上手:启用 pprof3. CPU Profiling:分析 C