gary.info

here be dragons

Laravel Package Creation: The Complete TL;DR Guide

create-laravel-package.md

Laravel Package Creation: The Complete TL;DR Guide

Laravel Packages Are Just Structured PHP with Magic Wiring

Think of a Laravel package as a mini-application that plugs into any Laravel project. It's PHP code organized with specific conventions that Laravel recognizes and automatically integrates. The "magic" happens through Service Providers - special classes that tell Laravel "here's my code, here's how to use it."

Every package needs: a composer.json file (the package's ID card), a Service Provider (the integration bridge), and your actual code. Laravel handles the rest through auto-discovery.

Composer.json Is Your Package's Birth Certificate

{
    "name": "vendor/package-name",
    "type": "library",
    "require": {
        "illuminate/support": "^8.0|^9.0|^10.0"
    },
    "autoload": {
        "psr-4": {
            "VendorName\\PackageName\\": "src/"
        }
    },
    "extra": {
        "laravel": {
            "providers": [
                "VendorName\\PackageName\\PackageServiceProvider"
            ]
        }
    }
}

The name field determines how people install your package (composer require vendor/package-name). The autoload section maps namespaces to directories. The extra.laravel.providers enables auto-discovery - Laravel automatically finds and loads your Service Provider.

Service Providers Are the Universal Plugin System

class PackageServiceProvider extends ServiceProvider
{
    public function register()
    {
        // Bind services into container
        $this->app->singleton(MyService::class);
    }

    public function boot()
    {
        // Publish assets, register routes, load views
        $this->loadRoutesFrom(__DIR__.'/../routes/web.php');
        $this->loadViewsFrom(__DIR__.'/../resources/views', 'mypackage');
        $this->publishes([
            __DIR__.'/../config/mypackage.php' => config_path('mypackage.php'),
        ]);
    }
}

register() runs first and binds services. boot() runs after all packages are registered and handles publishing assets, loading routes, views, and migrations. This two-phase loading prevents dependency conflicts.

Package Structure Follows Convention Over Configuration

src/
├── PackageServiceProvider.php  # The bridge to Laravel
├── Facades/                   # Optional static access
├── Commands/                  # Artisan commands
├── Http/
│   ├── Controllers/          # Package controllers
│   └── Middleware/           # Package middleware
└── Services/                 # Core business logic

config/
└── mypackage.php             # Default configuration

resources/
├── views/                    # Blade templates
└── lang/                     # Translations

database/
├── migrations/               # Database changes
└── factories/                # Model factories

routes/
├── web.php                   # Web routes
└── api.php                   # API routes

This structure mirrors Laravel applications. Laravel automatically discovers and loads files from these locations when you use the appropriate Service Provider methods.

Testing Packages Requires Laravel's Test Environment

// tests/TestCase.php
abstract class TestCase extends Orchestra\TestCase
{
    protected function getPackageProviders($app)
    {
        return [PackageServiceProvider::class];
    }

    protected function defineEnvironment($app)
    {
        $app['config']->set('database.default', 'testing');
        $app['config']->set('database.connections.testing', [
            'driver' => 'sqlite',
            'database' => ':memory:',
        ]);
    }
}

Orchestra Testbench provides a minimal Laravel environment for testing. Your tests extend this base class, which loads your Service Provider and sets up a testing database. This ensures your package works in real Laravel applications.

Publishing Assets Lets Users Customize Your Package

// In your Service Provider's boot() method
$this->publishes([
    __DIR__.'/../config/mypackage.php' => config_path('mypackage.php'),
], 'config');

$this->publishes([
    __DIR__.'/../resources/views' => resource_path('views/vendor/mypackage'),
], 'views');

$this->publishes([
    __DIR__.'/../database/migrations' => database_path('migrations'),
], 'migrations');

Publishing copies files from your package to the user's Laravel application. Users run php artisan vendor:publish --provider=YourServiceProvider --tag=config to copy your config file to their app, where they can modify it. This balances sensible defaults with customization.

Distribution Through Packagist Makes Installation One Command

  • Push your package to GitHub/GitLab with proper versioning tags
  • Submit to Packagist.org (it's free and automated)
  • Users install with composer require vendor/package-name
  • Laravel auto-discovers and loads your Service Provider
Packagist syncs with your Git repository. When you push a new tag (git tag v1.0.0 && git push --tags), Packagist automatically creates a new release. Semantic versioning (1.0.0, 1.1.0, 2.0.0) helps users understand compatibility.

Advanced Features: Commands, Facades, and Configuration

// Artisan Commands
protected $commands = [
    Commands\MyPackageCommand::class,
];

// Facades for static access
class MyPackageFacade extends Facade
{
    protected static function getFacadeAccessor()
    {
        return MyService::class;
    }
}

// Configuration merging
$this->mergeConfigFrom(__DIR__.'/../config/mypackage.php', 'mypackage');

Commands let users interact with your package via php artisan. Facades provide static access (MyPackage::doSomething()). Configuration merging combines your defaults with user settings, ensuring your package works even if users don't publish your config file.


Transform any Laravel functionality into a reusable package following these patterns. Start small, test thoroughly, and publish early.