zpmlabs / laravel-package-quick-demo
Reusable quick demo installer for Laravel packages using isolated SQLite databases, routes, views, assets, storage, config publishing, and lifecycle hooks.
Package info
github.com/zpm-packages/laravel-package-quick-demo
pkg:composer/zpmlabs/laravel-package-quick-demo
Requires
- php: ^8.2
- illuminate/console: ^11.0|^12.0|^13.0
- illuminate/contracts: ^11.0|^12.0|^13.0
- illuminate/database: ^11.0|^12.0|^13.0
- illuminate/filesystem: ^11.0|^12.0|^13.0
- illuminate/routing: ^11.0|^12.0|^13.0
- illuminate/support: ^11.0|^12.0|^13.0
Requires (Dev)
- orchestra/testbench: ^9.0|^10.0
- pestphp/pest: ^3.0|^4.0
- pestphp/pest-plugin-laravel: ^3.0|^4.0
README
Reusable quick-demo installer for Laravel packages.
zpmlabs/laravel-package-quick-demo lets Laravel package authors ship isolated demo environments inside any host Laravel application.
It is built for packages that want a “try it now” experience without asking users to manually copy files, edit .env, create database connections, register routes, or pollute their main application database.
With this package, a user can install a package demo with one command:
php artisan quick-demo:install blog-demo
Features
- Isolated SQLite database per demo.
- Runtime Laravel database connection per demo.
- Demo migrations.
- Demo seeders.
- Demo routes.
- Demo route prefixes and route name prefixes.
- Demo Blade view namespaces.
- Demo view publishing.
- Demo asset publishing.
- Demo storage file publishing.
- Demo config file publishing.
- Install hooks.
- Uninstall hooks.
- Safe uninstall command.
- Config-based and runtime-based demo registration.
Installation
Install the package:
composer require zpmlabs/laravel-package-quick-demo
Publish the config file if needed:
php artisan vendor:publish --tag=package-quick-demo-config
This publishes:
config/package-quick-demo.php
Configuration
Default config:
<?php return [ 'database_directory' => database_path('quick-demos'), 'default_database_stub' => null, 'connection_prefix' => 'quick_demo_', 'route_prefix' => 'quick-demo', 'route_name_prefix' => 'quick-demo.', 'route_middleware' => ['web'], 'views_directory' => resource_path('views/vendor/quick-demos'), 'assets_directory' => public_path('vendor/quick-demos'), 'storage_directory' => storage_path('app/quick-demos'), 'config_directory' => config_path('quick-demos'), 'demos' => [ // Config-based demo definitions may be placed here. ], ];
Most package authors do not need users to publish this config. Demos can be registered directly from a package service provider.
Basic usage in another package
Example package: vendor/blog-package.
Inside the package service provider:
<?php namespace Vendor\BlogPackage; use Illuminate\Support\ServiceProvider; use ZPMLabs\LaravelPackageQuickDemo\Facades\QuickDemo; use ZPMLabs\LaravelPackageQuickDemo\Support\DemoDefinition; class BlogPackageServiceProvider extends ServiceProvider { public function boot(): void { 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', assetsPath: __DIR__ . '/../examples/assets', storagePath: __DIR__ . '/../examples/storage', configPath: __DIR__ . '/../examples/config/demo.php', ) ); } }
Then the user runs:
php artisan quick-demo:install blog-demo
Minimal database-only demo
A demo does not need routes, views, assets, storage, or config.
For a database-only demo, register only database, migrations, and seeders:
QuickDemo::register( DemoDefinition::make( key: 'users-demo', name: 'Users Demo', databaseName: 'users_demo', databaseStubPath: __DIR__ . '/../examples/database/demo.sqlite.stub', migrationsPath: __DIR__ . '/../examples/migrations', seeders: [ \Vendor\UsersPackage\Database\Seeders\UsersDemoSeeder::class, ], ) );
Install it:
php artisan quick-demo:install users-demo
This creates a separate SQLite database and runs only the configured migrations and seeders.
Full demo package structure
Recommended structure for a package that ships a complete mini demo app:
blog-package/
composer.json
src/
BlogPackageServiceProvider.php
Http/
Controllers/
BlogDemoController.php
Models/
DemoPost.php
Database/
Seeders/
BlogDemoSeeder.php
examples/
database/
demo.sqlite.stub
migrations/
2026_01_01_000001_create_demo_posts_table.php
routes/
demo.php
views/
index.blade.php
show.blade.php
assets/
demo.css
demo.js
storage/
images/
demo-cover.jpg
config/
demo.php
Commands
List all registered demos:
php artisan quick-demo:list
Show details for one demo:
php artisan quick-demo:show blog-demo
Install a demo:
php artisan quick-demo:install blog-demo
Reinstall a demo from scratch:
php artisan quick-demo:install blog-demo --fresh
Reinstall from scratch and overwrite generated files:
php artisan quick-demo:install blog-demo --fresh --force
Skip migrations:
php artisan quick-demo:install blog-demo --no-migrate
Skip seeders:
php artisan quick-demo:install blog-demo --no-seed
Skip view publishing:
php artisan quick-demo:install blog-demo --no-views
Skip asset publishing:
php artisan quick-demo:install blog-demo --no-assets
Skip storage publishing:
php artisan quick-demo:install blog-demo --no-storage
Skip config publishing:
php artisan quick-demo:install blog-demo --no-config
Uninstall generated demo files:
php artisan quick-demo:uninstall blog-demo
Drop only the generated SQLite database:
php artisan quick-demo:uninstall blog-demo --drop-database
Remove everything generated by the demo:
php artisan quick-demo:uninstall blog-demo --all --force
SQLite database isolation
Every registered demo gets its own SQLite database.
For example:
databaseName: 'blog_demo'
creates:
database/quick-demos/blog_demo.sqlite
The package also registers a runtime Laravel connection:
quick_demo_blog_demo
Another package can register:
databaseName: 'users_demo'
which creates:
database/quick-demos/users_demo.sqlite
and registers:
quick_demo_users_demo
These databases are independent.
The package does not modify .env and does not require users to edit config/database.php.
SQLite stub files
Each demo can provide a SQLite stub file:
examples/database/demo.sqlite.stub
The stub may be:
- an empty SQLite file,
- a prebuilt SQLite database with schema,
- a prebuilt SQLite database with schema and demo data.
Recommended approach:
- keep the stub empty,
- create schema through migrations,
- create sample data through seeders.
Create an empty stub:
mkdir -p examples/database touch examples/database/demo.sqlite.stub
Demo migrations
Every demo migration should explicitly use the quick-demo connection.
examples/migrations/2026_01_01_000001_create_demo_posts_table.php
<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; use ZPMLabs\LaravelPackageQuickDemo\Facades\QuickDemo; return new class extends Migration { public function getConnection(): string { return QuickDemo::connectionName('blog-demo'); } public function up(): void { Schema::connection($this->getConnection())->create('demo_posts', function (Blueprint $table): void { $table->id(); $table->string('title'); $table->text('body')->nullable(); $table->boolean('is_published')->default(true); $table->timestamps(); }); } public function down(): void { Schema::connection($this->getConnection())->dropIfExists('demo_posts'); } };
This prevents demo tables from being created in the host application's default database.
Demo seeders
Every demo seeder should write to the quick-demo connection.
src/Database/Seeders/BlogDemoSeeder.php
<?php namespace Vendor\BlogPackage\Database\Seeders; use Illuminate\Database\Seeder; use Illuminate\Support\Facades\DB; use ZPMLabs\LaravelPackageQuickDemo\Facades\QuickDemo; class BlogDemoSeeder extends Seeder { public function run(): void { DB::connection(QuickDemo::connectionName('blog-demo')) ->table('demo_posts') ->insert([ [ 'title' => 'First demo post', 'body' => 'This post belongs to the isolated quick demo database.', 'is_published' => true, 'created_at' => now(), 'updated_at' => now(), ], [ 'title' => 'Draft demo post', 'body' => 'This is another demo record.', 'is_published' => false, 'created_at' => now(), 'updated_at' => now(), ], ]); } }
Demo models
Demo models should use the quick-demo connection.
src/Models/DemoPost.php
<?php namespace Vendor\BlogPackage\Models; use Illuminate\Database\Eloquent\Model; use ZPMLabs\LaravelPackageQuickDemo\Facades\QuickDemo; class DemoPost extends Model { protected $table = 'demo_posts'; protected $guarded = []; public function getConnectionName(): string { return QuickDemo::connectionName('blog-demo'); } }
Demo routes
A package can provide routes for a demo UI.
examples/routes/demo.php
<?php use Illuminate\Support\Facades\Route; use Vendor\BlogPackage\Http\Controllers\BlogDemoController; Route::get('/', [BlogDemoController::class, 'index'])->name('index'); Route::get('/posts/{post}', [BlogDemoController::class, 'show'])->name('show');
The package automatically mounts these routes under a safe prefix:
/quick-demo/blog-demo
Route names are prefixed too:
quick-demo.blog_demo.index quick-demo.blog_demo.show
This avoids collisions with the host application.
Demo controller
Example controller:
<?php namespace Vendor\BlogPackage\Http\Controllers; use Vendor\BlogPackage\Models\DemoPost; class BlogDemoController { public function index() { return view('quick-demo-blog-demo::index', [ 'posts' => DemoPost::query()->latest()->get(), ]); } public function show(DemoPost $post) { return view('quick-demo-blog-demo::show', [ 'post' => $post, ]); } }
Demo views
A package can ship Blade views in:
examples/views
Example:
examples/views/index.blade.php examples/views/show.blade.php
Views are registered under a generated namespace:
quick-demo-blog-demo
Usage:
return view('quick-demo-blog-demo::index', [ 'posts' => $posts, ]);
When installed, views are published to:
resources/views/vendor/quick-demos/blog-demo
The published path is checked first. The package source path is used as fallback.
Demo assets
A package can ship demo assets in:
examples/assets
Example:
examples/assets/demo.css examples/assets/demo.js
Assets are published to:
public/vendor/quick-demos/blog-demo
Usage in Blade:
<link rel="stylesheet" href="{{ asset('vendor/quick-demos/blog-demo/demo.css') }}"> <script src="{{ asset('vendor/quick-demos/blog-demo/demo.js') }}" defer></script>
Demo storage files
A package can ship sample storage files in:
examples/storage
Example:
examples/storage/images/demo-cover.jpg
Files are copied to:
storage/app/quick-demos/blog-demo
Usage:
storage_path('app/quick-demos/blog-demo/images/demo-cover.jpg')
Demo config files
A package can ship a demo config file:
examples/config/demo.php
It is copied to:
config/quick-demos/blog-demo.php
The package does not modify existing application config files.
Config-based demo registration
Demos may also be registered through config/package-quick-demo.php.
'demos' => [ 'blog-demo' => [ 'name' => 'Blog Demo', 'database_name' => 'blog_demo', 'database_stub_path' => base_path('vendor/vendor/blog-package/examples/database/demo.sqlite.stub'), 'migrations_path' => base_path('vendor/vendor/blog-package/examples/migrations'), 'seeders' => [ \Vendor\BlogPackage\Database\Seeders\BlogDemoSeeder::class, ], 'routes_path' => base_path('vendor/vendor/blog-package/examples/routes/demo.php'), 'views_path' => base_path('vendor/vendor/blog-package/examples/views'), 'assets_path' => base_path('vendor/vendor/blog-package/examples/assets'), 'storage_path' => base_path('vendor/vendor/blog-package/examples/storage'), 'config_path' => base_path('vendor/vendor/blog-package/examples/config/demo.php'), ], ],
Runtime registration from a package service provider is usually preferred.
Install and uninstall hooks
A demo can run custom logic before or after installation and uninstallation.
DemoDefinition::make( key: 'blog-demo', name: 'Blog Demo', databaseName: 'blog_demo', beforeInstall: \Vendor\BlogPackage\Demo\BeforeInstallBlogDemo::class, afterInstall: \Vendor\BlogPackage\Demo\AfterInstallBlogDemo::class, beforeUninstall: \Vendor\BlogPackage\Demo\BeforeUninstallBlogDemo::class, afterUninstall: \Vendor\BlogPackage\Demo\AfterUninstallBlogDemo::class, )
Hook classes should have a handle method:
<?php namespace Vendor\BlogPackage\Demo; use ZPMLabs\LaravelPackageQuickDemo\Support\QuickDemoContext; class AfterInstallBlogDemo { public function handle(QuickDemoContext $context): void { // Custom demo setup logic. } }
The context contains useful information:
$context->definition; $context->databasePath; $context->connectionName; $context->routePrefix; $context->routeNamePrefix; $context->viewNamespace; $context->assetsDestination; $context->storageDestination; $context->configDestination; $context->options;
Hooks can also be closures or callable arrays.
DemoDefinition::make( key: 'blog-demo', name: 'Blog Demo', databaseName: 'blog_demo', afterInstall: function (QuickDemoContext $context): void { // Custom logic. }, )
Safe uninstall behavior
By default, uninstall does not remove everything automatically.
php artisan quick-demo:uninstall blog-demo
Use specific flags:
php artisan quick-demo:uninstall blog-demo --drop-database php artisan quick-demo:uninstall blog-demo --remove-views php artisan quick-demo:uninstall blog-demo --remove-assets php artisan quick-demo:uninstall blog-demo --remove-storage php artisan quick-demo:uninstall blog-demo --remove-config
Remove everything:
php artisan quick-demo:uninstall blog-demo --all --force
Generated paths
For demo key:
blog-demo
and database name:
blog_demo
Generated paths are:
database/quick-demos/blog_demo.sqlite resources/views/vendor/quick-demos/blog-demo public/vendor/quick-demos/blog-demo storage/app/quick-demos/blog-demo config/quick-demos/blog-demo.php
Generated route prefix:
/quick-demo/blog-demo
Generated route name prefix:
quick-demo.blog_demo.
Generated view namespace:
quick-demo-blog-demo
Generated connection:
quick_demo_blog_demo
Recommended naming
Good demo keys:
blog-demo users-demo orders-demo
Avoid vague keys:
demo example test
Good database names:
blog_demo users_demo orders_demo
Good table names:
demo_posts demo_users demo_orders
Avoid using real application table names for demo tables:
posts users orders
Safety rules
This package is designed to avoid polluting the host Laravel application.
It does not require users to:
- edit
.env, - edit
config/database.php, - edit
routes/web.php, - create a database connection manually,
- copy demo files manually.
Package authors should avoid:
- writing demo tables into the default database,
- registering unprefixed routes,
- using generic route names,
- publishing files directly into root public/resource/storage folders,
- overwriting files without
--force, - using generic demo keys.
Testing a demo locally
From a Laravel test application:
composer require vendor/blog-package php artisan quick-demo:list php artisan quick-demo:show blog-demo php artisan quick-demo:install blog-demo --fresh --force php artisan serve
Open:
http://localhost:8000/quick-demo/blog-demo
Validate:
php artisan route:list
Check that:
- the demo route is prefixed,
- the demo route names are prefixed,
- the SQLite database exists,
- migrations ran on the quick-demo connection,
- seeders inserted demo data,
- views render correctly,
- assets load correctly,
- storage files were copied if configured,
- config file was copied if configured,
- the host default database was not modified.
License
MIT