Debugging (PHP) Applications
PHP Benelux, 28. 1. 2018
© David Buchmann, Liip AG
twitter.com/bramcohen/status/51714087842877440
Organisation
- Introduction
- Tutorial Application
- Bug Hunting
- --- break ---
- More Bug Hunting
- Bug Prevention
- Bug Detection
- Wrap-Up
Introduction
Debugging
- Methodical guessing
- Narrowing down the problem
- Quickly validate a guess
Security Bugs
- Missing access control
- Functionality exposed: e.g. eval(), exec(),
string concatenation for SQL queries
- Missing validation of input
This workshop focuses on finding the location of bugs. Scanning for security vulnerabilities is worth its own Workshop.
Bug Report
Look at the list of open bugs if already reported
Title
Platform/Environment
What did you do - how to reproduce?
What did you expect to happen?
What was the actual result?
Screenshot if visual, copy console output if text (use ``` in github)
For OSS: Pull request with failing test, or even a fix?
Respect!
Demo Tutorial Application
- Import data from CSV into a relational database
- Doctrine ORM models
- Website that shows imported data
Bug Hunting
Usage
git reset --hard
git checkout [branchname]
If you get random errors about files in
/vagrant/var/cache/dev/ not found, do:
rm -rf var/cache/dev/
I often use obvious errors to illustrate a case. Real bugs will often be more complicated, but the strategies apply...
[b1] Bug 1
Look at exception message
[b1] RTFEM: Wrong Typehint
Cannot autowire service "App\Importer\Likes": argument "$em" of method "__construct()" references class "Doctrine\ORM\EntityManager" but no such service exists. Try changing the type-hint to one of its parents: interface "Doctrine\O
RM\EntityManagerInterface", or interface "Doctrine\Common\Persistence\ObjectManager".
- The exception message suggests a solution
- Try that suggestion
✔ Exceptions are your friends, read them carefully.
[b2] Bug 2
bin/console app:import
Argument -v makes output more verbose.
Look at the stack trace.
[b2] Stacktrace
App\Importer\Mapper\LikeMapper->map() at .../Likes.php:49
App\Importer\Likes->import() at .../AppImportCommand.php:60
App\Command\AppImportCommand->execute() at .../Command.php:252 ...
- Figure out what we are calling
- Check where the data comes from
- Check what would make sense
$this->productRepository->find($data['CustomerId']);
✔ Exception stack trace provides context of errors
[b3] DDD: Die Driven Debugging
bin/console app:import
The CustomerMapper checks if customer id already exists.
Add
die('cache')|found|new in CustomerMapper::map
[b3] DDD
$customer = $this->customerRepository->find($data['ProductId']);
- Ok if you have a confident guess
- Quick way to ensure you look at the correct place
- Stop after second attempt though
- ProTip™ do not commit the die() to git
✔ At most, use die() to check if you are in the expected place of the code
[b4] Use proper exceptions
bin/console app:import
Search code for
"oops"
Search for
die( and throw exceptions with decent messages
[b4] Use proper exceptions
- An exception should tell what happened
- Not overly verbose
- But provide enough context to be useful
- ! Careful with user input / large data
✔ do not make your collegues and your future self hate you
Demo: Step by step debugger
; config to run the remote debugger
xdebug.remote_enable=1
xdebug.remote_autostart=1
xdebug.remote_connect_back=1
xdebug.idekey="XDEBUG-DRIFTER"
Demo: Step by step debugger
- Configure your IDE
- Set IDE to listen to debugging
- ifconfig to find IP of vboxnet1
- php -d xdebug.remote_host=10.10.10.1 ./bin/console app:import
- https://blog.theodo.fr/2016/08/configure-xdebug-phpstorm-vagrant/
[b5] Segfault/Infinite Loops
bin/phpunit
At which test does it hang? Try --debug
[b5] Infinite Loop
Last line says CsvProductLoaderTest::testLoadFileNotFound
✔ progressive report to see which test fails hard
[b6] Search codebase for context
http://debugging.lo
Look at the page source for some context, then search the codebase to see what happens around it.
[b6] Search codebase for context
<h2 style="color:green;">Found {{ customer_count }} customers {{ render(controller('App\\Controller\\HomeController::extra')) }}
- Set a breakpoint in HomeController::extra
✔ Program output can provide context where to look for errors
[b7] Search in logfiles
bin/console app:import --reset -q
http://debugging.lo shows only 99 customers instead of 100
Search for non-debug messages in logfile:
echo '' > var/log/dev.log
bin/console app:import --reset -q
cat var/log/dev.log | grep -v DEBUG
[b7] Search in logfiles
app.WARNING: Invalid number of columns for App\Importer\Loader\CsvLikeLoader at CSV row 26
- Look at the CSV file data/likes.csv
- Line 26 has a comma in the name and no quotes
- Tell provider of this CSV to fix their thing
✔ Search logfiles for id's or message fragments. It helps if you know when the problem happened.
[b8] Unit Test (for debugging)
We built a (horrible) security into our website. But its faulty:
http://debugging.lo/?secret=I%20am%20David
http://debugging.lo/?secret=David
Build a unit test for the HomeController::index method to cover all cases.
[b8] Unit Test
if (!stripos($secret, 'david')) {
- Use false === stripos() and 0 === stripos() to prevent errors
- (And never do security like this! Use a form login or basic auth, read up on token authentication.)
✔ Build a unit test when debugging a specific component. Often you want to keep that test afterwards.
[b9] Do not assume
bin/phpunit
all green, great?
run bin/console app:import