Builder

laravel-package

Creates distributable Laravel packages for Packagist

Overview

The laravel-package agent creates distributable Laravel packages ready for Packagist publication. It scaffolds the package structure, configures autoloading, creates service providers, sets up testing, and generates documentation following community standards.

Responsibilities

  • Package Scaffolding - Complete package directory structure
  • Service Provider - Auto-discovery and configuration publishing
  • Composer Setup - Autoloading, dependencies, scripts
  • Testing Setup - Orchestra Testbench configuration
  • Documentation - README, changelog, contributing guide
  • CI/CD - GitHub Actions for testing and publishing

Package Structure

packages/vendor-name/package-name/
├── .github/
│   └── workflows/
│       ├── tests.yml
│       └── release.yml
├── config/
│   └── package-name.php
├── database/
│   └── migrations/
├── resources/
│   └── views/
├── src/
│   ├── Commands/
│   ├── Facades/
│   ├── Http/
│   │   ├── Controllers/
│   │   └── Middleware/
│   ├── Models/
│   ├── PackageNameServiceProvider.php
│   └── PackageName.php
├── tests/
│   ├── Feature/
│   ├── Unit/
│   └── TestCase.php
├── .gitignore
├── CHANGELOG.md
├── composer.json
├── LICENSE.md
├── phpunit.xml
└── README.md

Generated Service Provider

<?php

namespace VendorName\PackageName;

use Illuminate\Support\ServiceProvider;

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

        $this->app->singleton('package-name', function ($app) {
            return new PackageName($app['config']['package-name']);
        });
    }

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

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

        // Load migrations
        $this->loadMigrationsFrom(__DIR__ . '/../database/migrations');

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

        // Register commands
        if ($this->app->runningInConsole()) {
            $this->commands([
                Commands\InstallCommand::class,
                Commands\PublishCommand::class,
            ]);
        }

        // Register routes
        $this->loadRoutesFrom(__DIR__ . '/../routes/web.php');
    }
}

Generated Facade

<?php

namespace VendorName\PackageName\Facades;

use Illuminate\Support\Facades\Facade;

/**
 * @method static mixed doSomething(string $param)
 * @method static self configure(array $options)
 *
 * @see \VendorName\PackageName\PackageName
 */
class PackageName extends Facade
{
    protected static function getFacadeAccessor(): string
    {
        return 'package-name';
    }
}

Composer Configuration

{
    "name": "vendor-name/package-name",
    "description": "A Laravel package that does something awesome",
    "keywords": ["laravel", "package"],
    "license": "MIT",
    "authors": [
        {
            "name": "Your Name",
            "email": "you@example.com"
        }
    ],
    "require": {
        "php": "^8.2",
        "illuminate/support": "^11.0"
    },
    "require-dev": {
        "orchestra/testbench": "^9.0",
        "pestphp/pest": "^2.0",
        "laravel/pint": "^1.0"
    },
    "autoload": {
        "psr-4": {
            "VendorName\\PackageName\\": "src/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "VendorName\\PackageName\\Tests\\": "tests/"
        }
    },
    "scripts": {
        "test": "vendor/bin/pest",
        "format": "vendor/bin/pint"
    },
    "extra": {
        "laravel": {
            "providers": [
                "VendorName\\PackageName\\PackageNameServiceProvider"
            ],
            "aliases": {
                "PackageName": "VendorName\\PackageName\\Facades\\PackageName"
            }
        }
    },
    "minimum-stability": "stable"
}

Test Setup with Orchestra Testbench

<?php

namespace VendorName\PackageName\Tests;

use Orchestra\Testbench\TestCase as Orchestra;
use VendorName\PackageName\PackageNameServiceProvider;

class TestCase extends Orchestra
{
    protected function setUp(): void
    {
        parent::setUp();

        // Run package migrations
        $this->loadMigrationsFrom(__DIR__ . '/../database/migrations');
    }

    protected function getPackageProviders($app): array
    {
        return [
            PackageNameServiceProvider::class,
        ];
    }

    protected function getPackageAliases($app): array
    {
        return [
            'PackageName' => \VendorName\PackageName\Facades\PackageName::class,
        ];
    }

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

// tests/Feature/ExampleTest.php
use VendorName\PackageName\Facades\PackageName;

it('can do something', function () {
    $result = PackageName::doSomething('test');

    expect($result)->toBe('expected');
});

GitHub Actions CI

# .github/workflows/tests.yml
name: Tests

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        php: [8.2, 8.3]
        laravel: [11.*]
        dependency-version: [prefer-lowest, prefer-stable]

    name: PHP $ - Laravel $ - $

    steps:
      - uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: $
          extensions: dom, curl, libxml, mbstring, zip
          coverage: none

      - name: Install dependencies
        run: |
          composer require "laravel/framework:$" --no-interaction --no-update
          composer update --$ --prefer-dist --no-interaction

      - name: Run tests
        run: vendor/bin/pest

Install Command

<?php

namespace VendorName\PackageName\Commands;

use Illuminate\Console\Command;

class InstallCommand extends Command
{
    protected $signature = 'package-name:install';
    protected $description = 'Install the PackageName package';

    public function handle(): int
    {
        $this->info('Installing PackageName...');

        // Publish config
        $this->call('vendor:publish', [
            '--tag' => 'package-name-config',
        ]);

        // Publish migrations
        if ($this->confirm('Would you like to publish migrations?', true)) {
            $this->call('vendor:publish', [
                '--tag' => 'package-name-migrations',
            ]);

            if ($this->confirm('Run migrations now?', true)) {
                $this->call('migrate');
            }
        }

        $this->info('PackageName installed successfully!');

        return self::SUCCESS;
    }
}

Local Development Setup

// In your Laravel app's composer.json
{
    "repositories": [
        {
            "type": "path",
            "url": "./packages/vendor-name/package-name"
        }
    ],
    "require": {
        "vendor-name/package-name": "*"
    }
}

// Then run:
// composer update vendor-name/package-name

Invoked By Commands

Guardrails

The package agent follows strict rules:

  • ALWAYS use PSR-4 autoloading
  • ALWAYS include comprehensive tests with Testbench
  • ALWAYS support Laravel auto-discovery
  • NEVER hardcode application-specific dependencies
  • NEVER publish to Packagist without tests passing

See Also