zpmlabs/laravel-package-quick-demo
zpmlabs/laravel-package-quick-demo is a compact Laravel package example for quickly understanding a minimal reusable package setup. It serves as a starter reference for package structure, service provider registration, and Composer integration in Laravel applications, ideal for developers prototypin...
The package solves one problem: shipping a runnable demo with a Laravel package without touching the host app's .env, config/database.php, or routes/web.php. It does this by registering a DemoDefinition per demo key, spinning up an isolated SQLite connection in database/quick-demos/{name}.sqlite, running migrations/seeders against that connection, and mounting routes/views/assets/storage/config under prefixed namespaces.
Minimal steps:
composer require zpmlabs/laravel-package-quick-demo:^0.1examples/ in your package with only what you need:
examples/
database/demo.sqlite.stub # empty file is fine: touch it
migrations/2026_01_01_000001_create_demo_posts_table.php
routes/demo.php
views/index.blade.php
boot():
use ZPMLabs\LaravelPackageQuickDemo\Facades\QuickDemo;
use ZPMLabs\LaravelPackageQuickDemo\Support\DemoDefinition;
QuickDemo::register(
DemoDefinition::make(
key: 'blog-demo',
name: 'Blog Demo',
databaseName: 'blog_demo',
databaseStubPath: __DIR__.'/../examples/database/demo.sqlite.stub',
migrationsPath: __DIR__.'/../examples/migrations',
seeders: [\Vendor\BlogPackage\Database\Seeders\BlogDemoSeeder::class],
routesPath: __DIR__.'/../examples/routes/demo.php',
viewsPath: __DIR__.'/../examples/views',
)
);
php artisan quick-demo:list
php artisan quick-demo:show blog-demo
php artisan quick-demo:install blog-demo
php artisan serve
http://localhost:8000/quick-demo/blog-demo.Where to look first:
quick-demo:show {key} — prints every resolved path, connection name, and prefix. This is your ground truth; if a path is wrong here, the install will misbehave.QuickDemoContext — the object handed to lifecycle hooks. Fields like $context->databasePath, $context->connectionName, $context->routePrefix tell you exactly what the package decided.First use case: a read-only demo of your package's public surface (list + show pages over seeded data), no auth, no writes. Start database-only (stub + migrations), add routes/views once data renders.
Isolation is the whole contract. Every layer must be scoped to the demo key. The three places people break isolation:
// Migration — override getConnection()
public function getConnection(): string
{
return QuickDemo::connectionName('blog-demo');
}
// Seeder — target the demo connection explicitly
DB::connection(QuickDemo::connectionName('blog-demo'))
->table('demo_posts')->insert([...]);
// Model — override getConnectionName()
public function getConnectionName(): string
{
return QuickDemo::connectionName('blog-demo');
}
Naming conventions that actually matter. The demo key (blog-demo) becomes a route prefix, a name prefix, and a view namespace. Prefixing is not cosmetic — it prevents collisions with a host app that may already own posts/index or /demo. Use package-specific keys (blog-demo), database names (blog_demo), and table names (demo_posts). Generic keys (demo, test, example) are called out as hard "don't"s because they collide across packages in the same host app.
Route/view namespacing is automatic. Your examples/routes/demo.php should define unprefixed routes:
Route::get('/', [BlogDemoController::class, 'index'])->name('index');
The package mounts them under /quick-demo/blog-demo and names them quick-demo.blog_demo.index. Views resolve via quick-demo-blog-demo::index.
Publishing destinations are fixed by the package — use these in Blade/controllers instead of inventing your own:
public/vendor/quick-demos/blog-demo → {{ asset('vendor/quick-demos/blog-demo/demo.css') }}storage/app/quick-demos/blog-demo → storage_path('app/quick-demos/blog-demo/images/demo-cover.jpg')config/quick-demos/blog-demo.phpOmit paths you don't have. DemoDefinition::make() accepts optional paths; passing a non-existent path (e.g. configPath when you have no config file) is a classic source of install failures. Register only what exists.
Use hooks sparingly. beforeInstall/afterInstall (context: QuickDemoContext) are the escape hatch for things the standard publish pipeline can't do — e.g., generating derived demo assets, calling an external service, or verifying a dependency. For anything covered by migrations/seeders/routes/views/assets/storage/config, don't reach for a hook.
Idempotent re-install. php artisan quick-demo:install blog-demo --fresh --force is the dev loop. --fresh resets the SQLite database; --force overwrites published files. Without these, a re-install won't clobber your demo after you change a migration or seeder.
Multi-demo packages. Register multiple QuickDemo::register() calls with distinct keys — the connection is registered per-demo (quick_demo_blog_demo, quick_demo_orders_demo), so demos are independent. Each has its own SQLite file, route prefix, and view namespace.
Schema::create() without ->connection() is the single most common mistake. The migration runs but on the default DB, creating fake production tables and leaving the demo blank. Always Schema::connection($this->getConnection()). Same trap with DB::table() in seeders — must use DB::connection(...).
Models not honoring the connection. If getConnectionName() is omitted (or a trait/relationship bypasses it), queries hit the default connection. Watch for Model::with() eager loads and pivot models in demo data — they need the same override.
Route name prefix collision on the host app. Route names use quick-demo.{dbname}.{name}. If you use a generic key like demo, a second package with the same key will collide on route names and the second install will fail or overwrite route names. Pick a package-specific key.
Config publishing requires the configPath to exist and be a file. A directory or missing file typically produces a confusing copy error. Same for databaseStubPath — the stub must exist; an empty file is fine, but missing is not.
Don't hand-edit the generated database or connection config. They live in database/quick-demos/ and the runtime connection config (registered via the package) — the demo must remain re-installable with --fresh. If you need seeded state, put it in a seeder, not a commit of the .sqlite file.
--force is required after edits. During development, edits to migrations/seeders won't apply on plain quick-demo:install — the published files already exist and the DB already has tables. Use --fresh --force.
Debugging checklist when a demo installs but renders empty/wrong:
php artisan quick-demo:show {key} — confirm resolved paths and connection name.storage/logs/laravel.log for SQL "no such table" — usually a missing getConnection() override.database/quick-demos/{dbname}.sqlite exists and is non-empty after install.php artisan route:list | grep quick-demo — confirm the route prefix.config/database.php and .env are unchanged (isolation contract).Validation the package expects after you implement:
php artisan quick-demo:list
php artisan quick-demo:show blog-demo
php artisan quick-demo:install blog-demo --fresh --force
php artisan route:list
Confirm demo appears, DB exists, migrations/seeders ran on the demo connection, no demo tables leaked into the default DB, routes are under /quick-demo/{key}, assets/storage/config copied where expected, and host .env/config/database.php/routes/web.php untouched.
Extension point summary: DemoDefinition fields (afterInstall, beforeInstall, seeders, paths) and QuickDemoContext (definition, databasePath, connectionName, routePrefix, routeNamePrefix, viewNamespace, assetsDestination, storageDestination, configDestination, options). Hooks receive the context and can log/validate/verify — but should stay stateless and idempotent since --fresh re-runs them.
Keep naming in English — the package's own convention treats non-English identifiers/comments as a review defect in package additions.
How can I help you explore Laravel packages today?