Emacs is a serious PHP development environment once you wire the right packages together. This guide covers a complete, production-grade Emacs setup for PHP in 2026: language server integration with Eglot, Tree-sitter syntax highlighting, code formatting, and step-through debugging via DAP Mode and Xdebug 3. All configuration targets Emacs 29+ with use-package.
Prerequisites
- Emacs 29.1 or newer (built with tree-sitter support)
- PHP 8.2+ on the host or in Docker
- Intelephense or phpactor language server
- Xdebug 3 installed in your PHP environment
Tree-sitter Grammar for PHP
Emacs 29 ships php-ts-mode in contrib. First, install the grammar:
;; In your init.el — run once interactively, then keep for new machines
(setq treesit-language-source-alist
'((php "https://github.com/tree-sitter/tree-sitter-php" "main" "php/src")))
(treesit-install-language-grammar 'php)Enable the mode automatically:
(use-package php-ts-mode
:ensure nil ; built-in since Emacs 30, or install php-mode from MELPA for 29
:mode ("\\.php\\'" "\\.phtml\\'")
:hook (php-ts-mode . eglot-ensure))If you are on Emacs 29 and php-ts-mode is not available, install php-mode from MELPA and add the tree-sitter font-lock upgrade manually:
(use-package php-mode
:ensure t
:mode ("\\.php\\'" "\\.phtml\\'")
:hook ((php-mode . eglot-ensure)
(php-mode . (lambda ()
(when (treesit-available-p)
(treesit-parser-create 'php))))))Language Server: Intelephense via Eglot
Intelephense is the most capable PHP language server. Install it globally with npm:
npm install -g intelephenseEglot knows about Intelephense out of the box. The only configuration you need is to point it at your project and optionally pass a licence key for premium features:
(use-package eglot
:ensure nil
:bind (:map eglot-mode-map
("C-c r" . eglot-rename)
("C-c a" . eglot-code-actions)
("C-c f" . eglot-format-buffer)
("M-." . xref-find-definitions)
("M-," . xref-go-back))
:config
;; Pass initializationOptions for Intelephense
(setq-default eglot-workspace-configuration
'((:intelephense
:files (:maxSize 5000000)
:environment (:phpVersion "8.2")
:format (:enable t)
:licenceKey "YOUR_KEY_HERE")))
;; Increase timeout for large Drupal codebases
(setq eglot-connect-timeout 60)
;; Don't log full LSP traffic in production use
(setq eglot-events-buffer-size 0))Using phpactor Instead
phpactor is open-source and understands Drupal container configuration. Install it via Composer:
composer global require phpactor/phpactorRegister it with Eglot:
(with-eval-after-load 'eglot
(add-to-list 'eglot-server-programs
'(php-mode . ("phpactor" "language-server")))
(add-to-list 'eglot-server-programs
'(php-ts-mode . ("phpactor" "language-server"))))Code Style: PHP CS Fixer
Install PHP CS Fixer project-locally and wire it as a formatter:
composer require --dev friendsofphp/php-cs-fixer(use-package reformatter
:ensure t
:config
(reformatter-define php-cs-fix
:program "vendor/bin/php-cs-fixer"
:args (list "fix" "--using-cache=no" "-"))
:hook (php-mode . php-cs-fix-on-save-mode)
:hook (php-ts-mode . php-cs-fix-on-save-mode))Your .php-cs-fixer.php at project root controls rules:
<?php
// .php-cs-fixer.php
use PhpCsFixer\Config;
use PhpCsFixer\Finder;
$finder = Finder::create()
->in(__DIR__ . '/web/modules/custom')
->in(__DIR__ . '/web/themes/custom')
->name('*.php');
return (new Config())
->setRules([
'@PSR12' => true,
'array_syntax' => ['syntax' => 'short'],
'ordered_imports' => ['sort_algorithm' => 'alpha'],
'no_unused_imports' => true,
'trailing_comma_in_multiline' => true,
])
->setFinder($finder);Xdebug 3 Setup
Installing Xdebug in Docker
Add to your PHP Dockerfile:
RUN pecl install xdebug \
&& docker-php-ext-enable xdebugIn docker-compose.yml, pass configuration via environment variables — the cleanest approach for toggling Xdebug without rebuilding:
services:
php:
build: .
environment:
XDEBUG_MODE: "debug"
XDEBUG_CONFIG: "client_host=host.docker.internal client_port=9003 start_with_request=yes"
PHP_IDE_CONFIG: "serverName=drupal-local"For local (non-Docker) PHP, add to php.ini or conf.d/xdebug.ini:
[xdebug]
zend_extension=xdebug.so
xdebug.mode=debug
xdebug.start_with_request=yes
xdebug.client_host=127.0.0.1
xdebug.client_port=9003
xdebug.log=/tmp/xdebug.log
xdebug.idekey=emacsDAP Mode: Step-Through Debugging in Emacs
DAP Mode implements the Debug Adapter Protocol. For PHP it delegates to vscode-php-debug.
npm install -g @xdebug/vscode-php-debug(use-package dap-mode
:ensure t
:after eglot
:config
(require 'dap-php)
(dap-mode 1)
(dap-ui-mode 1)
(dap-tooltip-mode 1)
;; Show variable values in fringe
(dap-ui-controls-mode 1)
(dap-register-debug-template
"PHP: Listen for Xdebug"
(list :type "php"
:request "launch"
:name "PHP: Listen for Xdebug"
:port 9003
:pathMappings (let ((m (make-hash-table :test 'equal)))
(puthash "/var/www/html" (expand-file-name "~/projects/mysite") m)
m)))
:bind (:map dap-mode-map
("" . dap-debug)
("" . dap-next)
("" . dap-step-in)
("" . dap-step-out)
("" . dap-breakpoint-toggle)
("C-c d" . dap-hydra))) The :pathMappings key maps the container path (left) to the host path (right). Adjust this to match your Docker volume mount.
Starting a Debug Session
- Set a breakpoint with F9 on any PHP line
- Launch the template with F5 → "PHP: Listen for Xdebug" — Emacs now listens on port 9003
- Load the page in your browser. Execution halts at the breakpoint
- Step through with F6 (next), F7 (step in), F8 (step out)
- Inspect locals in the
*dap-ui-locals*buffer
Useful Complementary Packages
Flymake for Inline Diagnostics
Eglot uses Flymake by default. Ensure it runs PHP syntax checks as a secondary backend:
(use-package flymake-phpstan
:ensure t
:hook (php-mode . flymake-phpstan-turn-on)
:hook (php-ts-mode . flymake-phpstan-turn-on))Yasnippet for PHP Snippets
(use-package yasnippet
:ensure t
:hook ((php-mode php-ts-mode) . yas-minor-mode))
(use-package yasnippet-snippets
:ensure t)Projectile for Drupal Projects
(use-package projectile
:ensure t
:config
(projectile-mode +1)
;; Drupal projects have composer.json and web/ directory
(add-to-list 'projectile-project-root-files "composer.json")
:bind-keymap ("C-c p" . projectile-command-map))Drush Integration
Run Drush commands without leaving Emacs using compile or async-shell-command:
(defun drush-cache-rebuild ()
"Run drush cache:rebuild from the Drupal project root."
(interactive)
(let ((default-directory (projectile-project-root)))
(compile "vendor/bin/drush cache:rebuild")))
(global-set-key (kbd "C-c d r") #'drush-cache-rebuild)Complete init.el Snippet
A minimal, self-contained PHP development setup:
;; PHP development stack for Emacs 29+
(use-package php-mode
:ensure t
:mode ("\\.php\\'" "\\.module\\'" "\\.inc\\'" "\\.theme\\'")
:hook ((php-mode . eglot-ensure)
(php-mode . yas-minor-mode)
(php-mode . flymake-mode)))
(use-package eglot
:ensure nil
:config
(setq-default eglot-workspace-configuration
'((:intelephense
:environment (:phpVersion "8.2")
:format (:enable t))))
(setq eglot-connect-timeout 60
eglot-events-buffer-size 0))
(use-package dap-mode
:ensure t
:config
(require 'dap-php)
(dap-mode 1)
(dap-ui-mode 1))
(use-package flymake-phpstan
:ensure t
:hook (php-mode . flymake-phpstan-turn-on))Summary
- Install
tree-sitter-phpgrammar withtreesit-install-language-grammar - Use Eglot with Intelephense (
npm install -g intelephense) or phpactor for open-source LSP - Configure
eglot-workspace-configurationto set the target PHP version - Wire PHP CS Fixer via
reformatterfor automatic on-save formatting - Install Xdebug 3 and set
XDEBUG_MODE=debug— use env vars in Docker to avoid rebuilds - Install
@xdebug/vscode-php-debugand configure DAP Mode with a:pathMappingsentry per project - Set breakpoints with F9, start listening with F5, step with F6–F8
- Add
flymake-phpstanfor inline static analysis warnings alongside LSP diagnostics