使用 Phan 为你的 PHP 项目保驾护航 - 代码静态扫描

简介: 原文:我的个人博客 https://mengkang.net/1356.html 很多时候,最大的优势在某些情况下就会变成最大的劣势。PHP 语法非常灵活,也不用编译。但是在项目比较复杂的时候,可能会导致一些意想不到的 bug。

原文:我的个人博客 https://mengkang.net/1356.html

很多时候,最大的优势在某些情况下就会变成最大的劣势。PHP 语法非常灵活,也不用编译。但是在项目比较复杂的时候,可能会导致一些意想不到的 bug。

背景分析

不知道你的项目是否有遇到过类似的线上故障呢?比如

继承类语法错误导致的故障

文件1

class Animal
{
    public $hasLeg = false;
}

文件2

include "Animal.php";

class Dog extends Animal
{
    protected $hasLeg = false;
}

$dog = new Dog();
php Dog.php

Fatal error: Access level to Dog::$hasLeg must be public (as in class Animal) in /Users/mengkang/vagrant-develop/project/untitled1/Dog.php on line 5

image.png
(注意 IDE 并没有提示有预发错误的哟,我专门截图)

今天在看代码的时候看到一个变量一直重复查询,就是用户是否是管理员的身份。我想既然这样,不然在第一次用的地方就放入到成员变量里,免得后面都重复查询。

结果发现我在父类定义的变量名$isAdmin,之前的代码已经在某一个子类里面单独定义过了。父类里是public属性,而子类里是private导致了这个故障。

如果是 java 这种错误,无法编译通过。但是 php 不需要编译,只要测试没有覆盖到刚刚修改的文件就不会发现这个问题,既是优势也是弱势。

参数不符合预期

image.png

有时候a.php,b.php,c.php三个文件都引用d.php的的一个函数,但是修改了d.php里面的一个函数的参数个数,如果前面使用的3个文件里面的没有改全,只改了a.php,而测试的时候又没有覆盖到b.phpc.php,那么上线了,就会触发bug和错误了。

错把数组当对象

你可能认为这种错误太低级了,不可能发生在自己身上,但是根据我的经验的确会发生,高强度的需求之下,很容易复制粘贴一些东西,只复制一半。而且恰巧因为某些逻辑判断,自己在日常环境开发的时候,出现问题的地方没有被执行到。
比如下面这段代码:

$article = $this->getParam('article');

// 假设下面这段代码是复制的
$isPowerEditer = "xxxxx 演示代码";

if(!$isPowerEditer){
    if ($article->getUserId() != $uid)
    {
        ...
    }
}

因为复制的来源处,$article是一个对象,所以调用了getUserId的方法。但是上面的$article是一个从客户端获取的参数,不是对象。

Call to a member function getUserId() on a non-object

而自己测试的时候,因为if(!$isPowerEditer)的判断导致没有执行到里面去。直到上线之后才发现问题。

错把对象当数组

image.png

Cannot use object of type DataObject\Article as array

不禁反思,如果这个项目是 java 的,肯定不会出现上面两个问题了,因为在项目构建的时候就已经没法通过了。

不存在的数组

image.png
这也不飘红?多写了个s呢,可能因为外面包了一个empty所以IDE没有标记为错误吧。所以我们不能太相信IDE。

思考与改进

自造轮子实验

进一步思考,我们是否能够做一个工具来自己模拟编译呢?写了一个小 demo ,依赖nikic/php-parser

https://github.com/nikic/PHP-Parser

PHP-Parser 可以把PHP代码解析为AST,方便我们做语法分析。比如上面的例子
文件1

class Animal
{
    public $hasLeg = false;
}

文件2(Dog.php)

include "Animal.php";

class Dog extends Animal
{
    protected $hasLeg = false;
}

$dog = new Dog();

我们利用 PHP-Parser 做了语法解析检测,代码如下:

include dirname(__DIR__)."/vendor/autoload.php";

use PhpParser\Error;
use PhpParser\Node\Stmt\Property;
use PhpParser\ParserFactory;
use PhpParser\Node\Stmt\Class_;

$code = file_get_contents("Dog.php");

$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP5);

try {
    $ast = $parser->parse($code);
} catch (Error $error) {
    echo "Parse error: {$error->getMessage()}\n";
    return;
}

$classCheck = new ClassCheck($ast);
$classCheck->extendsCheck();


class ClassCheck{

    /**
     * @var Class_[]|null
     */
    private $classTable;

    public function __construct($nodes)
    {
        foreach ($nodes as $node){
            if ($node instanceof Class_){
                $name = $node->name;
                if (!isset($this->classTable[$name])) {
                    $this->classTable[$name] = $node;
                }else{
                    // 报错哪里类重复了
                    echo $node->getLine();
                }
            }
        }
    }

    public function extendsCheck(){

        foreach ($this->classTable as $node){
            if (!$node->extends){
                continue;
            }

            $parentClassName = $node->extends->getFirst();

            if (!isset($this->classTable[$parentClassName])) {
                exit($parentClassName."不存在");
            }

            $parentNode = $this->classTable[$parentClassName];

            foreach ($node->stmts as $stmt){
                if ($stmt instanceof Property){
                    // 查看该属性是否存在于父类中
                    $this->propertyCheck($stmt,$parentNode);
                }
            }
        }
    }

    /**
     * @param Property $property
     * @param Class_ $parentNode
     */
    private function propertyCheck($property,$parentNode){
        foreach ($parentNode->stmts as $stmt){
            if ($stmt instanceof Property){
                if ($stmt->props[0]->name != $property->props[0]->name){
                    continue;
                }

                if ($stmt->isProtected() && $property->isPrivate()) {
                    echo $stmt->getLine()."\n";
                    echo $property->getLine()."\n";
                }
            }
        }
    }
}

原理能就是对解析出来的AST继续做分析,但是前人栽树后人乘凉,这样的完整工具已经有大神帮我们做好了。

使用现有工具

https://github.com/phan/phan

可以说它与上面介绍的nikic/php-parser师出同门,依赖nikic/php-astPHP扩展

先安装php-ast扩展

大概描述安装步骤

git clone https://github.com/nikic/php-ast
cd php-ast/
phpize
sudo ./configure --enable-ast
sudo make
sudo make install
cd /etc/php.d
# 引入扩展
sudo vim ast.ini
# 就能看到扩展啦
php -m | grep ast

安装 composer

大概描述安装步骤

curl -sS https://getcomposer.org/installer | php

安装plan

mkdir test
cd test
~/composer.phar require --dev "phan/phan:1.x"

实验

实验1

新建个项目,随便写个有问题的代码

路径是src/a.php

<?php

class A extends B
{
    public function a1()
    {
        return $this->a2(1);
    }

    /**
     * @param array $b
     *
     * @return int
     */
    private function a2($b)
    {
        return $b + 1;
    }
}

写个shell脚本

#!/bin/bash

function log()
{
    echo -e -n "\033[01;35m[YUNQI] \033[01;31m"
    echo $@
    echo -e -n "\033[00m"
}

Color_Text()
{
  echo -e " \e[0;$2m$1\e[0m"
}

Echo_Red()
{
  echo $(Color_Text "$1" "31")
}

Echo_Green()
{
  echo $(Color_Text "$1" "32")
}

Echo_Yellow()
{
  echo $(Color_Text "$1" "33")
}

: > file.list

for file in $(ls src/*)
do
  echo $file >> file.list
done

Echo_Green "file list:\n"
Echo_Green "========================\n"

cat file.list

Echo_Green "========================\n"


Echo_Yellow "Phan run\n"
Echo_Yellow "========================\n"

./vendor/bin/phan -f file.list -o res.out

Echo_Yellow "========================\n"

Echo_Red "error log\n"
Echo_Red "========================\n"

cat res.out

Echo_Red "========================\n"
执行结果

案例中的错误

  1. 类不存在
  2. 参数类型错误
  3. 语法运算类型推断

image.png

实验2

新增一个src/b.php

<?php
class B{

}
执行结果

能过自动查找到class B了,不用我们做自动加载规则的指定

image.png

实验3

刚刚两个都是测试的单独的脚本,没有测试项目,其实Plan已经支持了。假如我有一个项目如下
image.png

我在composer.json里面指定自动加载规则

{
  "require-dev": {
    "phan/phan": "1.x"
  },
  "autoload": {
    "psr-4": {
      "Mk\\": "src"
    }
  }
}

然后在项目根目录执行

./vendor/bin/phan --init --init-level=3

然后就会生成默认的配置文件在.phan目录里,最后就可以执行静态检测命令了

./vendor/bin/phan --progress-bar

image.png

如图所示呢,说明根据项目的自动加载规则A,B,C三个类呢都被扫描到了。

看到这里,是不是有想把自己项目上线流程里面加上静态语法检测呢?心动不如行动。

目录
相关文章
|
8月前
|
消息中间件 缓存 弹性计算
纯PHP+MySQL手搓高性能论坛系统!代码精简,拒绝臃肿
本内容分享了一套经实战验证的社交系统架构设计,支撑从1到100万用户的发展,并历经6次流量洪峰考验。架构涵盖客户端层(App、小程序、公众号)、接入层(API网关、负载均衡、CDN)、业务服务层(用户、内容、关系、消息等服务)、数据层(MySQL、Redis、MongoDB等)及运维监控层(日志、监控、告警)。核心设计包括数据库分库分表、多级缓存体系、消息队列削峰填谷、CQRS模式与热点数据动态缓存。同时提供应对流量洪峰的弹性伸缩方案及降级熔断机制,并通过Prometheus实现全链路监控。开源建议结构清晰,适合大型社交平台构建与优化。
334 11
|
3月前
|
Web App开发 安全 PHP
刚上线的PHP项目被攻击了怎么办
近期因忙于多个项目,代码质量有所疏忽,导致项目上线后遭攻击。攻击者通过文件上传漏洞和测试登录方式入侵,修改了 index.php 并植入恶意文件。经排查,问题源于未限制上传文件类型及未关闭测试登录配置。修复措施包括锁定文件、限制上传后缀、关闭测试登录、删除高危用户并限制其访问。此次事件提醒我们,细节疏漏可能引发严重风险,需时刻保持警惕。
|
3月前
|
存储 小程序 Java
热门小程序源码合集:微信抖音小程序源码支持PHP/Java/uni-app完整项目实践指南
小程序已成为企业获客与开发者创业的重要载体。本文详解PHP、Java、uni-app三大技术栈在电商、工具、服务类小程序中的源码应用,提供从开发到部署的全流程指南,并分享选型避坑与商业化落地策略,助力开发者高效构建稳定可扩展项目。
|
5月前
|
JavaScript Linux PHP
composer如何安装以及举例在PHP项目中使用Composer安装TCPDF库-优雅草卓伊凡
composer如何安装以及举例在PHP项目中使用Composer安装TCPDF库-优雅草卓伊凡
202 3
composer如何安装以及举例在PHP项目中使用Composer安装TCPDF库-优雅草卓伊凡
|
5月前
|
存储 缓存 安全
php项目源码加密之Zend Guard Loader加密的代码解密可能性很小-优雅草卓伊凡
php项目源码加密之Zend Guard Loader加密的代码解密可能性很小-优雅草卓伊凡
222 2
|
5月前
|
PHP
Composer在PHP项目中的手动类自动加载策略
为了确保文件名和类名的一致性及其可预测性,类名通常应该与文件名相匹配,所有的类都应属于一个命名空间,并且目录结构应该反映命名空间的结构。如此一来,当新的类被创建时,它们会自动遵守已有的约定,从而无需修改自动加载逻辑,确保了代码的模块化和可管理性。
70 8
|
9月前
|
JSON 自然语言处理 前端开发
【01】对APP进行语言包功能开发-APP自动识别地区ip后分配对应的语言功能复杂吗?-成熟app项目语言包功能定制开发-前端以uniapp-基于vue.js后端以laravel基于php为例项目实战-优雅草卓伊凡
【01】对APP进行语言包功能开发-APP自动识别地区ip后分配对应的语言功能复杂吗?-成熟app项目语言包功能定制开发-前端以uniapp-基于vue.js后端以laravel基于php为例项目实战-优雅草卓伊凡
511 72
【01】对APP进行语言包功能开发-APP自动识别地区ip后分配对应的语言功能复杂吗?-成熟app项目语言包功能定制开发-前端以uniapp-基于vue.js后端以laravel基于php为例项目实战-优雅草卓伊凡
|
6月前
|
安全 API PHP
Composer在现代PHP项目中的进阶实践
简介:本文深入探讨PHP依赖管理工具Composer的核心功能与企业实践。首先介绍其如何通过`composer.json`实现声明式依赖管理,变革PHP生态;接着解析自动加载优化、版本策略(如`~`和`^`)及私有仓库集成等关键特性;然后分享CI/CD集成、多环境配置与性能优化技巧(如阿里云镜像加速)的企业级方案;最后提供常见问题排查方法,如内存不足和依赖冲突解决,助力开发者高效管理项目依赖。
161 3
|
PHP 开发者 容器
PHP命名空间深度解析:避免命名冲突与提升代码组织####
本文深入探讨了PHP中命名空间的概念、用途及最佳实践,揭示其在解决全局命名冲突、提高代码可维护性方面的重要性。通过生动实例和详尽分析,本文将帮助开发者有效利用命名空间来优化大型项目结构,确保代码的清晰与高效。 ####
185 20
|
监控 关系型数据库 PHP
PHP技术月刊第1期:使用 Phan 为你的 PHP 项目保驾护航
云栖社区“世界上最好的编程语言”——PHP开始发布技术月刊啦。PHP技术月刊将会为大家介绍最新的PHP技术与动态、预告活动、最热问答以及技术直播等,欢迎大家订阅。
2380 0