Laravel Framework

Building Reusable Packages

18 min Lesson 44 of 45

Building Reusable Packages

Laravel packages allow you to add functionality to Laravel applications in a modular, reusable way. Whether you're building internal tools or open-source libraries, understanding package development is essential for advanced Laravel developers.

Package Structure and Setup

Why Create Packages?
  • Reusability: Share code across multiple projects
  • Modularity: Keep features separate and maintainable
  • Open Source: Contribute to the Laravel ecosystem
  • Organization: Better code organization for large applications
  • Testing: Easier to test isolated functionality

Creating a Package

// Recommended package structure packages/ └── yourname/ └── package-name/ ├── src/ │ ├── PackageServiceProvider.php │ ├── Facades/ │ ├── Commands/ │ ├── Controllers/ │ ├── Models/ │ ├── Middleware/ │ └── config/ ├── resources/ │ ├── views/ │ ├── lang/ │ └── assets/ ├── database/ │ ├── migrations/ │ └── seeders/ ├── routes/ │ ├── web.php │ └── api.php ├── tests/ │ ├── Unit/ │ └── Feature/ ├── composer.json ├── README.md └── LICENSE // composer.json for your package { "name": "yourname/package-name", "description": "A Laravel package for...", "type": "library", "license": "MIT", "authors": [ { "name": "Your Name", "email": "your@email.com" } ], "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": { "YourName\\PackageName\\": "src/" } }, "autoload-dev": { "psr-4": { "YourName\\PackageName\\Tests\\": "tests/" } }, "extra": { "laravel": { "providers": [ "YourName\\PackageName\\PackageServiceProvider" ], "aliases": { "PackageName": "YourName\\PackageName\\Facades\\PackageName" } } }, "minimum-stability": "dev", "prefer-stable": true }

Service Provider - The Heart of Your Package

// src/PackageServiceProvider.php namespace YourName\PackageName; use Illuminate\Support\ServiceProvider; use YourName\PackageName\Commands\InstallCommand; use YourName\PackageName\View\Components\Alert; class PackageServiceProvider extends ServiceProvider { /** * Register services. */ public function register(): void { // Merge package config with app config $this->mergeConfigFrom( __DIR__.'/../config/package-name.php', 'package-name' ); // Register singleton services $this->app->singleton('package-name', function ($app) { return new PackageClass($app['config']['package-name']); }); // Register facades $this->app->alias('package-name', PackageName::class); } /** * Bootstrap services. */ public function boot(): void { // Publish configuration $this->publishes([ __DIR__.'/../config/package-name.php' => config_path('package-name.php'), ], 'config'); // Publish migrations $this->publishes([ __DIR__.'/../database/migrations/' => database_path('migrations'), ], 'migrations'); // Load migrations automatically (optional) $this->loadMigrationsFrom(__DIR__.'/../database/migrations'); // Publish views $this->publishes([ __DIR__.'/../resources/views' => resource_path('views/vendor/package-name'), ], 'views'); // Load views $this->loadViewsFrom(__DIR__.'/../resources/views', 'package-name'); // Load translations $this->loadTranslationsFrom(__DIR__.'/../resources/lang', 'package-name'); // Publish translations $this->publishes([ __DIR__.'/../resources/lang' => lang_path('vendor/package-name'), ], 'lang'); // Register routes $this->loadRoutesFrom(__DIR__.'/../routes/web.php'); // Register commands if ($this->app->runningInConsole()) { $this->commands([ InstallCommand::class, ]); } // Register Blade components $this->loadViewComponentsAs('package', [ Alert::class, ]); // Publish assets $this->publishes([ __DIR__.'/../resources/assets' => public_path('vendor/package-name'), ], 'public'); } }
Service Provider Best Practices:
  • Use register() for binding services to the container
  • Use boot() for actions that depend on other services
  • Provide sensible defaults in your config
  • Tag publishable assets with descriptive names
  • Make migrations optional - let users publish them

Creating Package Configuration

// config/package-name.php return [ /* |-------------------------------------------------------------------------- | Default Settings |-------------------------------------------------------------------------- */ 'enabled' => env('PACKAGE_ENABLED', true), 'api_key' => env('PACKAGE_API_KEY'), 'cache' => [ 'driver' => env('PACKAGE_CACHE_DRIVER', 'redis'), 'ttl' => env('PACKAGE_CACHE_TTL', 3600), ], /* |-------------------------------------------------------------------------- | Advanced Options |-------------------------------------------------------------------------- */ 'features' => [ 'feature_one' => true, 'feature_two' => false, ], 'routes' => [ 'prefix' => 'package', 'middleware' => ['web', 'auth'], ], ]; // Accessing configuration in your package namespace YourName\PackageName; class PackageClass { public function __construct(protected array $config) { } public function isEnabled(): bool { return config('package-name.enabled', true); } public function getApiKey(): ?string { return config('package-name.api_key'); } }

Package Routes and Controllers

// routes/web.php use Illuminate\Support\Facades\Route; use YourName\PackageName\Http\Controllers\PackageController; Route::prefix(config('package-name.routes.prefix', 'package')) ->middleware(config('package-name.routes.middleware', ['web'])) ->name('package.') ->group(function () { Route::get('/', [PackageController::class, 'index'])->name('index'); Route::get('/settings', [PackageController::class, 'settings'])->name('settings'); Route::post('/settings', [PackageController::class, 'updateSettings'])->name('settings.update'); }); // src/Http/Controllers/PackageController.php namespace YourName\PackageName\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Routing\Controller; class PackageController extends Controller { public function index() { return view('package-name::index', [ 'title' => __('package-name::messages.welcome'), ]); } public function settings() { return view('package-name::settings', [ 'config' => config('package-name'), ]); } public function updateSettings(Request $request) { $validated = $request->validate([ 'api_key' => 'required|string', 'enabled' => 'boolean', ]); // Update settings logic return redirect() ->route('package.settings') ->with('success', __('package-name::messages.settings_updated')); } }

Package Views and Components

// resources/views/index.blade.php <!DOCTYPE html> <html> <head> <title>{{ $title }}</title> <link href="{{ asset('vendor/package-name/css/style.css') }}" rel="stylesheet"> </head> <body> <h1>{{ $title }}</h1> <x-package::alert type="success"> Package is working correctly! </x-package::alert> @yield('content') <script src="{{ asset('vendor/package-name/js/script.js') }}"></script> </body> </html> // src/View/Components/Alert.php namespace YourName\PackageName\View\Components; use Illuminate\View\Component; class Alert extends Component { public function __construct( public string $type = 'info', public ?string $title = null, ) {} public function render() { return view('package-name::components.alert'); } } // resources/views/components/alert.blade.php <div class="alert alert-{{ $type }}" role="alert"> @if($title) <h4 class="alert-heading">{{ $title }}</h4> @endif {{ $slot }} </div>

Package Commands

// src/Commands/InstallCommand.php namespace YourName\PackageName\Commands; use Illuminate\Console\Command; class InstallCommand extends Command { protected $signature = 'package:install {--force : Overwrite existing files} {--without-migrations : Skip publishing migrations}'; protected $description = 'Install the Package Name package'; public function handle() { $this->info('Installing Package Name...'); // Publish configuration $this->call('vendor:publish', [ '--provider' => 'YourName\\PackageName\\PackageServiceProvider', '--tag' => 'config', '--force' => $this->option('force'), ]); // Publish migrations if (!$this->option('without-migrations')) { $this->call('vendor:publish', [ '--provider' => 'YourName\\PackageName\\PackageServiceProvider', '--tag' => 'migrations', '--force' => $this->option('force'), ]); } // Publish assets $this->call('vendor:publish', [ '--provider' => 'YourName\\PackageName\\PackageServiceProvider', '--tag' => 'public', '--force' => $this->option('force'), ]); // Run migrations if ($this->confirm('Would you like to run the migrations now?')) { $this->call('migrate'); } $this->info('Package Name installed successfully!'); $this->comment('Please run: php artisan package:install to complete setup'); } }
Package Migration Considerations:
  • Use descriptive migration names with timestamps
  • Make migrations idempotent (can be run multiple times)
  • Provide down() methods for rollbacks
  • Consider making migrations optional via publish
  • Document any required manual steps

Testing Your Package

// tests/TestCase.php namespace YourName\PackageName\Tests; use Orchestra\Testbench\TestCase as Orchestra; use YourName\PackageName\PackageServiceProvider; abstract class TestCase extends Orchestra { protected function setUp(): void { parent::setUp(); // Load migrations $this->loadMigrationsFrom(__DIR__ . '/../database/migrations'); // Additional setup } protected function getPackageProviders($app) { return [ PackageServiceProvider::class, ]; } protected function getEnvironmentSetUp($app) { // Setup test environment $app['config']->set('database.default', 'testing'); $app['config']->set('package-name.enabled', true); } } // tests/Feature/PackageTest.php namespace YourName\PackageName\Tests\Feature; use YourName\PackageName\Tests\TestCase; class PackageTest extends TestCase { /** @test */ public function it_can_access_package_routes() { $response = $this->get(route('package.index')); $response->assertStatus(200); $response->assertSee('Package is working'); } /** @test */ public function it_loads_configuration_correctly() { $this->assertTrue(config('package-name.enabled')); $this->assertEquals('package', config('package-name.routes.prefix')); } } // tests/Unit/PackageClassTest.php namespace YourName\PackageName\Tests\Unit; use YourName\PackageName\PackageClass; use YourName\PackageName\Tests\TestCase; class PackageClassTest extends TestCase { /** @test */ public function it_can_instantiate_package_class() { $package = app('package-name'); $this->assertInstanceOf(PackageClass::class, $package); } } // Run tests ./vendor/bin/phpunit // Or with coverage ./vendor/bin/phpunit --coverage-html coverage

Publishing to Packagist

// 1. Prepare your package // - Ensure composer.json is complete // - Add README.md with installation and usage instructions // - Add LICENSE file (MIT is common) // - Add CHANGELOG.md // - Tag a release // 2. Create a repository on GitHub git init git add . git commit -m "Initial commit" git remote add origin https://github.com/yourname/package-name.git git push -u origin main // 3. Tag your first release git tag -a v1.0.0 -m "First release" git push origin v1.0.0 // 4. Submit to Packagist // - Go to https://packagist.org // - Click "Submit" // - Enter your GitHub repository URL // - Packagist will automatically fetch updates on new tags // 5. Set up auto-updating (optional) // Add a GitHub webhook in your repository settings: // URL: https://packagist.org/api/github?username=YOUR_USERNAME // Content type: application/json // Events: Just the push event // README.md example # Package Name [![Latest Version on Packagist](https://img.shields.io/packagist/v/yourname/package-name.svg)](https://packagist.org/packages/yourname/package-name) [![Total Downloads](https://img.shields.io/packagist/dt/yourname/package-name.svg)](https://packagist.org/packages/yourname/package-name) Description of your package. ## Installation ```bash composer require yourname/package-name ``` ## Configuration Publish the config file: ```bash php artisan vendor:publish --provider="YourName\PackageName\PackageServiceProvider" --tag="config" ``` ## Usage ```php use YourName\PackageName\Facades\PackageName; PackageName::doSomething(); ``` ## Testing ```bash composer test ``` ## Changelog Please see [CHANGELOG](CHANGELOG.md) for more information. ## Contributing Please see [CONTRIBUTING](CONTRIBUTING.md) for details. ## Credits - [Your Name](https://github.com/yourname) ## License The MIT License (MIT). Please see [License File](LICENSE.md) for more information.
Exercise 1: Create a simple analytics package:
  1. Create a package structure with service provider
  2. Add a middleware to track page views
  3. Create migrations for storing analytics data
  4. Build a dashboard controller with charts
  5. Add Blade components for displaying metrics
  6. Write tests for tracking functionality
Exercise 2: Build a notification center package:
  1. Create models for notifications with types (info, warning, error)
  2. Add a service provider with configuration
  3. Create artisan commands to send notifications
  4. Build API endpoints for fetching notifications
  5. Add a Blade component for notification dropdown
  6. Publish package to GitHub and tag v1.0.0
Exercise 3: Develop a settings management package:
  1. Create a service provider with publishable config
  2. Build migrations for storing settings in database
  3. Add a facade for easy access to settings
  4. Create a UI for managing settings
  5. Add caching for better performance
  6. Write comprehensive tests and documentation