Neovim & PHP: A Step-By-Step Guide To The Perfect Setup

I am developing with (neo)vim in PHP now for about 6 years. Over that period I have fine tuned my vim setup and tried probably every PHP plugin available. In the following post I’ve collected everything that makes your life as a PHP developer in vim easier.

Switch from vim to neovim

I’ve switched from vim to neovim (thanks to my former colleague). As there have been many blog posts written about how and why to make the switch I won’t go into detail here why you should do it as well.

Clean start

With neovim

If you haven’t made the switch to neovim just give it a shot. Switching before you follow this article 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 all your PHP plugins only when you open a PHP file :)

Dependencies

  • git
  • curl

We can setup vim to install vim-plug automatically if missing:

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:

PHP Autocompletion With Neovim

Dependencies

  • in global composer json “minimum-stability”:“dev”
  • Padawan: composer global require mkusher/padawan

I am using deoplete for autocompletion and padawan.php for getting the completion candidates. deoplete-padawan is nice because it automatically starts the padawan server if it’s not running yet.

For now there are some drawbacks like multiline completion is not working, but it’s the quickest autocompletion I’ve found so far.

Other autocompletions I’ve tried:

  • phpcomplete: good but too slow.

  • php-language-server: good but too slow. It’s also showing private properties in autocompletion. I still use it but not for autocompletion - see below

  • phpactor: a little bit slow. It doesn’t complete local variables or class names. It needs composer autoloading -> not suitable for legacy projects. I still use it though for omnicompletion in projects that use composer via au filetype php setlocal omnifunc=phpactor#Complete

  • phpcd.vim: could not make it to work

call plug#begin('~/.config/nvim/plugged')
Plug 'Shougo/deoplete.nvim', { 'do': ':UpdateRemotePlugins' }
Plug 'padawan-php/deoplete-padawan', { 'for': 'php' }
call plug#end()

let g:deoplete#sources#padawan#add_parentheses = 1
" needed for echodoc to work if add_parentheses is 1
let g:deoplete#skip_chars = ['$']

let g:deoplete#sources = {}
let g:deoplete#sources.php = ['padawan', 'ultisnips', 'tags', 'buffer']

" cycle through menu items 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 it to only create ctags for PHP. Below are the steps from Tim Pope’s original article including my modification.

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

Setting up the global hook directory

mkdir -p ~/.git_template/hooks
git config --global init.templatedir '~/.git_template'

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"

Create the post commit hook in file ~/.git_template/hooks/post-commit with following contents:

#!/bin/sh
.git/hooks/ctags >/dev/null 2>&1 &

After that create the other hooks:

cp ~/.git_template/hooks/post-commit ~/.git_template/hooks/post-merge
cp ~/.git_template/hooks/post-commit ~/.git_template/hooks/post-checkout

Add the last hook for rebasing in ~/.git_template/hooks/post-rewrite:

#!/bin/sh
case "$1" in
  rebase) exec .git/hooks/post-merge ;;
esac

Last step: make everything executable:

chmod +x ~/.git_template/hooks/{ctags,post-commit,post-merge,post-checkout,post-rewrite}

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 file was created:
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('~/.vim/plugged')
[...]
Plug 'StanAngeloff/php.vim'
call plug#end()

Just run :PlugInstall afterwards.

Language Server Protocol

If you haven’t yet heard of the Language Server Protocol (LSP): it’s basically an approach to provide IDE-functionality IDE-independently. A standalone server application indexes your projects and provides functionality like go to definition, find references, autocompletion, and a few more.

Felix Becker maintains a PHP-adoption of the LSP: php-language-server

Dependencies

php-language-server:

git clone https://github.com/felixfbecker/php-language-server.git ~/.tooling/php-language-server
cd ~/.tooling/php-language-server
composer install
composer parse-stubs

Junfeng Li has created a neovim LanguageClient plugin that provides a neovim client for the LSP API and also takes care of starting the language server for you:

call plug#begin('~/.vim/plugged')
[...]
Plug 'autozimu/LanguageClient-neovim', { 'do': ':UpdateRemotePlugins' }
call plug#end()


" only start lsp when opening php files
au filetype php LanguageClientStart

" use LSP completion on ctrl-x ctrl-o as fallback for padawan in legacy projects
au filetype php set omnifunc=LanguageClient#complete

" no need for diagnostics, we're going to use neomake for that
let g:LanguageClient_diagnosticsEnable  = 0
let g:LanguageClient_signColumnAlwaysOn = 0

" I only use these 3 mappings
nnoremap <silent> gd :call LanguageClient_textDocument_definition()<CR>
nnoremap <silent> gr :call LanguageClient_textDocument_references()<CR>
nnoremap K :call LanguageClient_textDocument_hover()<cr>

let g:LanguageClient_serverCommands = {
	    \ 'php': ['php', '~/.tooling/php-language-server/bin/php-language-server.php']
	    \ }

Write Less Messy Code with NeoMake

neomake 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). Neomake will display warning and error signs next to each line.

Dependencies

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

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

call plug#begin('~/.vim/plugged')
[...]
Plug 'neomake/neomake'
call plug#end()

autocmd BufWritePost * Neomake
let g:neomake_error_sign   = {'text': '✖', 'texthl': 'NeomakeErrorSign'}
let g:neomake_warning_sign = {'text': '∆', 'texthl': 'NeomakeWarningSign'}
let g:neomake_message_sign = {'text': '➤', 'texthl': 'NeomakeMessageSign'}
let g:neomake_info_sign    = {'text': 'ℹ', 'texthl': 'NeomakeInfoSign'}

After that :PlugInstall.

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.

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.

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

call plug#begin('~/.vim/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.

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('~/.vim/plugged')
[...]
Plug 'adoy/vim-php-refactoring-toolbox'
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>
vnoremap <leader>rem :call PhpExtractMethod()<CR>
nnoremap <leader>rnp :call PhpCreateProperty()<CR>
nnoremap <leader>rdu :call PhpDetectUnusedUseStatements()<CR>
vnoremap <leader>r== :call PhpAlignAssigns()<CR>
nnoremap <leader>rsg :call PhpCreateSettersAndGetters()<CR>

PHP Refactoring Browser

The PHP Refactoring Browser has some functionalities already covered by the mentioned refactoring toolbox but these work way better with this plugin.

Let’s add it as well:

Dependencies

call plug#begin('~/.vim/plugged')
[...]
Plug 'vim-php/vim-php-refactoring'
call plug#end()

let g:php_refactor_command='php /path/to/downloaded/refactor.phar'

" here we're overriding the mappings from above
nnoremap <leader>rep :call PhpRefactorLocalVariableToInstanceVariable()<cr>
vnoremap <leader>rem :call PhpRefactorExtractMethodDirectly()<CR>
nnoremap <leader>rrv :call PhpRefactorRenameLocalVariable()<cr>

phpactor

The last refactoring tool we’re going to add is phpactor. It also comes with a vim plugin but I like my custom tailored functions more.

Dependencies

  • phpactor :)
call plug#begin('~/.vim/plugged')
[...]
Plug 'phpactor/phpactor', { 'do': ':call phpactor#Update()' }
let g:phpactor_executable = '~/.config/nvim/plugged/phpactor/bin/phpactor'
call plug#end()

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

Helper function

For the next three shortcuts you need following helper function:

function! PHPModify(transformer)
    :w
    normal! ggdG
    execute "read !".g:phpactor_executable." class:transform ".expand('%').' --transform='.a:transformer
    normal! ggdd
    :w
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: 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

Self-explanatory.

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>

Extract Interface

<leader>rei takes all public methods from the current file (except the constructor) and creates an interface from it.

nnoremap <leader>rei :call PHPExtractInterface()<cr>
function! PHPExtractInterface()
    :w
    let l:interfaceFile = substitute(expand('%'), '.php', 'Interface.php', '')
    execute "!".g:phpactor_executable."  class:inflect ".expand('%').' '.l:interfaceFile.' interface'
    execute "e ". l:interfaceFile
endfunction

Custom functions

Extract visual selection to variable

Check Extract visual selection to variable.

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('~/.vim/plugged')
[...]
Plug 'arnaud-lb/vim-php-namespace'
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 simply pressing <leader>k.

call plug#begin('~/.vim/plugged')
[...]
Plug 'alvan/vim-php-manual'
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 chances are that you 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('~/.vim/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
    let g:phpqa_codesniffer_args = "--standard=Symfony"
    map <c-s> :Silent php-cs-fixer fix %:p --level=symfony<cr>
    setlocal omnifunc=phpactor#Complete
endfunction

fzf

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

  • open files
  • search in all files of your codebase 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('~/.vim/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>
" grep with 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>
" just 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? Definitely some general plugins like NERDTree,fugitive, etc. I will write a separate article for these not-PHP-specific plugins.

Tags// , , ,
comments powered by Disqus