Tuesday, August 16, 2016

Emacs + PHP - A Modern and (Far More Complete) Recipe

Recently, I had a chance to do a bit of emacs evangelism. Man, is that a soapbox I like to climb on! I hit all my favorite features from dired and dynamic abbreviation expansion to under appreciated gems like ispell. I talked about the power of having a fully programmable, self documenting editor at your fingertips. When I was done, I thought for sure I had managed to add another member to the tribe.

Then, yesterday, my possible convert came to me with a simple question: what mode do you use to edit PHP? Considering that most of the code I write is PHP, you'd think I would be ready to deliver a solid answer. Instead, I mumbled something about having a hacked bit of code I relied on, but really wouldn't recommend it for the general use. Yeah, not cool. I closed out the conversation with a promise: I'd see what the latest PHP options were and report back.

PHP is actually a fairly tricky language to build an editor for. That's because depending on the style it's written in, it can range from disciplined C like code to a gobbly gook C like code with HTML, CSS and JavaScript all mixed in. Add to that the classic emacs problem of having too many 85% solutions, and there's definitely room for frustration. Check out the emacs wiki to see what I mean. You'll find lots of promising options, but no one definitive solution.

After reading up on my choices and trying out some options, I do believe I have a new recipe for PHP + emacs success. Here goes.

Step 1: Setup Melpa

Melpa is a code repository that emacs can magically pull in packages from. Back in the day, adding packages to emacs meant downloading, untarring, and running make. Like all things emacs, the process has been both streamlined, and of course, is fully executable from within emacs. To add Melpa, you'll need to follow the instructions here. Assuming you have a modern version of emacs, this probably just means adding the following to your .emacs:

(add-to-list 'package-archives
             '("melpa" . "https://melpa.org/packages/"))

Step 2: Install the Melpa available PHP Mode

Emacs comes with a PHP mode out of the box, but it seems as though this one (also named php-mode) is more modern. I love that the README talks about PHP 7, showing just how up to date this mode is with respect to the latest language constructs.

Installing this mode, assuming Melpa is cooperating, couldn't be easier. Just run: M-x package-install and enter php-mode.

My preference for indentation is 2 spaces and no insertion of tab characters. Two other tweaks I was after was to turn off HTML support from within PHP and enable subword-mode. All these tweaks are stored in a function and attached to the php-mode-hook. This is all standard .emacs stuff:

(defun bs-php-mode-hook ()
  (setq indent-tabs-mode nil)
  (setq c-basic-offset 2)
  (setq php-template-compatibility nil)
  (subword-mode 1))

(add-hook 'php-mode-hook 'bs-php-mode-hook)

Step 3: Install Web-Mode.el

php-mode above is ideal for code centric PHP files. But what about those pesky layout files that contain HTML, CSS and JavaScript? For that, web-mode.el looks especially promising. Installation and configuration mirrors php-mode. Here's the code I use to customize it:

(defun bs-web-mode-hook ()
  (local-set-key '[backtab] 'indent-relative)
  (setq indent-tabs-mode nil)
  (setq web-mode-markup-indent-offset 2
        web-mode-css-indent-offset 2
        web-mode-code-indent-offset 2))

(add-hook 'web-mode-hook 'bs-web-mode-hook)

Web-mode.el is quite impressive and who knows, it may actually fulfill all my PHP needs. If that's the case, I may choose to stop using the php-mode. However, for now, I like the idea of being able to switch between the two modes. Which brings me to the next step...

Step 4: Add a Quick Mode Toggle

Inspired by this tip on the EmacsWiki, I went ahead and setup a quick toggle between php-mode and web-mode. Here's that code:

(defun toggle-php-flavor-mode ()
  "Toggle mode between PHP & Web-Mode Helper modes"
  (cond ((string= mode-name "PHP")
        ((string= mode-name "Web")

(global-set-key [f5] 'toggle-php-flavor-mode)

Now I'm only an F5 keystroke away from two different editor strategies.

When I have a say in the matter, I tend to be pretty particular about which files in my source tree are pure code and which are pure markup. At some point, I could codify this so that files in the snippets directory, for example, always open in web-mode, whereas files loaded from under lib start off in php-mode. Such is the joy of having a fully programmable editor at your fingertips. You make up the rules!

Step 5: Bonus - Setup aggressive auto-completion

For bonus points, I decided to play with ac-php, a library that supports auto completion of function and class names. I followed the install here, updated my .emacs file as noted below, created the empty file named .ac-php-conf.json in my project's root and then ran M-x ac-php-remake-tags-all. Once that's done, emacs now shouts completions like crazy at me:

It's slick, I'll give you that. I think I may have to see if I can turn down the volume, a bit. Here's what my .emacs now looks like to configure php-mode: now looks like:

(defun bs-php-mode-hook ()
  (auto-complete-mode t)                 ;; «
  (require 'ac-php)                      ;; «
  (setq ac-sources  '(ac-source-php ))   ;; «
  (yas-global-mode 1)                    ;; «
  (setq indent-tabs-mode nil)
  (setq php-template-compatibility nil)
  (setq c-basic-offset 2))

Bye-bye hacked PHP code. Hello modern, feature filled, super easy to install, mega powerful code.

Update: updated the web-mode hook I'm using to make sure all code on the page, not just markup code, is indented 2 steps.


  1. web-mode rocks; very thoughtful high quality mode

  2. Anonymous3:39 PM

    One quick note - the MAJOR-MODE buffer-local variable identifies the currently selected major mode perhaps more reliably than a string match, so it might be worth considering its use in your mode-toggle binding - (eq major-mode 'php-mode), et al.

  3. I've been working on and off on https://github.com/Fuco1/php-refactor ... a simple-minded refactoring, I wrote my own very very incomplete php parser (but it's fast! only parsing the state I care about). So far it can't do much but already it helps me a lot. Feature requests or code welcome :)

    The installation is also a bit of a PITA, that could be worked out too.