dhorrigan/capsule
Capsule is a Laravel/PHP package by Dan Horrigan that provides a lightweight way to encapsulate reusable application logic. It integrates cleanly with Laravel, helps keep code organized, and makes components easier to test, extend, and maintain across projects.
This package is a thin, opinionated wrapper around illuminate/database's Capsule\Manager, and its entire purpose is to let you use Laravel's query builder and Eloquent outside a Laravel application — legacy PHP apps, Slim/Silex services, WordPress plugins, CLI tools, PyroCMS-style projects. Keep that scope in mind: you are not getting a framework, you are getting a database layer.
Provenance check before you start. The package name (dhorrigan/capsule) and the linked upstream repo (philsturgeon/capsule) do not match. Before installing anywhere near production, verify the source: read the repo at the tag you plan to use, check the composer.json constraint on illuminate/database, and run composer why-not illuminate/database against your lockfile. If resolution fails or the pin is on an EOL Illuminate 4.x branch, stop here — see the Gotchas section for the drop-in replacement.
Steps to get running:
illuminate/database version — the wrapper does not pin one for you, so check the constraint first:
composer require dhorrigan/capsule illuminate/database:^4.2
Illuminate\Database\Capsule\Manager. That determines whether you can call setEventDispatcher(), getConnection(), connection() etc. directly or have to reach through to an inner manager.use Dhorrigan\Capsule\Capsule;
$capsule = new Capsule;
$capsule->addConnection([
'driver' => 'mysql',
'host' => '127.0.0.1',
'database' => 'app',
'username' => 'app',
'password' => 'secret',
'charset' => 'utf8',
'collation' => 'utf8_unicode_ci',
'prefix' => '',
]);
$capsule->setAsGlobal(); // enables Capsule::table() statically
$capsule->bootEloquent(); // wires up the Eloquent model resolver
First use case. Replace a pile of hand-rolled mysqli_* calls in an existing app with a fluent builder, without rewriting the app:
$users = Capsule::table('users')->where('active', 1)->get();
Then define one Eloquent model and confirm bootEloquent() took effect:
class User extends Illuminate\Database\Eloquent\Model {}
$active = User::where('active', 1)->get();
If the query builder works but Eloquent throws RuntimeException: A facade root has not been set or a "no connection resolver has been set" error, you booted in the wrong order, or you have two Capsule instances in play — see below.
Centralise the bootstrap. Do not instantiate the capsule in controllers or models. Create a single bootstrap/database.php (or a small factory class) that returns the configured capsule, and require it from every entry point — web front controller, CLI script, queue worker, test bootstrap. Multiple instantiation is the single most common source of "my model is hitting the wrong database" bugs.
// bootstrap/database.php
return function (array $config) {
$capsule = new \Dhorrigan\Capsule\Capsule;
$capsule->addConnection($config);
$capsule->setAsGlobal();
$capsule->bootEloquent();
return $capsule;
};
Wrap it in your own interface. This is the highest-value pattern given the package's maintenance posture. Define an internal contract in your app and bind the wrapper behind it:
interface DatabaseBootstrapper {
public function boot(array $config): void;
}
Application and model code depends on DatabaseBootstrapper, never on Dhorrigan\Capsule\Capsule directly. When you later swap to native Illuminate\Database\Capsule\Manager (near drop-in), only the one implementation class changes. This also makes the package mockable in tests.
Multi-connection setups. addConnection($config, $name) and getConnection($name) work as you'd expect from Capsule. Register all connections at boot, mark one setAsGlobal(), and select per query with Capsule::connection('reporting')->table(...). For read/write splitting, pass read/write keys in the config array as Illuminate expects — this package does not add or remove that capability.
Model conventions. Since there is no Laravel app, PSR-4 your models in a Models/ (or App/Models) namespace and point Composer at them. Set $table, $primaryKey, and $timestamps explicitly on legacy tables — auto-detection is fragile, and legacy schemas rarely match Laravel conventions. Use protected $connection = 'name' when working with multiple databases.
Migrations without a Laravel console. Illuminate's Migrations\Migrator and DatabaseMigrations can be driven from a custom CLI script — instantiate the migrator with the capsule's connection resolver and the filesystem repository. This is the second-biggest productivity win after Eloquent models and is often the reason teams reach for this package.
Event dispatcher. If you need model events (creating, saved, observers), install illuminate/events and call $capsule->setEventDispatcher(new Dispatcher). Without it, events are silently dropped — no error, no warning.
Testing. For tests, boot the capsule against SQLite :memory: with setAsGlobal() and bootEloquent() in your test bootstrap. Because the global resolver is a singleton, tests that swap databases concurrently will interfere; reset between test classes if you use a shared process.
Exit path. Treat the wrapper as an adapter, not a foundation. The migration to native Illuminate\Database\Capsule\Manager is roughly:
$capsule = new \Illuminate\Database\Capsule\Manager;
$capsule->addConnection($config);
$capsule->setAsGlobal();
$capsule->bootEloquent();
with call sites updated to the Illuminate\Database\Capsule\Manager class name (or, better, left alone because they go through your own interface).
The package is unmaintained. Zero stars, zero dependents, no CVE response path, no guarantee of compatibility with future PHP or Illuminate versions. Budget for ownership: pin an exact version, vendor or fork it if it must ship, and document the decision. Prefer depending on illuminate/database directly — its Capsule\Manager is the maintained, first-party version of exactly this functionality. This package's only real value is a shorter bootstrap; that is not worth an unmaintained pin.
Class collision and dual-global-capsule state. Dhorrigan\Capsule\Capsule and Illuminate\Database\Capsule\Manager both look like "the capsule". Mixing them — or instantiating either twice — creates two connection registries and two setAsGlobal() states. Symptoms: Capsule::table() hits the wrong database, model events fire twice, or bootEloquent() appears to have no effect. Fix: exactly one instance per process, created in one bootstrap file, and application code never references either Capsule class directly.
Static facade errors. RuntimeException: A facade root has not been set means setAsGlobal() was not called, or was called on a different instance than the one the static call resolves. Order matters: addConnection() → setAsGlobal() → bootEloquent(). bootEloquent() alone wires models but does not make the static Capsule::table() work.
Composer resolution is the first thing to break in modern projects. The wrapper pins old Illuminate transitively (typically 4.x), which in turn caps PHP support and blocks security patching up the stack. Run composer why-not illuminate/database and composer audit early in a throwaway branch; if resolution fails, the evaluation is over. Do not try to force it with --ignore-platform-reqs — that trades a clean failure for a runtime one.
Connection lifecycle in long-lived processes. With Swoole, RoadRunner, or persistent queue workers, the global capsule holds PDO connections open across requests. After a DB failover or idle timeout you get stale-connection errors, and per-request connection counts can exhaust the server. Call Capsule::connection()->disconnect() at the end of each unit of work (job, request) and reconnect lazily on the next query.
No event dispatcher by default. creating / saving / deleting model events and observers do nothing until setEventDispatcher() is wired. If migrations or seeders "work" but observers never fire, this is why. No error is raised.
Configuration drift. This package takes raw config arrays, so connection settings diverge from whatever the rest of your org documents for Laravel services. Keep a single well-commented config array — or env-based loader — in one place, and mirror Laravel's key names (driver, host, database, username, password, charset, collation, prefix) so the eventual move to Laravel needs no rewriting.
Debugging. Illuminate's query log is your fastest tool — Capsule::connection()->enableQueryLog() then getQueryLog() — but it must be enabled per connection and per process, and stays off in long-lived workers where "enable at boot" is too broad. Wrap it in a debug flag tied to the specific job or request you're investigating.
Extension points. Because this is a thin wrapper, anything Illuminate supports you get for free: custom ConnectionResolver, custom connection classes via DatabaseManager::extend() (if the wrapper exposes the manager), grammar and grammar-level tweaks, custom casts on models, and DB::listen() for query instrumentation. The one thing you cannot do cleanly through this package is anything requiring a service container — the Laravel Container is not bootstrapped for you, so app()->make(...)-style code from tutorials will not work. Use plain constructors and factories.
Silent failures to watch for. No test suite is visible and there are no downstream consumers, so PHP minor upgrades surface as production errors rather than CI failures. Add a smoke test that asserts connection + one Eloquent model hydration on every CI run, and pin both this package and its illuminate/database dependency with a documented upgrade path.
How can I help you explore Laravel packages today?