
September 07, 2026
12 min read
By Kokil Thapa | Last reviewed: September 2026
Laravel package development from scratch is how you turn repeated application logic into a reusable Composer module that any Laravel 13 project can install with one command. You copy the same validation helpers, payment adapters, or SEO utilities across client projects until maintenance becomes painful. Packaging that code behind a service provider, config file, and test suite keeps your main application lean and gives you a versioned artefact you can share internally or publish on Packagist. This guide walks through the full workflow—from folder structure to CI and release tags—using patterns I rely on across production Laravel development in Nepal and client work worldwide.
register() and boot(), PHPUnit tests, and a composer.json typed as laravel-package so Laravel auto-discovers it. Publish to Packagist when the API is stable.What is Laravel package development and when should you build one?
A Laravel package is a standalone PHP library that integrates with the framework through a service provider. Unlike copying a folder into app/, a package lives in vendor/, follows semantic versioning, and declares its Laravel compatibility in composer.json. You reach for package extraction when the same code appears in two or more projects, when you want unit tests isolated from application noise, or when you plan to open-source a integration—think a Khalti webhook normaliser or a reusable slug helper tied to SEO setup for Laravel sites.
Do not package everything on day one. I have seen teams spend a week extracting a 40-line helper that never leaves one repo. A practical threshold: the module has a clear public API, at least one config surface, and you expect a second consumer within six months. For one-off client logic on a legal-tech portal, keep it in the app until the pattern repeats—I've maintained sister sites on a shared Deployer pipeline where only the third duplicate justified a package.
Compare your options before committing:
| Approach | Best for | Trade-off |
|---|---|---|
Inline app/ code | Single-project logic, fast iteration | No reuse, harder to test in isolation |
| Local path repository | Active package development | Requires symlink or path mapping in Composer |
| Private Composer repo | Agency-internal shared libraries | Needs Satis or GitLab package registry |
| Public Packagist package | Open-source or community tools | API stability and semver discipline required |
For background on extracting logic you already wrote inside an app, see the companion walkthrough on how to create custom Laravel packages. The steps below assume you are starting clean with Laravel 13.x on PHP 8.3 or higher and Composer 2.10.
How do you scaffold a Laravel package from scratch?
Start outside any existing Laravel project. Create a directory, initialise Composer, and define PSR-4 autoloading. A minimal layout looks like this:
acme/invoice-sdk/
├── composer.json
├── config/
│ └── invoice-sdk.php
├── database/
│ └── migrations/
├── resources/
│ └── views/
├── routes/
│ └── web.php
├── src/
│ ├── InvoiceSdkServiceProvider.php
│ ├── Facades/
│ │ └── InvoiceSdk.php
│ └── InvoiceCalculator.php
└── tests/
├── TestCase.php
└── InvoiceCalculatorTest.php composer.json essentials
Your composer.json is the contract. Include Laravel package discovery metadata so consuming apps auto-register your provider:
{
"name": "acme/invoice-sdk",
"description": "Invoice calculation helpers for Laravel",
"type": "library",
"license": "MIT",
"require": {
"php": "^8.3",
"illuminate/support": "^13.0"
},
"require-dev": {
"orchestra/testbench": "^10.0",
"phpunit/phpunit": "^11.0"
},
"autoload": {
"psr-4": {
"Acme\\InvoiceSdk\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Acme\\InvoiceSdk\\Tests\\": "tests/"
}
},
"extra": {
"laravel": {
"providers": [
"Acme\\InvoiceSdk\\InvoiceSdkServiceProvider"
],
"aliases": {
"InvoiceSdk": "Acme\\InvoiceSdk\\Facades\\InvoiceSdk"
}
}
},
"minimum-stability": "stable"
} Use illuminate/support rather than laravel/framework when your package does not need the full stack—this keeps dependency trees lighter. Require the specific Illuminate components you actually touch: illuminate/database for migrations, illuminate/routing for route files, and so on. The official Laravel package documentation lists each integration point.
Local development with path repositories
While building, point a sandbox Laravel app at your package folder:
{
"repositories": [
{
"type": "path",
"url": "../invoice-sdk",
"options": { "symlink": true }
}
],
"require": {
"acme/invoice-sdk": "@dev"
}
} Run composer update acme/invoice-sdk after each change to refresh the symlink. For day-to-day work, modern Laravel architecture often keeps the sandbox app in the same mono-repo or a sibling directory on your dev machine.
How do you register config, routes, and assets in a Laravel package?
The service provider is the integration hub. Split responsibilities correctly: bind services in register(), touch the framework in boot().
<?php
namespace Acme\InvoiceSdk;
use Illuminate\Support\ServiceProvider;
class InvoiceSdkServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->mergeConfigFrom(
__DIR__.'/../config/invoice-sdk.php', 'invoice-sdk'
);
$this->app->singleton(InvoiceCalculator::class, function ($app) {
return new InvoiceCalculator(
taxRate: $app['config']['invoice-sdk.tax_rate']
);
});
}
public function boot(): void
{
if ($this->app->runningInConsole()) {
$this->publishes([
__DIR__.'/../config/invoice-sdk.php' => config_path('invoice-sdk.php'),
], 'invoice-sdk-config');
$this->publishes([
__DIR__.'/../resources/views' => resource_path('views/vendor/invoice-sdk'),
], 'invoice-sdk-views');
}
$this->loadRoutesFrom(__DIR__.'/../routes/web.php');
$this->loadViewsFrom(__DIR__.'/../resources/views', 'invoice-sdk');
$this->loadMigrationsFrom(__DIR__.'/../database/migrations');
}
} Config merging vs publishing
mergeConfigFrom() lets your package ship defaults while the host app overrides keys in its own config file—ideal for API endpoints or feature flags. Use publishes() only when the consumer must edit the full file, such as a Blade layout they will customise. On projects like Nepal Gift Card, I publish config for payment gateway keys but merge defaults for formatting rules.
Routes and middleware
Load routes from a dedicated file and apply middleware groups explicitly:
<?php
use Illuminate\Support\Facades\Route;
Route::middleware(['web', 'auth'])
->prefix('invoice-sdk')
->group(function () {
Route::get('/preview', function () {
return view('invoice-sdk::preview');
});
}); If your package exposes an API, follow the same versioning and response conventions you would use in application code—see Laravel API best practices and building RESTful APIs with Laravel for patterns that translate directly into package route groups.
Artisan commands and scheduled tasks
Register commands conditionally so web requests never load CLI code unnecessarily:
if ($this->app->runningInConsole()) {
$this->commands([
Commands\SyncInvoicesCommand::class,
]);
} Consumers schedule your command in their routes/console.php or bootstrap/app.php scheduler—your package documents the signature; it does not hijack their cron table.
How do you test a Laravel package before publishing?
Package tests should not require a full application checkout. Orchestra Testbench boots a minimal Laravel kernel around your provider. Create a base test case:
<?php
namespace Acme\InvoiceSdk\Tests;
use Acme\InvoiceSdk\InvoiceSdkServiceProvider;
use Orchestra\Testbench\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase
{
protected function getPackageProviders($app): array
{
return [InvoiceSdkServiceProvider::class];
}
protected function defineEnvironment($app): void
{
$app['config']->set('invoice-sdk.tax_rate', 0.13);
}
} Write focused unit tests against your public API:
<?php
namespace Acme\InvoiceSdk\Tests;
use Acme\InvoiceSdk\InvoiceCalculator;
class InvoiceCalculatorTest extends TestCase
{
public function test_it_calculates_tax(): void
{
$calc = new InvoiceCalculator(taxRate: 0.13);
$this->assertSame(113.0, $calc->total(100.0));
}
} Run vendor/bin/phpunit locally and in CI. A GitLab pipeline might lint with Laravel Pint, run tests on PHP 8.3 and 8.5, and only then tag a release—the same pattern I use on shared EC2 deploy pipelines for production Laravel applications. Validate JSON config files during development with a JSON formatter and validator before they ship in your package tree.
Testing package integrations
When your package registers routes or views, use Testbench HTTP tests:
public function test_preview_route_returns_view(): void
{
$user = \Orchestra\Testbench\Factories\UserFactory::new()->create();
$response = $this->actingAs($user)->get('/invoice-sdk/preview');
$response->assertOk();
$response->assertViewIs('invoice-sdk::preview');
} Study how mature packages structure tests—event sourcing with the Spatie package is a useful reference for service-provider-heavy libraries even if your domain is unrelated.
What are the steps to publish a Laravel package on Packagist?
Publishing makes your package installable worldwide. The sequence:
- Push the repository to a public Git host (GitHub, GitLab, etc.).
- Ensure
composer.jsonhas correctname, license, and autoload blocks. - Create a Packagist account and submit your repository URL.
- Enable the Packagist webhook so new tags trigger index updates.
- Tag releases with semantic versioning:
git tag v1.0.0 && git push origin v1.0.0. - Document install steps in a README with PHP and Laravel version constraints.
Declare compatibility honestly. If you support Laravel 12 and 13, use "illuminate/support": "^12.0|^13.0" and run CI against both. Laravel 11 reached end-of-life in March 2026; new packages should target 12.x or 13.x unless you maintain legacy clients deliberately.
For agency-internal packages, skip public Packagist. GitLab's Composer registry or a private Satis mirror works well when you build multiple client apps and want shared payment or directory logic without exposing code—relevant for teams doing custom software development across several repos.
What mistakes break Laravel packages in production?
These issues appear repeatedly when packages move from sandbox to real apps:
- Heavy work in
register()— loading routes or hitting the database during registration breaks provider order. Keep bindings lean. - Missing
mergeConfigFrom()— consumers cannot override defaults; they fork your vendor files instead. - Unconstrained Illuminate versions — a package locked to
^13.0fails on Laravel 12 apps; test a sensible range or split major branches. - Global state and facades in core logic — inject dependencies; facades make unit testing outside Laravel harder.
- Unnamespaced migrations — prefix table names or publish migrations so they do not collide with host apps.
- No deferred provider when appropriate — packages that only register bindings can implement
DeferrableProviderto speed boot.
Authorization-heavy packages should document how host apps wire gates and policies—your package should not assume Spatie Permission is installed unless you declare it as a dependency. See Laravel policies and gates for integration patterns consumers expect.
On eCommerce projects, payment packages must treat webhooks as idempotent and document callback URLs clearly—patterns covered in Laravel payment integrations apply whether the code lives in app/ or vendor/. For large modular apps, consider how your package boundary fits a modular monolith before extracting ten interdependent packages at once.
If you maintain packages alongside application work, treat semver as a contract. Breaking constructor signatures without a major bump destroys trust faster than a bug—I've rolled back package updates on production deployments when a minor release renamed a config key without notice.
Key Takeaways
- Start Laravel package development from scratch with PSR-4 autoloading, a dedicated service provider, and Testbench—not by copying files into
app/. - Bind services in
register(); load routes, views, migrations, and publish tags inboot(). - Use path repositories and symlinks for local iteration; tag semver releases only after PHPUnit passes on supported PHP and Laravel versions.
- Prefer
illuminate/*component dependencies over requiring the full framework when possible. - Document install, config keys, and upgrade paths in README and CHANGELOG before Packagist submission.
- Extract packages when reuse is real— not because folder structure looks cleaner in a diagram.
People Also Ask
Do I need a service provider for every Laravel package?
Yes, for anything that integrates with Laravel beyond plain PHP classes. The service provider registers bindings, config, routes, views, commands, and migrations. Pure utility classes with no framework coupling can ship without a provider, but most Laravel packages need one for auto-discovery via the extra.laravel block in composer.json.
Can I develop a Laravel package inside an existing project?
You can use a path repository pointing at a subdirectory, but a sibling directory with its own git history is cleaner. It forces proper Composer boundaries and makes Packagist publication a copy-paste instead of a refactor. Many teams keep packages/ in a mono-repo for internal tools and split to a separate repo when going public.
What PHP version should a new Laravel package require in 2026?
Target PHP 8.3 as the minimum if you support Laravel 13, which itself requires PHP 8.3+. Testing on PHP 8.5 catches forward compatibility early. Laravel 12 projects need only PHP 8.2, so dual-support packages often declare "php": "^8.2" with Illuminate constraints covering both framework lines.
How is a Laravel package different from a Composer library?
Every Laravel package is a Composer library, but not every Composer library is Laravel-aware. A Laravel package adds a service provider, optional facade alias, config merging, and sometimes migrations or Blade namespaces. A generic PHP library lacks those hooks and cannot auto-register with the framework.
Ship reusable code the right way
Laravel package development from scratch pays off when you treat the package as a product: stable public API, tested provider boot, honest semver, and documentation a stranger can follow. Whether you are extracting shared logic from a booking platform like Adventure Third Pole Trek or preparing an open-source integration for the community, the scaffold-and-provider pattern stays the same on Laravel 13 and PHP 8.3+. Need help architecting reusable modules for an enterprise app or publishing a private agency library? Get in touch or explore enterprise application development services. For front-end integration patterns in consuming apps, see Vue with Laravel setup and essential Laravel plugins worth studying before you reinvent them.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

