很多时候,最大的优势在某些情况下就会变成最大的劣势。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
(注意 IDE 并没有提示有预发错误的哟,我专门截图)
今天在看代码的时候看到一个变量一直重复查询,就是用户是否是管理员的身份。我想既然这样,不然在第一次用的地方就放入到成员变量里,免得后面都重复查询。
结果发现我在父类定义的变量名$isAdmin
,之前的代码已经在某一个子类里面单独定义过了。父类里是public
属性,而子类里是private
导致了这个故障。
如果是 java 这种错误,无法编译通过。但是 php 不需要编译,只要测试没有覆盖到刚刚修改的文件就不会发现这个问题,既是优势也是弱势。
参数不符合预期
有时候a.php
,b.php
,c.php
三个文件都引用d.php
的的一个函数,但是修改了d.php
里面的一个函数的参数个数,如果前面使用的3个文件里面的没有改全,只改了a.php
,而测试的时候又没有覆盖到b.php
和c.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)
的判断导致没有执行到里面去。直到上线之后才发现问题。
错把对象当数组
Cannot use object of type DataObject\Article as array
不禁反思,如果这个项目是 java 的,肯定不会出现上面两个问题了,因为在项目构建的时候就已经没法通过了。
不存在的数组
这也不飘红?多写了个s
呢,可能因为外面包了一个empty
所以IDE没有标记为错误吧。所以我们不能太相信IDE。
自造轮子
进一步思考,我们是否能够做一个工具来自己模拟编译呢?写了一个小 demo ,依赖nikic/php-parser
PHP-Parser 可以把PHP代码解析为AST,方便我们做语法分析。比如上面的例子
//a.php
class AAA{
protected $a = 1;
}
/**
* Class BBB
* @author zmk
*/
class BBB extends AAA{
private $a = 10;
public function test($a){
echo $this->a;
}
}
$b = new BBB();
$b->test(1);
<?php
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("a.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;
/**
* MyCheck constructor.
*/
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";
}
}
}
}
}