Framework-agnostic PHP toolkit for OpenCode v2. Hard-to-miss defaults for PHP 8.1+ developers: review, static analysis, tests, security, modernize, composer, architecture, debug.
Laravel/Symfony specifics live in sibling repos (opencode-laravel-toolkit, opencode-symfony-toolkit). This package never assumes a framework — it detects and defers.
Built on the newest v2 plugin framework: Plugin.define({ id, setup(ctx) }) + ctx.skill/command/tool.transform + ctx.session.hook. See https://opencode.ai/v2/docs/build/plugins.
Published package (after npm publish):
On install, the package copies the four
agents/php-*.mdfiles into~/.config/opencode/agents/as a convenience (see below). Existing files are never overwritten — a customized agent keeps its content across installs and upgrades; delete the file to get the bundled version back. SetPHP_TOOLKIT_SKIP_AGENT_COPY=1before installing to skip that copy; the rest of the plugin is unaffected.
From git (no npm publish needed — dist/ is committed, so clones work
as-is; always run npm run build before committing):
# Latest on main
opencode plugin add github:nexusnv/opencode-php-toolkit
# Pinned (recommended for teams)
opencode plugin add github:nexusnv/opencode-php-toolkit#v0.1.1Or per-project in opencode.jsonc (same spec forms, plus branch/commit):
{
"plugins": [
"github:nexusnv/opencode-php-toolkit#v0.1.1",
"github:nexusnv/opencode-php-toolkit#main",
"git+ssh://git@github.com/nexusnv/opencode-php-toolkit.git#6eb66d4"
]
}Note: some environments disable git dependency fetching (
EALLOWGIT/git dep preparation failed). That is a local npm policy, not a repo problem — on such machines use the local path form below or the npm package instead.
Local dev:
{
"plugins": ["file:///home/leonidas/dev/nexusnv/opencode-php-toolkit"]
}Or CLI:
opencode plugin add file:///home/leonidas/dev/nexusnv/opencode-php-toolkit- Agents (
agents/php-*.md,mode: subagent):php-reviewer,php-debugger,php-modernizer,php-tester. On install, the package'spostinstallscript copies these four files into~/.config/opencode/agents/as a convenience, never overwriting files that already exist (v2AgentEditorhas noadd; transforms can't create agents — files are the source of truth). SetPHP_TOOLKIT_SKIP_AGENT_COPY=1in the environment to opt out. The script writes nothing besides those four files (the OpenCodecommands/directory is not touched). To undo after install, deletephp-reviewer.md,php-debugger.md,php-modernizer.mdandphp-tester.mdfrom~/.config/opencode/agents/. - Skills (8, autoinvoke):
php-standards,php-static-analysis,php-testing,php-security,php-modernize,php-composer,php-architecture,php-debug. - Commands (7):
/php-review,/php-analyze,/php-fix-style,/php-test,/php-audit,/php-upgrade,/php-scaffold. - Tools (
php_*namespace, read-only unless noted):php_project_detect— composer intel incl. analyzer/fixer config files andcomposer config.platform.php; no execphp_lint—php -l+composer validatephp_composer_audit— known-vulnerable dependencies (network)php_composer_outdated— direct deps with a newer version (network; the repositories configured in the project'scomposer.jsondetermine which hosts Composer contacts)php_static_analysis— PHPStan (or Psalm when absent), JSON output; analysis errors count as findings; executes project-configured code (analyzer binaries run bootstrap files, autoload and Psalm plugins with this session's privileges)php_test— runsvendor/bin/pestorvendor/bin/phpunitagainst JUnit output; executes project code (tests only, 300s timeout, never a bare PATH binary)php_metrics— local usage metrics;reset: trueclears this project's metrics,export: truedumps the snapshot as raw JSON (absolute project path omitted)
- Hooks:
prompttags PHP context,contextadds typed-PHP system hint (only when the project has acomposer.jsonor the request's user messages mention PHP — no tokens spent on unrelated sessions),tool.execute.beforewarns on destructivecomposer update/rector processwithout--dry-run/php-cs-fixer fixwithout--dry-run.
php_metrics shows what the toolkit actually did in the project you are
working in:
PHP toolkit metrics (local-only, never leaves this machine)
Project: my-app
Tracking since: 2026-10-21 09:00Z
PHP-mentioning prompts: 12
Tools
php_lint 3 runs 3 ok / 0 error 2 findings last 2026-10-21 09:12Z
php_project_detect 5 runs 5 ok / 0 error 0 findings last 2026-10-21 09:14Z
Commands
php-review 2 invocations last 2026-10-21 09:15Z
Impact: 2 issues surfaced by the toolkit in this project.
- Runs / ok / error — how often a tool ran, and whether it completed the
work it was asked to do. A tool that could not complete it (missing
php, a timeout, a file it could not read) is an error. - Findings — issues the toolkit surfaced in your code: parse errors found
by
php_lint, advisories reported byphp_composer_audit, a malformed or invalidcomposer.json. A lint that finds a broken file is a successful lint with a finding, not an error. - Per project. Each project root gets its own document, so the snapshot
describes the codebase you are in. A tool pointed at a directory outside the
active project is counted against that directory instead, so one project's
numbers never absorb another's. Pass
allProjects: truetophp_metricsfor the machine-wide breakdown. That view is eventually consistent: a session running elsewhere may still have unsaved events.php_metricsalso acceptsreset: trueto clear this project's metrics (immediate, no undo — the document is replaced with an empty one, which also prevents machine-global legacy history from being re-adopted here) andexport: trueto return the snapshot as raw JSON instead of the rendered view (export reflects the state before anyresetis applied; the absoluteprojectPathis omitted so the export keeps the rendered view's no-absolute-paths promise; combined withreset, the JSON is followed by the reset confirmation).
Metrics themselves never leave your machine:
- Metrics are written to OpenCode's local storage only
(
~/.local/share/opencode/opencode.db, keyphp-toolkit.metrics.<hash>). - The plugin makes no outbound requests of its own.
- Only counts, timestamps and the project path are recorded — never file contents, file names, code or prompt text.
Two things worth knowing precisely:
php_composer_auditandphp_composer_outdatedreach the network. They runcomposer audit/composer outdated, which contact the repositories configured in the project'scomposer.json(Packagist by default) to look up known vulnerabilities and newer releases. That request is made by Composer, on your behalf, when you ask for it — the plugin does not transmit anything itself, and a project pointing Composer at other hosts determines where the requests go.php_testexecutes project code. It runs the project's own test suite (vendor binary only, explicit working directory, 300s timeout) — treat it like runningvendor/bin/phpunityourself.- The snapshot is tool output.
php_metricsreturns the snapshot as tool content, so with a remote model provider it is included in that request. The snapshot therefore contains no absolute paths, no file names and no code — only counts, timings and the project folder name.
Removing the plugin does not delete the metrics: the rows stay in OpenCode's
local database. To drop a project's history, delete its
php-toolkit.metrics.<hash> row (or the whole database).
npm install
npm run typecheck
npm test
npm run build
node -e "import('./dist/index.js').then(m=>console.log(m.default.id))"
php -l fixtures/php-sample/src/Calculator.phpThe test suite needs Node >= 22.12 (vitest 5). The published plugin itself runs on Node >= 20. The tests use fake
phpandcomposerexecutables, so they do not need PHP or Composer installed.
CI runs the same checks on every push to main and on pull requests
(Node 22/24 with a dist/-drift check — any change to src/ must ship with
the rebuilt dist/ in the same commit — plus a Node 20 compat job that skips
the suite). See CHANGELOG.md for what changed between
releases.
index.js — package entrypoint shim (required: OpenCode loads a plugin
directory via <dir>/index.ts|index.js, not package.json main).
Re-exports ./dist/index.js. Keep it in `files` for publishing.
src/index.ts — Plugin.define entry
src/content.ts — markdown loader + skill/command manifests
src/tools/detect.ts — composer.json intel, no exec
src/tools/php-tools.ts — php -l, composer validate/audit/outdated, static analysis (phpstan/psalm), test runner (phpunit/pest); timeouts + AbortSignal throughout; each returns its own outcome + findings
src/hooks/php-context.ts — system hint, triggers, destructive-pattern guard
src/metrics/store.ts — per-project metrics document (storage, migration, merged writes)
src/metrics/registry.ts — one document per project, work attributed to the right one
src/metrics/format.ts — snapshot rendering
agents/php-*.md, commands/php-*.md, skills/php-*/SKILL.md
fixtures/php-sample/ — minimal PSR-4 app for smoke tests
test/ — vitest suite (metrics store, registry, snapshot format, tool outcomes)
Sibling plugins (
opencode-laravel-toolkit,opencode-symfony-toolkit) must follow the same layout: rootindex.jsshim +dist/build +php-*-style ID prefix (laravel-*,symfony-*) to avoid collisions.
MIT — NexusNV.
npm run build(commits freshdist/— required for git installs).- Bump
versioninpackage.json, commit, tagvX.Y.Z, push all three. npm publish --no-provenance(local publishes can't attest; use a Trusted Publishing workflow instead if you want provenance).- Fresh versions take a few minutes to appear (staged
0.0.0-stageplaceholder first). Machines withmin-release-agein~/.npmrcwon'tplugin addthem until that age passes — allow0temporarily to verify, then restore.