Skip to content

Fix and document php-ide, and complete its Eglot integration - #826

Merged
zonuexe merged 12 commits into
masterfrom
improve/php-ide
Aug 5, 2026
Merged

Fix and document php-ide, and complete its Eglot integration#826
zonuexe merged 12 commits into
masterfrom
improve/php-ide

Conversation

@zonuexe

@zonuexe zonuexe commented Aug 4, 2026

Copy link
Copy Markdown
Member

php-ide has been experimental since 1.24.3 and had seen little use, so several of its advertised features never actually worked. This fixes them, completes the Eglot integration, and documents the feature.

Fixed

  • phpactor never deactivated. Its :deactivate pointed at php-ide-phpactor-activate, so turning php-ide-mode off re-activated Phpactor.
  • lsp-mode deactivation always errored. lsp-workspace-shutdown requires a WORKSPACE argument; switched to the buffer-scoped lsp-disconnect.
  • Symbolic php-ide-eglot-executable values were unusable. The assq result was used without cdr, so intelephense / phpactor produced a malformed command list.
  • php-ide-eglot-executable had no effect at all. Nothing ever registered it into eglot-server-programs; php-ide-eglot-activate now does, buffer-locally.
  • The :safe predicates never applied. Emacs checks directory-local values before php-ide.el loads, so they ran as copied into the autoloads file and hit void-function cl-loop / void-variable. safe-local-variable-p demotes such errors to nil, so every project setting these variables was prompted for confirmation anyway.
  • Phpactor hover suppression was inverted. php-ide-phpactor-disable-hover-at-point-functions was combined with AND despite documenting "any", so an empty list disabled hover everywhere.
  • Deactivating one buffer stopped hover in all others. The hover timer is shared but was cancelled unconditionally; it is now retired only once no live buffer needs it.

Security

php-ide-mode-functions holds functions that php-ide-mode calls automatically, and its :safe predicate accepted any fboundp symbol — enough for a repository to run code in a visitor's Emacs just by having them open a file. It now has no :safe predicate. php-ide-features and php-ide-eglot-executable are restricted to this package's own known feature symbols and bundled presets, so a raw executable path in .dir-locals.el no longer applies silently.

The variable was always meant to be set globally with add-hook, as its own Commentary shows, so this costs no documented use case. Users who do want a directory-local value can approve it through Emacs' normal mechanisms (! at the prompt, or safe-local-variable-directories on Emacs 30.1+); the CHANGELOG spells this out.

Changed

  • php-ide-turn-on is a no-op when php-ide-features is unset, instead of a user-error. The README recipe adds it to hack-local-variables-hook, so it previously errored on every PHP file until a feature was configured.
  • php-ide-features now also accepts a bare symbol, as its own Commentary already showed.

Added

  • php-ide-set-feature — pick a feature interactively, among those actually available.
  • php-ide-status — report whether PHP-IDE is on, what is configured, and what is available.
  • A PHP-IDE section in README.md / README.ja.md; php-ide had no documentation beyond one line in a config sample.

Testing

eask test ert ./tests/php-mode-test.el passes (82 tests, 2 skipped). Every fix above has a regression test, including one that runs the :safe predicates in a subprocess loading only the autoloads file. php-ide.el and php-ide-phpactor.el are clean under byte-compile and checkdoc.

zonuexe added 12 commits August 4, 2026 23:17
The custom-set-variables example used invalid quoting for
`php-ide-features' and the per-project snippet, and the "no IDE
support" note referenced a variable name that never existed
(`php-ide-feature`).
- phpactor's :deactivate pointed at php-ide-phpactor-activate instead
  of php-ide-phpactor-deactivate, so turning php-ide-mode off actually
  re-activated Phpactor
- lsp-mode's :deactivate called lsp-workspace-shutdown, which requires
  a WORKSPACE argument and errored on every deactivation; switched to
  the buffer-scoped lsp-disconnect
- php-ide-eglot-server-program used the assq cons cell itself instead
  of its cdr, so any symbolic php-ide-eglot-executable value (e.g.
  intelephense, phpactor) produced a malformed command list
- Add php-ide-eglot-activate, which buffer-locally registers
  php-ide-eglot-executable into eglot-server-programs; previously the
  variable had no effect on Eglot at all
- Restrict :safe on php-ide-features/php-ide-eglot-executable to
  PHP-IDE's own known feature symbols and bundled executable presets,
  and drop :safe from php-ide-mode-functions entirely; previously
  these let a .dir-locals.el silently run an attacker-chosen command
  or Lisp function without Emacs's usual confirmation
- php-ide-mode now accepts php-ide-features set to a bare symbol, not
  just a list, matching what the Commentary already showed
- php-ide-turn-on no longer signals a user-error when php-ide-features
  is unset; it is now a silent no-op, so it is safe to add
  unconditionally to hack-local-variables-hook
- Remove now-unreachable dead code in php-ide-mode's activation loop
  and rename php-ide--avilable-features (typo) to
  php-ide--available-features
- Add php-ide-set-feature and php-ide-status commands to interactively
  choose/enable an available feature and report the current state
Add a PHP-IDE section covering configuration, the interactive commands
(php-ide-mode, php-ide-turn-on, php-ide-set-feature, php-ide-status),
per-project setup, and which values are safe in .dir-locals.el.
Cover php-ide-eglot-server-program's resolution of every
php-ide-eglot-executable shape, the feature-alist's :test/:activate/
:deactivate arity contract, the phpactor activate/deactivate fix, bare
-symbol php-ide-features, php-ide-turn-on's no-op behavior, the new
php-ide-set-feature/php-ide-status commands, the :safe predicates, and
php-ide-eglot-activate's buffer-local eglot-server-programs
registration.
Emacs decides whether a .dir-locals.el value is safe while hacking local
variables, which happens *before* php-ide.el gets loaded: the documented
recipe only pulls it in from hack-local-variables-hook, which runs
afterwards.  The predicates therefore execute as copied into
php-mode-autoloads.el, where neither cl-lib nor php-ide.el's own
variables are available yet:

    php-ide-features         -> (void-function cl-loop)
    php-ide-eglot-executable -> (void-variable php-ide-lsp-command-alist)

safe-local-variable-p demotes those errors and returns nil, so both
variables were treated as unsafe and prompted for confirmation in every
project that set them -- exactly the per-project workflow the README
documents.

Autoload the two alists so the predicates can consult them from the
autoloads file, and rewrite the predicates without cl-lib.  Also give
both alists the docstrings they should have had as public variables.
php-ide-phpactor--disable-hover-at-point-p promises to return non-nil
when *any* predicate in php-ide-phpactor-disable-hover-at-point-functions
matches, but was written as `never (not ...)', i.e. logical AND.  An
empty list therefore suppressed hover everywhere -- the opposite of the
intent -- and several predicates only fired when all of them matched.

The stock one-element value happens to behave the same either way, so
this only bit users who customized the variable.  Switch to `thereis'
and document the variable.
The hover timer is a single global object shared by every buffer, but
php-ide-phpactor-deactivate cancelled it unconditionally.  Turning
php-ide-mode off in one PHP buffer therefore silently stopped hover in
every other one, while those buffers still believed it was active.

Cancel it only once no live buffer has hover enabled, and let the timer
function retire the timer itself, so a buffer killed while active does
not leave it running forever.  Also clear the buffer-local hover cache
on deactivation.
php-ide-mode-lighter described itself as "A symbol of PHP-IDE feature",
which says nothing about the mode line string it actually holds.  The
remaining undocumented defvars now have docstrings, and
php-ide-eglot-managed-modes opens with a complete sentence so checkdoc
is clean.
Emacs 28 was the first to copy a defcustom's :safe predicate into the
generated autoloads file, so on Emacs 27 -- still supported -- the
subprocess found no predicate to call and the test failed rather than
skipping.  Mirrors the guard the equivalent php-complete test uses.
@zonuexe
zonuexe merged commit 2d5f839 into master Aug 5, 2026
14 checks passed
@zonuexe
zonuexe deleted the improve/php-ide branch August 5, 2026 11:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant