PHPInspect is a minor mode that provides code intelligence for PHP in Emacs. At
its core is a PHP parser implemented in Emacs Lisp. PHPInspect comes with
backends for completion-at-point
, company-mode
and eldoc
. A backend for
xref
(which provides go-to-definition functionality) is planned to be
implemented at a later date. The main documentation of the mode is in the
docstring of the mode itself (C-h f phpinspect-mode RET
to view, or read it in
the source code of phpinspect.el).
phpinspect.el is available as a package in GNU ELPA. Install it via package.el or an emacs package manager of your choice.
When installing from git, make sure to checkout the master branch instead of the devel branch if you prioritize stability. The devel branch will be home to the latest features, but also the latest bugs 🐛 .
git clone https://github.com/hugot/phpinspect.el ~/projects/phpinspect.el
cd ~/projects/phpinspect.el
make
(add-to-list 'load-path "~/projects/phpinspect.el")
(require 'phpinspect)
By default, phpinspect will recognize composer projects and read their
composer.json files for autoload information which is used to find files in
which the types/classes/functions you use in your code are defined. It is also
possible to add an "include directory" of files that should always be read and
indexed for a certain project. To do this, open a file in a project and run M-x phpinspect-project-add-include-dir
. You can also edit the list of include
directories via M-x customize-goup RET phpinspect RET
.
If you already have a completion UI setup that is able to use
completion-at-point-functions
as completion source, you can basically just
enable phpinspect-mode and you'll be good to go. An example of a basic mode hook
configuration to get the most out of phpinspect is the following:
(defun my-php-personal-hook ()
;; Shortcut to add use statements for classes you use.
(define-key php-mode-map (kbd \"C-c u\") 'phpinspect-fix-imports)
;; Shortcuts to quickly search/open files of PHP classes.
;; You can make these local to php-mode, but making them global
;; like this makes them work in other modes/filetypes as well, which
;; can be handy when jumping between templates, config files and PHP code.
(global-set-key (kbd \"C-c a\") 'phpinspect-find-class-file)
(global-set-key (kbd \"C-c c\") 'phpinspect-find-own-class-file)
;; Enable phpinspect-mode
(phpinspect-mode))
(add-hook 'php-mode-hook #'my-php-personal-hook)
;;;###autoload
(defun my-php-personal-hook ()
;; It is important to enable `company-mode' before setting
;; the variables below.
(company-mode)
(setq-local company-minimum-prefix-length 0)
(setq-local company-tooltip-align-annotations t)
(setq-local company-idle-delay 0.1)
(setq-local company-backends '(phpinspect-company-backend))
;; Shortcut to add use statements for classes you use.
(define-key php-mode-map (kbd "C-c u") 'phpinspect-fix-imports)
;; Shortcuts to quickly search/open files of PHP classes.
(global-set-key (kbd "C-c a") 'phpinspect-find-class-file)
(global-set-key (kbd "C-c c") 'phpinspect-find-own-class-file)
(phpinspect-mode))
(add-hook 'php-mode-hook #'my-php-personal-hook)
It is highly recommended to byte- or native compile phpinspect. Aside from the normal performance boost that this brings to most packages, it can reduce phpinspect's parsing time by up to 90%. It especially makes a difference when incrementally parsing edited buffers. For example:
Incremental parse (warmup):
Elapsed time: 0.168390 (0.019751 in 1 GC’s)
Incremental parse:
Elapsed time: 0.143811 (0.000000 in 0 GC’s)
Incremental parse (no edits):
Elapsed time: 0.000284 (0.000000 in 0 GC’s)
Incremental parse repeat (no edits):
Elapsed time: 0.000241 (0.000000 in 0 GC’s)
Incremental parse after buffer edit:
Elapsed time: 0.012449 (0.000000 in 0 GC’s)
Incremental parse after 2 more edits:
Elapsed time: 0.015839 (0.000000 in 0 GC’s)
Bare (no token reuse) parse (warmup):
Elapsed time: 0.048996 (0.000000 in 0 GC’s)
Bare (no token reuse) parse:
Elapsed time: 0.052495 (0.000000 in 0 GC’s)
Incremental parse (warmup):
Elapsed time: 0.023432 (0.000000 in 0 GC’s)
Incremental parse:
Elapsed time: 0.018350 (0.000000 in 0 GC’s)
Incremental parse (no edits):
Elapsed time: 0.000076 (0.000000 in 0 GC’s)
Incremental parse repeat (no edits):
Elapsed time: 0.000058 (0.000000 in 0 GC’s)
Incremental parse after buffer edit:
Elapsed time: 0.001212 (0.000000 in 0 GC’s)
Incremental parse after 2 more edits:
Elapsed time: 0.001381 (0.000000 in 0 GC’s)
Bare (no token reuse) parse (warmup):
Elapsed time: 0.013874 (0.000000 in 0 GC’s)
Bare (no token reuse) parse:
Elapsed time: 0.013878 (0.000000 in 0 GC’s)
The codebase of phpinspect is relatively large. As a new contributor, you'll probably feel a bit lost in the many files, functions and datatypes of the package. Let me tell you a bit about phpinspect's architecture.
There are roughly four main domains under which code in phpinspect can fall: parsing, indexation, interpetation and user-facing functions. Below is a short explanation of each domain.
The parser provides the underpinnings for almost all of phpinspect's
functionalities. It is implemented with specialized macros and supports two main
parsing modes: bare and incremental. The parser code is located in
phpinspect-parser.el
.
The parser produces a parse result in the form of a basic list, where each parsed token is a list itself. The car of each list contains a keyword, describing its meaning in the parsed code. Below is a lisp form and its resulting insertion to demonstrate the structure of a syntax tree produced by the bare parser.
(insert (pp-to-string (phpinspect-parse-string "function foo(Bar $bar) { return $bar->baz; }")))
(:root
(:function
(:declaration
(:word "function")
(:word "foo")
(:list
(:word "Bar")
(:variable "bar")))
(:block
(:word "return")
(:variable "bar")
(:object-attrib
(:word "baz"))
(:terminator ";"))))
Bare parsing is fast, because it makes use of simple lisp datastructures and
keeps the parser logic simple. A downside of this parsing mode is that the
result lacks any extra metadata about the tokens, like what their start- and
endpoints are in a buffer. It also makes it hard to partially update the tree
after a buffer has been edited. As both are essential for a program that needs
to work efficiently in live-edited buffers, the incremental parser was
implemented. When parsing incrementally, the parser still produces the same
structure of nested lists as a result. But it also stores metadata about each
parsed token in an n-ary tree of phpinspect-meta
objects (see
phpinspect-meta.el
). The incremental parser is able to partially update an
existing tree after code has been edited, making it efficient for live buffers.
The incremental parsing is efficient for use with live edited buffers. Due to its higher complexity it is not as fast at parsing entire files as the bare parser. For this reason, both the bare and the incremental parser are used for what they are best at.
Within phpinspect, indexation is referred to as the process of extracting information from parsed code and project configuration files.
The code in
phpinspect-index.el
consumes bare syntax trees and extracts information about
PHP functions and types from them. Code in phpinspect-buffer.el
implements the
same functionality, but for live buffers based on the data produced by the
incremental parser.
Code in phpinspect-autoload.el
implements logic for psr-4
, psr-0
and
files
autoload strategies. It is able to read a composer.json file and
generate autoload information based on which a project's types and functions can
be found.
Code in phpinspect-cache.el
and phpinspect-class.el
allows phpinspect to
store the result of code indexations in memory, so that they can be accessed
instantly when a user requests information about their code.
Indexation takes time. Because emacs is single-threaded, you might think that
indexing a project would lock up your emacs for a while. Do not despair!
phpinspect-worker.el
contains code for a worker with a job queue that uses
collaborative threads to do work when emacs is idle. When you start interacting
with emacs, it will back off and let you do your thing! phpinspect-pipeline.el
contains code for something similar to generators in PHP, combined with
collaborative threading. Pipelines are used for more intense processes that
should be completed with a bit more of a hurry. Emacs should remain responsive
while a pipeline is running, but there may be a slightly noticeable sluggishness
while they run. At the moment of writing, pipelines are mainly used to refresh a
projects autoloader.
Having parsing, indexation and caching infrastructure is all well and good, but
how to we determine what information is actually useful to show to a user? This
is where code interpretation plays a big role. The main goal of interpretation
in phpinspect is the determination of the PHP type to display information
about. Determining this type is referred to as "resolving" in phpinspect's
codebase. Resolving a type is built around the concept of a "resolvecontext". A
resolvecontext is an object that contains information about a location in a
buffer and its surroundings. Based on the informatoin in the resolvecontext, the
type that a statement is expected to evaluate to can be determined. The code for
resolving types is mostly contained in phpinspect-type.el
,
phpinspect-resolve.el
and phpinspect-resolvecontext.el
. The main entrypoints
are the functions phpinspect-get-resolvecontext
and
phpinspect-resolve-type-from-context
.
User-facing functions in phpinspect are mostly integrations with existing
infrastructure within emacs. For completion there is
completion-at-point-functions
, for tooltips there is eldoc
and for
go-to-definition there is xref
. Aside from these integrations, phpinspect aims
to provide functionalities for code formatting.
Completion is implemented in phpinspect-completion.el
as a strategy pattern. A
completion strategy can be added by implementing the methods
phpinspect-comp-strategy-supports
and
phpinspect-comp-strategy-execute
. Completion strategies are high level
abstractions that build on top of the type resolving code and the code in
phpinspect-suggest.el
.
Eldoc support is implemented in a strategy pattern similar to that of
completion. An eldoc strategy can be added by implementing the methods
phpinspect-eld-strategy-supports
and phpinspect-eld-strategy-execute
.
phpinspect-fix-imports
adds, removes and sorts use statements. At the time of
writing, this is the only code formatting functionality that phpinspect
provides. See phpinspect-imports.el
for the implementation.
A thing that would be possible to implement on top of phpinspect's
infrastructure, but not much time has been spent on yet, is an integration with
xref
. Xref integration would enable functionalities like go-to-definition.
make
Tests are implemented using ert
. You can run them in batch mode with the following
command:
make test
# or:
emacs -L ./ -batch -l ert -l ./test/phpinspect-test.el -f ert-run-tests-batch-and-exit