摘要:比如上面的例子文件文件我們利用做了語法解析檢測,代碼如下報錯哪里類重復了不存在查看該屬性是否存在于父類中原理能就是對解析出來的繼續做分析,但是前人栽樹后人乘涼,這樣的完整工具已經有大神幫我們做好了。
原文:我的個人博客 https://mengkang.net/1356.html
工作了兩三年,技術停滯不前,迷茫沒有方向,不如看下我的直播 PHP 進階之路 (金三銀四跳槽必考,一般人我不告訴他)
很多時候,最大的優勢在某些情況下就會變成最大的劣勢。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 DataObjectArticle as array
不禁反思,如果這個項目是 java 的,肯定不會出現上面兩個問題了,因為在項目構建的時候就已經沒法通過了。
不存在的數組
這也不飄紅?多寫了個s呢,可能因為外面包了一個empty所以IDE沒有標記為錯誤吧。所以我們不能太相信IDE。
進一步思考,我們是否能夠做一個工具來自己模擬編譯呢?寫了一個小 demo ,依賴nikic/php-parser
https://github.com/nikic/PHP-...
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 PhpParserError; use PhpParserNodeStmtProperty; use PhpParserParserFactory; use PhpParserNodeStmtClass_; $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()} "; 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()." "; echo $property->getLine()." "; } } } } }
原理能就是對解析出來的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
a2(1); } /** * @param array $b * * @return int */ private function a2($b) { return $b + 1; } }
寫個shell腳本
#!/bin/bash function log() { echo -e -n "