Creating Your First Laravel Package

· 3 min read
laravel php packages open-source

What We’re Building

A Laravel package you can share across projects or publish to Packagist. We’ll cover service providers, configuration, migrations, and testing.

Prerequisites

  • Laravel application for testing
  • Composer installed globally
  • Understanding of service providers

The Approach

  1. Set up package structure
  2. Create service provider
  3. Add configuration
  4. Include migrations and views
  5. Write tests
  6. Prepare for publishing

Step 1: Create Package Structure

mkdir -p packages/your-vendor/your-package
cd packages/your-vendor/your-package

Create composer.json:

{
    "name": "your-vendor/your-package",
    "description": "A useful Laravel package",
    "type": "library",
    "license": "MIT",
    "require": {
        "php": "^8.1",
        "illuminate/support": "^10.0|^11.0"
    },
    "require-dev": {
        "orchestra/testbench": "^8.0|^9.0",
        "phpunit/phpunit": "^10.0"
    },
    "autoload": {
        "psr-4": {
            "YourVendor\\YourPackage\\": "src/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "YourVendor\\YourPackage\\Tests\\": "tests/"
        }
    },
    "extra": {
        "laravel": {
            "providers": [
                "YourVendor\\YourPackage\\YourPackageServiceProvider"
            ]
        }
    }
}

Directory structure:

your-package/
├── config/
│   └── your-package.php
├── database/
│   └── migrations/
├── resources/
│   └── views/
├── src/
│   ├── YourPackageServiceProvider.php
│   └── YourPackage.php
├── tests/
│   ├── TestCase.php
│   └── Feature/
├── composer.json
└── phpunit.xml

Step 2: Service Provider

// src/YourPackageServiceProvider.php
namespace YourVendor\YourPackage;

use Illuminate\Support\ServiceProvider;

class YourPackageServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->mergeConfigFrom(
            __DIR__.'/../config/your-package.php',
            'your-package'
        );

        $this->app->singleton(YourPackage::class, function ($app) {
            return new YourPackage(
                config('your-package')
            );
        });
    }

    public function boot(): void
    {
        if ($this->app->runningInConsole()) {
            $this->publishes([
                __DIR__.'/../config/your-package.php' => config_path('your-package.php'),
            ], 'your-package-config');

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

        $this->loadMigrationsFrom(__DIR__.'/../database/migrations');
        $this->loadViewsFrom(__DIR__.'/../resources/views', 'your-package');
        $this->loadRoutesFrom(__DIR__.'/../routes/web.php');
    }
}

Step 3: Configuration File

// config/your-package.php
return [
    'enabled' => env('YOUR_PACKAGE_ENABLED', true),
    'api_key' => env('YOUR_PACKAGE_API_KEY'),
    'cache_ttl' => env('YOUR_PACKAGE_CACHE_TTL', 3600),
];

Step 4: Main Package Class

// src/YourPackage.php
namespace YourVendor\YourPackage;

class YourPackage
{
    public function __construct(private array $config) {}

    public function doSomething(): string
    {
        if (!$this->config['enabled']) {
            return 'Package is disabled';
        }

        return 'Package is working!';
    }
}

Step 5: Add a Facade

// src/Facades/YourPackage.php
namespace YourVendor\YourPackage\Facades;

use Illuminate\Support\Facades\Facade;

class YourPackage extends Facade
{
    protected static function getFacadeAccessor(): string
    {
        return \YourVendor\YourPackage\YourPackage::class;
    }
}

Register in service provider:

public function register(): void
{
    // ...existing code...

    $this->app->alias(YourPackage::class, 'your-package');
}

Step 6: Set Up Testing

// tests/TestCase.php
namespace YourVendor\YourPackage\Tests;

use Orchestra\Testbench\TestCase as Orchestra;
use YourVendor\YourPackage\YourPackageServiceProvider;

class TestCase extends Orchestra
{
    protected function getPackageProviders($app): array
    {
        return [
            YourPackageServiceProvider::class,
        ];
    }

    protected function defineEnvironment($app): void
    {
        $app['config']->set('your-package.enabled', true);
    }
}
// tests/Feature/YourPackageTest.php
namespace YourVendor\YourPackage\Tests\Feature;

use YourVendor\YourPackage\Tests\TestCase;
use YourVendor\YourPackage\YourPackage;

class YourPackageTest extends TestCase
{
    public function test_package_works(): void
    {
        $package = app(YourPackage::class);

        $this->assertEquals('Package is working!', $package->doSomething());
    }

    public function test_package_can_be_disabled(): void
    {
        config(['your-package.enabled' => false]);

        $package = app(YourPackage::class);

        $this->assertEquals('Package is disabled', $package->doSomething());
    }
}
<!-- phpunit.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<phpunit colors="true" bootstrap="vendor/autoload.php">
    <testsuites>
        <testsuite name="Feature">
            <directory>tests/Feature</directory>
        </testsuite>
    </testsuites>
</phpunit>

Step 7: Local Development

In your Laravel app’s composer.json:

{
    "repositories": [
        {
            "type": "path",
            "url": "./packages/your-vendor/your-package"
        }
    ],
    "require": {
        "your-vendor/your-package": "@dev"
    }
}
composer update your-vendor/your-package

Step 8: Publish to Packagist

  1. Push to GitHub
  2. Tag a release: git tag v1.0.0 && git push --tags
  3. Register on packagist.org
  4. Submit your repository URL

The Result

  • Reusable package with proper service provider
  • Configuration publishing
  • Migrations and views
  • Testable with Orchestra Testbench
  • Ready for Packagist

What I’d Do Differently

Start with tests. I wrote the package first, then struggled to retrofit tests. Orchestra Testbench makes testing packages easy-use it from the start.


Once you’ve built a few packages, you’ll start seeing extraction opportunities everywhere. That’s a good instinct-reusable code is maintainable code.

Related Posts

Comments