原文:我的個人部落格 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

(注意 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
https://github.com/nikic/PHP-Parser
PHP-Parser 可以把PHP代碼解析為AST,友善我們做文法分析。比如上面的例子
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-ast
PHP擴充
先安裝 php-ast
擴充
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"
執行結果
案例中的錯誤
- 類不存在
- 參數類型錯誤
- 文法運算類型推斷
實驗2
新增一個
src/b.php
<?php
class B{
}
能過自動查找到
class B
了,不用我們做自動加載規則的指定
實驗3
剛剛兩個都是測試的單獨的腳本,沒有測試項目,其實
Plan
已經支援了。假如我有一個項目如下
我在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
如圖所示呢,說明根據項目的自動加載規則
A
B
C
三個類呢都被掃描到了。
看到這裡,是不是有想把自己項目上線流程裡面加上靜态文法檢測呢?心動不如行動。