November 5, 2017

Neovim & PHP: Step By Step To Your Personal IDE

I am developing with (neo)vim in PHP now for about 6 years. Over that period I have experimented with most PHP vim plugins available. In the following post I’ve collected everything that makes your life as a PHP developer in vim easier.

neovim php ide screenshot

Switch from vim to neovim

I’ve switched from vim to neovim (thanks to my former colleague ). A lot of blog posts exist about how and why I won’t go into detail here.

Clean start

If you haven’t made the switch to neovim, give it a shot. Switching before you follow this article is not necessary, all plugins should work with vim8/neovim. But it makes sense because you will have a blank config and won’t get any drawbacks.

To build it from source:

git clone https://github.com/neovim/neovim.git && cd neovim
make CMAKE_BUILD_TYPE=Release
sudo make install
mkdir -p ~/.config/nvim # this is your "~/.vim"
touch ~/.config/nvim/init.vim # this is your "~/.vimrc"

Manage Plugins With vim-plug

For easy plugin handling we will use a plugin manager. My favourite plugin manager is vim-plug. It works asynchronously and lets you load plugins on demand. For example, it makes sense to load PHP plugins only when you open a PHP file :)

Dependencies

  • git
  • curl

We can setup nvim to install vim-plug automatically by adding following to your ~/.config/nvim/init.vim:

if empty(glob('~/.config/nvim/autoload/plug.vim'))
    silent !curl -fLo ~/.config/nvim/autoload/plug.vim --create-dirs
                \ https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim
    autocmd!
    autocmd VimEnter * PlugInstall
endif

Now add the vim-plug bootstrapping to your ~/.config/nvim/init.vim:

call plug#begin('~/.config/nvim/plugged')
" your plugins will go here
call plug#end()

PHP Plugins

I use following plugins for PHP:

Avoid Repetitive Typing With Snippets

Automatic code generation is key to quick coding and avoiding boredom. Especially when it’s made so simple but powerful like UltiSnips. It’s my favourite snippet solution because the ease of use and really cool things like python interpolation. Bonus: it integrates with the ncm2 autocompletion plugin (see autocompletion section below).

Paste into your .vimrc or ~/.config/nvim/init.vim:

call plug#begin('~/.config/nvim/plugged')
[...]
Plug 'SirVer/ultisnips' | Plug 'phux/vim-snippets'
call plug#end()

let g:UltiSnipsExpandTrigger="<c-j>"
let g:UltiSnipsJumpForwardTrigger="<c-j>"
let g:UltiSnipsJumpBackwardTrigger="<c-b>"

" PHP7
let g:ultisnips_php_scalar_types = 1

Custom snippets

After executing :PlugInstall in vim followed by a restart you can start triggering your snippets via CTRL+j. Open a PHP file and type for example fore and hit CTRL+j to trigger the snippet for a foreach loop. To view, edit or add new snippets for a language just type :UltiSnipsEdit while you’re editing a file of that type.

PHP Autocompletion With neovim & phpactor

I am using ncm2 for an auto-completion popup and phpactor for getting the completion candidates. ncm2-phpactor makes them talk with each other :) One thing I really love about ncm2: in combination with ultisnips it provides you with parameter expansion.

call plug#begin('~/.config/nvim/plugged')
  Plug 'ncm2/ncm2'
  Plug 'roxma/nvim-yarp'
  Plug 'roxma/vim-hug-neovim-rpc'

  Plug 'phpactor/phpactor', { 'do': ':call phpactor#Update()', 'for': 'php'}
  Plug 'phpactor/ncm2-phpactor', {'for': 'php'}
  Plug 'ncm2/ncm2-ultisnips'
  " Plug 'SirVer/ultisnips' should have been already added in previous
  " section
call plug#end()

augroup ncm2
  au!
  autocmd BufEnter * call ncm2#enable_for_buffer()
  au User Ncm2PopupOpen set completeopt=noinsert,menuone,noselect
  au User Ncm2PopupClose set completeopt=menuone
augroup END

" parameter expansion for selected entry via Enter
inoremap <silent> <expr> <CR> (pumvisible() ? ncm2_ultisnips#expand_or("\<CR>", 'n') : "\<CR>")

" cycle through completion entries with tab/shift+tab
inoremap <expr> <TAB> pumvisible() ? "\<c-n>" : "\<TAB>"
inoremap <expr> <s-tab> pumvisible() ? "\<c-p>" : "\<TAB>"

Automatic Tags Update With Git Hooks

I am using a slightly modified version of Tim Pope’s Effortless Ctags with Git . I altered the hook file to create ctags for PHP only.

Other ctag plugins I’ve used but dumped because of the simplicity and completeness of the githooks/BufWritePost hook:

Paste following into ~/.git_template/hooks/ctags:

#!/bin/sh
set -e
PATH="/usr/local/bin:$PATH"
dir="`git rev-parse --git-dir`"
trap 'rm -f "$dir/$$.tags"' EXIT
ctags --tag-relative -R -f "$dir/$$.tags" --fields=+aimlS --languages=php --PHP-kinds=+cf-va
mv "$dir/$$.tags" "$dir/tags"

The hooks will be automatically applied to new projects. To apply the hooks to existing projects simply execute git init in the project’s root.

Automatic Tag File Update After Write

We want of course to have the tags updated whenever we save a file. For that we can use an autocommand that executes the same file like our githooks:

" update tags in background whenever you write a php file
au BufWritePost *.php silent! !eval '[ -f ".git/hooks/ctags" ] && .git/hooks/ctags' &

Let’s test this by creating a test project:

mkdir /tmp/test-ctags && cd /tmp/test-ctags
git init

# create a test file:
echo "<?php class Foo {}" > foo.php
# open the testfile and write&close the file to trigger tag creation:
vim -c ":x" foo.php

# verify the tags file does exist now
ls -l .git/tags
# should return something like this:
-rw-rw-r-- 1 jm jm 389 Dez  3 18:06 .git/tags

Updated PHP Syntax File

Improve syntax highlighting by a constantly updated php syntax file => php.vim

call plug#begin('~/.config/nvim/plugged')
[...]
Plug 'StanAngeloff/php.vim', {'for': 'php'}
call plug#end()

Write Less Messy Code with ale

ale is an asynchronous linting framework for neovim/vim. It not only supports PHP but a huge amount of languages. For PHP I use php syntax error checks, CodeSniffer (phpcs) and PHPMessDetector (PHPMD). ale will display warning and error signs next to each line. ale also let’s you specify fixers, which are automatically executed on saving a file.

Dependencies

  • php for checking for syntax errors :)
  • PHPMD composer global require phpmd/phpmd
  • PHPCS composer global require squizlabs/php_codesniffer
  • phpstan composer global require phpstan/phpsan

In your ~/.config/nvim/init.vim:

call plug#begin('~/.config/nvim/plugged')
[...]
Plug 'w0rp/ale'
call plug#end()

" disable linting while typing
let g:ale_lint_on_text_changed = 'never'
let g:ale_lint_on_enter = 0
let g:ale_echo_msg_format = '[%linter%] %s [%severity%]'
let g:ale_open_list = 1
let g:ale_keep_list_window_open=0
let g:ale_set_quickfix=0
let g:ale_list_window_size = 5
let g:ale_php_phpcbf_standard='PSR2'
let g:ale_php_phpcs_standard='phpcs.xml.dist'
let g:ale_php_phpmd_ruleset='phpmd.xml'
let g:ale_fixers = {
  \ '*': ['remove_trailing_lines', 'trim_whitespace'],
  \ 'php': ['phpcbf', 'php_cs_fixer', 'remove_trailing_lines', 'trim_whitespace'],
  \}
let g:ale_fix_on_save = 1

After that :PlugInstall.

To check whether ale can find the linters, open a php file and enter :ALEInfo. Look for “Enabled Linters”.

Applying Coding Standards With php-cs-fixer

Dependencies

  • php-cs-fixer: composer global require friendsofphp/php-cs-fixer

In your .vimrc or ~/.config/nvim/init.vim:

command! -nargs=1 Silent execute ':silent !'.<q-args> | execute ':redraw!'
map <c-s> <esc>:w<cr>:Silent php-cs-fixer fix %:p --level=symfony<cr>

Now you can format the current buffer by pressing CTRL+s.

Enter php-cs-fixer help fix in your terminal for all available levels and options.

Switch Between Unit Tests And Their Classes

In another article I’ve shown you a function to toggle between PHPUnit tests and their respective class files via a key press. This is essential when you’re doing TDD. Go and check the article out.

Refactoring Done Right

Refactoring is an important part in a professional developer’s life. I use a combination of different plugins and self-written functions because none of the existing plugins fulfills 100% of my needs.

Vim PHP refactoring toolbox

The php refactoring toolbox gives you a bunch of common refactoring operations at your hand.

In your .vimrc or ~/.config/nvim/init.vim:

call plug#begin('~/.config/nvim/plugged')
[...]
Plug 'adoy/vim-php-refactoring-toolbox', {'for': 'php'}
call plug#end()

let g:vim_php_refactoring_default_property_visibility = 'private'
let g:vim_php_refactoring_default_method_visibility = 'private'
let g:vim_php_refactoring_auto_validate_visibility = 1
let g:vim_php_refactoring_phpdoc = "pdv#DocumentCurrentLine"

I have disabled the default mappings and mapped every command to a prefix with <leader>r:

let g:vim_php_refactoring_use_default_mapping = 0
nnoremap <leader>rlv :call PhpRenameLocalVariable()<CR>
nnoremap <leader>rcv :call PhpRenameClassVariable()<CR>
nnoremap <leader>rrm :call PhpRenameMethod()<CR>
nnoremap <leader>reu :call PhpExtractUse()<CR>
vnoremap <leader>rec :call PhpExtractConst()<CR>
nnoremap <leader>rep :call PhpExtractClassProperty()<CR>
nnoremap <leader>rnp :call PhpCreateProperty()<CR>
nnoremap <leader>rdu :call PhpDetectUnusedUseStatements()<CR>
nnoremap <leader>rsg :call PhpCreateSettersAndGetters()<CR>

phpactor

The last refactoring tool we will talk about (already mentioned in the autocompletion section) is phpactor. It also comes with a vim plugin. This plugin gives you a cursor-context-aware menu to access all the functions. But I like my custom tailored functions more, because they are quicker than always going through a menu. For everything which I don’t need that often, I use the cursor-context-aware menu of phpactor via ALT-m.

In case you skipped the autocompletion part, you need to add the Plug part to your init.vim:

call plug#begin('~/.config/nvim/plugged')
[...]
Plug 'phpactor/phpactor', { 'do': ':call phpactor#Update()', 'for': 'php'}
call plug#end()
phpactor Standard Mappings
" context-aware menu with all functions (ALT-m)
nnoremap <m-m> :call phpactor#ContextMenu()<cr>

nnoremap gd :call phpactor#GotoDefinition()<CR>
nnoremap gr :call phpactor#FindReferences()<CR>

" Extract method from selection
vmap <silent><Leader>em :<C-U>call phpactor#ExtractMethod()<CR>
" extract variable
vnoremap <silent><Leader>ee :<C-U>call phpactor#ExtractExpression(v:true)<CR>
nnoremap <silent><Leader>ee :call phpactor#ExtractExpression(v:false)<CR>
" extract interface
nnoremap <silent><Leader>rei :call phpactor#ClassInflect()<CR>
```
Define Path To Executable
let g:phpactor_executable = '~/.config/nvim/plugged/phpactor/bin/phpactor'
Helper function

For the next three shortcuts you need following helper function:

function! PHPModify(transformer)
    :update
    let l:cmd = "silent !".g:phpactor_executable." class:transform ".expand('%').' --transform='.a:transformer
    execute l:cmd
endfunction
Constructor Magic

This is awesome. Given you have following file:

<?php
class Foo {
    public function __construct(BarInterface $bar) {
    }
}

and hit <leader>rcc from anywhere in the file it will create following:

<?php
class Foo {
    /**
      * @var BarInterface
      */
      private $bar;

    /**
      * @param BarInterface $bar
      */
    public function __construct(BarInterface $bar) {
        $this->bar = $bar;
    }
}

Of course you can always hit the shortcut again, also for existing Constructors.

Optional dependency: my UpdatePhpDocIfExists function

nnoremap <leader>rcc :call PhpConstructorArgumentMagic()<cr>
function! PhpConstructorArgumentMagic()
    " update phpdoc
    if exists("*UpdatePhpDocIfExists")
        normal! gg
        /__construct
        normal! n
        :call UpdatePhpDocIfExists()
        :w
    endif
    :call PHPModify("complete_constructor")
endfunction
Implement missing functions from Interface/Abstract class

When executed it adds all missing function declarations for the current class’ parent/interfaces.

nnoremap <leader>ric :call PHPModify("implement_contracts")<cr>
Add missing assignments

If you refer $this->nonExistentProperty anywhere in your class and the property is not defined in this or any parent class, <leader>raa will add the property declaration.

nnoremap <leader>raa :call PHPModify("add_missing_properties")<cr>
Move Class

Moves the current file to a new path, updates references and namepaces. (default: current file)

nnoremap <leader>rmc :call PHPMoveClass()<cr>
function! PHPMoveClass()
    :w
    let l:oldPath = expand('%')
    let l:newPath = input("New path: ", l:oldPath)
    execute "!".g:phpactor_executable." class:move ".l:oldPath.' '.l:newPath
    execute "bd ".l:oldPath
    execute "e ". l:newPath
endfunction
Move Directory

Moves directory and updates namespaces. (default: current dir)

nnoremap <leader>rmd :call PHPMoveDir()<cr>
function! PHPMoveDir()
    :w
    let l:oldPath = input("old path: ", expand('%:p:h'))
    let l:newPath = input("New path: ", l:oldPath)
    execute "!".g:phpactor_executable." class:move ".l:oldPath.' '.l:newPath
endfunction

Custom functions

Automating PHPDoc blocks with php-doc-modded

Check Automatic phpDoc creation

Easy PHP Namespace Handling

To automatically insert use statements for the class under your cursor use the vim-php-namespace plugin. Nice to know: it also supports sorting all use statements alphabetically and can expand namespaces.

call plug#begin('~/.config/nvim/plugged')
[...]
Plug 'arnaud-lb/vim-php-namespace', {'for': 'php'}
call plug#end()

nnoremap <Leader>u :PHPImportClass<cr>
nnoremap <Leader>e :PHPExpandFQCNAbsolute<cr>
nnoremap <Leader>E :PHPExpandFQCN<cr>

Open the PHP Manual For Word Under Cursor

I cannot remember the order - was it $haystack, $needle? Or vice versa? With the following plugin you can open the PHP Manual for the word under the cursor by pressing <leader>k.

call plug#begin('~/.vim/plugged')
[...]
Plug 'alvan/vim-php-manual', {'for': 'php'}
call plug#end()

let g:php_manual_online_search_shortcut = '<leader>k'

Managing Your Projects With vim-project

If you work in different projects at the same time, you probably need to switch to different frameworks, coding standards etc.

For this vim-project is handy. It gives you a starting screen for all your projects to quickly switch between them. Plus: it executes callbacks whenever the cwd is set to a project’s cwd or you open a file in that cwd.

call plug#begin('~/.config/nvim/plugged')
[...]
Plug 'amiorin/vim-project'
call plug#end()

let g:project_use_nerdtree = 1
let g:project_enable_welcome = 1
set rtp+=~/.vim/bundle/vim-project/
call project#rc("~/code")

" I prefer to have my project's configuration in a separate file
so ~/.vimprojects
nmap <leader><F2> :e ~/.vimprojects<cr>

Example project file:

Project 'aSymfony3Project'
Callback 'aSymfony3Project', 'ApplySf3Settings'

function! ApplySf3Settings(...) abort
    map <c-s> :Silent php-cs-fixer fix %:p --level=symfony<cr>
endfunction

fzf

fzf in combination with fzf.vim provides a lot of useful fuzzy searching functionalities:

  • open files
  • search in all files of your code base for a string and narrow down the results
  • jump to tags (global or current buffer only)
  • switch buffers
  • open MRU (most recent used) files
  • open files with staged & unstaged changes and untracked files (= git status output)

On top of that it provides you with a toggleable preview window and the possibility to move all matches to the quickfix list. Bonus: the LanguageClient-neovim plugin is able to display results in an fzf buffer.

In your .vimrc or ~/.config/nvim/init.vim:

call plug#begin('~/.config/nvim/plugged')
[...]
Plug 'junegunn/fzf', { 'dir': '~/.fzf', 'do': './install --all' }
Plug 'junegunn/fzf.vim'

Plug 'pbogut/fzf-mru.vim'
" only show MRU files from within your cwd
let g:fzf_mru_relative = 1
call plug#end()

nnoremap <leader><Enter> :FZFMru<cr>
" to enable found references displayed in fzf
let g:LanguageClient_selectionUI = 'fzf'

Mappings I use most frequently:

nnoremap <leader>s :Rg<space>
" word under cursor
nnoremap <leader>R :exec "Rg ".expand("<cword>")<cr>
" search for visual selection
vnoremap // "hy:exec "Rg ".escape('<C-R>h', "/\.*$^~[()")<cr>

autocmd! VimEnter * command! -bang -nargs=* Rg
  \ call fzf#vim#grep(
  \   'rg --column --smart-case --line-number --color=always --no-heading --fixed-strings --follow --glob "!.git/*" '.shellescape(<q-args>), 1,
  \   <bang>0 ? fzf#vim#with_preview('up:60%')
  \           : fzf#vim#with_preview('right:50%:hidden', '?'),
  \   <bang>0)

nnoremap <leader>, :Files<cr>
" vertical split
nnoremap <leader>. :call fzf#run({'sink': 'e', 'right': '40%'})<cr>
nnoremap <leader>d :BTags<cr>
" word under cursor
nnoremap <leader>D :BTags <C-R><C-W><cr>
nnoremap <leader>S :Tags<cr>
" hit enter to jump to last buffer
nnoremap <leader><tab> :Buffers<cr>

Conclusion

With some effort we’ve build our own “IDE” for PHP. What’s missing? Do you have some PHP specific vim/neovim plugins that you cannot live without? Then please share in the comments!