beyondcode/tinkerwell
Tinkerwell is a desktop REPL and code editor for PHP and Laravel that lets you run code instantly against local or remote projects, inspect output, and query databases without touching the command line.
Confirm the prerequisite before writing a line of code. This package is the Composer-side SDK for the Tinkerwell desktop client — drivers (how the client boots and runs code against your app) and panels (custom sidebar widgets). Without a licensed client installed, there is nothing to execute. If you don't have a seat, stop here; php artisan tinker, Ray, and your IDE debugger already cover the same inner loop.
Assuming seats exist:
composer require beyondcode/tinkerwell --dev
composer dump-autoload
src/Drivers and src/Panels first. That's the entire contract. Pay particular attention to how the built-in drivers shell out to ssh and docker exec; that's the pattern every custom driver copies.A minimal driver:
namespace App\Tinkerwell\Drivers;
use BeyondCode\Tinkerwell\Drivers\Driver;
class DockerDriver extends Driver
{
public function connect(): void
{
$this->container = config('tinkerwell.docker.container');
}
public function execute(string $code): string
{
return $this->runProcess([
'docker', 'exec', '-i', $this->container,
'php', 'artisan', 'tinker', '--execute=' . $code,
]);
}
}
Run composer dump-autoload, reopen the project, and the driver appears in the connection picker. That's the whole loop.
One driver per environment, named for the environment. LocalDriver, DockerDriver, StagingDriver — not MyDriver and MyDriver2. The picker then maps 1:1 to environments, and any behavioral difference (read-only credentials, different container, different PHP version) lives in code review instead of tribal memory.
Make every driver config-driven. Publish a config/tinkerwell.php (or reuse your existing app config) and read hosts, container names, SSH users, and flags from it. This is the single difference between a driver the whole team can use and a driver only its author can run on their laptop.
// config/tinkerwell.php
return [
'docker' => [
'container' => env('TINKERWELL_DOCKER_CONTAINER', 'app'),
],
'staging' => [
'host' => env('TINKERWELL_STAGING_HOST'),
'user' => env('TINKERWELL_STAGING_USER', 'deploy'),
'path' => env('TINKERWELL_STAGING_PATH', '/var/www/app'),
],
];
Panel per domain concept, not per query. Panels are where the compounding value sits, and each one should encode a diagnostic workflow rather than a single count. Tenant state, feature-flag resolution, queue backlog, billing lifecycle position. Build them with the same query primitives you'd type into artisan tinker, then commit them so a support engineer can answer a routine question without opening the codebase.
namespace App\Tinkerwell\Panels;
use BeyondCode\Tinkerwell\Panels\Panel;
use Illuminate\Support\Facades\DB;
class QueueHealthPanel extends Panel
{
public function render(): array
{
return [
'pending' => DB::table('jobs')->count(),
'oldest' => DB::table('jobs')->min('created_at'),
'failed_24h'=> DB::table('failed_jobs')
->where('failed_at', '>=', now()->subDay())->count(),
'by_queue' => DB::table('jobs')
->select('queue', DB::raw('count(*) as total'))
->groupBy('queue')->pluck('total', 'queue')->all(),
];
}
}
Pair panels with the editor, don't duplicate it. The panel is the summary; the code tab is where you drill in. A good panel surfaces an identifier (tenant ID, job ID, flag key) that you immediately paste into an ad-hoc snippet. If a panel tries to be a full admin UI, it belongs in Telescope/Horizon instead.
Share helpers, not snippets. When two panels need the same "resolve tenant from session" logic, extract it into a small App\Tinkerwell\Support class and call it from both. Panel code is application code; treat duplication the way you'd treat duplication in a controller.
Document the workflow next to the code. A short docs/tinkerwell.md (or a section in your onboarding doc) listing which driver to pick for which environment and which panel answers which question converts the whole thing from a personal shortcut into a team asset. A new hire runs composer install (dev deps included), opens the project, and inherits every standard workflow.
Keep it adjacent to your existing tools. Tinkerwell is not a replacement for breakpoint debugging or for one-liner artisan tinker calls in CI scripts. Use the driver for multi-environment connection management and the panels for app-specific state; keep Xdebug in the IDE and keep throwaway scripts in artisan tinker.
Dev-only, verified. --dev is not optional. After installing, run composer install --no-dev once and confirm the package and your App\Tinkerwell classes are absent from the resulting artifact. A driver class accidentally autoloaded in production is dead weight at best and an information leak at worst.
Discovery is autoloader-driven. If your driver or panel doesn't show up, the cause is almost always one of: no composer dump-autoload after creating the class, the class not being under an autoloaded namespace, or the client pointing at a different project root than the one whose autoloader knows about it. There is no provider to debug.
Shell-out drivers fail in the shell, not in your PHP. When execute() returns empty or times out, copy the exact command array into a terminal and run it by hand. Every time I've seen this, the bug was a wrong container name, an SSH key not loaded in the agent, or a TTY flag the shell needed. -i on docker exec and -T on ssh are the usual suspects.
Never point a driver at production. A REPL wired to a live database can run unbounded writes, and there's no undo. Restrict drivers to local, ephemeral, or staging targets with scoped read-only credentials, and write that rule down. A committed connection string to a shared environment is a security incident, so add secret scanning on config/tinkerwell.php.
Panels run on every refresh — keep them cheap. Whatever render() does happens repeatedly while the client is open. Avoid unbounded table scans, N+1 loops over Model::all(), and anything that hits a remote API on each tick. Cache expensive aggregates for 30–60 seconds and cap pluck/take sizes so you never dump 50k rows into the sidebar.
Pin the version and check the client. With a small star count, zero dependents, and a single maintainer, you are effectively self-supporting. Pin an exact version in composer.json, and record which Tinkerwell client version your drivers were written against. The driver/panel API is defined by a closed-source client, so an auto-updated desktop app is as much an upgrade risk as a Composer bump.
Re-verify at every Laravel major. The package's PHP/Laravel constraints are the likeliest thing to fall behind. When they do, it surfaces as a broken composer install for developers — annoying, but not a production incident. Budget an hour at each Laravel upgrade to re-check, and be ready to fork the MIT SDK or strip the dependency if it lags.
Extension points are just base classes. Where the built-in driver or panel almost does what you need, subclass it and override the one method rather than rewriting from scratch — connect() for connection setup, render() for the data shape. Read the built-in implementations, copy the structure, and don't invent new lifecycle hooks; there aren't any.
License lapse leaves harmless dead code — but plan for it. If the seats go away, the drivers and panels simply stop being used. Know that MIT lets you fork the SDK but not the closed-source client, and assign an owner: this is now first-party code you review, version, and maintain like anything else in app/, or it dies with the engineer who wrote it.
How can I help you explore Laravel packages today?