The PHP language server setup in my Emacs used to be a single setting that offered Eglot a list of servers, phpactor and Intelephense, to choose from. In October, I replaced it with a small function and a settings function. Between them, they handle three things the list couldn't express: that my projects don't run PHP on the host, that Drupal keeps PHP in files that don't end in .php, and which PHP version each project runs.
Choosing the server with a function
eglot-server-programs maps a major mode to a "contact": usually a command line. Eglot's documentation lists the other shapes a contact can take, and two of them matter here. A contact can be a command line followed by :initializationOptions, which Eglot sends to the server when it starts. And a contact can be a function of two arguments, called when Eglot needs to start a server, which returns any of the other shapes.
The old setting used eglot-alternatives, a helper that builds such a function from a list of programs and chooses one of them, interactively or automatically. The new function decides with a fixed rule:
- if
intelephenseis on the path, use it, with initialization options - otherwise, use phpactor, but only if a
phpbinary exists on the host - otherwise, stop with an error that says how to install Intelephense
The order is the point. phpactor is written in PHP, so it needs PHP on the host, and my projects keep PHP inside DDEV or a docker compose stack. Intelephense runs on Node, installed per user with npm install -g --prefix ~/.local intelephense, so it works on a machine with no host PHP at all.
Initialization options
Intelephense's documentation lists its initialization options, and the function sends three:
storagePath: where it keeps its index. The default is the operating system's temporary directory; mine is~/.cache/intelephense.globalStoragePath: a global storage directory, here under~/.local/share.licenceKey: only if a licence file exists. The function reads the key from~/.config/intelephense/licence.txtand leaves the option out otherwise, so the same configuration works with and without a licence.
Workspace settings from the project itself
The other half is eglot-workspace-configuration, the settings Eglot hands the server. Its documentation says it can be a function instead of a fixed value: Eglot calls it with the server, with default-directory set to the root of the current project. That's what makes per-project settings possible without a .dir-locals.el in every project.
Mine sets three Intelephense settings:
intelephense.files.associations: the default is*.phpand*.phtml. Drupal also keeps PHP in.inc,.module,.install,.theme,.profileand.enginefiles, so the list includes those. The same list of extensions decides which files open inphp-ts-mode, so the editor and the server agree on what counts as PHP.intelephense.files.maxSize: the default is 1,000,000 bytes, and larger files are skipped. Mine is 5,000,000.intelephense.environment.phpVersion: the PHP version to check code against, which defaults to 7.4.0.
The PHP version comes from the project. The function looks for the nearest .ddev/config.yaml, reads its php_version line, and passes it on. One detail: DDEV writes a version like 8.3, and Intelephense's documentation describes the setting as a semver-compatible string, so the function appends .0. In a project without DDEV, the setting is left out and Intelephense uses its default.
Built-ins, declared as built-ins
Both php-ts-mode and Eglot are part of Emacs now; php-ts-mode arrived in Emacs 30. The configuration declares them with use-package and :ensure nil, not :ensure t, which is the keyword that makes use-package install a package that isn't there yet. The tree-sitter grammar for PHP is registered in treesit-language-source-alist, pointing at the php/src directory of the tree-sitter-php repository, so it can be built from Emacs when it's missing.
None of this is much code: the whole file is 77 lines. The idea that carries over to other languages is to let functions answer the questions a static setting can't: which server can run on this machine, and what does this project need. Eglot asks those questions at exactly the right moment, if you give it a function instead of a value.
I compared Eglot with lsp-mode in more general terms in an earlier article; this is what the Eglot side looks like in practice.
Leave a comment
Comments are reviewed before they appear.