Creating Laravel packages has taught me more about good software design than almost any other activity. When you build a package, you're forced to think about clean APIs, clear documentation, and maintainable code because other developers will judge your work (sometimes harshly). Here's what I've learned about building packages that people actually want to use and contribute to.
The best packages solve specific problems elegantly. They have clear APIs, excellent documentation, and work reliably without surprising developers.
Finding Problems Worth Solving
Not every piece of code deserves to become a package. The most successful packages I've seen (and built) solve real problems that multiple developers face repeatedly.
Good Package Ideas
Worth Building
- • Code you've copy-pasted across projects
- • Solutions to framework limitations
- • Integration with external services
- • Developer experience improvements
- • Performance optimizations
Think Twice
- • Features Laravel already provides
- • Highly specific business logic
- • One-off utility functions
- • Complex domain-specific tools
- • Packages that already exist and work well
Package Structure That Scales
A well-organized package is easier to maintain, test, and contribute to. Here's the structure I use for most Laravel packages:
my-laravel-package/
├── .github/
│ ├── workflows/
│ │ └── tests.yml
│ └── ISSUE_TEMPLATE.md
├── config/
│ └── my-package.php
├── database/
│ └── migrations/
│ └── create_package_tables.php
├── resources/
│ ├── views/
│ └── lang/
├── src/
│ ├── Commands/
│ ├── Http/
│ │ ├── Controllers/
│ │ ├── Middleware/
│ │ └── Requests/
│ ├── Models/
│ ├── Services/
│ ├── Facades/
│ ├── MyPackageServiceProvider.php
│ └── MyPackage.php
├── tests/
│ ├── Feature/
│ ├── Unit/
│ └── TestCase.php
├── composer.json
├── phpunit.xml
├── README.md
├── CHANGELOG.md
└── LICENSE
Service Provider Foundation
The service provider is the heart of your package. Here's how I structure them:
class MyPackageServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->mergeConfigFrom(
__DIR__.'/../config/my-package.php',
'my-package'
);
$this->app->singleton(MyPackage::class, function ($app) {
return new MyPackage($app['config']['my-package']);
});
$this->app->alias(MyPackage::class, 'my-package');
}
public function boot(): void
{
if ($this->app->runningInConsole()) {
$this->publishes([
__DIR__.'/../config/my-package.php' => config_path('my-package.php'),
], 'config');
$this->publishes([
__DIR__.'/../database/migrations' => database_path('migrations'),
], 'migrations');
$this->commands([
MyPackageCommand::class,
]);
}
$this->loadRoutesFrom(__DIR__.'/../routes/web.php');
$this->loadViewsFrom(__DIR__.'/../resources/views', 'my-package');
$this->loadTranslationsFrom(__DIR__.'/../resources/lang', 'my-package');
$this->publishes([
__DIR__.'/../resources/views' => resource_path('views/vendor/my-package'),
], 'views');
}
}
API Design That Developers Love
The public API of your package is the most important part. Once people start using it, changes become breaking changes that affect real applications.
Fluent Interface Example
class QueryBuilder
{
protected array $wheres = [];
protected array $orders = [];
protected ?int $limit = null;
public function where(string $column, mixed $value): self
{
$this->wheres[] = ['column' => $column, 'value' => $value];
return $this;
}
public function orderBy(string $column, string $direction = 'asc'): self
{
$this->orders[] = ['column' => $column, 'direction' => $direction];
return $this;
}
public function limit(int $limit): self
{
$this->limit = $limit;
return $this;
}
public function get(): Collection
{
// Build and execute query
return $this->buildQuery()->get();
}
}
// Usage feels natural
$results = MyPackage::query()
->where('status', 'active')
->where('type', 'premium')
->orderBy('created_at', 'desc')
->limit(10)
->get();
Configuration Design
// config/my-package.php
return [
// Essential settings with sensible defaults
'enabled' => env('MY_PACKAGE_ENABLED', true),
'api_key' => env('MY_PACKAGE_API_KEY'),
'cache' => [
'enabled' => env('MY_PACKAGE_CACHE_ENABLED', true),
'ttl' => env('MY_PACKAGE_CACHE_TTL', 3600),
'prefix' => env('MY_PACKAGE_CACHE_PREFIX', 'my_package'),
],
'defaults' => [
'timeout' => 30,
'retry_attempts' => 3,
'batch_size' => 100,
],
// Advanced configuration for power users
'advanced' => [
'custom_handlers' => [],
'middleware' => [],
'transformers' => [],
],
];
Testing Strategy for Packages
Package testing is different from application testing. You need to test against multiple Laravel versions and ensure your package doesn't break existing functionality.
Test Environment Setup
// tests/TestCase.php
abstract class TestCase extends Orchestra\Testbench\TestCase
{
protected function setUp(): void
{
parent::setUp();
$this->loadLaravelMigrations();
$this->loadMigrationsFrom(__DIR__.'/../database/migrations');
}
protected function getPackageProviders($app): array
{
return [MyPackageServiceProvider::class];
}
protected function getPackageAliases($app): array
{
return [
'MyPackage' => MyPackageFacade::class,
];
}
protected function defineEnvironment($app): void
{
$app['config']->set('database.default', 'testbench');
$app['config']->set('database.connections.testbench', [
'driver' => 'sqlite',
'database' => ':memory:',
'prefix' => '',
]);
$app['config']->set('my-package.api_key', 'test-key');
}
}
Feature Testing
class PackageIntegrationTest extends TestCase
{
/** @test */
public function it_can_be_configured_via_config_file()
{
config(['my-package.api_key' => 'custom-key']);
$package = app(MyPackage::class);
$this->assertEquals('custom-key', $package->getApiKey());
}
/** @test */
public function it_registers_commands()
{
$commands = Artisan::all();
$this->assertArrayHasKey('my-package:install', $commands);
$this->assertArrayHasKey('my-package:sync', $commands);
}
/** @test */
public function it_can_publish_config()
{
$this->artisan('vendor:publish', [
'--provider' => MyPackageServiceProvider::class,
'--tag' => 'config'
]);
$this->assertFileExists(config_path('my-package.php'));
}
/** @test */
public function it_provides_facade_access()
{
$result = MyPackage::query()->where('test', 'value')->get();
$this->assertInstanceOf(Collection::class, $result);
}
}
Documentation That Actually Helps
Good documentation can make or break a package. I spend almost as much time on docs as I do on code.
README Structure
Essential README Sections
- • One-line description: What does this package do?
- • Installation: Composer install + setup steps
- • Quick start: 5-minute example that works
- • Configuration: Available options explained
- • Usage examples: Common use cases with code
- • API reference: All public methods documented
- • Contributing: How others can help
- • License: Clear licensing terms
Effective Code Examples
## Quick Start
1. Install the package:
```bash
composer require yourname/my-package
```
2. Publish the config file:
```bash
php artisan vendor:publish --provider="YourName\MyPackage\MyPackageServiceProvider"
```
3. Add your API key to `.env`:
```
MY_PACKAGE_API_KEY=your-api-key-here
```
4. Use it in your code:
```php
use YourName\MyPackage\Facades\MyPackage;
// Simple usage
$results = MyPackage::query()
->where('status', 'active')
->get();
// With options
$results = MyPackage::query()
->where('type', 'premium')
->orderBy('created_at', 'desc')
->limit(10)
->get();
```
That's it! You're ready to start using the package.
Continuous Integration Setup
Automated testing across PHP and Laravel versions helps ensure your package works reliably for all users.
GitHub Actions Configuration
# .github/workflows/tests.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: [8.1, 8.2, 8.3]
laravel: [9.*, 10.*, 11.*]
stability: [prefer-lowest, prefer-stable]
include:
- laravel: 9.*
testbench: 7.*
- laravel: 10.*
testbench: 8.*
- laravel: 11.*
testbench: 9.*
steps:
- uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv
coverage: xdebug
- name: Install dependencies
run: |
composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update
composer update --${{ matrix.stability }} --prefer-dist --no-interaction
- name: Run tests
run: vendor/bin/phpunit --coverage-text --coverage-clover=coverage.clover
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
file: ./coverage.clover
Publishing to Packagist
Composer.json Essentials
{
"name": "yourname/my-package",
"description": "A brief, clear description of what your package does",
"keywords": ["laravel", "package", "relevant", "tags"],
"type": "library",
"license": "MIT",
"authors": [
{
"name": "Your Name",
"email": "your.email@example.com"
}
],
"require": {
"php": "^8.1",
"illuminate/support": "^9.0|^10.0|^11.0"
},
"require-dev": {
"orchestra/testbench": "^7.0|^8.0|^9.0",
"phpunit/phpunit": "^9.0|^10.0"
},
"autoload": {
"psr-4": {
"YourName\\MyPackage\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"YourName\\MyPackage\\Tests\\": "tests/"
}
},
"extra": {
"laravel": {
"providers": [
"YourName\\MyPackage\\MyPackageServiceProvider"
],
"aliases": {
"MyPackage": "YourName\\MyPackage\\Facades\\MyPackage"
}
}
},
"scripts": {
"test": "vendor/bin/phpunit",
"test-coverage": "vendor/bin/phpunit --coverage-html coverage"
},
"minimum-stability": "dev",
"prefer-stable": true
}
Version Tagging Strategy
Semantic Versioning
- • 1.0.0: Initial stable release
- • 1.1.0: New features, backward compatible
- • 1.0.1: Bug fixes, backward compatible
- • 2.0.0: Breaking changes
Maintenance and Community
Issue Management
Good Practices
Pull Request Guidelines
Common Pitfalls to Avoid
Breaking Changes Without Notice
Always follow semantic versioning and clearly document breaking changes in your changelog.
Poor Error Messages
Provide clear, actionable error messages that help developers understand what went wrong and how to fix it.
Overcomplicating the API
Start simple and add complexity only when needed. The best packages have clean, intuitive APIs.
Insufficient Testing
Test against multiple Laravel versions and edge cases. Your users depend on reliability.
Real Package Example
Here's a simplified example of a package that adds structured logging capabilities to Laravel:
// src/StructuredLogger.php
class StructuredLogger
{
public function __construct(
private LoggerInterface $logger,
private array $config
) {}
public function logUserAction(User $user, string $action, array $context = []): void
{
$this->logger->info('User action performed', [
'user_id' => $user->id,
'user_email' => $user->email,
'action' => $action,
'timestamp' => now()->toISOString(),
'context' => $context,
'session_id' => session()->getId(),
'ip_address' => request()->ip(),
]);
}
public function logApiRequest(Request $request, Response $response): void
{
$this->logger->info('API request processed', [
'method' => $request->method(),
'url' => $request->fullUrl(),
'status_code' => $response->getStatusCode(),
'response_time' => $this->getResponseTime(),
'user_id' => $request->user()?->id,
'ip_address' => $request->ip(),
]);
}
}
// Usage
StructuredLogger::logUserAction($user, 'post_created', ['post_id' => $post->id]);
StructuredLogger::logApiRequest($request, $response);
The Package Lifecycle
Building a successful package is just the beginning. Maintaining it, supporting users, and evolving it over time is where the real work happens. The most rewarding part is seeing other developers solve problems with something you built.
Remember that every popular Laravel package started as someone's side project. Focus on solving real problems well, write clear documentation, and be responsive to your community. The ecosystem will reward quality work.
🎯 Package Success Factors
Solve a real problem, provide a clean API, write excellent documentation, test thoroughly, and support your users. These fundamentals never go out of style.
The Laravel community is incredibly supportive of quality packages. If you build something useful and maintain it well, you'll find contributors and users who help it grow.
Working on a Laravel package or thinking about creating one? I'd love to hear about what you're building and any challenges you're facing. Package development is one of the best ways to level up your Laravel skills and contribute to the community.

Chris Page
Fractional CTO and Software Engineer with 25+ years of experience. I help startups scale from 0 to 7 figures using AI-assisted development and proven frameworks.