nyoncode/laravel-package-toolkit

Tools for easy creating Laravel packages

Maintainers

Package info

github.com/NyonCode/laravel-package-toolkit

pkg:composer/nyoncode/laravel-package-toolkit

Transparency log

Statistics

Installs: 5 326

Dependents: 10

Suggesters: 0

Stars: 0

Open Issues: 0

2.4.0 2026-08-09 13:00 UTC

This package is auto-updated.

Last update: 2026-08-09 23:09:51 UTC


README

Laravel Package toolkit is a powerful tool designed to streamline the process of creating and managing packages for Laravel. It provides a set of intuitive abstractions and helper methods for common package development tasks, enabling developers to focus on building features rather than boilerplate code.

πŸ“– Read the documentation β€” a page per resource type, with extended examples, gotchas and a full API reference. The Markdown lives in docs/; the site that renders it lives in site/.

Features

  • Simple and expressive package configuration
  • Automatic handling of routes, migrations, translations, and views
  • Support for view components
  • Built-in exception handling for package-specific errors
  • Comprehensive language support
  • Install command with customizable publishing options
  • Conditional resource loading based on environment
  • Lifecycle hooks for advanced customization
  • Middleware registration and management
  • Event listener and subscriber registration
  • Optimize command registration (php artisan optimize / optimize:clear)
  • Broadcast channel registration
  • Publishable seeders, factories and generator stubs

Support Laravel

  • Laravel 12.x (>= 12.61.1)
  • Laravel 13.x (>= 13.12.0)

Note: Laravel 10.x and 11.x support was removed in v2.1. Both branches have passed their security-support end-of-life and the June 2026 advisories (including a High-severity CRLF injection, CVE-2026-48019) were only patched in Laravel 12.60+/13.9+, never backported to 10.x or 11.x. The minimum supported versions are pinned to the first patched releases. If you still need Laravel 10/11, use the ~2.0.0 release; for Laravel 9, use ^1.0.

Table of Contents

Installation

You can install the package via composer:

composer require nyoncode/laravel-package-toolkit

Usage

Basic Configuration

To use Laravel Package Builder, create a ServiceProvider for your package that extends NyonCode\LaravelPackageToolkit\PackageServiceProvider:

use NyonCode\LaravelPackageToolkit\PackageServiceProvider;
use NyonCode\LaravelPackageToolkit\Packager;
use NyonCode\LaravelPackageToolkit\Contracts\Packable;

class MyAwesomePackageServiceProvider extends PackageServiceProvider implements
    Packable
{
    public function configure(Packager $packager): void
    {
        $packager
            ->name('My Awesome Package')
            ->hasConfig()
            ->hasRoutes()
            ->hasMigrations()
            ->hasTranslations()
            ->hasViews();
    }
}

Advanced Configuration

For more control over your package configuration, you can use additional methods and specify custom paths:

use NyonCode\LaravelPackageToolkit\PackageServiceProvider;
use NyonCode\LaravelPackageToolkit\Packager;
use NyonCode\LaravelPackageToolkit\Contracts\Packable;

class AdvancedPackageServiceProvider extends PackageServiceProvider implements
    Packable
{
    public function configure(Packager $packager): void
    {
        $packager
            ->name('Advanced package')
            ->hasShortName('adv-pkg')
            ->hasConfig('custom-config.php')
            ->hasRoutes(['api.php', 'web.php'])
            ->hasMigrations('custom-migrations')
            ->hasTranslations('lang')
            ->hasViews('custom-views')
            ->hasComponents([
                'data-table' => DataTable::class,
                'modal' => Modal::class,
            ]);
    }

    public function registeringPackage(): void
    {
        // Custom logic before package registration
    }

    public function bootingPackage(): void
    {
        // Custom logic before package boot
    }
}

Conditional registration resources

You can also use the when() method to conditionally register resources:

use NyonCode\LaravelPackageToolkit\PackageServiceProvider;
use NyonCode\LaravelPackageToolkit\Packager;
use NyonCode\LaravelPackageToolkit\Contracts\Packable;

class ConditionalPackageServiceProvider extends PackageServiceProvider implements
    Packable
{
    public function configure(Packager $packager): void
    {
        $packager
            ->name('Conditional package')
            ->hasRoutes(['api.php', 'web.php'])
            ->hasMigrations('custom-migrations')
            ->hasTranslations('lang')
            ->hasViews('custom-views')
            ->when($this->isInLocal(), function ($packager) {
                $packager->hasConfig('local-config.php');
                $packager->hasCommands();
            })->when($this->isInProduction(), function ($packager) {
                $packager->hasConfig('production-config.php');
                $packager->hasRoutes('web.php');
            });
    }
}

Local and production resources will be registered when the isInLocal() and isInProduction() methods return true.

Additional Conditional Methods

The package provides several convenient methods for conditional loading:

$packager
    // Environment-based conditions
    ->whenEnvironment(['local', 'testing'], function ($packager) {
        $packager->hasCommands(['DevCommand::class']);
    })
    ->whenProduction(function ($packager) {
        $packager->hasConfig('production-config.php');
    })
    ->whenLocal(function ($packager) {
        $packager->hasConfig('local-config.php');
    })
    
    // Runtime conditions
    ->whenConsole(function ($packager) {
        $packager->hasCommands();
    })
    
    // Class/extension existence
    ->whenClassExists('SomeClass', function ($packager) {
        $packager->hasConfig('optional-config.php');
    })
    ->whenExtensionLoaded('redis', function ($packager) {
        $packager->hasConfig('redis-config.php');
    });

Lifecycle Hooks

The package provides lifecycle hooks that allow you to execute custom logic at specific points during package registration and booting:

Hook Method Description
registeringPackage() Called before register() is called
registeredPackage() Called after register() is called
bootingPackage() Called before boot() is called
bootedPackage() Called after boot() is called

Using Lifecycle Hooks in Configuration

You can define lifecycle hooks directly in your package configuration:

$packager
    ->name('My Package')
    ->registeringPackage(function ($packager) {
        // Logic executed before package registration
        Log::info('Registering My Package');
    })
    ->registeredPackage(function ($packager) {
        // Logic executed after package registration
        $this->app->singleton('my-service', MyService::class);
    })
    ->bootingPackage(function ($packager) {
        // Logic executed before package boot
        Event::listen('my-event', MyListener::class);
    })
    ->bootedPackage(function ($packager) {
        // Logic executed after package boot
        Log::info('My Package fully loaded');
    });

Name

Define a name for the package:

$packager->name('Package name');

Short name

Define a custom short name for the package. The hasShortName method is used to modify the name defined by name() if you prefer not to use the short version from $packager->name('Package name'):

$packager->hasShortName('custom-short-name');

The short name must be in kebab-case format and contain only lowercase letters, numbers, and hyphens.

Config

To enable configuration in your package:

$packager->hasConfig();

By default, this will load configuration from the config directory. For custom config files:

$packager->hasConfig(['config.php', 'other-config.php']);

Or for specific file paths:

$packager->hasConfig([
    '../www/config/config.php',
    '../api/config/other-config.php',
]);

To use an alternative directory for config files.

$package->hasConfig(directory: 'customConfig');

Routing

To enable routing in your package:

$packager->hasRoutes();

By default, this will load routes from the routes directory. For custom route files:

$packager->hasRoutes(['api.php', 'web.php']);

Or for specific file paths:

$packager->hasRoute(['../www/routes/web.php', '../api/routes/api.php']);

To use an alternative directory for route files.

$package->hasRoute(directory: 'webRouter');

Middlewares

To register middleware for your package, use these methods:

Register Middleware Aliases

To define route middleware aliases:

$packager->hasMiddlewareAliases([
    'custom.alias' => \Vendor\Package\Http\Middleware\CustomMiddleware::class,
    'auth.custom' => \Vendor\Package\Http\Middleware\CustomAuthMiddleware::class,
]);

This allows you to assign the middleware to routes using its alias:

Route::get('/example', fn () => 'Hello')->middleware('custom.alias');

Register Middleware Groups

To push middleware into existing middleware groups:

$packager->hasMiddlewareGroups([
    'web' => [
        \Vendor\Package\Http\Middleware\WebMiddleware::class,
    ],
    'api' => [
        \Vendor\Package\Http\Middleware\ApiMiddleware::class,
        \Vendor\Package\Http\Middleware\RateLimitMiddleware::class,
    ],
]);

This will automatically add your middleware to the specified groups (e.g. web, api).

Register Middleware Globally

To register global middleware (executed for every request):

$packager->hasMiddlewareGlobals([
    \Vendor\Package\Http\Middleware\GlobalMiddleware::class,
    \Vendor\Package\Http\Middleware\SecurityMiddleware::class,
]);

This middleware will be added to the middleware stack and is useful for applying middleware to all routes regardless of their group.

Events

Register event listeners and subscribers for your package. Bindings are applied during boot() via the Event facade.

Register Event Listeners

Provide a map of event class to listener(s). A value may be a single listener, a closure, or an array of listeners:

$packager->hasEvents([
    \Vendor\Package\Events\OrderPlaced::class => [
        \Vendor\Package\Listeners\SendOrderConfirmation::class,
        \Vendor\Package\Listeners\LogOrder::class,
    ],
    \Vendor\Package\Events\OrderShipped::class => \Vendor\Package\Listeners\NotifyCustomer::class,
]);

To register a single event, use hasEvent():

$packager->hasEvent(
    \Vendor\Package\Events\OrderPlaced::class,
    \Vendor\Package\Listeners\SendOrderConfirmation::class
);

Register Event Subscribers

Subscriber classes (with a subscribe() method) are registered via Event::subscribe():

$packager
    ->hasSubscriber(\Vendor\Package\Listeners\OrderEventSubscriber::class)
    ->hasSubscribers([
        \Vendor\Package\Listeners\UserEventSubscriber::class,
        \Vendor\Package\Listeners\PaymentEventSubscriber::class,
    ]);

Broadcast Channels

Channel authorization callbacks (Broadcast::channel()) live in their own file, loaded straight into the broadcaster. By default the toolkit looks in the package's routes directory:

$packager->hasBroadcastChannels();

Specify files explicitly, or point at another directory:

$packager->hasBroadcastChannels(['channels.php']);

$packager->hasBroadcastChannels(
    channelFiles: ['channels.php', 'presence-channels.php'],
    directory: '../broadcasting'
);

A channel file looks exactly like an application's routes/channels.php:

use Illuminate\Support\Facades\Broadcast;

Broadcast::channel('blog.post.{postId}', function ($user, string $postId) {
    return $user->canRead($postId);
});

Two things to know:

  • Do not use hasRoutes() for channel files. That loads them into the router inside a route group, which is not where a channel authorization callback belongs.
  • Channels are not publishable. An application does not load routes/channels.php unless its own bootstrap asks for it, so a published copy would look authoritative while the package kept using its own. A consumer overrides authorization by re-registering the same channel name from their application β€” the last registration wins.

Registration is skipped silently when illuminate/broadcasting is not installed, so the call is safe in a package that only optionally broadcasts.

Optimize

Register artisan commands that run with php artisan optimize (cache warmup) and php artisan optimize:clear. The toolkit forwards these to Laravel's ServiceProvider::optimizes().

$packager->hasOptimizeCommands(
    optimize: 'my-package:cache',
    clear: 'my-package:clear',
);

At least one of optimize or clear must be provided. The cache key defaults to the package short name; when registering more than one entry, pass a distinct key for each, since Laravel keys optimize commands by provider:

$packager
    ->hasOptimizeCommands(optimize: 'my-package:cache-config', clear: 'my-package:clear-config', key: 'my-package-config')
    ->hasOptimizeCommands(optimize: 'my-package:cache-routes', clear: 'my-package:clear-routes', key: 'my-package-routes');

Migrations

The toolkit supports both timestamped and timeless migration files. Detection is automatic β€” simply call hasMigrations() and the toolkit will handle both formats correctly.

Timestamped migrations

Standard Laravel migration files with a date prefix:

database/migrations/
β”œβ”€β”€ 2025_01_01_000000_create_users_table.php
└── 2025_01_01_000001_create_roles_table.php

To enable migrations:

$packager->hasMigrations();

Timeless migrations

Migration files without a date prefix. When published, the toolkit automatically prepends a sequential timestamp to ensure correct execution order:

database/migrations/
β”œβ”€β”€ create_posts_table.php
└── create_comments_table.php

Usage is identical β€” no extra configuration is needed:

$packager->hasMigrations();

When a user runs vendor:publish, timeless files are published with a generated timestamp prefix (e.g. 2025_03_20_143022_create_posts_table.php). The original file names are preserved as the suffix.

Mixed migrations

You can freely combine both formats in the same directory. Timestamped files keep their original prefix, and timeless files receive an auto-generated one:

database/migrations/
β”œβ”€β”€ 2025_01_01_000000_create_users_table.php   ← keeps original timestamp
β”œβ”€β”€ create_posts_table.php                      ← gets timestamp on publish
└── create_comments_table.php                   ← gets timestamp on publish

Specifying migration files

For specific file paths:

$packager->hasMigrations([
    '../www/database/migrations/2023_01_01_000000_create_users_table.php',
    '../api/database/migrations/2023_01_01_000001_create_roles_table.php',
]);

This loads migrations from the database/migrations directory. For a custom directory:

$packager->hasMigrations(directory: 'custom-migrations');

To use an alternative directory for migration files:

$package->hasMigrations(
    ['2023_01_01_000000_create_users_table.php'],
    'userMigrations'
);

For more information about migrations, see Laravel migrations.

Loading migrations without publishing

$packager->canLoadMigrations();

This will load migrations directly when the package is registered, without requiring them to be published first. Works with both timestamped and timeless migration files.

Seeders

Seeders are a publish-only resource. By default all files in the package's database/seeders directory are registered:

$packager->hasSeeders();

Or name them explicitly:

$packager->hasSeeders(['BlogSeeder.php', 'BlogCategorySeeder.php']);

$packager->hasSeeders(
    seederFiles: ['BlogSeeder.php'],
    directory: '../database/seed'
);

Seeders publish flat into the application's database/seeders directory β€” not into a vendor/{package} subdirectory β€” because that is where the application's own Database\Seeders namespace resolves. A published seeder is therefore immediately runnable:

php artisan vendor:publish --tag=my-package::seeders
php artisan db:seed --class=Database\Seeders\BlogSeeder

A seeder may ship as an inert .stub file; it is published as .php, the same convention hasProvider() follows:

$packager->hasSeeders(['../stubs/BlogSeeder.stub']);   // published as BlogSeeder.php

Factories

Model factories work the same way and publish flat into database/factories, where the application's Database\Factories namespace resolves them:

$packager->hasFactories();

$packager->hasFactories(['PostFactory.php']);

Note: Laravel has no framework hook for loading factories out of a package β€” loadFactoriesFrom() was removed in Laravel 8. A package that wants its factories used without publishing them must point at them from its model's newFactory() method; the toolkit cannot do that on the model's behalf.

Translations

To enable translations:

$packager->hasTranslations();

This loads translations from the lang directory and automatically supports JSON translations.

For a custom directory:

$packager->hasTranslations('custom-lang-directory');

The package automatically validates language directory names against supported language codes and detects JSON translation files.

Commands

To enable commands:

$packager->hasCommands();

Defaults to loading commands from the Commands directory. To use an alternative directory for command files.

$packager->hasCommands(directory: 'custom-commands');

For single command:

$packager->hasCommand('\Vendor\Package\Commands\CustomCommand::class');

Or for specific file names:

$packager->hasCommands([
    '\Vendor\Package\Commands\CustomCommand::class',
    '\Vendor\Package\Commands\OtherCommand::class',
]);

For more information about commands, see Laravel commands.

Views

To enable views:

$packager->hasViews();

This loads views from the resources/views directory. For a custom directory:

$packager->hasViews('custom-views');

You can also specify a custom views directory with a different path:

$packager->hasViews(
    viewsPath: 'my-views', 
    directory: '../resources/my-views'
);

or

$packager->hasViews(__DIR__.'/../resources/views/admin', 'admin', 'mypackage-admin');

View Components

To register multiple view components:

$packager->hasComponents(
    prefix: 'nyon',
    components: [
        'data-table' => DataTable::class,
        'modal' => Modal::class,
        Sidebar::class,
    ]
);

To register a single view component with an optional alias:

$packager->hasComponent('nyon', Alert::class, 'custom-alert');

You can then use these components in your Blade templates:

<x-nyon-data-table :data="$users"/>
<x-nyon-modal title="User Details">
    <!-- Modal content -->
</x-modal>
<x-nyon-sidebar id="sidebar"/>
<x-nyon-custom-alert type="warning" message="This is a warning!"/>

View Component Namespaces

To register multiple view component namespaces:

$packager->hasComponentNamespaces(
    namespaces: [
        'nyon' => 'App\View\Components\Alert',
        'admin' => 'App\View\Components\Modal',
    ]
);

To register a single view component namespace with an optional alias:

$packager->hasComponentNamespace('nyon', 'App\View\Components\Alert');

You can then use these namespaces in your Blade templates:

<x-nyon::alert :data="$users"/>
<x-admin::modal title="User Details">
    <!-- Modal content -->
</x-admin-modal>

View Composers

To register multiple view composers:

$packager
    ->hasViewComposer(
        views: 'nyon',
        composers: fn($view) => $view->with('test', 'test-value')
    )->hasViewComposer(
        views: ['viewName', 'anotherViewName'],
        composers: MyViewComposer::class
    );

You can also bind a composer to all views using the wildcard *:

$packager->hasViewComposer('*', function ($view) {
    $view->with('globalData', 'available-everywhere');
});

View Shared Data

To add shared data to views:

$packager->hasSharedDataForAllViews(['key' => 'value', 'user' => 'john']);

This adds key-value pairs to the shared data array in the view. The shared data must have string keys and values must be scalar, array, null, or implement the Arrayable interface.

For more information about shared data, see Laravel shared data.

Assets

To enable assets:

$packager->hasAssets();

This loads assets from the dist directory by default. For a custom directory:

$packager->hasAssets('public');

Assets will be published to public/vendor/{package-short-name} when using the publish command, under both the package's own {package-short-name}::assets tag and Laravel's conventional laravel-assets tag β€” the latter is what the application skeleton already runs from composer's post-update-cmd (vendor:publish --tag=laravel-assets --ansi --force), so one command covers every installed package.

The asset mirror

Publishing is an optimisation, not a requirement. The toolkit registers a shared PublishedAssets resolver in the container that keeps public/vendor/{package-short-name} in step with your asset directory by itself:

use NyonCode\LaravelPackageToolkit\Support\PublishedAssets;

$url = app(PublishedAssets::class)->url('my-package', __DIR__.'/../dist/js/index.js');
// => http://example.test/vendor/my-package/js/index.js?id=1730000000

This is the layer, not the way to reach it. Since 2.4.0 an entry named in hasAssets(entries: [...]) is asked for by its key β€” @packageAssetUrl('my-package', 'js/index.js'), or app(PackageAssets::class)->url('my-package', 'js/index.js') β€” which resolves the same mirror underneath, goes through the application's Vite build when that build covers the entry, and validates the path at declaration instead of resolving a typo to null. Reach for PublishedAssets directly for what is not a declared entry β€” an image or font the template composes itself β€” and for isStale(), which has no counterpart on PackageAssets. Building the tag by hand around it costs rather more than it looks.

The sync is lazy, incremental and self-correcting: the first asset of a package to resolve a URL in a request compares each shipped file against its published counterpart and copies only what is missing or older, so in steady state it is a handful of stat calls and no writes. Copies land through a temporary file and rename(), so a concurrent request never sees a half-written file. Where public/ cannot be written β€” a read-only container, Vapor β€” nothing throws: url() returns null and you fall back to however you served the asset before. The returned URL is cache-busted by the published copy's mtime, which is what makes Livewire's data-navigate-track pick up an upgrade.

To opt out and rely on vendor:publish alone:

$packager->hasAssets(mirror: false);

Rendering them in a template

Name the files a template renders and the toolkit registers the directives that render them β€” no helper class, no hand-written Blade::directive():

$packager->hasAssets(entries: ['css/index.css', 'js/index.js']);
<head>
    @packageStyles('my-package')
</head>
<body>
    @packageScripts('my-package')
</body>

@packageAssets('my-package') renders both, @packageAssetUrl('my-package', 'js/index.js') gives the bare URL, and any of them takes further arguments to render only the entries you name. Paths are checked at registration, so a typo throws where it was declared. .js renders as type="module"; a shipped IIFE bundle says so with Asset::make('js/index.js')->classic().

Naming nothing discovers them

Name no entries and the asset directory answers for itself, the way hasRoutes() and hasViews() already discover theirs:

$packager->hasAssets();

Discovery looks in the directory hasAssets() was given β€” at its root and in its css/ and js/ subdirectories β€” and registers the stylesheets and scripts it finds there, alphabetically. Extensions are an allowlist (css, scss, sass, less, styl, pcss, js, mjs, cjs), so source maps, fonts, images and a manifest.json in the same directory are passed over.

It is not a recursive walk. A code-split build writes its chunks to a subdirectory of its own (assets/ by default), and a chunk is imported by an entry point rather than loaded beside it β€” giving one its own <script> runs the module twice, in the wrong order. Stopping at three directories leaves such a build alone:

dist/
β”œβ”€β”€ assets/blog-DkS9x2.js     βœ— a chunk, and not discovered
β”œβ”€β”€ css/index.css             βœ“
└── js/index.js               βœ“

Naming any entry replaces discovery outright β€” the two do not merge β€” which is how the two things a directory listing cannot answer get said: a code-split build names its entry points, and an IIFE bundle says Asset::make('js/index.js')->classic(), since a discovered script is emitted as a module.

Discovery runs where hasAssets() is called, once per boot, so unlike the mirror it is not deferred until something renders. Naming the entries explicitly is how you skip it.

What the hand-written tag misses

On 2.3.0 the mirror existed and the directives did not, so a package's layout had one way to reach a URL and built the tag around it:

<script src="{{ app(PublishedAssets::class)->url('my-package', $js) }}"></script>

That release has been withdrawn, which retires the pattern with it: from 2.4 there is no version where this is the only option, so it is code to replace rather than a trade-off to weigh. It renders fine, which is what keeps it in codebases β€” here is what it leaves out, almost all of it silently:

  • $js has to come from somewhere. url() takes an absolute filesystem path, so the package needs a class or a view composer holding __DIR__.'/../dist/js/index.js' and handing it to the view β€” the boilerplate hasAssets(entries: [...]) exists to remove.
  • url() returns ?string. Where public/ cannot be written and nothing was published before, this renders src="" β€” which a browser resolves against the current page and fetches the HTML as a script. Nothing throws, nothing 404s, and the page is broken. The directives emit no tag at all in that situation.
  • No type="module", so a Vite bundle's top-level import is a syntax error.
  • No data-navigate-track="reload", which makes ?id=<mtime> a query string nobody reads: Livewire has no reason to full-page-reload a wire:navigate visit, so an upgrade lands as new markup running against the JavaScript the browser already cached.
  • No CSP nonce, so a strict policy under Vite::useCspNonce() blocks the tag.
  • No Vite resolution, so an application compiling this entry into its own build still gets the shipped copy here while every directive on the same page serves the built one.
  • The path is unchecked, so a typo resolves to null β€” the second point again.
  • defer, and stylesheets before scripts, are then also yours to remember.

The declaration knows every one of these, which is why it is the declaration that renders.

Vite β€” in the application, not in the package

The toolkit ships no Vite config and builds nothing. What it supports is the consuming application compiling your package inside its build β€” which is what an application on Tailwind needs anyway, since its config has to see your Blade markup.

$packager
    ->hasAssets(entries: ['css/index.css', 'js/index.js'])
    ->hasViteAssets([
        // Vite source in the package => the shipped file it stands in for
        'resources/css/index.css' => 'css/index.css',
        'resources/js/index.js' => 'js/index.js',
    ]);

An application that wants in lists the sources in its own vite.config.js ('vendor/acme/my-package/resources/js/index.js') and changes nothing else. Each entry then resolves per request: the dev server while npm run dev runs, the application's manifest once it is built, and the shipped file from the mirror otherwise. The template keeps saying @packageAssets('my-package').

Stubs

Stubs are the templates a package's generator commands scaffold from, published so a consumer can customise them:

$packager->hasStubs();

$packager->hasStubs(['command.stub', 'model.stub']);

$packager->hasStubs(
    stubFiles: ['command.stub'],
    directory: '../resources/stubs'
);

They are published to stubs/{package-short-name}/, keeping their original extension:

php artisan vendor:publish --tag=my-package::stubs
# β†’ stubs/my-package/command.stub

The short-name subdirectory matters: stubs/ is a single flat directory shared with php artisan stub:publish and with every other installed package, so publishing there directly invites collisions.

Providers

To enable service providers:

$packager->hasProvider('../stubs/MyProvider.stub');

Support for multiple service providers:

$packager->hasProvider('../stubs/MyProvider.stub')
    ->hasProvider('../stubs/MyOtherProvider.stub');
$packager->hasProviders([
    '../stubs/MyProvider.stub',
    '../stubs/MyOtherProvider.stub',
]);

Service providers will be published to app/Providers/{ProviderName}.php when using the publish command.

Install Command

The package provides a powerful install command system that allows users to easily install and configure your package.

Basic Install Command

To enable the install command:

$packager->hasInstallCommand();

This creates a command {package-short-name}:install that users can run to install your package.

Configuring the Install Command

You can configure what gets installed using a callback:

$packager->hasInstallCommand(function (InstallCommand $command) {
    $command->publishConfig()
        ->publishMigrations()
        ->publishAssets()
        ->publishViews();
});

Install Command Options

The install command supports several configuration options:

$packager
    // Custom command name
    ->installCommandName('setup')  // Creates package:setup instead of package:install
    
    // Hide command from artisan list
    ->installCommandHidden(true)
    
    // Auto-install when package loads
    ->installOnRun(true)
    
    // Install only in specific environments
    ->installOnRunInEnvironment(['local', 'testing'])
    ->installOnRunInLocal()
    ->installOnRunInProduction();

Pre-built Install Configurations

The package provides several pre-built installation configurations:

// Quick install (config, migrations, assets)
$packager->hasQuickInstall();

// Full install (everything)
$packager->hasFullInstall();

// Minimal install (config only)
$packager->hasMinimalInstall();

// Development install (config, migrations, views, assets, routes in local only)
$packager->hasDevInstall();

Advanced Install Command Configuration

For more advanced configurations, you can use the full callback approach:

$packager->hasInstallCommand(function (InstallCommand $command) {
    $command
        ->publishConfig()
        ->publishMigrations()
        ->publishAssets()
        ->publishForEnvironment(['local'], 'routes')
        ->publishForProduction('config')
        ->beforeInstallation(function ($command) {
            $command->info('Starting installation...');
        })
        ->afterInstallation(function ($command) {
            $command->info('Installation completed!');
            $command->call('migrate');
        })
        ->askToStarRepoOnGitHub('https://github.com/your/repo');
});

Available Publishing Methods

The install command supports the following publishing methods:

  • publishConfig() / publishConfigFile() / publishConfigFiles()
  • publishMigrations()
  • publishSeeders()
  • publishFactories()
  • publishRoutes() / publishRouteFiles()
  • publishTranslations() / publishTranslationFiles() / publishLanguageFiles()
  • publishAssets() / publishPublicAssets()
  • publishViews() / publishViewFiles()
  • publishProviders() / publishServiceProviders()
  • publishStubs()
  • publishComponents() / publishViewComponents()
  • publishComponentNamespaces() / publishViewComponentNamespaces()
  • publishEverything() / publishAll()
  • publishEssentials() (config, migrations, assets)

Conditional Publishing

You can conditionally publish resources:

$command
    ->publishIf($someCondition, 'config', 'migrations')
    ->publishUnless($otherCondition, 'routes')
    ->publishForEnvironment(['local', 'testing'], 'routes')
    ->publishForProduction('config')
    ->publishForLocal('assets');

About Command

Laravel Package Builder provides methods to add package information to Laravel's php artisan about command.

hasAbout()

The hasAbout() method allows you to include your package's information in the Laravel About command. By default, it will include the package's version.

$packager->hasAbout();

hasVersion()

The hasVersion() method lets you manually set the version of your package:

$packager->hasVersion('1.0.0');

If no version is manually set, the package will automatically retrieve the version from your composer.json file.

Customizing About Command Data

You can extend the about command information by implementing the aboutData() method in your service provider:

public function aboutData(): array
{
    return [
        'Repository' => 'https://github.com/your/package',
        'Author' => 'Your Name',
        'License' => 'MIT',
        'Documentation' => 'https://docs.example.com',
    ];
}

This method allows you to add custom key-value pairs to the About command output for your package. When you run php artisan about, your package's information will be displayed in a dedicated section. This implementation allows for flexible and easy inclusion of package metadata in Laravel's system information command.

Publishing

For publishing, you can use the following commands:

php artisan vendor:publish

vendor:publish show all the tags that can be used for publishing.

Available Publishing Tags

Each resource type has its own publishing tag in the format {package-short-name}::{resource-type}:

  • {package-name}::config - Configuration files
  • {package-name}::migrations - Database migrations
  • {package-name}::seeders - Database seeders
  • {package-name}::factories - Model factories
  • {package-name}::routes - Route files
  • {package-name}::translations - Translation files
  • {package-name}::assets - Public assets
  • {package-name}::views - View files
  • {package-name}::providers - Service providers
  • {package-name}::stubs - Generator stubs
  • {package-name}::view-components - View components
  • {package-name}::view-component-namespaces - View component namespaces

Example of using tags:

Use php artisan vendor:publish --tag=package-short-name::config for publish configuration files.

# Publish specific resources
php artisan vendor:publish --tag=my-package::config
php artisan vendor:publish --tag=my-package::migrations
php artisan vendor:publish --tag=my-package::assets

# Publish with force (overwrite existing files)
php artisan vendor:publish --tag=my-package::config --force

Custom tag separator (classic flat format)

By default tags use the :: separator (my-package::config). If you prefer the classic flat format that many packages use (my-package-config), set a custom separator with hasPublishTagSeparator():

$packager
    ->name('Backup Manager')
    ->hasPublishTagSeparator('-')   // tags become {short-name}-{resource}
    ->hasConfig();

The separator applies to every publish tag and to the install command consistently:

php artisan vendor:publish --tag="backup-manager-config"
php artisan vendor:publish --tag="backup-manager-migrations"

To register every group under multiple tag forms at once β€” so consumers can publish with either β€” pass an array. The first separator is treated as primary (used by the install command):

$packager->hasPublishTagSeparator(['::', '-']);
# both work and publish the same resource
php artisan vendor:publish --tag="backup-manager::config"
php artisan vendor:publish --tag="backup-manager-config"

Migration publishing behavior

When publishing migrations, the behavior depends on the file format:

  • Timestamped migrations (e.g. 2025_01_01_000000_create_users_table.php) are published as-is with their original filename.
  • Timeless migrations (e.g. create_posts_table.php) automatically receive a timestamp prefix at the time of publishing to ensure correct execution order.
  • Mixed directories are handled per-file β€” each file is treated individually based on whether it has a date prefix or not.

AI agents

An agent asked to add a resource to your package will otherwise guess at this API from whatever release was in its training data. The toolkit ships its own documentation for agents, inside the package, so what they read is the version in your composer.lock:

vendor/bin/package-toolkit-ai install

That does three independent things, each of which works without the others:

AGENTS.md A delimited block referencing vendor/nyoncode/laravel-package-toolkit/ai/AGENTS.md β€” the complete public API in one file. Read by Claude Code, Cursor, Codex, Copilot, Windsurf, Zed.
Claude Code skill .claude/skills/laravel-package-toolkit/SKILL.md, loaded when the work is about the toolkit rather than on every turn.
MCP server search_docs, list_docs, get_doc, list_api and describe_api β€” the last two parse the installed src/, so signatures come from the release you have. Node 18+, no dependencies.

Re-running after an upgrade refreshes what changed; status reports what is wired up, remove undoes all of it, and --dry-run writes nothing. Skip pieces with --no-skill / --no-mcp.

The documentation site also publishes itself in machine-readable form β€” llms.txt, llms-full.txt, and a .md twin of every page. Full detail: AI agents.

Testing

composer test

The package includes comprehensive tests for all features including:

  • Configuration loading and publishing
  • Route registration
  • Middleware registration
  • Migration handling (timestamped, timeless, and mixed)
  • Translation loading
  • View and component registration
  • Command registration
  • Install command functionality
  • Lifecycle hooks
  • Conditional loading

Upgrading to v2.1

Version 2.1 drops support for Laravel 10.x and 11.x. Both have reached security-support end-of-life, and the June 2026 security advisories were only patched in Laravel 12.60+/13.9+ β€” never backported to 10.x or 11.x β€” so there is no secure release on those branches. The package now requires Laravel 12 (>= 12.61.1) or 13 (>= 13.12.0).

If your project still depends on Laravel 10 or 11, stay on the ~2.0.0 release. Otherwise no code changes are required β€” update the constraint and run composer update:

{
	"require": {
		"nyoncode/laravel-package-toolkit": "^2.1"
	}
}

Upgrading from v1.x

Version 2.0 introduces the following breaking changes:

  • Dropped Laravel 9.x support β€” Laravel 9 reached end-of-life and no longer receives security updates. If your project still depends on Laravel 9, continue using ^1.0.
  • Minimum PHP version raised to 8.2 β€” Aligning with Laravel 11+ requirements.
  • Added Laravel 13.x support β€” Full compatibility with the latest Laravel release.
  • Timeless migrations β€” Migrations without a date prefix are now supported. This is a non-breaking addition but changes the internal publishing behavior when timeless files are detected. Existing timestamped migrations are unaffected.

To upgrade, update your composer.json:

{
	"require": {
		"nyoncode/laravel-package-toolkit": "^2.0"
	}
}

Then run composer update. No code changes are required unless your package explicitly depends on Laravel 9 or PHP 8.1.

Versioning

This package follows Semantic Versioning (SemVer).

Given a version number MAJOR.MINOR.PATCH, we increment the:

  • MAJOR version when we make incompatible API changes
  • MINOR version when we add functionality in a backwards compatible manner
  • PATCH version when we make backwards compatible bug fixes

Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format.

Compatibility Promise

  • Major versions may contain breaking changes
  • Minor versions will maintain backward compatibility within the same major version
  • Patch versions will only contain bug fixes and security updates

We recommend using version constraints in your composer.json that allow for minor and patch updates but protect against major version changes:

{
	"require": {
		"nyoncode/laravel-package-toolkit": "^2.0"
	}
}

License

The MIT License (MIT). Please see License File for more information.