交互式的Debug工具PsySH



原文: Interactive PHP Debugging with PsySH 作者: Miguel Ibarra Romero 翻译:Symo

现在是凌晨一点,距离你的Web项目上线只有八个小时了,然而它却不能正常的工作。你不停向代码中填入var_dump()die()试图定位程序中的bug……

此时的你异常恼火,不得不一遍又一遍的更改源码,运行程序,查看某个函数的返回值或者检查某个变量是否被正确的赋值。最后也不确定是否要删掉代码中一大堆的var_dump()

这样的情景是否于你似曾相识呢?

PsySH来拯救你了

PsySH是一个交互式的解释环境(Read-Eval-Print Loop),如果你曾经使用浏览器的控制台调试过JavaScript,你会明白这是一个非常强大和方便的Debug方式。

谈到PHP,你也许用过PHP的交互控制台(php -a),在里面你可以直接写一些代码然后直接执行它们。

php -a
Interactive shell

php > $a = 'Hello world!';
php > echo $a;
Hello world!
php >

然而遗憾的是,这个交互式界面缺乏一个“P”(print),所以不是REPL。我必须执行一个echo语句来打印$a的值。使用真正的REPL,我们将会在给变量赋值的一瞬间看到结果。

要安装PsySH,可以使用composer g require,也可以下载PsySH的可执行文件。

Composer

composer g require psy/psysh:~0.1
psysh

直接下载(Linux、Mac)

wget psysh.org/psysh
chmod +x psysh
./psysh

另外,你也可以使用composer为每个项目单独的引入PsySH,就像一会儿你在将在文章的后面见到的。

现在,让我们开始使用一下PsySH吧。

./psysh
Psy Shell v0.1.11 (PHP 5.5.8 — cli) by Justin Hileman   >>>

它的主要帮助文件将会是你的好助手,它会告诉你所有的参数和它们的含义。

>>> help

  help      Show a list of commands. Type `help [foo]` for information about [foo].      Aliases: ?
  
  ls        List local, instance or class variables, methods and constants.              Aliases: list, dir
  
  dump      Dump an object or primitive.
  
  doc       Read the documentation for an object, class, constant, method or property.   Aliases: rtfm, man 
  
  show      Show the code for an object, class, constant, method or property.
  
  wtf       Show the backtrace of the most recent exception.                             Aliases: last-exception, wtf?
  
  trace     Show the current call stack.
  
  buffer    Show (or clear) the contents of the code input buffer.                       Aliases: buf
  
  clear     Clear the Psy Shell screen.
  
  history   Show the Psy Shell history.
  
  exit      End the current session and return to caller.
>>> help ls

Usage:

ls [--vars] [-c|--constants] [-f|--functions] [-k|--classes] [-I|--interfaces] [-t|--traits] [-p|--properties] [-m|--methods] [-G|--grep="..."] [-i|--insensitive] [-v|--invert] [-g|--globals] [-n|--internal] [-u|--user] [-C|--
category="..."] [-a|--all] [-l|--long] [target]

Aliases: list, dir

Arguments:

 target             A target class or object to list.
 
 
Options:

 --vars             Display variables.
 
 --constants (-c)   Display defined constants.
 
 --functions (-f)   Display defined functions.
 
 --classes (-k)     Display declared classes.
 
 --interfaces (-I)  Display declared interfaces.
 
 --traits (-t)      Display declared traits.
 
 --properties (-p)  Display class or object properties (public properties by default).
 
 --methods (-m)     Display class or object methods (public methods by default).
 
 --grep (-G)        Limit to items matching the given pattern (string or regex).
 
 --insensitive (-i) Case-insensitive search (requires --grep).
 
 --invert (-v)      Inverted search (requires --grep).
 
 --globals (-g)     Include global variables.
 
 --internal (-n)    Limit to internal functions and classes.
 
 --user (-u)        Limit to user-defined constants, functions and classes.
 
 --category (-C)    Limit to constants in a specific category (e.g. "date").
 
 --all (-a)         Include private and protected methods and properties.
 
 --long (-l)        List in long format: includes class names and method signatures.
 
 
 Help:
 
 List variables, constants, classes, interfaces, traits, functions, methods, and properties.
 
 Called without options, this will return a list of variables currently in scope.
 
 If a target object is provided, list properties, constants and methods of that target. If a class, interface or trait name is passed instead, list constants and methods on that class.
 
 e.g. 
 
 >>> ls
 >>> ls $foo
 >>> ls -k --grep mongo -i
 >>> ls -al ReflectionClass
 >>> ls --constants --category date
 >>> ls -l --functions --grep /^array_.*/
 >>>

最基本的,PsySH可以这样用:

>>> $a = 'hello';
=> "hello"
>>>

请注意PsySH和PHP控制台之间的区别,PsySH在$a被赋值的一瞬间就打印出了它的值。

下面是一个稍微复杂的例子:

>>> function say($a) {
...     echo $a;
... }
=> null
>>> say('hello');
hello
=> null
>>>

我们定义了一个say()函数并调用。你看到两个null是因为函数既没有定义也没有return一个值(函数直接echo出来变量的值),另外在定义一个函数的时候,提示符从>>>变成了...

那么我们能不能定义一个类然后实例化呢?

>>> class Foo
... {
...     protected $a;
...
...     public function setA($a) {
...         $this->a = $a;
...     }
...
...     public function getA() {
...         return $this->a;
...     }
... }
=> null
>>> $foo = new Foo();
=> <Foo #000000001dce50dd000000002dda326e> {}
>>> $foo->setA('hello');
=> null
>>> $foo->getA();
=> "hello"
>>>

Foo被实例化的时候,构造器返回了一个对象的引用,这是为什么PsySH打印了一个<Foo #000000001dce50dd000000002dda326e>。现在让我们看一看PsySH在对象上一些有趣的用法。

>>> ls $foo
Class Methods: getA, setA
>>>

假如因为某些原因你忘记了在Foo中定义了那些方法,现在你就有答案了。你用过Linux或者Mac的命令行界面吗?那么你应当很熟悉ls命令吧。还记得-la参数吗?

>>> ls -la $foo
Class Properties:

  $a   "hello" 
  

Class Methods:

  getA   public function getA()
  setA   public function setA($a)

很好玩,不是吗?

PsySH强大的能力在整合到项目之后才会真正展现出来,所以让我们来构建一个项目吧。

Demo app

我会使用装饰者模式来实现一个快捷的应用作为展示,模式的UML类图如下: 1

如果你并不熟悉UML或者设计模式,也不要担心,理解它们对这篇文章而言并不是必须的。

还有在这个项目中,我建立了一些能够在phpUnit中运行的测试方法,同样,如果你不熟悉单元测试的话,也不会影响你理解这篇文章。

这个小应用的完整源代码可以在这里找到:https://github.com/sitepoint-examples/PsySH

首先,让我们定义项目的composer.json来声明PsySH的依赖关系。

{
    "name": "example/psysh",
    "authors": [
        {
            "name": "John Doe",
            "email": "john@doe.tst"
        }
    ],
    "require": {
        "psy/psysh": "~0.1"
    },
    "autoload": {
        "psr-4": {"Acme\\": "src/"}
    }
}

在运行composer install之后,就可以开始了。

请先看一下public/decorator.php文件中的代码,它会实例化SimpleWindow, DecoratedWindow,和 TitledWindow对象来=展示装饰者模式。

<?php
chdir(dirname(__DIR__));

require_once('vendor/autoload.php');

use Acme\Patterns\Decorator\SimpleWindow;
use Acme\Patterns\Decorator\DecoratedWindow;
use Acme\Patterns\Decorator\TitledWindow;

echo PHP_EOL . 'Simple Window' . PHP_EOL;

$window = new SimpleWindow();

echo $window->render();

echo PHP_EOL . 'Decorated Simple Window' . PHP_EOL;

$decoratedWindow = new DecoratedWindow($window);

echo $decoratedWindow->render();

echo PHP_EOL . 'Titled Simple Window' . PHP_EOL;

$titledWindow = new TitledWindow($window);

echo $titledWindow->render();

我们可以使用PHP CLI(命令行界面)或者通过配置Web服务器来运行这些代码。也可以使用PHP内置的Web服务器。

使用CLI进行Debugging

使用命令行界面运行上面的代码将会看到这样的界面:

php public/decorator.php 

Simple Window
+-------------+
|             |
|             |
|             |
|             |
|             |
+-------------+

Decorated Simple Window
+-------------+
|             |
|             |
|             |
|             |
|             |
+-------------+

Titled Simple Window
+-------------+
|Title        |
+-------------+
|             |
|             |
|             |
|             |
|             |
+-------------+

那么如何让PsySH在项目中起作用呢?只需要在你想要debug的位置添加\Psy\Shell::debug(get_defined_vars());这段代码。也就是你打算插入var_dump()的地方。

<?php
chdir(dirname(__DIR__));

require_once('vendor/autoload.php');

//... a lot of code here

$titledWindow = new TitledWindow($window);

echo $titledWindow->render();

\Psy\Shell::debug(get_defined_vars()); //we want to debug our application here!

当我们保存文件时,就可以看到这样的输出:

php public/decorator.php 

Simple Window
+-------------+
|             |
|             |
|             |
|             |
|             |
+-------------+

Decorated Simple Window
+-------------+
|             |
|             |
|             |
|             |
|             |
+-------------+

Titled Simple Window
+-------------+
|Title        |
+-------------+
|             |
|             |
|             |
|             |
|             |
+-------------+

Psy Shell v0.1.11 (PHP 5.5.8 — cli) by Justin Hileman
>>>

脚本的执行将被挂起,然后出现了PsySH的提示符供我们使用。我把get_defined_var()当作参数传进了Psy\Shell::debug(),所以我可以在shell里查看到所有被定义过的变量:

>>> ls
Variables: $_COOKIE, $_FILES, $_GET, $_POST, $_SERVER, $argc, $argv, $decoratedWindow, $titledWindow, $window
>>>

来让我们检查一下$window变量:

>>> ls -al $window

Class Methods:
  render   public function render()  
>>>

PsySH贴心的一点就是我们可以在程序中对实例出来的对象中的代码进行检查。

>>> show $window
class Acme\Patterns\Decorator\SimpleWindow implements Acme\Patterns\Decorator\Window
class SimpleWindow implements Window
{
    public function render()
    {
        $returnString = <<<EOL
+-------------+
|             |
|             |
|             |
|             |
|             |
+-------------+
EOL;
        return $returnString . PHP_EOL;
    }
}
>>>

显然,$window是一个SimpleWindow对象的实例,它实现了一个窗口界面。我很好奇这个窗口界面的代码是什么样的:

>>> show Acme\Patterns\Decorator\Window
interface Acme\Patterns\Decorator\Window
interface Window
{
    public function render();
}
>>>

为什么SimpleWindow和DecoratedWindow会有相同的输出呢?让我们检查一下$DecoratedWindow对象:

>>> ls -al $decoratedWindow

Class Properties:
  $windowReference   <Digitec\Patterns\Decorator\SimpleWindow #000000003643d67700000000731101b7>  

Class Methods:
  __construct          public function __construct(Digitec\Patterns\Decorator\Window $windowReference)         
  getWindowReference   public function getWindowReference()                                                    
  render               public function render()                                                                
  setWindowReference   public function setWindowReference(Digitec\Patterns\Decorator\Window $windowReference)  
>>>

这个对象比SimpleWindow稍微“重”了一点儿,所以代码可能很长,让我们单独看一下render()方法的代码:

>>> show $decoratedWindow->render
public function render()
    public function render()
    {
        return $this->getWindowReference()->render();
    }

getWindowReference()方法被调用了,然后返回给了render()方法。我们来检查一下getWindowReference()方法的代码:

>>> show $decoratedWindow->getWindowReference
public function getWindowReference()
    public function getWindowReference()
    {
        return $this->windowReference;
    }
>>>

这个方法返回了对象的windowReference属性,就想我们在上面使用ls -al命令所看到的,这是一个Acme\Patterns\Decorator\SimpleWindow的实例。当然,我们可以只是看一下构造函数DecoratedWindow::__construct()是如何工作的,不过我们还有另一种办法来检查。

使用内置服务器Debug

很遗憾的是,使用Web服务器比如Apache httpd是不支持这种调试方式的。不管怎样,我们依然可以用PHP的内置服务器来调试:

$ cd public
$ php -S localhost:8080
PHP 5.5.8 Development Server started at Wed Jul 23 17:40:30 2014
Listening on http://localhost:8080
Document root is /home/action/workspace/lab/PsySH/public
Press Ctrl-C to quit.

这台开发服务器已经开始监听8080端口的链接,所以当我们通过([http://localhost:8080/decorator.php)]())访问`decorator.php`时,会看到如下输出:

Psy Shell v0.1.11 (PHP 5.5.8 — cli-server) by Justin Hileman
>>>

我们就可以像刚才在CLI下一样的使用PsySH了。

>>> ls -al

Variables:
  $_COOKIE           []                                                                              
  $_FILES            []                                                                              
  $_GET              []                                                                              
  $_POST             []                                                                              
  $_SERVER           Array(19)                                                                       
  $decoratedWindow   <Acme\Patterns\Decorator\DecoratedWindow #0000000031ef2e3e000000003c2d3a90>  
  $titledWindow      <Acme\Patterns\Decorator\TitledWindow #0000000031ef2e39000000003c2d3a90>     
  $window            <Acme\Patterns\Decorator\SimpleWindow #0000000031ef2e3f000000003c2d3a90>     
  $_                 null                                                                            
>>> exit
Exit:  Goodbye.

使用单元测试进行Debug

作为一个合格的开发者,你应该为自己的代码编写单元测试,来保证它们能够如期运行。在这个项目文件中你可以找到tests文件夹,如果你安装了phpUnit,你就可以在其中运行测试。

cd tests/
phpunit
PHPUnit 4.0.14 by Sebastian Bergmann.

Configuration read from /home/action/workspace/lab/PsySH/tests/phpunit.xml

...F+-------------+
|Title        |


Time: 66 ms, Memory: 4.50Mb

There was 1 failure:

1) AcmeTest\Patterns\Decorator\TitledWindowTest::testAddTitle
Failed asserting that true is false.

/home/action/workspace/lab/PsySH/tests/Patterns/Decorator/TitledWindowTest.php:46
                                     
FAILURES!                            
Tests: 4, Assertions: 7, Failures: 1.

当测试不通过的时候,你代码也许看上去完美无暇无懈可击。但是我们可以进一步检测出问题的测试:

$ phpunit --verbose --debug --filter=TitledWindowTest::testAddTitle
PHPUnit 4.0.14 by Sebastian Bergmann.

Configuration read from /home/action/workspace/lab/PsySH/tests/phpunit.xml


Starting test 'AcmeTest\Patterns\Decorator\TitledWindowTest::testAddTitle'.
F+-------------+
|Title        |


Time: 60 ms, Memory: 4.25Mb

There was 1 failure:

1) AcmeTest\Patterns\Decorator\TitledWindowTest::testAddTitle
Failed asserting that true is false.

/home/action/workspace/lab/PsySH/tests/Patterns/Decorator/TitledWindowTest.php:46
                                     
FAILURES!                            
Tests: 1, Assertions: 1, Failures: 1.

我们知道了错误出现的文件,行数和哪一项测试,于是来看一下TitledWindowTest.php文件

<?php
namespace AcmeTest\Patterns\Decorator;

use PHPUnit_Framework_TestCase;
use Acme\Patterns\Decorator\TitledWindow;
use ReflectionMethod;


class TitledWindowTest extends PHPUnit_Framework_TestCase 
{
    public function testRender()
    {
        /* some test code here */
        
    }
    
    public function testAddTitle()
    {
        $renderString = 'bar';
        
        $window = $this->getMock('Acme\Patterns\Decorator\SimpleWindow');
        $window->expects($this->any())->method('render')->will($this->returnValue($renderString));
        
        $titledWindow = new TitledWindow($window);
        
        $reflectionMethod = new ReflectionMethod($titledWindow, 'addTitle');
        $reflectionMethod->setAccessible(true);
        
        $rs = $reflectionMethod->invoke($titledWindow);
        
        $this->assertFalse(empty($rs));
        
    }
}

若是你并不熟悉phpUnit,不要在这些代码上过多的花费时间。总而言之,我针对TitledWindow::addTitle()方法设置了一些测试,然后期望得到一个非空的值。

那么,我们如何使用PsySH来检查运行的结果呢?只要像之前那样添加Shell::debug()方法就好了。

<?php
namespace DigitecTest\Patterns\Decorator;

use PHPUnit_Framework_TestCase;
use Digitec\Patterns\Decorator\TitledWindow;
use ReflectionMethod;

class TitledWindowTest extends PHPUnit_Framework_TestCase 
{
    public function testRender()
    {
        /* Some test code here */
    }
    
    public function testAddTitle()
    {
        $renderString = 'bar';
        
        $window = $this->getMock('Digitec\Patterns\Decorator\SimpleWindow');
        $window->expects($this->any())->method('render')->will($this->returnValue($renderString));
        
        $titledWindow = new TitledWindow($window);
        
        $reflectionMethod = new ReflectionMethod($titledWindow, 'addTitle');
        $reflectionMethod->setAccessible(true);
        
        $rs = $reflectionMethod->invoke($titledWindow);
        
        \Psy\Shell::debug(get_defined_vars());
        
        $this->assertFalse(empty($rs));
        
    }
}

于是就开始吧!

$ phpunit --verbose --debug --filter=TitledWindowTest::testAddTitle
PHPUnit 4.0.14 by Sebastian Bergmann.

Configuration read from /home/action/workspace/lab/PsySH/tests/phpunit.xml


Starting test 'AcmeTest\Patterns\Decorator\TitledWindowTest::testAddTitle'.
Psy Shell v0.1.11 (PHP 5.5.8 — cli) by Justin Hileman
>>>

$re此时应该被赋值了一个字符串,来让我们看一下是否如此:

>>> $rs
=> null

NULL值,难怪单元测试失败了。再让我们回头看一下TitledWindow::addTitle()的代码。如果我们执行ls命令,可以看到$titleWindow对象中的这个方法。

>>> show $titledWindow->addTitle
protected function addTitle()
    protected function addTitle()
    {
        $returnString = <<<EOL
+-------------+
|Title        |
EOL;
        echo $returnString . PHP_EOL;
    }
>>>

这就是出bug的地方,这个方法应该return一个值而不是直接echo出来。尽管这个程序看上去运行正常,但是通过单元测试和PsySH我还是发现了一个缺陷并修复了它。

结论

此篇文章并不打算详尽的挖掘PsySH的全部潜能。还有其他很Cool的功能(比如‘doc’)值得你尝试一下。单独使用PsySH也许不是非常有用,但是如果结合其他工具和你丰富的debug经验,它可以成为一项宝贵的资源。

comments powered by Disqus