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

相关文章

PHP轻松处理千万行数据的方法详解

《PHP轻松处理千万行数据的方法详解》说到处理大数据集,PHP通常不是第一个想到的语言,但如果你曾经需要处理数百万行数据而不让服务器崩溃或内存耗尽,你就会知道PHP用对了工具有多强大,下面小编就... 目录问题的本质php 中的数据流处理:为什么必不可少生成器:内存高效的迭代方式流量控制:避免系统过载一次性

PHP应用中处理限流和API节流的最佳实践

《PHP应用中处理限流和API节流的最佳实践》限流和API节流对于确保Web应用程序的可靠性、安全性和可扩展性至关重要,本文将详细介绍PHP应用中处理限流和API节流的最佳实践,下面就来和小编一起学习... 目录限流的重要性在 php 中实施限流的最佳实践使用集中式存储进行状态管理(如 Redis)采用滑动

Android 缓存日志Logcat导出与分析最佳实践

《Android缓存日志Logcat导出与分析最佳实践》本文全面介绍AndroidLogcat缓存日志的导出与分析方法,涵盖按进程、缓冲区类型及日志级别过滤,自动化工具使用,常见问题解决方案和最佳实... 目录android 缓存日志(Logcat)导出与分析全攻略为什么要导出缓存日志?按需过滤导出1. 按

深入理解go中interface机制

《深入理解go中interface机制》本文主要介绍了深入理解go中interface机制,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学... 目录前言interface使用类型判断总结前言go的interface是一组method的集合,不

Linux中的HTTPS协议原理分析

《Linux中的HTTPS协议原理分析》文章解释了HTTPS的必要性:HTTP明文传输易被篡改和劫持,HTTPS通过非对称加密协商对称密钥、CA证书认证和混合加密机制,有效防范中间人攻击,保障通信安全... 目录一、什么是加密和解密?二、为什么需要加密?三、常见的加密方式3.1 对称加密3.2非对称加密四、

MySQL中读写分离方案对比分析与选型建议

《MySQL中读写分离方案对比分析与选型建议》MySQL读写分离是提升数据库可用性和性能的常见手段,本文将围绕现实生产环境中常见的几种读写分离模式进行系统对比,希望对大家有所帮助... 目录一、问题背景介绍二、多种解决方案对比2.1 原生mysql主从复制2.2 Proxy层中间件:ProxySQL2.3

python使用Akshare与Streamlit实现股票估值分析教程(图文代码)

《python使用Akshare与Streamlit实现股票估值分析教程(图文代码)》入职测试中的一道题,要求:从Akshare下载某一个股票近十年的财务报表包括,资产负债表,利润表,现金流量表,保存... 目录一、前言二、核心知识点梳理1、Akshare数据获取2、Pandas数据处理3、Matplotl

python panda库从基础到高级操作分析

《pythonpanda库从基础到高级操作分析》本文介绍了Pandas库的核心功能,包括处理结构化数据的Series和DataFrame数据结构,数据读取、清洗、分组聚合、合并、时间序列分析及大数据... 目录1. Pandas 概述2. 基本操作:数据读取与查看3. 索引操作:精准定位数据4. Group

MySQL中EXISTS与IN用法使用与对比分析

《MySQL中EXISTS与IN用法使用与对比分析》在MySQL中,EXISTS和IN都用于子查询中根据另一个查询的结果来过滤主查询的记录,本文将基于工作原理、效率和应用场景进行全面对比... 目录一、基本用法详解1. IN 运算符2. EXISTS 运算符二、EXISTS 与 IN 的选择策略三、性能对比

MySQL 内存使用率常用分析语句

《MySQL内存使用率常用分析语句》用户整理了MySQL内存占用过高的分析方法,涵盖操作系统层确认及数据库层bufferpool、内存模块差值、线程状态、performance_schema性能数据... 目录一、 OS层二、 DB层1. 全局情况2. 内存占js用详情最近连续遇到mysql内存占用过高导致