User:ArielGlenn/Emacs as a PHP IDE

Last updated: --

I love my emacs. I like its multiple buffers and its key bindings and its split windows and all the rest. I don't love grep -r in the mediawiki git repo.

What's to be done? There's no open source PHP IDE that lets one use emacs as the editor in some sub-window, to my knowledge. How about closed source? Nope.

I looked at docs for PHPStorm, CodeLite, Atom, Aptana, and others. No joy. Enter LSP to save the day! Drumroll please...

Emacs with LSP step by step


The TL;DR:
 * build emacs 28-pre with native compilation and with a C json library
 * choose an LSP backend and install it
 * install the LSP-related emacs packages from elpa
 * install a few other elpa packages to round out the IDE experience
 * configure the packages and the use of the specific LSP backend
 * curse when it doesn't work until you determine that PEBCAK is to blame
 * tweak and enjoy

We'll go through each of these in detail.

What I wanted from a PHP IDE
There are really only four capabilities that I had to have:
 * 1) code navigation: moving to the code defining a class or method, from some other code using it
 * 2) docs on hover: display of the arguments and any related docs for a class or method, when the cursor is on a reference to it
 * 3) completion: variable, method and class name completion when coding
 * 4) speed: waiting for project indexing on first use is ok, but not afterwards.

No refactoring, no extra debugging features, none of that. Just the basics. Remember, my ide until now had been grep -r.

LSP, the game-changer
Language Server Protocol is a Microsoft specification for a protocol intended for use by a stand-alone server that responds to well-defined requests (where is the definition of this method? Where is this class used?) with the information requested in a specific format. Any number of clients may communicate with the server as long as they support the protocol. If your favorite editor supports it, and there's a server you like for your language, you can use it.

In other words, instead of having to build all of the capabilities we want into emacs or any other editor, a separate PHP language server can do the heavy lifting for emacs or any other editor that supports LSP from the client end.

I guess that most IDEs do not use this approach; they directly implement every feature that is desired. That's fine for IDEs with a large and demanding user base. But for just the basics, LSP is a fine choice. For more incindiary rhetoric on this topic, see the relevant HackerNews thread.

Building emacs
First things first; we need json handled by a C library:. Emacs 27 supports this, but I had only emacs 26.

We also would like native compilation, also known as. This provides support for compiling el files into binary (elf files)! This feature is not available in Emacs 27, but may be merged into master "soon" with an eye to making it available in emacs 28. For now, it's in a separate branch. Lots more information about gcc emacs is available on the updates page for the project.

I built emacs in the following environment:


 * OS: Fedora 31, nearly two versions old
 * GCC: 9.3.1 but GCC 10 will work as well

I did NOT build with mailutils support. If you want that, adjust the below accordingly.

Steps to build for installation in :


 * install any other devel packages you'll need, or wait until configure complains about them
 * as root,
 * install any other devel packages you'll need, or wait until configure complains about them
 * as root,
 * install any other devel packages you'll need, or wait until configure complains about them
 * as root,
 * as root,
 * as root,
 * as root,
 * as root,

If you have any packages in your, update them now. I used development packages where I could.

Test out your new emacs binary. The first time you run it, it will compile all of those packages. If you try to exit out before it's done, it will ask you whether you really want to interrupt those running compiles. You'll encounter this any time you install or upgrade a package. If you're curious about the output, have a look in.

You'll want to add

near the top of your init file, so that you can in fact get on with other editing while compilation goes on in the background.

Selecting an LSP backend
There are a few free as in beer PHP LSP servers, most of which are also free software. I looked at several of them: Intelephense, Serenata, Felix Becker's php language server. I did not try out the Tenkawa PHP Language Server or Psalm. Have a look at the features, how active the project is, and the license if you are a FLOSS supporter.

In the end, I went for Serenata, as it is currently maintained, open source, and has the features I needed. After setting up lsp-mode (see below), I double-checked the enabled features list in emacs for each server, and Serenata was the winner.

Installing Serenata is easy enough:


 * download the PHAR file corresponding to your version of PHP from the list of releases
 * put it in a location that doesn't trigger your OCD
 * make sure it is executable; if you don't, you will spend hours wondering why emacs cannot find it and offers to download intelephense

Installing emacs packages for LSP


You may decide to use some other combination of things, but you will want at a minimum:
 * php-mode (duh!)
 * lsp-mode
 * lsp-ui-mode (highly recommended)
 * company (for lsp-mode)
 * flycheck (for lsp-mode)
 * treemacs and lsp-treemacs (for lsp-mode)
 * which-key (for lsp-mode)
 * xref (for lsp-mode and lsp-ui-mode)
 * yasnippet (for lsp-mode)

I use treemacs for code navigation of the project as well as for display of trees via lsp. You may choose some other means. I also have phpunit installed.

Note that in very case where I could, I installed the latest development package.

Package configuration
I use " " for everything, with a mix of cargo-cult copy-pasting and rolling my own.

PHP mode configuration
MediaWiki PHP coding style is its own thing, so setting up for appropriate tabs is a must. Here is the stanza I use for that, stolen shamelessly from Stack Overflow. Note that there's a whole section in the MW coding conventions manual for that, but I haven't looked at that at all.
 * php-mode setup

(defun my-php-mode-hook  (setq indent-tabs-mode t)  (let ((my-tab-width 4)) (setq tab-width my-tab-width) (setq c-basic-indent my-tab-width) (set (make-local-variable 'tab-stop-list)        (number-sequence my-tab-width 200 my-tab-width))))

And here's the stanza for php-mode itself. (use-package php-mode :ensure t  :hook  (php-mode-hook. my-php-mode-hook) :mode  ("\\.php\\'". php-mode) ("\\.inc\\'". php-mode))

which-key, flycheck, phpunit
(use-package flycheck :ensure t  :init  (global-flycheck-mode))

(use-package which-key :ensure t  :config  (which-key-mode))

(use-package phpunit :ensure t  :after php-mode)

The Good Stuff (lsp-mode and friends)
You should set up a file with emacs local variables for the mediawiki-core repo. These settings will determine the tree that is indexed, and where the index is written. The file will be called " " and you should put it at the top level of your mediawiki-core repo.

Adjust these settings for your own directory layout.

Wherever you decide serenata's sqlite database and related files should live, make sure you have created all of the directories in the path.
 * serenata settings for mediawiki-core project

((nil. (	     (lsp-serenata-index-database-uri. "file:///home/ariel/.emacs.d/serenata-cache/mwcore-index.sqlite")	     (lsp-serenata-uris. )	     )	   ) ) Unfortunately, to get this to work properly, at least of Serenata 5.4.0 and lsp-mode 20201011.1353, you must edit   in the lsp-mode package you retrieved from elpa. It currently sends initialization parameters with the string " " and it should send the string " " as documented in the [Serenata config docs]. So you'll need to change `( :config ( :uris ,lsp-serenata-uris to `( :configuration ( :uris ,lsp-serenata-uris
 * where to stash index of files in project, what dir to index

If you forget to do this, indexing files will be written to  and you will be very sad at some point. (Upstream issue opened, if there's some better fix I should hear about it soon.)

On to package configuration. The key bindings chosen are an odd mix of crap which I will surely change later.

Note the setting for  which you should change to the directory where you put the PHAR you downloaded, or if you are not using Serenata, follow the instructions on the emacs LSP-mode page for your back end.

Note also the hoops one needs to jump through in order to get  as the prefix for lsp-mode. If you prefer, you can make this global and avoid having both an  and a   entry, see a bug report on this for more.

Note the  key binding; the intent of this is that once you have loaded up your first php file in a buffer, you can use this to start up treemacs and lsp-ui-imenu with the treemacs nodes automatically expanded. If you don't want this, just leave it out.

And finally, take note of the  entry. This says "start lsp only after local variables (i.e. the ones you defined in the  file) have been loaded." Make sure you don't accidentally launch lsp from some other part of your emacs init file, at least for php files.

(use-package company :ensure t  :after php-mode  :config  (setq company-idle-delay 0.3)  (push 'company-files company-backends)  (global-set-key (kbd "C- ") 'company-complete))

(use-package lsp-mode :ensure t  :after php-mode  :init  (setq lsp-keymap-prefix "C-c l")  :config  (define-key lsp-mode-map (kbd "C-c l") lsp-command-map)  (setq lsp-prefer-flymake nil lsp-serenata-server-path (substitute-in-file-name "$HOME/bin/php-7.3-serenata-distribution.phar") lsp-serenata-file-extensions ["php" "inc"]) :hook  (hack-local-variables. (lambda (when (derived-mode-p 'php-mode) (lsp))))  (lsp-mode. lsp-enable-which-key-integration) :bind  (:map lsp-mode-map ;; we always want these in php + lsp mode, so at least ;; have one key sequence to do them both ("C-c l i" . (lambda (interactive) (lsp-ui-imenu) ;; go back to the pane with the php file (other-window 1) ;; now invoke treemacs from that pane ;; so that expansion of the node will work (treemacs) ;; expand the top node always (lambda			 (setq pos (treemacs-project->position project) (treemacs--expand-root-node pos))			 ) )	 )) :commands  lsp)

(use-package lsp-ui :ensure t  :requires flycheck  :after lsp-mode  :config  (setq lsp-ui-doc-enable t  lsp-ui-doc-use-childframe t   lsp-ui-doc-position 'top lsp-ui-doc-include-signature t

lsp-ui-flycheck-enable t  lsp-ui-flycheck-list-position 'right lsp-ui-flycheck-live-reporting t

lsp-ui-imenu-enable t

lsp-ui-peek-enable t  lsp-ui-peek-list-width 60 lsp-ui-peek-peek-height 25

lsp-ui-sideline-enable t  lsp-ui-sideline-update-mode 'line lsp-ui-sideline-show-code-actions t  lsp-ui-sideline-show-hover nil)

(:map lsp-ui-mode-map	("C-c C-j". lsp-ui-peek-find-definitions)	("C-c i". lsp-ui-peek-find-implementation)	("C-c m". lsp-ui-imenu))

(lsp-mode . lsp-ui-mode))

(use-package treemacs :ensure t  :commands treemacs  :bind  (:map global-map ("M-0"      . treemacs-select-window) ("C-x t 1"  . treemacs-delete-other-windows) ("C-x t t"  . treemacs) ("C-x t B"  . treemacs-bookmark) ("C-x t C-t" . treemacs-find-file) ("C-x t M-t" . treemacs-find-tag)) :after lsp-mode)

(use-package lsp-treemacs :ensure t  :after (lsp-mode treemacs)  :commands lsp-treemacs-errors-list  :bind (:map lsp-mode-map ("C-c 9" . lsp-treemacs-errors-list)))

Cursing and stuff
Aborts, Memory: When I first started this up, I didn't have treemacs in there. Serenata spent some time indexing the entire mediawiki core repo, and then proceeded to one of the vendor subdirs with PHPStorm stubs. There, it died. If it does that to you too, you should NOT agree to a restart when asked, but should exit out of emacs, restart it, and Serenata will take up where it left off. If you restart, it will start from the beginning, dying once again somewhere in the vendor dir. You can increase the  in your   if this gets too annoying.

When I added treemacs and created a project (coincidentally the mediawiki core repo), Serenata indexed the entire thing again, dying once again in the vendor subdir, so I went through the same don't-restart-it-or-you'll-be-sorry song and dance. Now everything seeeeeems to be indexed properly.

Symlinks to project: If you have a symlink somewhere in your path to the mediawiki core repo, the files will all be indexed with the expanded (not symlink) name, but if you load a file into the buffer in emacs via the symlink path, that path will not be expanded when time comes for Serenata to look up the file in the index. The workaround for this is to alias  to " ".

Debugging: For debugging, you can add the setting  near the top of your emacs init file. This will generate a buffer called  where nnnn is some port number. It will show all messages sent to and from the lsp server.

You can also check what initially calls lsp, in case you are not getting per-project initialization and suspect that your  hook is not happening. Use  on lsp itself, before loading your first php file into a buffer. The output will show you whether you've got lsp starting from somewhere else in your config.

Don't forget to look at the  buffer and the   buffer in addition.

Use, tweak and enjoy
My intended use is to start emacs, pull up some MediaWiki php file, and then immediately invoke treemacs and lsp-ui-imenu by typing. After this, one can use treemacs or  etc. or basic emacs commands to get to other files.


 * I need to see how xref is used, what lsp-sideline features I might want, etc.
 * Do I want to run unit tests in an emacs window? Right now I don't run phan or any of that via emacs but in another terminal tab.
 * And the big question: emacs in the terminal, always my default until now, or emacs as a separate window?? Do I want pretty treemacs icons?

Comments welcome
I'm just getting started with this. Today as I clean up this page somewhat, I will literally have used this setup for a grand total of three days. Comments and suggestions are more than welcome! You know where the talk page is :-)