找回密码
 会员注册
查看: 12|回复: 0

PHP命名空间与类常量用法检查方案的探索与实践

[复制链接]

2万

主题

0

回帖

6万

积分

超级版主

积分
65944
发表于 2024-10-9 17:23:38 | 显示全部楼层 |阅读模式
PHP命名空间与类常量用法检查方案的探索与实践 PHP命名空间与类常量用法检查方案的探索与实践 高志红@贝壳找房 贝壳产品技术 贝壳产品技术 “贝壳产品技术公众号”作为贝壳官方产品技术号,致力打造贝壳产品、技术干货分享平台,面向互联网/O2O开发/产品从业者,每周推送优质产品技术文章、技术沙龙活动及招聘信息等。欢迎大家关注我们。 242篇内容 2021年03月06日 00:20 1 背景1.1 命名空间解决什么问题提到命名空间,想必PHP开发的同学,都不陌生吧,PHP命名空间可以解决以下两类问题:用户编写的代码与PHP内部的类/函数/常量或第三方类/函数/常量之间的名字冲突。为很长的标识符名称(通常是为了缓解第一类问题而定义的)创建一个别名(或简短)的名称,提高源代码的可读性。因此,贝壳的很多PHP项目都开启了命名空间。1.2 命名空间和类常量的使用容易出现的错误命名空间的一个重要特征,是允许通过别名引用或导入外部的完全限定名称,导入方式:使用 use 操作符导入,但是如果没有导入命名空间,直接使用类的话,根据命名空间解析规则解析后,如果没有对应的类文件,就会报致命错误:“Class xxx not found”。同样,类常量没有定义的情况,也会造成"常量没有定义"的致命错误。这种致命错误,一般在研发自测和测试过程中可以发现,但是多人合作开发过程中,很有可能会出现合并分支时,导入代码被合掉,或者出现在逻辑异常分支,而自测和qa测试没有覆盖,都可能会带到线上。这类错误一旦上线,就会造成接口500,还会阻断业务流程,并且排查加回滚耗时,影响十分严重,以上都是血的教训啊。命名空间&类常量自动检测,可以让我们在开发阶段,借助工具主动发现问题,避免把问题带到线上。2 检测分析2.1 检测分析2.1.1 命名空间检测分析要检测一个文件中,用到的类的命名空间是否导入,check的思路很明确:看每个使用的类,根据命名空间解析规则转换后,是否对应有对应的类文件。若没有对应类文件,那这个类的使用就会出现“类找不到”的致命错误了。根据这个思路,我们要通过扫描待检测文件,获取这些信息:所有使用的类、所有导入的命名空间、当前文件的命名空间、命名空间对应的文件。2.1.2 类常量检测分析要检测一个文件中,用到的类常量是否定义,我的思路是:对于一个类常量,获取到类对应的文件路径,然后check类文件中是否定义该常量,若没有定义,继续check其父类文件,父类文件没有定义,再check父类的父类···,也就是递归check父类文件,看类常量是否定义。根据这个思路,我们要通过扫描待检测文件,获取这些信息:文件中所有使用的类常量及类名,类的文件地址,父类文件。其中,类的文件地址,在命名空间的检测中,我们已经可以根据类名,获取到类的文件地址,因此,上述需要的信息可以进一步提炼为:文件中所有使用的类常量及其对应类名、父类类名。综上,需要在文件中解析的内容如下:所有使用的类、所有导入的命名空间、当前文件的命名空间、命名空间对应的文件、所有使用的类常量、父类。2.2 文件解析2.2.1 扫描获取文件中使用的类类的使用有如下四种方式://new实例化一个对象$a=newTestClass();//静态调用ClassName::test();self::test();//当前类parent::test();//调用父类方法其中,self 和 parent这俩个特殊的关键字是用于在类定义的内部对其属性或方法进行访问的,因此,我们根据另外两种使用方式,使用正则匹配的方式,解析文件中使用的类:$regExr="/(fileString,$matches);2.2.2 扫描获取文件中导入的命名空间命名空间的导入是通过操作符use来实现的,因此,通过正则匹配方式也可以获取到文件中导入的所有命名空间,哦对了,别忘了PHP7命名空间导入的新语法。useApp\Models\TestModel;useApp\Library\Enums\{Error,MetricasMetricEnums};//PHP7命名空间导入的新语法得到导入的命名空间后,还要考虑另一个问题:根据命名空间的解析规则获取到转换后的完整命名空间,因此,还需要解析出每个导入的命名空间的别名/类名。privatefunctionsetDeclaireSpace(){//检索文件中声明的命名空间$regExr="/(fileString,$namespaceMatches);$classFullName=$namespaceMatches[0][];//别名和php7新语法foreach($classFullNameas$fullName){$fullNames=[];//是否使用php7语法声明了多个命名空间$usephp7=strpos($fullName,"{");if($usephp7){$spacePrefix=substr($fullName,0,$usephp7);$allNames=substr($fullName,$usephp7+1,(strpos($fullName,"}")-1-$usephp7));$allNames=explode(',',$allNames);foreach($allNamesas$item){$fullNames[]=$spacePrefix.trim($item);}}else{$fullNames=[$fullName];}foreach($fullNamesas$value){if(preg_match("/(\S+)\sas\s(\w+)/i",$value,$matches)){//有别名$spaceName=$matches[1];$aliasName=$matches[2];}else{$spaceName=$value;$aliasName=substr($value,strrpos($value,self::FANXIEGANG)+1);}!isset($this->declaireSpace[$aliasName])&$this->declaireSpace[$aliasName]=trim($spaceName,self::FANXIEGANG);}}}2.2.3 获取待扫描文件的命名空间和父类关于获取文件的命名空间和父类,根据其对应定义语法,通过正则匹配,也可以获取到。但值得注意的是,PHP虽然是单继承,但是类可是以实现多个接口的,因此“父类”可能有多个,我们要进行很多次的正则匹配,比较考验正则功底,性能也不高,因此考察之后,我们选用了另一种方法获取这些内容:使用token_get_all函数-解析PHP文件。简单介绍下token_get_all函数:说明token_get_all(string$source):arraytoken_get_all()解析提供的source源码字符,然后使用Zend引擎的语法分析器获取源码中的PHP语言的解析器代号参数source:需要解析的 PHP 源码.解析内容示例如下,解释器代号见附录:显然,这个函数可以轻松获取到文件的命名空间,父类等信息,示例代码如下:publicfunctioninit(){$namespace='';$extends='';$tokens=token_get_all($this->fileString);for($index=0;isset($tokens[$index]);$index++){if(!isset($tokens[$index][0])){continue;}if(T_NAMESPACE===$tokens[$index][0]){$index+=2;//Skipnamespacekeywordandwhitespacewhile(isset($tokens[$index])&is_array($tokens[$index])){$namespace.=$tokens[$index++][1];}$this->namespace=$namespace;}if((T_CLASS===$tokens[$index][0]||T_TRAIT===$tokens[$index][0]||T_INTERFACE===$tokens[$index][0])&T_WHITESPACE===$tokens[$index+1][0]&T_STRING===$tokens[$index+2][0]){$this->type=self::CLASS_TYPE_MAP[$tokens[$index][0]]'';$index+=2;//Skipclasskeywordandwhitespace$this->class=$tokens[$index][1];}if(T_EXTENDS===$tokens[$index][0]&T_WHITESPACE===$tokens[$index+1][0]){$index+=2;//Skipnamespacekeywordandwhitespacewhile(isset($tokens[$index])&is_array($tokens[$index])&T_WHITESPACE!==$tokens[$index][0]){$extends.=$tokens[$index++][1];}$this->baseClass=$extends;$this->baseClassType=self::TYPE_CLASS;break;}if(T_IMPLEMENTS===$tokens[$index][0]&T_WHITESPACE===$tokens[$index+1][0]){$index+=2;//Skipnamespacekeywordandwhitespace$implements='';while(isset($tokens[$index])){if(!is_array($tokens[$index])&$tokens[$index]!==','){break;}if(is_array($tokens[$index])&T_WHITESPACE!==$tokens[$index][0]){$implements.=$tokens[$index][1];}if(is_string($tokens[$index])){$implements.=$tokens[$index];}$index++;}$this->implements=explode(',',$implements);$this->baseClassType=self::TYPE_INTERFACE;break;}}}2.2.4 扫描获取文件中使用的类常量及对应类名类常量的访问方式和静态成员类似,可以通过类名或在成员方法中使用self访问,但在PHP 5.3.0之后也可以使用对象来访问(这种case暂不检测),其次,根据贝壳的代码规范,类中的常量名称必须是大写,根据以上特点,可以使用正则匹配的方式,获取文件中所有使用的类常量:publicfunctioninit(){$namespace='';$extends='';$tokens=token_get_all($this->fileString);for($index=0;isset($tokens[$index]);$index++){if(!isset($tokens[$index][0])){continue;}if(T_NAMESPACE===$tokens[$index][0]){$index+=2;//Skipnamespacekeywordandwhitespacewhile(isset($tokens[$index])&is_array($tokens[$index])){$namespace.=$tokens[$index++][1];}$this->namespace=$namespace;}if((T_CLASS===$tokens[$index][0]||T_TRAIT===$tokens[$index][0]||T_INTERFACE===$tokens[$index][0])&T_WHITESPACE===$tokens[$index+1][0]&T_STRING===$tokens[$index+2][0]){$this->type=self::CLASS_TYPE_MAP[$tokens[$index][0]]'';$index+=2;//Skipclasskeywordandwhitespace$this->class=$tokens[$index][1];}if(T_EXTENDS===$tokens[$index][0]&T_WHITESPACE===$tokens[$index+1][0]){$index+=2;//Skipnamespacekeywordandwhitespacewhile(isset($tokens[$index])&is_array($tokens[$index])&T_WHITESPACE!==$tokens[$index][0]){$extends.=$tokens[$index++][1];}$this->baseClass=$extends;$this->baseClassType=self::TYPE_CLASS;break;}if(T_IMPLEMENTS===$tokens[$index][0]&T_WHITESPACE===$tokens[$index+1][0]){$index+=2;//Skipnamespacekeywordandwhitespace$implements='';while(isset($tokens[$index])){if(!is_array($tokens[$index])&$tokens[$index]!==','){break;}if(is_array($tokens[$index])&T_WHITESPACE!==$tokens[$index][0]){$implements.=$tokens[$index][1];}if(is_string($tokens[$index])){$implements.=$tokens[$index];}$index++;}$this->implements=explode(',',$implements);$this->baseClassType=self::TYPE_INTERFACE;break;}}}2.2.5 获取命名空间对应的文件我们使用的是贝壳封装的laravel框架,根据composer自动加载原理,在项目的 vendor/composer/autoload_classmap.php 中,有全部的命名空间和文件的映射关系。综上,已经完成了文件解析,封装成一个文件类,如下:OK,检测的前置条件都做好了,我们开始进行检测吧。3 检测实现3.1 检测流程检测想要在代码提交时触发,因此,使用gitlab-ci工具。gitlab-ci是一个简易版的jenkins,runner可以理解为是Jenkins的slave,机器(或docker)通过runner程序与git服务器进行通信,当有新的任务发布到当前runner时,runner会执行.gitlab-ci.yml所定义的CI指令。检测流程如下:3.2 检测核心实现3.2.1 命名空间检测核心实现检测流程如下图:根据命名空间规则依次转换为类的全命名空间:publicstaticfunctiongetClassNameSpace($class,$declaireSpace,$namespace){if(empty($class)){return'';}//获取完整的命名空间声明$classInfo=explode("\\",$class);$aliasName=$classInfo[0];if(empty($aliasName)){//以\开头,完全限定名称returntrim($class,"\\");}//为了提高效率,先看是否有严格声明的。if(isset($declaireSpace[$aliasName])){unset($classInfo[0]);returnrtrim($declaireSpace[$aliasName]."\\".implode("\\",$classInfo),"\\");}//命名空间大小写不敏感foreach($declaireSpaceas$classKey=>$classSpace){if(strtolower($aliasName)==strtolower($classKey)){unset($classInfo[0]);returnrtrim($declaireSpace[$classKey]."\\".implode("\\",$classInfo),"\\");}}//以上不区分大小写,仍没有匹配声明的类returnempty($namespace)$classnamespace."\\".$class;}根据class_map查找对应文件,判断文件是否存在:publicstaticfunctioncheckNamespace($class,$namespace,$declaireSpace,$classMap){if(empty($class)){returnfalse;}//获取完整的命名空间声明$fullClass=self::getClassNameSpace($class,$declaireSpace,$namespace);//是否有该命名空间的映射关系$lowerFullClass=strtolower($fullClass);if(isset($classMap[$lowerFullClass])){return['fullClass'=>$fullClass,'classFile'=>$classMap[$lowerFullClass],];}//未正确引入命名空间Log::error("{$class},fullClass={$fullClass},无命名空间映射");returnfalse;}3.2.2 类常量检测核心实现检测流程如下:常量是否定义常量使用关键字const定义,因此,可以使用正则匹配的方式,check常量是否定义:preg_match("/($filePath,'constValue'=>$define[$constant],];}$baseFile=self::getBaseFile($filePath,$classMap,$project,$projectDir);foreach($baseFileas$item){$checkRes=self::checkConstant($item,$constant,$classMap,$project,$projectDir);if(!empty($checkRes)){break;}}return$checkRes;}3.3 检测结果示例借助gitlab-runner的能力,可以做到在每次代码提交时进行扫描检测,检测结果以企业微信方式通知,以下是一些检测出的报警case:4 未来规划综上,我们基于laravel框架搭建的这个检测平台,能够对业务代码中命名空间是否导入、导入的命名空间类源文件是否存在以及类常量是否定义的进行检测。在每次代码提交时进行扫描检测,检测结果以企业微信方式通知。目前二手9个项目已接入检测扫描,能够有效主动发现问题,杜绝这类线上问题产生。未来,我们将接入公司内部的KeOnes代码检测平台,让更多的laravel项目可以使用! 预览时标签不可点 后端27php5后端 · 目录#后端上一篇Druid在贝壳的应用实践下一篇PHP协程多任务调度实践关闭更多小程序广告搜索「undefined」网络结果
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2024-12-31 04:18 , Processed in 0.520617 second(s), 25 queries .

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表