Full-Stack Debugging: PHP and Node.js Together in PhpStorm
AI generated
IDE
{ }
PhpStorm · PHP · Docker
Debug PHP and Node.js at the same time
One session for backend logic and the build pipeline

In Hyva theme projects, a bug rarely lives cleanly on one side: is a Tailwind class generated wrong, or does the watcher simply never see it? PhpStorm can bundle an Xdebug session and a Node process into a single Compound configuration so both sides pause in sync.

13 min read Xdebug Node Inspector Compound Configs

1. Why separate debugging breaks down on Hyva projects

A typical Hyva setup runs two processes in parallel: the Magento PHP backend rendering ViewModels and phtml templates, and a Node-based Tailwind watcher that regenerates CSS classes on every change. When a layout bug appears, it is often unclear which side is responsible: does the PHP layer produce a wrong class, or does the Tailwind scanner simply fail to detect the pattern and purge the class away.

Debugging both sides separately means constantly switching between an Xdebug session and a separate Node inspector connection in a Chrome DevTools window. The timing relationship between PHP output and Node processing gets lost, because both breakpoints can never be observed at the same moment. This is exactly where a shared debug session in PhpStorm brings both worlds into one window. For teams onboarding onto an existing Hyva project, this approach saves considerable ramp-up time, because the relationship between backend and build pipeline becomes directly visible instead of having to be painstakingly reconstructed from two separate tools.

2. The core idea: Compound configurations bundle sessions

Alongside individual Run/Debug configurations, PhpStorm offers a Compound type. A Compound configuration starts several stored configurations with a single click and keeps them active for the whole session. In our case, two child configurations are combined: a PHP Remote Debug configuration for the Docker container and an Attach to Node.js/Chrome configuration for the running watcher process.

Each child configuration keeps its own breakpoints, watches, and variable views, but appears as its own tab in the same debug tool window. When one side pauses at a breakpoint, the other keeps running unaffected until it hits its own breakpoint. That produces a shared timeline without ever switching windows or manually syncing tools.


<!-- .idea/runConfigurations/Compound_PHP_Node.xml -->
<component name="ProjectRunConfigurationManager">
  <configuration name="PHP + Node Watcher" type="CompoundRunConfigurationType">
    <toRun name="Listen for PHP Debug Connections" type="PHP Remote Debug" />
    <toRun name="Attach Tailwind Watcher" type="NodeJSRemoteDebugRunConfigurationType" />
    <method v="2" />
  </configuration>
</component>

3. Setting the Xdebug entry point correctly inside the container

For PhpStorm to hit breakpoints in PHP code at all, Xdebug must be active in the container and the client_host set correctly. In a Mark Shust setup, Xdebug is enabled via the wrapper bin/xdebug enable, which loads the matching ini file and restarts the container. The path mapping in PhpStorm matters too, so the container path /var/www/html/app/code maps correctly to the local src/app/code directory.

A common pitfall is a wrong xdebug.client_host when the container runs in its own Docker network. On Linux the Docker bridge IP usually works, on macOS and Windows host.docker.internal is the more reliable choice. Without correct mapping, Xdebug connects but PhpStorm shows breakpoints as unreachable, which is often misread as an Xdebug bug rather than a networking issue.


; docker/php/xdebug.ini
zend_extension=xdebug.so
xdebug.mode=debug
xdebug.start_with_request=yes
xdebug.client_host=host.docker.internal
xdebug.client_port=9003
xdebug.idekey=PHPSTORM

4. Starting the watcher's Node process with the inspector flag

The Tailwind watcher usually runs via an npm script that keeps the build process alive in watch mode. For PhpStorm to attach via Attach to Node.js/Chrome, the Node process needs the --inspect or --inspect-brk flag, which opens a debug port. In a Docker-based npm setup, this flag is added directly to the package.json script so it is active on every start.

If the Node process also runs inside a container, the same networking consideration as for Xdebug applies: the inspector port must be exposed from the container and configured in PhpStorm's Node.js remote configuration with the correct host address. A common mistake is binding the port only internally, so PhpStorm cannot connect from outside even though the process itself starts without errors.


{
  "scripts": {
    "watch:tailwind": "node --inspect=0.0.0.0:9229 ./node_modules/.bin/tailwindcss -i ./web/tailwind/tailwind-source.css -o ./web/css/styles.css --watch"
  }
}

5. Creating the Compound configuration in the PhpStorm UI

In practice, go to Run, Edit Configurations, and first create the two individual configurations: a PHP Remote Debug for the Docker server and an Attach to Node.js/Chrome with the host and port of the inspector socket. Only afterward do you create a new Compound configuration via the plus icon and select both existing entries as children.

A descriptive name like PHP + Node Watcher and an assigned keyboard shortcut make the configuration practical for daily use. It's worth knowing that stopping the Compound session via the stop button cleanly disconnects both child sessions, while the Docker container itself keeps running unaffected, so no environment restart is needed.

6. Coordinating breakpoints across language boundaries

The real payoff shows up when you set a PHP breakpoint in the ViewModel class that assembles a dynamic class list, and simultaneously a Node breakpoint in the watcher's transform function that writes the CSS output. When the page loads, the PHP breakpoint hits first, because rendering happens before every CSS rebuild.

Only when you save a template file the Tailwind scanner watches does the Node breakpoint fire again. Knowing this order avoids wrong conclusions, such as assuming a class is missing from the CSS when it simply has not been rebuilt yet because the watcher never observed the affected file path.


// app/code/Mironsoft/Theme/ViewModel/HeroBanner.php
public function getWrapperClasses(): string
{
    $classes = ['flex', 'items-center'];
    if ($this->isFullWidth()) {
        $classes[] = 'w-full'; // <- set breakpoint here
    }
    return implode(' ', $classes);
}

7. Practical example: tracking down a missing Tailwind class

A concrete case: a conditional class built via string concatenation in a phtml template, for example 'bg-' . $color . '-500'. The Tailwind JIT scanner cannot detect this pattern statically, because it only finds complete class names in source code, not strings assembled at runtime. The debug session shows on the PHP side that the correct class name is generated.

On the Node side, a log statement in the watcher confirms the class is missing from the final CSS because it never appears as a complete string during the scan. The fix is either to write out every possible class combination explicitly, or to add it to the safelist in the Tailwind configuration so the purge step does not remove it.


// tailwind.config.js
module.exports = {
  content: ['../**/*.phtml'],
  safelist: [
    'bg-red-500', 'bg-green-500', 'bg-blue-500',
  ],
};

8. Performance pitfalls of running two debuggers at once

Two active debug engines attached to one Docker container noticeably raise the latency of every step action, especially when many watch expressions are evaluated at once. Every watch observing a complex object or a large array gets recomputed at each breakpoint, which visibly slows down the IDE's response time.

In practice it helps to keep watches to the essentials and to reduce the maximum number of displayed array elements in the debugger settings. The Force step into option should also stay disabled globally, since with an active Compound session it otherwise tries to step into framework and node_modules code, making the session feel noticeably sluggish.

9. When it is not worth the effort and what the alternatives are

For simple, clearly localized bugs, a single debug session is still faster to set up than a Compound configuration. The extra effort pays off mainly for hard-to-reproduce problems that arise exactly at the boundary between server-side rendering and client-side build, such as timing issues between cache invalidation and watcher triggers.

A lightweight alternative is correlated log output: both processes write timestamps into a shared log format, so you can reconstruct the sequence of events afterward without a live debug session. For recurring analysis this is often more practical than restarting a full Compound session every time.

Approach Setup effort Strength Limit
Separate sessions Low Fast for isolated bugs No visible timing relationship
Compound configuration Medium, one-time Synchronous timeline of both processes Higher IDE overhead
Correlated logs Low, ongoing Retrospective analysis without a live session No interactive stepping
Manual window switching None No extra setup Timing windows easily missed

Mironsoft

PhpStorm setup, Docker integration, and team productivity

PhpStorm that actually runs optimally for Magento and PHP projects?

We review existing PhpStorm setups for slow indexing, unused Docker integration, and missing team conventions, then set up a configuration that is productive from the first second.

Setup Review

Optimizing indexing, interpreter, and memory settings for large Magento projects.

Docker Integration

Cleanly connecting Xdebug, PHPUnit, and database tools to the Docker setup.

Team Conventions

Standardizing inspection profiles, code style, and live templates project-wide.

10. Summary

Full-Stack Debugging in PhpStorm: The Essentials at a Glance

Core tool

A Compound Run/Debug Configuration links PHP Remote Debug and Attach to Node.js/Chrome.

Prerequisite

Xdebug with the correct client_host and a Node process started with the --inspect flag.

Typical case

Bugs at the boundary between PHP rendering and the Tailwind watcher build.

Limit

For simple bugs, a single session remains faster to set up.

11. FAQ: Full-Stack Debugging in PhpStorm: The Essentials at a Glance

1What is a Compound Run Configuration in PhpStorm?
A configuration type that runs several existing run or debug configurations together with a single start click, showing each as a separate tab in the same debug window.
2Does the Node process have to run in the same container as PHP?
No, it can run in its own container or directly on the host. What matters is that the inspector port is reachable from PhpStorm.
3Which flag does Node.js need for inspector access?
The --inspect flag opens a debug port, --inspect-brk additionally pauses the process immediately at startup until a debugger connects.
4Why do my PHP breakpoints show as unreachable?
Usually due to a wrong xdebug.client_host or a broken path mapping between the container path and the local project directory.
5Can more than two configurations be bundled into one Compound session?
Yes, a Compound configuration can hold any number of child configurations, for example an additional database console start or another Node process.
6Does the Compound session affect the Docker container's performance?
The container itself is barely affected, the noticeable slowdown usually happens in the IDE due to many watch expressions being evaluated at once.
7How do you find the right inspector port for a running Node process?
The port is printed to the terminal log at startup, the default is 9229 unless it was explicitly changed via the --inspect flag.
8Is full-stack debugging useful without Docker too?
Yes, the principle works the same way with locally installed PHP and Node, and the networking is even simpler since no container mapping is needed.
9What happens if only one of the two child configurations starts?
PhpStorm shows only the successful tab in the debug window and reports a connection error for the other configuration, while the session keeps running partially.
10Is this effort worth it for small projects?
For small projects with rare cross-layer bugs, usually not, separate debugging is enough. For complex Hyva themes with many dynamic classes, the one-time setup effort pays off.