Laravel Framework
Building Reusable Packages
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
[](https://packagist.org/packages/yourname/package-name)
[](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:
- Create a package structure with service provider
- Add a middleware to track page views
- Create migrations for storing analytics data
- Build a dashboard controller with charts
- Add Blade components for displaying metrics
- Write tests for tracking functionality
Exercise 2: Build a notification center package:
- Create models for notifications with types (info, warning, error)
- Add a service provider with configuration
- Create artisan commands to send notifications
- Build API endpoints for fetching notifications
- Add a Blade component for notification dropdown
- Publish package to GitHub and tag v1.0.0
Exercise 3: Develop a settings management package:
- Create a service provider with publishable config
- Build migrations for storing settings in database
- Add a facade for easy access to settings
- Create a UI for managing settings
- Add caching for better performance
- Write comprehensive tests and documentation