Visualize the architecture of any WordPress plugin as an interactive graph.
Plugin Profiler is a Dockerized static analysis tool that scans a WordPress plugin directory, parses all PHP, JavaScript, and block.json files, and produces an interactive Cytoscape.js graph of its architecture — classes, hooks, data sources, REST endpoints, Gutenberg blocks, and more. Optionally, an LLM generates a plain-English description for every node.
Run the tool against your plugin, then open
http://localhost:9000to see a graph like this.
| Graph view | Node inspector sidebar |
|---|---|
![]() |
![]() |
(Screenshots will appear here after your first run.)
| Language / File | What's extracted |
|---|---|
| PHP | Classes, interfaces, traits, methods, functions |
| PHP | add_action / add_filter hook registrations, do_action / apply_filters triggers |
| PHP | Options API, post meta, user meta, transients, $wpdb queries |
| PHP | REST routes, AJAX handlers, shortcodes, admin pages, cron jobs, post types, taxonomies, HTTP calls |
| PHP | include / require file relationships |
| JavaScript / JSX / TS / TSX | registerBlockType, addAction, addFilter, apiFetch calls |
| block.json | Gutenberg block definitions, render templates, enqueued scripts |
| Requirement | Notes |
|---|---|
| Docker Desktop ≥ 4.x | Or Docker Engine + Compose v2 on Linux |
| Docker Compose v2 | docker compose version must work (note: no hyphen) |
No PHP, Node.js, or Composer installation needed on the host machine.
# Clone the repo
git clone https://github.com/bu-ist/plugin-profiler.git
cd plugin-profiler
# Make the CLI script executable (once)
chmod +x bin/plugin-profiler
# Analyze a plugin — skipping LLM descriptions for speed
./bin/plugin-profiler analyze /path/to/your-wp-plugin --no-descriptionsThe command will:
- Build the Docker images (first run only — ~2 min)
- Scan and parse the plugin
- Write
graph-data.jsonto a shared Docker volume - Start a local web server on port 9000
- Open
http://localhost:9000in your default browser
The bin/plugin-profiler shell script is the primary interface. Symlink it onto your PATH for convenience:
sudo ln -s "$(pwd)/bin/plugin-profiler" /usr/local/bin/plugin-profilerThen from any directory:
plugin-profiler analyze ~/Sites/woocommerce --no-descriptionsPLUGIN_PATH=/absolute/path/to/plugin \
docker compose run --rm analyzer /plugin --no-descriptions
PLUGIN_PATH=/absolute/path/to/plugin \
docker compose up -d webThen visit http://localhost:9000.
Usage: plugin-profiler analyze <plugin-path> [options]
Arguments:
<plugin-path> Absolute or relative path to the WordPress plugin directory
Options:
--port <n> Port for the web UI (default: 9000)
--llm <provider> LLM provider: claude, ollama, openai, gemini (default: ollama)
--model <name> LLM model identifier (default: qwen2.5-coder:7b)
--api-key <key> API key for external LLM providers
--no-descriptions Skip LLM description generation (much faster)
--json-only Write graph-data.json only; do not start the web server
--output <dir> Output directory inside the container (default: /output)
--help Show help
# Basic analysis, no descriptions
plugin-profiler analyze ./my-plugin --no-descriptions
# Use Google Gemini for descriptions
plugin-profiler analyze ./my-plugin \
--llm gemini \
--model gemini-2.5-flash \
--api-key YOUR_GEMINI_KEY
# Use OpenAI GPT-4o mini
plugin-profiler analyze ./my-plugin \
--llm openai \
--model gpt-4o-mini \
--api-key sk-...
# Use Claude (Anthropic)
plugin-profiler analyze ./my-plugin \
--llm claude \
--model claude-haiku-4-5-20251001 \
--api-key sk-ant-...
# Use local Ollama (starts Ollama container automatically)
plugin-profiler analyze ./my-plugin \
--llm ollama \
--model qwen2.5-coder:7b
# Output JSON only, then serve manually
plugin-profiler analyze ./my-plugin --no-descriptions --json-only
# graph-data.json is now at /output/graph-data.json inside Docker volumePlugin Profiler can generate a 2–3 sentence description for every node in the graph using an LLM. This is optional — pass --no-descriptions to skip it.
| Provider | --llm value |
Cost (Gravity Forms ~250 entities) |
|---|---|---|
| Ollama (local) | ollama |
Free |
| Claude Haiku | claude |
~$0.01 |
| OpenAI GPT-4o mini | openai |
~$0.11 |
| Google Gemini 2.0 Flash | gemini |
~$0.03 |
When --llm ollama is used, Plugin Profiler starts an ollama Docker container alongside the analyzer. The model is pulled automatically on first use.
plugin-profiler analyze ./my-plugin --llm ollama --model qwen2.5-coder:7bFirst run with Ollama will take extra time while the model downloads (~4 GB for qwen2.5-coder:7b).
All external providers use the OpenAI-compatible chat completions API format.
# Gemini
plugin-profiler analyze ./my-plugin \
--llm gemini \
--model gemini-2.5-flash \
--api-key AIza...
# OpenAI
plugin-profiler analyze ./my-plugin \
--llm openai \
--model gpt-4o-mini \
--api-key sk-...Open http://localhost:9000 (or the port you configured).
| Action | Result |
|---|---|
| Click a node | Opens the right-hand inspector panel |
| Hover a node | Highlights the node and its direct connections; dims everything else |
| Double-click a node | Zooms to fit the node and its immediate neighbors |
| Click canvas | Deselects and clears highlighting |
Clicking a node opens a panel showing:
- Entity type badge + label
- AI-generated description (if available)
- File path + line number (VS Code link:
vscode://file/...) - All incoming and outgoing connections, grouped by relationship type — click any to navigate to that node
- Syntax-highlighted source preview (PHP or JS)
- PHPDoc / docblock
| Control | Purpose |
|---|---|
| Search box | Filter nodes by label in real time |
| Type filter buttons | Toggle visibility for each node type |
| Layout dropdown | Switch between Dagre (hierarchy), CoSE (force), Breadth-first, Grid |
| + / − / Fit | Zoom controls |
| Key | Action |
|---|---|
/ |
Focus the search box |
F |
Fit graph to screen |
Esc |
Close sidebar, clear highlighting |
1 – 9 |
Toggle node type filters in order |
| Double-click | Zoom to node neighborhood |
| Type | Shape | Color | What it represents |
|---|---|---|---|
class |
Rounded rectangle | Blue | PHP class |
interface |
Rounded rectangle (dashed border) | Blue | PHP interface |
trait |
Rounded rectangle (dotted border) | Blue | PHP trait |
function |
Rounded rectangle | Teal | Standalone PHP function |
method |
Rounded rectangle | Teal | Class method |
hook (action) |
Diamond | Orange | add_action / do_action hook |
hook (filter) |
Diamond | Yellow | add_filter / apply_filters hook |
js_hook |
Diamond | Orange | JavaScript addAction / addFilter |
rest_endpoint |
Hexagon | Green | register_rest_route |
ajax_handler |
Hexagon | Green | wp_ajax_* / wp_ajax_nopriv_* |
shortcode |
Tag | Green | add_shortcode |
admin_page |
Rectangle | Green | add_menu_page / add_submenu_page |
cron_job |
Ellipse | Green | wp_schedule_event |
post_type |
Barrel | Green | register_post_type |
taxonomy |
Barrel | Green | register_taxonomy |
data_source |
Barrel | Purple | Options, post_meta, user_meta, transients, $wpdb |
http_call |
Ellipse | Red | wp_remote_get / wp_remote_post |
file |
Rectangle | Gray | PHP file (via include/require) |
gutenberg_block |
Rounded rectangle | Pink | block.json / registerBlockType |
js_api_call |
Ellipse | Green | apiFetch REST call |
| Type | Style | Meaning |
|---|---|---|
extends |
Solid blue | Class inheritance |
implements |
Solid blue | Interface implementation |
has_method |
Default | Class → method |
registers_hook |
Dashed orange | Function/method registers a hook |
triggers_hook |
Dashed orange | Function/method triggers a hook |
reads_data |
Thick purple | Reads from a data source |
writes_data |
Thick dashed purple | Writes to a data source |
http_request |
Red | Outbound HTTP call |
includes |
Default | File include/require |
renders_block |
Dotted pink | Gutenberg block → PHP render template |
enqueues_script |
Dotted pink | Gutenberg block → JS asset |
Copy .env.example to .env and edit as needed:
cp .env.example .env# Web UI port
PORT=9000
# Absolute path to the plugin directory on your host
PLUGIN_PATH=./my-plugin
# LLM provider: claude | ollama | openai | gemini
LLM_PROVIDER=ollama
LLM_MODEL=qwen2.5-coder:7b
# Required for external providers
LLM_API_KEY=
# Ollama host (leave as-is when using the bundled container)
OLLAMA_HOST=http://ollama:11434
# Tuning
LLM_BATCH_SIZE=25
LLM_TIMEOUT=120┌─────────────────────────────────────────────────────┐
│ Docker Compose │
│ │
│ ┌──────────────┐ graph-data.json ┌───────────┐ │
│ │ analyzer │ ─────────────────► │ web │ │
│ │ php:8.1-cli │ shared volume │ nginx │ │
│ └──────────────┘ └───────────┘ │
│ │ │ │
│ ┌──────┴──────┐ http://localhost:9000 │
│ │ ollama │ (optional, --llm ollama profile) │
│ └─────────────┘ │
└─────────────────────────────────────────────────────┘
Host machine
└── /path/to/plugin → bind-mounted read-only as /plugin
- FileScanner — Discovers
.php,.js,.jsx,.ts,.tsx,block.jsonfiles. Skipsvendor/,node_modules/,.git/. - PluginParser — Runs the AST visitors on each file type.
- Visitors (PHP via nikic/php-parser v5, JS via mck89/peast):
ClassVisitor— classes, interfaces, traits, inheritanceFunctionVisitor— functions and methodsHookVisitor—add_action,add_filter,do_action,apply_filtersDataSourceVisitor— Options API, post/user meta, transients,$wpdbExternalInterfaceVisitor— REST, AJAX, shortcodes, admin pages, cron, post types, taxonomies, HTTPFileVisitor—include/requirerelationshipsJavaScriptVisitor—registerBlockType,addAction,addFilter,apiFetchBlockJsonVisitor—block.jsonblocks
- GraphBuilder — Resolves cross-references, drops edges to missing nodes.
- DescriptionGenerator (optional) — Batches entities to LLM, attaches descriptions.
- JsonExporter — Writes Cytoscape.js-compatible
graph-data.json.
Vanilla JavaScript — no build step. Loaded via CDN:
- Cytoscape.js + cytoscape-dagre
- Tailwind CSS
- Prism.js (syntax highlighting)
All tests require Docker (local PHP is likely not 8.1):
# Build the test image and run all 139 tests
docker build --target test -t plugin-profiler-test ./analyzer
docker run --rm plugin-profiler-testOr using the CLAUDE.md test command shorthand:
docker build --target test -t plugin-profiler-test ./analyzer && docker run --rm plugin-profiler-testcd analyzer && ./vendor/bin/php-cs-fixer fix --dry-run --diff
# Auto-fix:
cd analyzer && ./vendor/bin/php-cs-fixer fixThe web frontend uses no build step. To develop locally with live reload:
cd web && npm install && npm run dev
# Serves on http://localhost:9000
# Note: /data/graph-data.json must exist — run the analyzer firstplugin-profiler/
├── bin/
│ └── plugin-profiler # Main CLI entry point (bash)
├── analyzer/
│ ├── bin/analyze # PHP CLI entry (called inside Docker)
│ ├── src/
│ │ ├── Command/ # Symfony Console command
│ │ ├── Scanner/ # File discovery
│ │ ├── Parser/
│ │ │ └── Visitors/ # AST visitors (PHP + JS + block.json)
│ │ ├── Graph/ # Node, Edge, EntityCollection, GraphBuilder
│ │ ├── LLM/ # OllamaClient, ApiClient, DescriptionGenerator
│ │ └── Export/ # JsonExporter
│ └── tests/
│ ├── Unit/ # Per-class unit tests
│ ├── Integration/ # Full pipeline test
│ └── fixtures/ # Sample plugin for testing
├── web/
│ ├── index.html
│ ├── js/
│ │ ├── app.js # Entry: fetch data, init graph
│ │ ├── graph.js # Cytoscape config + node/edge styles
│ │ ├── sidebar.js # Inspector panel
│ │ ├── search.js # Search + type filter toggles
│ │ └── layouts.js # Layout algorithm configs
│ └── nginx.conf
├── docker-compose.yml
├── .env.example
└── agent_docs/ # Internal architecture specs
The analyzer writes graph-data.json to the shared Docker volume, served at /data/graph-data.json. The format is Cytoscape.js-compatible:
{
"plugin": {
"name": "My Plugin",
"version": "1.2.3",
"description": "...",
"main_file": "my-plugin.php",
"total_files": 42,
"total_entities": 138,
"analyzed_at": "2026-02-20T14:30:00+00:00",
"analyzer_version": "0.1.0"
},
"nodes": [
{
"data": {
"id": "class_MyPlugin_Controller",
"label": "Controller",
"type": "class",
"subtype": null,
"file": "/plugin/src/Controller.php",
"line": 12,
"metadata": { "namespace": "MyPlugin", "extends": null, ... },
"docblock": "Handles all form submissions.",
"description": "AI-generated description here.",
"source_preview": "class Controller {\n ..."
}
}
],
"edges": [
{
"data": {
"id": "e_0",
"source": "class_MyPlugin_Controller",
"target": "hook_action_init",
"type": "registers_hook",
"label": "registers"
}
}
]
}Install Docker Desktop and ensure it's running.
You need Docker Compose v2. With Docker Desktop this is included. On Linux:
sudo apt install docker-compose-plugin # Debian/Ubuntu- Verify the plugin path is correct and contains
.phpfiles - Ensure the path is readable and not inside a network drive
The first run with --llm ollama downloads the model (~4 GB for qwen2.5-coder:7b). Subsequent runs reuse the cached ollama_models Docker volume. Use a smaller model to speed this up:
plugin-profiler analyze ./plugin --llm ollama --model qwen2.5-coder:3bRun the analyzer before opening the browser:
plugin-profiler analyze ./my-plugin --no-descriptionsplugin-profiler analyze ./my-plugin --no-descriptions --port 8080docker compose buildThis repository ships a GitHub Actions workflow (.github/workflows/ci.yml) with three jobs:
| Job | What it checks |
|---|---|
| PHP Tests | Runs all 139 PHPUnit tests on PHP 8.1; checks PSR-12 style with php-cs-fixer |
| Docker Build | Confirms both analyzer and web images build cleanly |
| JS Lint | Runs ESLint on web/js/ |
MIT — see LICENSE.
- Fork and clone
- Make changes in a feature branch
- Run the test suite:
docker build --target test -t plugin-profiler-test ./analyzer && docker run --rm plugin-profiler-test - Check code style:
cd analyzer && ./vendor/bin/php-cs-fixer fix --dry-run - Open a pull request against
main

