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.

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 && cd neovim
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 :)


  • 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
    autocmd VimEnter * PlugInstall

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


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

I am using deoplete for auto-completion and padawan.php for getting the completion candidates. deoplete-padawan automatically starts the padawan server if it’s not running yet as soon as you start typing.

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

Other php auto-completion providers 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.

  • phpcd.vim: could not make it to work/some problems with echodoc

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 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:

set -e
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('~/.vim/plugged')
Plug 'StanAngeloff/php.vim', {'for': 'php'}
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



git clone ~/.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' }
Plug 'roxma/LanguageServer-php-neovim',  {'do': 'composer install && composer run-script parse-stubs', 'for': 'php'}
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>

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.


  • 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


  • 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', {'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>
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:


call plug#begin('~/.vim/plugged')
Plug 'vim-php/vim-php-refactoring', {'for': 'php'}
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>


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.


  • phpactor :)
call plug#begin('~/.vim/plugged')
Plug 'phpactor/phpactor', { 'do': ':call phpactor#Update()', 'for': 'php'}
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()
    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

Move Directory

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

nnoremap <leader>rmd :call PHPMoveDir()<cr>
function! PHPMoveDir()
    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

Helper function

For the next three shortcuts you need following helper function:

function! PHPModify(transformer)
    normal! ggdG
    execute "read !".g:phpactor_executable." class:transform ".expand('%').' --transform='.a:transformer
    normal! ggdd

Constructor Magic

This is awesome. Given you have following file:

class Foo {
    public function __construct(BarInterface $bar) {

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

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
        normal! n
        :call UpdatePhpDocIfExists()
    :call PHPModify("complete_constructor")

Implement missing functions from Interface/Abstract class


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()
    let l:interfaceFile = substitute(expand('%'), '.php', 'Interface.php', '')
    execute "!".g:phpactor_executable."  class:inflect ".expand('%').' '.l:interfaceFile.' interface'
    execute "e ". l:interfaceFile

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', {'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('~/.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
    map <c-s> :Silent php-cs-fixer fix %:p --level=symfony<cr>


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('~/.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>
" 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>


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!

Tags// , , ,
comments powered by Disqus