Posts | About

My VSCode PHP Development Setup

October 14, 2020 by Areg Sarkissian

In this post I will share the VSCode php development extensions and keybindings that I use to test and debug Laravel projects using PHPUnit and XDebug.

My setup is based on resources listed at the end of this article.

PHP Extensions

First thing to do is install the following VSCode extensions:

Install and configure PHP XDebug extension

The following shows how to install and configure XDebug.

Install the extension

First we need to install the PHP XDebug extension:

pecl channel-update pecl.php.net
pecl clear-cache
# force install the extension
pecl install --force xdebug
# check that the xdebug shows in the list of PHP extensions
php -m | grep 'xdebug'

If you run into errors during installation on a Mac, make sure that the xcode tools are installed. You can use the command xcode-select --install to install them if not installed.

Configure the extension

Next we need configure the extension:

Locate your php.ini file:

php --ini

This print the location /usr/local/etc/php/7.4/php.ini on my MacBook.

Opening up the file:

cat /usr/local/etc/php/7.4/php.ini

I see the extension shown at the top of the file:

zend_extension="xdebug.so"

According the to the XDebug documentation we need to include the full path of the PHP extensions directory. Run pecl config-get ext_dir to get the full path of the PHP extensions directory. On my Mac the output is: /usr/local/lib/php/pecl/20190902

We also need to add the setting to enable remote debugging and set the remote port number. On my machine I run Laravel Valet which runs php-fpm on port 9000 so I set the XDebug remote port value to 9001. These additions are shown below:

zend_extension="/usr/local/lib/php/pecl/20190902/xdebug.so"
xdebug.remote_enable=1
xdebug.remote_port=9001

If you are using Laravel valet you should run the valet restart command to pick up the XDebug changes.

Alternate way to configure the extension

There is an alternate approach to configuring XDebug that uses a separate configuration file for the XDebug settings instead of modifying the main php.ini file.

Instead of updating the /usr/local/etc/php/7.4/php.ini file first comment out or remove the zend_extension="xdebug.so" line from the file.

You can comment it our by prepending a semicolon in front of the line:

; zend_extension="xdebug.so"

Add a new file named ext-xdebug.ini to the /usr/local/etc/php/7.4/conf.d/

cd /usr/local/etc/php/7.4/conf.d
touch ext-xdebug.ini

Add the following lines to the /usr/local/etc/php/7.4/conf.d/ext-xdebug.ini file:

[xdebug]
zend_extension="/usr/local/lib/php/pecl/20190902/xdebug.so"
xdebug.remote_enable=1
xdebug.remote_port=9001

;other optional configuration settings
;xdebug.remote_autostart=1
;xdebug.default_enable=1
;xdebug.remote_host=127.0.0.1
;xdebug.remote_connect_back=1
;xdebug.idekey=VSCODE

Configuring VSCode with XDebug for debugging

Open your shell configuration file which, for me is the ~/.zshrc file, and add the following:

export XDEBUG_CONFIG="idekey=VSCODE"

Close and source the file:

source ~/.zshrc

Note: The following steps need to be performed once per project to set up the per project .vscode/launch.json file. Alternatively once its done for one project, you can just copy the generated .vscode/launch.json to other projects, instead of going through the process again for each new project.

Open VSCode from within your project directory:

cd myproject
# open current directory in vscode
code .

Open the debug panel by pressing cmd+sh+d keyboard shortcut.

Click on the show link where it says show all automatic debug configurations.

Click on add configuration and select PHP from the drop down.

This will add launch.json debugging configuration to your project with settings for XDebug listening on port 9000.

As mentioned before need to change the XDebug port value to 9001 since to avoid conflict with php-fpm running on port 9000.

The .vscode/launch.json configuration file generated by VSCode is shown here with the port number changed:

{
  // Use IntelliSense to learn about possible attributes.
  // Hover to view descriptions of existing attributes.
  // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Listen for XDebug",
      "type": "php",
      "request": "launch",
      "port": 9001
    },
    {
      "name": "Launch currently open script",
      "type": "php",
      "request": "launch",
      "program": "${file}",
      "cwd": "${fileDirname}",
      "port": 9001
    }
  ]
}

Running a XDebug debugging session from VSCode for a Laravel project

Now you are ready to add breakpoints to your project files and debug your application.

Perform the following steps:

1-Run

php artisan serve

2-Open the VSCode debug panel and Click the “listen for XDebug” play button.

This launches the debugger toolbar and starts the debugger.

You can click the stop button to stop a session.

You can use the continue and step buttons to continue execution after hitting a breakpoint.

3- navigate to http://localhost:8000 in your browser.

You should see your application in the browser.

4-If you inspect the page you should see a XDEBUG_SESSION=VSCODE cookie written by the response.

If you don’t see the cookie try navigating to http://localhost:8000?XDEBUG_SESSION_START=VSCODE instead and make sure the response contains the cookie.

You can inspect cookies by using the browser developer tools.

Alternatively you can check if the debugger breakpoints are working as described below. If they aren’t being hit, then it probably means the cookie is not set and you can try adding the XDEBUG_SESSION_START=VSCODE parameter to the query string to make it work.

To test breakpoints, set a break point on the line that returns the welcome view in routes/web.php file of a new Laravel project.

Route::get('/', function () {
    return view('welcome'); //set breakpoint on this line
});

Now Refresh the page in the web browser and you should hit the breakpoint where you can inspect variables and the stack trace. Click the continue button in the debugger toolbar to continue and return the response.

If the breakpoint doesn’t get hit make sure the the response contains the XDEBUG_SESSION=VSCODE cookie as indicated before, by adding the XDEBUG_SESSION_START query string parameter to the request URL.

Debugging using Valet

If you have Valet running then you don’t need to run php artisan serve. Just follow the remaining steps of the previous section except navigate to http://yourappname.test?XDEBUG_SESSION_START=VSCODE instead.

Without the XDEBUG_SESSION_START parameter the XDEBUG_SESSION cookie is not written to the response, when using Valet to serve our app. Once the cookie is written though, we don’t need to add the XDEBUG_SESSION_START parameter to the browser requests anymore.

Laravel encrypts all cookies by default. Since the XDEBUG_SESSION cookie is injected by the PHP debug extension in the HTTP response, it is not encrypted by Laravel. This means that there will be an exception thrown by the Laravel when it tries to decrypt the cookie in the following request. These exceptions are caught higher up in the code execution stack by Laravel so the request execution will continue and eventually your breakpoints will be hit and the response will be returned.

However if the Exceptions breakpoint and Everything breakpoint settings checkboxes in the VSCode debugger are checked, then the thrown framework exceptions will break at the exception location making for an annoying debugging experience.

See Breakpoints checkboxes in VSCode bottom left debug window.

For the curious, the exception breakpoint occurs here in the class Illuminate\Encryption\Encrypter:

class Encrypter implements EncrypterContract
{
  protected function getJsonPayload($payload)
    {
        $payload = json_decode(base64_decode($payload), true);

        // If the payload is not valid JSON or does not have the proper keys set we will
        // assume it is invalid and bail out of the routine since we will not be able
        // to decrypt the given value. We'll also check the MAC for this encryption.
        if (! $this->validPayload($payload)) {
            //Exception is thrown here
            throw new DecryptException('The payload is invalid.');
        }

        if (! $this->validMac($payload)) {
            throw new DecryptException('The MAC is invalid.');
        }

        return $payload;
    }
}

And the exception is caught further up the stack in the class Illuminate\Cookie\Middleware\EncryptCookies:

class EncryptCookies
{
    protected function decrypt(Request $request)
    {
        foreach ($request->cookies as $key => $cookie) {
            if ($this->isDisabled($key)) {
                continue;
            }

            try {
                $value = $this->decryptCookie($key, $cookie);

                $hasValidPrefix = strpos($value, CookieValuePrefix::create($key, $this->encrypter->getKey())) === 0;

                $request->cookies->set(
                    $key, $hasValidPrefix ? CookieValuePrefix::remove($value) : null
                );
            } catch (DecryptException $e) {
                //The cookie decryption Exception is caught here and execution continues
                $request->cookies->set($key, null);
            }
        }

        return $request;
    }
 }

One solution to avoid the XDEBUG_SESSION cookie exception breakpoint will be to disable it in VSCode breakpoint settings. In the Breakpoints sub panel of the debug side panel in VSCode you can uncheck the break on Exceptions and break on Everything checkboxes to avoid breaking on any exceptions.

The exception will still happen, but VSCode will not break on exceptions.

See Breakpoints checkboxes in VSCode bottom left debug window.

If you like to break on other framework exceptions you have another option besides turning off exception breakpoints.

In your Laravel application you will have a App\Http\Middleware\EncryptCookies class shown below:

class EncryptCookies extends Middleware
{
    /**
     * The names of the cookies that should not be encrypted.
     *
     * @var array
     */
    protected $except = [
       //
    ];
}

You can add cookies that should not be decrypted by Laravel to the $except array:

Below you can that I have added the XDEBUG_SESSION cookie to tell Laravel it should ignore that cookie.

Once this is set, the XDEBUG_SESSION cookie decryption exception will not happen anymore because Laravel will skip decrypting it.

class EncryptCookies extends Middleware
{
    /**
     * The names of the cookies that should not be encrypted.
     *
     * @var array
     */
    protected $except = [
       'XDEBUG_SESSION'
    ];
}

Finally, sometimes there might be other cached cookies leftover from other debug sessions that will cause the cookie decryption exception. You can manually delete these cookies using your browser tools to avoid the exception happening.

For example since I also use PHPStorm, I somehow had a cookie named Phpstorm-b639e020 being sent in the request causing the cookie decryption exception. I removed this unused cookie so Laravel would not throw the exception when trying to decrypt this cookie.

We cant just remove the XDEBUG_SESSION session cookie since it is actually required for debugging.

Aside: How XDebug works with the VSCode Debugger

For the curious, I am going to explain how the PHP XDebug extension collaborates with your VSCode PHP Debug extension to allow setting breakpoints and debugging your application.

The XDebug extension running on the server collaborates with the VSCode installed PHP Debug extension to execute a debug session.

Every time a request is made from the browser to the server a new XDebug session is started which completes when the response is returned to the browser.

Below I detail the steps that happen when a XDebug session is established.

The steps describe the communication that happens during the debug session, between the PHP server with the XDebug extension running on the back end server and the server that is running within VSCode that is part of the VSCode Debug extension:

1 - We run\start the php server with the php interpreter XDebug extension settings configured in php.ini. This happens either using the command php artisan serve running the server on port 8000 or by using Laravel Valet that runs NGINX on port 80.

2- We launch\start VSCode IDE debug extension server which runs on port 9001 based on my .vscode/launch.json settings

Note: Valet also runs php-fpm on port 9000 which is why I changed the port for the server run by VSCode PHP Debug extension to 9001, to not to conflict with php-fpm port.

3- Using the web browser, we make a http request to the local php server at localhost:8000 or valethostedexample.test if we are using Valet, passing XDEBUG_SESSION_START=VSCODE in query string param to let the XDebug extension know that it is the start of a debug session.

Example request when running php artisan serve on port 8000:

http://localhost:8000?XDEBUG_SESSION_START=VSCODE

Example request when running Valet which uses dnsmasq server to resolve the .test domain to nginx http://localhost port 80:

http://valethostedexample.test?XDEBUG_SESSION_START=VSCODE

Note when using the php artisan serve XDebug works even without including XDEBUG_SESSION_START=VSCODE in query string param. This will be further explained below.

4- the PHP interpreter’s XDebug extension will detect a XDebug session for VSCode based on passed XDEBUG_SESSION_START=VSCODE query param and will connect to the VSCODE IDE Debug extension server running on port 9001.

5- As the PHP interpreter executes the http request it communicates with the Debug server running in VSCode to debug the executed code. When the request completes and returns the http response back to the browser, the PHP XDebug extension will inject the cookie XDEBUG_SESSION=VSCODE into the response.

6- We make an new request to the backend PHP server to continue our debug session. This starts the flow in steps 3 to 5 all over again. The requests following the original request will send the XDEBUG_SESSION=VSCODE cookie in the request so that the php XDebug extension on the server detects that the request should establish a XDebug session again.

So the XDEBUG_SESSION_START query string parameter in not required anymore after the first request because the cookie can take its place. It can be removed from the query string although wont hurt if it remains.

Note: When using Laravel valet to serve our app, we need to add XDEBUG_SESSION_START=VSCODE to the query string parameter when making the first request http://valethostedexample.test?XDEBUG_SESSION_START=VSCODE so the PHP debug extension can determine that the request is a debug session request. However when we use php artisan serve to serve our app, by default the first response will send back the XDEBUG_SESSION cookie without needing the XDEBUG_SESSION_START query string parameter to be present in the first request. Somehow the XDebug extension, when serving on localhost, detects we are in a debug session without requiring the XDEBUG_SESSION_START parameter. The requests after the first request will include the XDEBUG_SESSION cookie just as in the case when serving using Valet.

The resource understanding-and-using-xdebug has nice diagrams showing the session negotiation process between the XDebug web server and IDE Debug extension server.

Executing PHPUnit test from VSCode

There are several ways to run PHPUnit tests from within VSCode.

Running PHPUnit from the command line using the VSCode terminal

To run your PHPUnit tests from within VSCode, open the terminal by selecting the ctrl+backtick keyboard shortcut then type in vendor/bin/phpunit in the terminal window to execute the tests.

I have added the alias alias phpunit=vendor/bin/phpunit to my .zshrc shell profile file so I can just type phpunit instead of vendor/bin/phpunit.

You can also use the standard phpunit command filters from within the VSCode terminal to for example run only feature tests or only unit test.

Using the VSCode Better PHPUnit extension to run tests from the command palette

If you have the Better PHPUnit VSCode extension installed, you can click various locations within PHPUnit test files to execute specific tests using command palette selections provided by the Better PHPUnit extension. The steps to do so are listed below:

1-Click on a test method name or within the test method to run the individual test. Or click within the class outside of any methods or on the class name to run all the tests in the class. Or click anywhere outside the class to run all the tests in the file.

2-Type cmd+sh+p keyboard shortcut to open the command panel.

3-Type phpunit to see the following options run, run file, run suite and run previous.

4-Select run to run the test for the selected method or the selected class or selected file.

Tip: You can just hit enter if that is the default selection.

If you select run file it will run all the tests in the selected file regardless of where you click within the file.

If you select run previous the last tests that were run will run again even if we click away to another method, class or file.

If you select run suite, all tests in all test files will run regardless of which file you have selected.

Using the VSCode Better PHPUnit extension keyboard shortcuts

Alternatively you can use the Better PHPUnit extension’s keyboard shortcuts to run tests:

1-Click on a test method name or within the test method to run the individual test. Or click within the class outside of any methods or on the class name to run all the tests in the class. Or click anywhere outside the class to run all the tests in the file.

2-Type cmd+k cmd+r to execute the run to run the test for the selected method or the selected class or selected file.

You can type cmd+k cmd+f to execute the run file command

You can type cmd+k cmd+p to execute the run previous command

There is no preconfigured shortcut to execute the run suite command. But you can just type vendor/bin/phpunit in the terminal which is equivalent to the run suite command.

If you rather re-map these shortcuts to other keys, open keyboard shortcuts UI from command palette and type better phpunit in the search box to bring up all the shortcuts for that extension where they can be remapped.

Running XUnit with XDebug

We can run the VSCOde debugger while running our XUnit tests to be able to break within the tests or the code that is under test.

To verify that breakpoints are hit within our feature and unit tests when can run the steps below in a new Laravel project:

1- Set breakpoints in the testBasicTest method in the tests/feature/ExampleTest.php file of a new Laravel project:

class ExampleTest extends TestCase
{
    /**
     * A basic test example.
     *
     * @return void
     */
    public function testBasicTest()
    {
        $response = $this->get('/'); //set breakpoint here

        $response->assertStatus(200); //set breakpoint here
    }
}

2- Set a breakpoint in routes/web.php file of a new Laravel project:

Route::get('/', function () {
    return view('welcome'); //set breakpoint here
});

3- Start\Run the PHP debugger in VSCode as was detailed in the previous sections.(i.e. Open the VSCode debug panel using cmd+sh+d then click the listen for debug button)

4- Click within the testBasicTest method and run the test method by typing the keyboard shortcut shortcut (cmd+k cmd+r)

Note: After running the test it may first hit a breakpoint due a ReflectionException issue with the Better PHPUnit runner which I cover in the next section. If this happens,just click the continue debugging button in the debugger toolbar to continue with running the test.

Once we run the test we should hit the first breakpoint in the test method then when we hit the continue debugging button in the debugger toolbar, we will hit the breakpoint in the application route file.

Then if we hit continue again you will hit the second break point in the test and if we hit continue yet again, the test will complete with test results shown as usual when running without the debugger.

Similarly a breakpoint can be placed in the testBasicTest method of the tests/unit/ExampleTest.php file and the code can be debugged in the same way as the feature test code.

ReflectionException issue when debugging with the Better PHPUnit runner

There is an issue when we run tests the Better PHPUnit runner while running the VSCode debugger.

The first thing that happens is we hit a breakpoint on an exception with the message:

Exception has occurred.ReflectionException: Method suite does not exist

If we continue debugging we hit our normal breakpoints and can continue debugging and running the tests.

However it is annoying to keep hitting this exception every time we run tests while debugging.

Note: When we run the VSCode debugger and we run vendor/bin/phpunit command in the VSCode terminal, this exception is not hit. So this section only applies to the Better PHPUnit’s test runner.

Below is the method where the exception breakpoint is hit:

//in PHPUnit source code Runner/BaseTestRunner.php
abstract class BaseTestRunner
{

    public function getTest(string $suiteClassFile, $suffixes = ''): ?TestSuite
    {

        try {
                //this line throws the exception
                $suiteMethod = $testClass->getMethod(self::SUITE_METHODNAME);

This initial breakpoint is hit every time we run tests using the Better PHPUnit runner while debugging.

The way to fix this issue so the that we don’t break on this exception is to uncheck the “Everything” box in VSCode debugger settings.

See Breakpoints checkboxes in VSCode bottom left debug window.

The exception will still be thrown but the code won’t break at the exception.

Resources

better-phpunit

vscode-php-debug

configure-vscode-to-debug-phpunit-tests-with-xdebug

visual-studio-code-for-php-developers

setup-xdebug-in-laravel-valet-with-php-7-3-and-phpstorm

understanding-and-using-xdebug