Skip to content

CLI Reference

PHPantom is a language server, but it also ships CLI tools for batch analysis, automated fixing, and formatting. These run the same engine that powers the editor, so results are consistent between what you see in your editor and what CI reports.

Modes

Command Purpose
phpantom_lsp Start the LSP server over stdin/stdout (the default)
phpantom_lsp --tcp 9257 Start the LSP server listening on a TCP port
phpantom_lsp analyze Report diagnostics across the project
phpantom_lsp fix Apply automated code fixes across the project
phpantom_lsp format Format PHP files and Blade templates
phpantom_lsp move Move classes or namespaces and update references
phpantom_lsp init Generate a default .phpantom.toml config file

Running with no subcommand starts the language server. Editors launch this automatically.


move

Moves a class or namespace and updates its declarations, imports, and references across the project. Inputs can be fully-qualified names or PSR-4 paths:

phpantom_lsp move 'App\Old\Widget' 'App\Domain\Gadget'
phpantom_lsp move src/Old/Widget.php src/Domain/Gadget.php
phpantom_lsp move 'App\Old' 'App\Domain'
phpantom_lsp move src/Old src/Domain
phpantom_lsp move --dry-run --format json src/Old src/Domain

FROM decides what is being moved, and TO is read the same way: a class moves to a full destination name, so move 'App\Old\Widget' 'App\Domain' renames the class to Domain, and moving it into App\Domain under its own name is written out as 'App\Domain\Widget'.

Path forms require a Composer PSR-4 mapping. A file identifies one class and a directory identifies its namespace prefix. The command refuses occupied class destinations and namespace merges with clashing class names before writing any files.

A destination no PSR-4 mapping covers rewrites the declarations and their references, but no file can follow them, which leaves the autoloader unable to find what moved. That is reported as a warning, so a script can catch it. A destination under a different mapping is an ordinary move: the files follow it to that mapping's directory.

What a move could not reach

The rewriter only reaches what it can resolve as a symbol. A namespace named in a Blade template, a Doctrine or Symfony YAML config, a PHPStan baseline, or a plain path string is invisible to it, and so is a directory spelled out inside app_path('Elastic/Config/ILM/'). Rewriting those is out of scope: nothing can tell whether a string is a path or a label, and the matching key in a deployment secret is out of reach entirely.

So the move reports them instead. Once the plan is built, the project is scanned as it will look afterwards, and every leftover mention of the old name or the old location becomes a warning with the file and line it sits on:

Would move namespace `App\Entity` to `App\Domain\Entity` (71 file(s) changed, 1 path(s) moved).
Warning: config/packages/doctrine.yaml:26: The old path `src/Entity` still appears here. ...
Warning: src/Repository/SeasonRepository.php:65: The old name `App\Entity` still appears here. ...

files_changed can then be read against a stated list of what was left alone, rather than assumed complete. The scan covers every file regardless of extension, skipping vendor/, .git/, and anything .gitignore or [indexing] exclude rules out.

Options

Flag Description
FROM Source class, namespace, PHP file, or PSR-4 directory.
TO Destination name or path, of the same kind as FROM.
--dry-run Validate and report the move without changing the project.
--no-colour Disable coloured output.
--project-root <DIR> Project root directory. Defaults to the current working directory.
--format <FORMAT> Output format: table (default), github, or json.

--format json emits the same {"totals": …, "files": …, "errors": []} shape analyze does, with the move's own counters under a move key, so both commands can be consumed by the same tooling. --format github emits workflow annotations; the default table adds them automatically when GITHUB_ACTIONS is set.

Exit codes

Code Meaning
0 Move applied, or valid dry-run completed
1 Invalid input, conflict, or filesystem error

TCP Transport

By default PHPantom communicates over stdin/stdout, which is what most editors expect. The --tcp flag switches to TCP transport instead, which is useful when you want to attach a debugger to the server process, connect from an IDE plugin that prefers a network socket, or just poke at the JSON-RPC stream with nc or socat.

The server binds to the given address, accepts a single client connection, serves it, and exits when the client disconnects.

phpantom_lsp --tcp 9257                  # listen on 127.0.0.1:9257
phpantom_lsp --tcp 127.0.0.1:9257       # same, explicit host
phpantom_lsp --tcp 0.0.0.0:9257         # listen on all interfaces
phpantom_lsp --tcp 0                    # OS picks a free port

When the server starts it prints the bound address to stderr:

PHPantom LSP listening on tcp://127.0.0.1:9257

This is especially handy with port 0: the OS assigns an available port and the server tells you which one it picked.

Connecting

Any LSP client that supports TCP can connect. For quick manual testing:

# In one terminal, start the server:
phpantom_lsp --tcp 9257

# In another terminal, connect and send JSON-RPC:
nc 127.0.0.1 9257

Options

Flag Description
--tcp <ADDR> Address to listen on. Full HOST:PORT or just PORT (defaults to 127.0.0.1).

analyze

Scans PHP files and reports PHPantom diagnostics in a PHPStan-style table format. The goal is full symbol resolution: every class, member, and function call in your codebase should be resolvable. When that holds, completion and hover work everywhere, and PHPStan gets the type information it needs at every level.

Use it to find and fix the spots where the editor can't resolve a symbol, so you can achieve and maintain full type coverage across the project.

What it checks

It doesn't try to find every possible bug. Its main focus is symbol resolution: every class, member, and function call should point to something real. It also catches basic correctness issues like wrong argument counts and missing interface implementations. When a codebase passes cleanly, completions work everywhere for every developer on the team.

That makes it useful in a few situations:

  • Teams that just want completions to work in their editor. If your goal is "every -> and :: resolves to something" rather than "catch every possible runtime error," PHPantom's analysis covers exactly that.
  • As a companion to PHPStan at a moderate level. A team running PHPStan at level 4 (catching dead code and type mismatches) can add PHPantom's analysis to enforce that every class, member, and function is resolvable across the full codebase. PHPStan catches logic errors, PHPantom catches structural gaps. Together they cover a useful quality surface without the effort of configuring PHPStan at max level.
  • As a quick sanity check. Point it at a Composer project and it reports what it finds. No baselines, no ignore files, no level to choose. The only configuration worth knowing about is unresolved-member-access: enable it in .phpantom.toml to also flag member access on variables that are mixed, or whose type could not be worked out (off by default because it is noisy on untyped codebases).

[!NOTE] There are still occasional false positives, though they are getting fewer with each release. If you hit one, please report it.

Usage

phpantom_lsp analyze                             # scan entire project
phpantom_lsp analyze src/                        # scan a subdirectory
phpantom_lsp analyze src/Foo.php                 # scan a single file
phpantom_lsp analyze app/ lib/Helper.php         # scan several paths at once
phpantom_lsp analyze --severity warning          # errors and warnings only
phpantom_lsp analyze --severity error            # errors only
phpantom_lsp analyze --project-root /path/to/app # explicit project root
phpantom_lsp analyze --no-colour                 # plain text output
phpantom_lsp analyze --debug -vv                 # trace file-by-file progress

Options

Flag Description
[PATH]... Files or directories to analyze. Repeatable; the results are the union of every path given. Defaults to the entire project.
--severity <LEVEL> Minimum severity: all (default), warning, or error.
--project-root <DIR> Project root directory. Defaults to the current working directory.
--no-colour Disable ANSI colour output.
--format <FORMAT> Output format: table (default), github, or json.
--debug Print each file path as it is analyzed and disable the progress bar. Also reports files that take unusually long.
-v, -vv, -vvv Increase verbosity. With --debug: -v adds per-file durations, -vv adds worker ids and parse-phase tracing, -vvv adds memory usage. -vv and above imply --debug. -v alone prints a timing summary for the parse, class-population, and diagnostic phases.

Exit codes

Code Meaning
0 No diagnostics found
1 Diagnostics were found
2 A PATH argument does not exist

Example output

 ------ -------------------------------------------
   Line   src/Service/UserService.php
 ------ -------------------------------------------
   15     Unknown class 'App\Models\LegacyUser'.
          🪪  unknown_class
   42     Call to undefined method Post::archive().
          🪪  unknown_member
 ------ -------------------------------------------

Reported diagnostics

The analyze command reports the same diagnostics you see in your editor. Each has a rule identifier shown below the message.

Identifier Severity Description
syntax_error Error PHP parse errors
unknown_class Warning Class, interface, trait, or enum not resolvable
unknown_member Warning Property or method not found on the resolved class
unknown_function Error Function call not resolvable
argument_count Error Wrong number of arguments to a function or method
implementation_error Error Missing required interface or abstract methods
scalar_member_access Error Member access on a scalar type (int, string, etc.)
invalid_member_access Error private or protected member reached from outside
unused_import Hint use statement with no references in the file
unreachable_code Hint Statements after a return, throw, exit, or break
deprecated Hint Reference to a @deprecated symbol

fix

Applies code fixes across the project. Specify which rules to run, or omit --rule to run all preferred native fixers.

This is useful for cleaning up an entire codebase in one pass. For example, a project with hundreds of unused use statements can be cleaned up in seconds rather than file by file.

phpantom_lsp fix                                  # apply all preferred fixers
phpantom_lsp fix --rule unused_import             # only remove unused imports
phpantom_lsp fix --rule unused_import --rule deprecated  # multiple rules
phpantom_lsp fix --dry-run                        # preview without writing
phpantom_lsp fix src/                             # restrict to a subdirectory
phpantom_lsp fix src/Foo.php                      # fix a single file
phpantom_lsp fix --project-root /path/to/app      # explicit project root

Options

Flag Description
[PATH] File or directory to fix. Defaults to the entire project.
--rule <RULE> Rule to apply (repeatable). Omit to run all preferred native rules.
--dry-run Report what would change without writing files.
--with-phpstan Enable PHPStan-based fixers (future feature).
--project-root <DIR> Project root directory. Defaults to the current working directory.
--no-colour Disable ANSI colour output.

Exit codes

Code Meaning
0 Fixes applied successfully (or nothing to fix)
1 Error (bad arguments, write failure, etc.)
2 Dry-run found fixable issues (nothing written)

Available rules

Rules correspond to diagnostic identifiers.

Rule Description
unused_import Remove unused use statements

Example output

 ------ -------------------------------------------
   Line   src/Service/UserService.php
 ------ -------------------------------------------
    5     Unused import 'App\Models\LegacyUser'
          🔧  unused_import
    6     Unused import 'App\Support\OldHelper'
          🔧  unused_import
 ------ -------------------------------------------

 [FIXED] Applied 2 fixes across 1 file

Dry-run example

phpantom_lsp fix --dry-run --project-root /path/to/app
 ------ -------------------------------------------
   Line   src/Service/UserService.php
 ------ -------------------------------------------
    5     Unused import 'App\Models\LegacyUser'
          🔧  unused_import
 ------ -------------------------------------------

 [DRY RUN] 1 fix in 1 file (not applied)

Idempotency

Running fix twice produces the same result as running it once. If all issues are already resolved, the command exits with code 0 and writes nothing.


format

Formats every PHP file and Blade template in the project with the same formatter the editor runs on save, or, with --check, reports the files that are not formatted and exits non-zero without writing anything. That is the role blade-formatter -c, phpcs, and php-cs-fixer --dry-run play in a pipeline, so a CI job can require that a pull request ran the formatter.

phpantom_lsp format                               # format the whole project
phpantom_lsp format --check                       # report unformatted files, write nothing
phpantom_lsp format resources/views               # restrict to a subdirectory
phpantom_lsp format app/Foo.php                   # format a single file
phpantom_lsp format --check --format github       # annotate a pull request diff
phpantom_lsp format --project-root /path/to/app   # explicit project root

Each file goes through the strategy PHPantom resolves for the project, so a run honours a Laravel Pint, php-cs-fixer, or PHP_CodeSniffer the project depends on, and uses the built-in formatter otherwise. Blade templates resolve separately: Pint when the project formats Blade with it, the built-in reindenter otherwise. See [formatting] for how that is decided and how to override it. The run opens with a line on stderr naming what it resolved, so a CI log records which formatter enforced the result.

Templates whose indentation is output rather than layout (Envoy task files, Markdown mail templates, Laravel Boost guidelines) are left alone, and --check never fails a project for having one. Formatting turned off in .phpantom.toml exits 0 with a note rather than reporting every file as formatted.

Options

Flag Description
[PATH]... Files or directories to format. Defaults to the entire project.
--check List the files that are not formatted and write nothing.
--indent-size <N> Spaces per indentation level for Blade templates (default 4).
--use-tabs Indent Blade templates with tabs instead of spaces.
--project-root <DIR> Project root directory. Defaults to the current working directory.
--no-colour Disable ANSI colour output.
--format <FORMAT> table (default), github, or json.

--indent-size and --use-tabs reach the built-in Blade reindenter only, which takes indentation from the editor over LSP and has no other source for it on the command line. PHP files are formatted to the project's own rules either way.

Exit codes

Code Meaning
0 Every file is formatted, or every file was written
1 A file could not be read, formatted, or written
2 --check found files that are not formatted

Example output

phpantom_lsp format --check
 resources/views/home.blade.php
 src/Service/UserService.php

 [CHECK] 2 files would be reformatted
phpantom_lsp format
 resources/views/home.blade.php
 src/Service/UserService.php

 [FORMATTED] Reformatted 2 files

Idempotency

Running format twice produces the same result as running it once, and --check passes immediately afterwards. That is what makes the pair usable as a CI gate: the job runs --check, and a contributor clears it by running the command without it.


init

Creates a .phpantom.toml in the current directory with a JSON schema directive and a link to the configuration reference. The file is intentionally minimal — add only the settings you want to override. Editors with TOML schema support (Zed, VS Code + Even Better TOML, Neovim) provide autocomplete and hover documentation for all available options via the schema. Safe to run if the file already exists (it will not overwrite).

phpantom_lsp init
phpantom_lsp init --global   # user-wide defaults, inherited by every project

--global writes to the platform config directory (~/.config/phpantom_lsp/.phpantom.toml on Linux and macOS) instead, creating it if needed. Every project reads that file first and merges its own .phpantom.toml over it key by key, so put the settings you want everywhere in the global file and keep project configs to the differences.

See the Configuration Reference for details on available settings.


CI Integration

PHPantom works well as a lightweight CI gate. It's a single static binary with no runtime dependencies (no PHP, no Composer, no Node). Drop it into a pipeline and point it at your project root.

Why use it in CI?

Editor diagnostics only help the developer who has the editor open. CI analysis protects the whole team: no PR can merge if it introduces an unresolvable symbol, regardless of which editor each developer uses. Over time this keeps the codebase fully navigable, so completions, hover, and go-to-definition work everywhere for everyone.

It also complements PHPStan rather than replacing it. PHPStan is better at catching logical errors, type mismatches, and dead code. PHPantom is better at catching structural gaps: unknown classes, unresolvable members, missing implementations. Running both gives you broad coverage without needing PHPStan at max level to get full symbol resolution.

GitHub Actions example

name: Type Coverage
on: [push, pull_request]

jobs:
  phpantom:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # Use --no-dev to catch production code that depends on dev-only
      # packages (e.g. calling PHPUnit classes from application code).
      - name: Install Composer dependencies
        run: composer install --no-interaction --prefer-dist --no-dev

      - name: Install PHPantom
        run: |
          curl -sL https://github.com/PHPantom-dev/phpantom_lsp/releases/download/0.6.0/phpantom_lsp-x86_64-unknown-linux-gnu.tar.gz | tar xz
          chmod +x phpantom_lsp

      - name: Check type coverage
        run: ./phpantom_lsp analyze --severity warning --no-colour src/

The analyze step fails the build if any class, member, or function cannot be resolved (including unused imports). The output is clean and readable in the CI log.

Common patterns

Diagnostics gate. Fail the build when PHPantom finds unresolvable symbols:

phpantom_lsp analyze --severity warning --project-root . --no-colour

Enforce clean imports. Fail the build when unused imports exist:

phpantom_lsp fix --dry-run --rule unused_import --project-root . --no-colour

Formatting gate. Fail the build when a file was committed unformatted:

phpantom_lsp format --check --project-root . --no-colour

Pre-commit hook. Clean up imports before every commit:

phpantom_lsp fix --rule unused_import --project-root .

Combine analyze and fix. Run fixes first, then check what remains:

phpantom_lsp fix --project-root .
phpantom_lsp analyze --project-root .