Getting started
Alpha · Under active development
php-periscope is pre-release. Things will move, break, and change shape — including this install flow. If you're trying it out, please file issues on GitHub. Don't use this in production yet.
In about three minutes you'll have the extension loaded, the daemon running, and your first Laravel request showing up in the UI.
Prerequisites
- macOS 13+ or Linux (Ubuntu 22.04+ tested). Windows: see the Windows section below.
- PHP 8.3 or 8.4. Install via Herd or
brew install [email protected]. v1 doesn't support 8.1 / 8.2 (v1.1 sprint). - A C++17 toolchain. Clang on macOS (Xcode CLT), gcc on Linux.
- A Laravel app. Adapter targets Laravel 12 / 13.
The install script checks for everything else (Rust, Cap'n Proto, PhpStorm, VSCode/Cursor) and offers to install what's missing — you'll see a green ✓ for each requirement that's already present, and a Y/n prompt before anything is installed. No surprise downloads.
Windows (WSL2)
Windows native is not supported and never will be — the C extension hooks into the Zend engine in ways that don't have a clean Windows-native equivalent in scope for v1. The supported path is WSL2 with Ubuntu, which is the same setup Laravel Sail and most modern PHP-on-Windows tooling uses.
One-time setup (open PowerShell as administrator):
wsl --install -d Ubuntu-22.04
wsl --set-default-version 2Reboot, finish Ubuntu's first-run user setup, then open the Ubuntu terminal. From there everything in this guide works exactly as on a native Linux box — apt-get install libcapnp-dev capnproto php8.3-dev, run the install script, composer require thamibn/laravel-periscope, etc.
Your Windows IDE (VSCode, PhpStorm) connects to WSL the standard way: VSCode's Remote - WSL extension or PhpStorm's WSL Interpreter setting. The periscope UI at http://localhost:9999 is reachable from Windows because WSL2 forwards localhost transparently.
Install in one line
bash <(curl -fsSL https://raw.githubusercontent.com/thamibn/php-periscope/main/scripts/install.sh)The script:
- Builds the C extension (
phpize→./configure→make). - Drops
periscope.sointo your PHP'sextension_dir. - Writes
99-periscope.iniinto your PHP'sconf.dso the extension auto-loads. - Builds
periscope-daemonviacargo build --release. - Installs the daemon + helper binaries into
/opt/homebrew/binor/usr/local/bin.
Add --dry-run to see exactly what would happen, or -v for verbose output. See bash scripts/install.sh --help for all flags.
Homebrew alternative
If you have brew and prefer formula-style installs:
brew tap periscopephp/php-periscope https://github.com/thamibn/php-periscope.git
brew install periscopephp/php-periscope/php-periscopeThe formula builds the extension once per detected brew PHP (php, [email protected], [email protected]).
Verify
php -m | grep periscope # should print "periscope"
periscope-daemon --version # should print a version stringIf either fails, re-read the script output — it tells you exactly which step missed.
Wire into your Laravel app
cd your-laravel-app
composer require thamibn/laravel-periscopeThat's the whole adapter setup. Service-provider auto-discovery picks it up; defaults are sensible.
On first install the adapter appends two lines to your .env:
# php-periscope — set to false to disable on this environment
PERISCOPE_TOOLBAR_ENABLED=true
PERISCOPE_UI_ENABLED=trueThis is idempotent: if either key is already present (set to anything, including false), the adapter leaves your .env alone. It only runs in console context (e.g. during composer require) — web requests never touch the filesystem.
Visit any route in your app — the first request writes a .cptrace file to /tmp/periscope/.
Trace recording is independent
These two flags only control how you access traces. Trace recording itself is always on (controlled by the C extension's periscope.enabled ini knob, default 1) — turning the toolbar or in-app UI off doesn't disable observability. You can still open http://localhost:9999, use the IDE plugins, or query traces via the MCP server with both flags set to false.
PERISCOPE_TOOLBAR_ENABLED
When true (auto-installed default): a InjectToolbar middleware is pushed onto Laravel's web middleware group. After your controller renders an HTML response, the middleware injects a small floating chip just before </body>. The chip shows: request duration, SQL query count, peak memory, HTTP status. Clicking it opens the trace in the in-app UI (/periscope) or — if PERISCOPE_UI_ENABLED=false — at http://localhost:9999.
What this affects:
- HTML responses only. The middleware checks
Content-Typeand skips JSON, XML, plain text, downloads, and anything without a</body>tag (so partial responses, htmx fragments, and Inertia JSON aren't touched). - Response body size: ~1 KB extra HTML/CSS/JS per page.
- Per-request cost: a single regex on the response body. Sub-millisecond on a typical page; profile yourself if your responses are very large.
- Production note: don't leave on in prod for end users — set it
falsein production envs, leave ittrueinlocal/staging.
When false: middleware never registers, zero per-request work, no chip in the browser. Trace recording continues unchanged. Use this in prod or when the chip clashes with your own page footer.
# .env — production override
PERISCOPE_TOOLBAR_ENABLED=falsePERISCOPE_UI_ENABLED
When true (auto-installed default): a route group is registered at /periscope (path is configurable via PERISCOPE_UI_PATH). Hits to app.test/periscope serve the same SolidJS UI the standalone periscope-daemon hosts at http://localhost:9999 — same trace list, same time-travel scrubber, same panels. Convenient when you'd rather not juggle two browser tabs.
What this affects:
- One route group added to your Laravel router (
/periscope,/periscope/{trace},/periscope/assets/*). - The route group uses the
webmiddleware by default — i.e. anyone with web-session access can view it. Override viaPERISCOPE_UI_MIDDLEWARE=web,auth(or stricter) for shared dev environments. - Talks to the daemon over WebSocket —
periscope-daemonstill needs to be running on the host (http://127.0.0.1:9999by default; override withPERISCOPE_UI_DAEMON_BASE). - Static assets are served by Laravel, not the daemon.
When false: no /periscope route added. Open the daemon's UI directly at http://localhost:9999 instead. Use this when:
- You don't want a debugger UI accessible inside your Laravel app's domain (defense in depth on shared dev / staging boxes).
- The
/periscopepath conflicts with one of your own app routes. - You only ever use the IDE plugins or AI agents (MCP) to read traces.
# .env — disable in-app mount, use the daemon's port instead
PERISCOPE_UI_ENABLED=falseWhat about production?
Recommended in .env.production:
PERISCOPE_TOOLBAR_ENABLED=false # don't ship a debugger chip to end users
PERISCOPE_UI_ENABLED=false # don't expose the trace UI on your public domain
PERISCOPE_ENABLED=false # if you want zero overhead, fully disable the adapterv1 ships local-dev only; production sampling lands in v2.
Open the UI
periscope-daemonThen open http://localhost:9999 and trigger any route in your Laravel app. The most recent trace lands at the top of the sidebar — click it, drag the timeline scrubber, expand panels.
Wire AI agents
The Laravel adapter auto-registers an MCP server. To expose it to Claude Code, Cursor, Codex, or any MCP-speaking agent:
claude mcp add periscope -- php artisan mcp:start periscopeFrom there your AI can call list_traces, get_summary, get_insights, query_events, get_state (time-travel to a specific microsecond), and read_file. See Architecture → AI-native for the tool set.
IDE integration
The install script you ran above already installed the extension into VSCode (or Cursor, or VSCodium) if it found a code CLI on your machine — no separate step. Restart your editor, then hit F5 in your Laravel project — periscope synthesises a launch config that opens the most recent trace. Step Over / Step Into / Step Out / Step Back all work via the daemon's DAP transport.
Didn't have an editor at install time? Install VSCode/Cursor and re-run the install script — it's idempotent and will pick up the new install.
What's next?
- PhpStorm setup — register the plugin's Run/Debug config + first breakpoint.
- Architecture — what's inside the extension, the daemon, and the adapter.
- Known limitations — what doesn't work in v1.
- FAQ — common questions about overhead, data privacy, and compatibility.