Creating Your First Laravel Package
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
- Set up package structure
- Create service provider
- Add configuration
- Include migrations and views
- Write tests
- 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
- Push to GitHub
- Tag a release:
git tag v1.0.0 && git push --tags - Register on packagist.org
- 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.