Debugging Magento 2 with Xdebug 3 and PhpStorm
Magento is hard to follow by reading code alone. A call to a repository goes through an interceptor, several plugins, a resource model and maybe an observer before it does anything. Logging with var_dump can get you there eventually. A step debugger gets you there in minutes, because you can see the actual call stack and the actual values at each step.
This article covers Xdebug 3 with PhpStorm for Magento 2.4.8 in a Docker environment, including the traps that make breakpoints silently ignored.
How Xdebug connects
With Xdebug 3, the PHP process connects out to your IDE, not the other way around. When a request starts with a debug trigger, Xdebug opens a connection to xdebug.client_host on xdebug.client_port, where PhpStorm is listening. PhpStorm then maps the file paths reported by PHP to files in your project and stops at breakpoints.
So three things must line up: PHP has to know where the IDE is, the IDE has to listen on the same port, and the paths inside the container must be mapped to the paths on your machine.
PHP configuration
A minimal configuration for local development:
zend_extension=xdebug
xdebug.mode=debug
xdebug.start_with_request=trigger
xdebug.client_host=host.docker.internal
xdebug.client_port=9003
xdebug.idekey=PHPSTORM
xdebug.log=/tmp/xdebug.log
xdebug.log_level=3
start_with_request=triggeronly starts a debug session when the request carries a trigger, such as theXDEBUG_SESSIONcookie. Other requests run normally, which matters because Magento with an active debug session is slow.host.docker.internalresolves to the host machine from inside a container on Docker Desktop. On Linux, you may need to add it as an extra host or use the bridge IP.- The log is the first place to look when nothing happens. Remove it or lower the level once things work, since it grows quickly.
Xdebug slows PHP even when no session is active. A good setup keeps it in a separate PHP-FPM pool or container and routes only debug requests there. Warden does exactly that: requests with the XDEBUG_SESSION cookie go to a php-debug container and everything else goes to the normal one. The environment setup is in the Warden article.
PhpStorm configuration
Debug port
In Settings, PHP, Debug, set the Xdebug port to 9003 and make sure "Can accept external connections" is on. Then turn on "Start Listening for PHP Debug Connections" with the phone icon in the toolbar.
Server and path mappings
In Settings, PHP, Servers, add a server:
- Name: the host name of your store, for example
app.myshop.test - Host and port: the same host, port 443 for HTTPS
- Use path mappings: on. Map the project root on your machine to the path in the container, usually
/var/www/html
The server name matters. For web requests PhpStorm matches it against the host header. For CLI it uses the PHP_IDE_CONFIG variable, shown below. If the mapping is wrong, PhpStorm either does not stop or opens a window asking which local file corresponds to the remote path.
Debugging web requests
Install a browser extension that sets the XDEBUG_SESSION cookie, for example the Xdebug helper extensions available for Chrome and Firefox, and enable it on your store's tab. Set a breakpoint and reload the page.
For API clients such as Postman or a GraphQL client, add the cookie header manually, or add the query parameter XDEBUG_SESSION_START=PHPSTORM to the URL:
curl -k -H "Cookie: XDEBUG_SESSION=PHPSTORM" \
-H "Content-Type: application/json" \
-d '{"query":"{ storeConfig { store_name } }"}' \
https://app.myshop.test/graphql
The full page cache trap
If the page is cached, PHP may never run the code you want to debug. Varnish serves the page without reaching PHP at all. The built in full page cache does reach PHP, but returns the cached response before most of the application runs. For storefront debugging, either disable the full_page cache type locally or turn off Varnish in the environment. The same applies to block cache when you debug a block's output.
Debugging CLI, cron and consumers
For commands, set the trigger and the server name in the environment, inside the container:
export XDEBUG_SESSION=1
export PHP_IDE_CONFIG="serverName=app.myshop.test"
php bin/magento indexer:reindex catalog_product_price
In Warden, warden debug opens a shell in the debug container where these can be set. Without the debug container, make sure the PHP CLI you run has Xdebug loaded with php -m | grep -i xdebug.
Cron jobs are easier to debug by calling the job class directly than by waiting for cron:run to pick them up. Write a small CLI command or a test that calls the execute() method, then debug that. For message queue consumers, run a single consumer with a limit so it exits after one message:
php bin/magento queue:consumers:start product_action_attribute.update --max-messages=1
Publish a test message first, then start the consumer with the debugger listening. More on consumers in RabbitMQ consumers.
Where to put the breakpoint
Interceptors and plugins
Classes with plugins are replaced at runtime by generated Interceptor classes in generated/code. A breakpoint in the original method still works, because the interceptor calls the parent method in the end. If you want to see which plugins run and in which order, step into the interceptor's method and follow ___callPlugins.
When a breakpoint in a plugin is never hit, check the plugin is actually registered: the right area in di.xml, the right class and method name, and generated code refreshed after the change.
Following events
To see which observers run for an event, set a breakpoint in Magento\Framework\Event\Manager::dispatch() with a condition on the event name, for example $eventName === 'sales_order_place_after'. Conditional breakpoints are the most useful PhpStorm feature for Magento, because many core methods run hundreds of times per request.
Finding where a value changes
When a price or a quote total is wrong, set a breakpoint where you see the wrong value and walk up the call stack. The frames panel lets you click any earlier frame and inspect its variables. For totals, the collectors run in a specific order, so breaking in each collector's collect() shows where the value first goes wrong. See custom totals collectors for how they are wired.
Timeouts
A request paused at a breakpoint will hit timeouts in nginx and PHP-FPM if you take long enough. You will see a 504 in the browser while PhpStorm is still paused. For local environments, raise fastcgi_read_timeout in nginx and request_terminate_timeout in the FPM pool used for debugging. max_execution_time does not count time spent paused in the debugger the same way, but setting it to 0 locally avoids surprises.
When it still does not work
- Check
php -vorphpinfo()in the process you are debugging and confirm Xdebug 3 is loaded withmode=debug. - Read the Xdebug log. A "could not connect to debugging client" line means a host or port problem, often a firewall or the wrong
client_host. - Confirm the trigger reaches PHP. Proxies such as Varnish can strip or vary on cookies.
- Check the path mappings. If PhpStorm asks which file to open, the mapping is wrong.
- Make sure you are not looking at cached output.
Beyond step debugging
Xdebug also has a profiling mode, but for performance problems a dedicated profiler gives clearer results with less overhead. See profiling Magento with Blackfire for that. For bugs that only happen in production, where you cannot attach a debugger, good logging is the tool: a dedicated log file per integration with useful context, as described in the custom logger article on this blog.
FAQ
Why does PhpStorm never stop at my breakpoint?
The most common causes are a missing trigger cookie, wrong path mappings between the container and the project, the page being served from full page cache or Varnish, or Xdebug connecting to the wrong host. The Xdebug log usually tells you which one.
Which port does Xdebug 3 use?
9003 by default. Xdebug 2 used 9000, which clashes with PHP-FPM. Make sure PhpStorm listens on the same port as xdebug.client_port.
How do I debug a bin/magento command?
Set XDEBUG_SESSION=1 and PHP_IDE_CONFIG with the server name configured in PhpStorm in the environment of the command, then run it inside the container with PhpStorm listening.
Should Xdebug be installed on production?
No. Even when idle it adds overhead, and an enabled debugger on a public server is a security risk. Use it locally, and use a profiler such as Blackfire or New Relic in production.
I am available for freelance Magento and Adobe Commerce work, including tracking down hard bugs in custom and third party code. If you need help with this, get in touch.
Related articles
Magento 2 Custom Log File with Monolog (No Extra Classes)
Write Magento 2 logs to a custom file with Monolog using virtual types in di.xml, plus custom handlers, log levels, exception context and log rotation tips.
Common Magento 2 Errors and How to Fix Them
Fixes for classic Magento 2 errors: Area code is not set, Class does not exist, Invalid Form Key, stuck indexers, Unable to unserialize value, static 404s.
Magento 2 Local Environment with Docker and Warden
Set up a Magento 2.4.8 local environment with Docker and Warden: env-init, the .env file, installing or importing a project, Xdebug and daily commands.
AI Tools for Magento Development: What Works, What Fails
Using AI coding assistants on Magento 2 projects: where they save time, where they invent code, how to give them context and how to verify their output.