Статьи

Интерактивная отладка PHP с помощью PsySH

Сейчас 1:00, крайний срок доставки вашего веб-приложения — 8 часов … и он не работает. Когда вы пытаетесь выяснить, что происходит, вы заполняете свой код var_dump() и die() везде, чтобы увидеть, где ошибка …

Вы раздражены. Каждый раз, когда вы хотите проверить возвращаемое значение или присвоение переменной, вы должны изменить свой исходный код, выполнить свое приложение и посмотреть результаты … В конце вы не уверены, удалили ли вы все эти var_dumps из код. Вам знакома эта ситуация?

PsySH на помощь

PsySH — это цикл чтения-проверки-печати (или REPL ). Возможно, вы уже использовали REPL через консоль javascript вашего браузера. Если у вас есть, вы знаете, что он обладает большой мощностью и может быть полезен при отладке кода JS.

Говоря о PHP, вы могли раньше использовать интерактивную консоль php -a ( php -a ). Там вы можете написать некоторый код, и консоль выполнит его, как только вы нажмете Enter:

 php -a Interactive shell php > $a = 'Hello world!'; php > echo $a; Hello world! php > 

К сожалению, интерактивная оболочка не является REPL, так как в ней отсутствует буква «P» (печать). Мне пришлось выполнить оператор echo чтобы увидеть содержимое $a . В истинном REPL мы бы увидели его сразу после присвоения ему значения.

Вы можете установить PsySH глобально либо с помощью composer g require , либо загрузить исполняемый файл PsySH:

Композитор

 composer g require psy/psysh:~0.1 psysh 

Прямая загрузка (Linux / Mac)

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

Кроме того, вы можете включить его для каждого проекта с помощью composer, как вы увидите позже в этой статье.

Теперь давайте немного поиграем с 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. Aliases: quit, q 
 >>> 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 (eg "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. eg >>> ls >>> ls $foo >>> ls -k --grep mongo -i >>> ls -al ReflectionClass >>> ls --constants --category date >>> ls -l --functions --grep /^array_.*/ >>> 

В основном, что может сделать REPL:

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

Обратите внимание, что если мы сравниваем PsySH с интерактивной консолью PHP, PsySH печатает значение $a как только оно назначено.

Более сложный пример может быть следующим:

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

Я определил функцию say() и вызвал ее. Эти два null вы видите потому, что ни определение функции, ни ее выполнение не вернули значение (функция отображает значение). Кроме того, при определении функции приглашение изменилось с >>> на ...

Можем ли мы определить класс и создать его экземпляр?

 >>> 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 проявляется при интеграции с веб-приложением, поэтому давайте создадим его.

Демо приложение

Я собираюсь реализовать быстрое приложение для демонстрации шаблона дизайна декоратора . Диаграмма классов UML такого шаблона выглядит следующим образом:
Decorator Design Pattern

Не беспокойтесь, если вы не очень много знаете о 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(); 

Мы можем выполнить код через CLI PHP (интерфейс командной строки) или через веб-сервер, если он настроен. Мы также можем использовать внутренний веб-сервер PHP.

Отладка в кли

Выполнение приведенного выше кода через интерфейс командной строки будет выглядеть так:

 php public/decorator.php Simple Window +-------------+ | | | | | | | | | | +-------------+ Decorated Simple Window +-------------+ | | | | | | | | | | +-------------+ Titled Simple Window +-------------+ |Title | +-------------+ | | | | | | | | | | +-------------+ 

Как мы можем взаимодействовать с PsySH? Просто добавьте \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_vars() в качестве параметра в Psy\Shell::debug() поэтому у меня есть доступ ко всем определенным переменным внутри оболочки:

 >>> 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, который реализует интерфейс Window … Интересно, как выглядит исходный код интерфейса Window …

 >>> 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() , но это еще один способ проверки.

Отладка со встроенным сервером

К сожалению, отладка через веб-сервер, такой как Apache, не поддерживается. Однако мы можем отлаживать наше приложение, используя встроенный сервер 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, поэтому, как только мы decorator.php файл decorator.php через этот веб-сервер ( http: // localhost: 8080 / decorator.php ), мы должны увидеть следующее:

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

Мы можем начать играть с PsySH так же, как и с CLI

 >>> 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. 

Отладка с помощью юнит-тестов

Как хороший разработчик, вы должны написать модульные тесты для своего кода в качестве доказательства того, что он работает как положено. В файлах проекта вы найдете папку 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() говоря, я настраиваю все, чтобы проверить метод 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 >>> 

Так что в $rs у нас должна быть строка; посмотрим, что у нас на самом деле есть.

 >>> $rs => null 

Нулевое значение, неудивительно, что тест не удался… Давайте проверим исходный код TitledWindow::addTitle() . Если мы выполним команду ls , мы увидим, что у нас есть метод этого объекта, доступный через объект $titledWindow .

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

Есть ошибка. Метод повторяет значение, а не возвращает его. Несмотря на то, что приложение работает нормально, с помощью модульного тестирования и PsySH мы обнаружили дефект и теперь можем его исправить.

Вывод

Эта статья не должна была быть исчерпывающей, чтобы продемонстрировать весь потенциал PsySH. Есть и другие интересные функции (например, «док»), которые вы должны попробовать. Сам по себе PsySH может быть не очень полезен, но в сочетании с другими инструментами и вашими умными возможностями отладки он может оказаться ценным активом.