Laravel Package Development: Providers &amp; Config | Mohamed Said       [Skip to content](#main)  [ ![](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png) Mohamed SaidLaravel Backend Engineer ](https://www.msaied.com/public) - [Home](https://www.msaied.com/public)
- [Projects](https://www.msaied.com/public/projects)
- [Articles](https://www.msaied.com/public/articles)
- [Certificates](https://www.msaied.com/public/certificates)
- [About](https://www.msaied.com/public#about)

           [  Contact](https://www.msaied.com/public#contact) Menu 

Menu
----

Close 

 - [HomeStart here](https://www.msaied.com/public)
- [ProjectsCase studies](https://www.msaied.com/public/projects)
- [ArticlesEngineering notes](https://www.msaied.com/public/articles)
- [CertificatesCredentials](https://www.msaied.com/public/certificates)
- [AboutHow I work](https://www.msaied.com/public#about)
- [ContactGet in touch](https://www.msaied.com/public#contact)

  [Start a conversation](https://www.msaied.com/public#contact) [WhatsApp](https://wa.me/201094619204) [Email](mailto:hello@msaied.com) 

 1. [Home](https://www.msaied.com/public)
2. /
3. [Articles](https://www.msaied.com/public/articles)
4. /
5. Building a Laravel Package: Service Providers, Auto-Discovery, and Config Merging

 Building a Laravel Package: Service Providers, Auto-Discovery, and Config Merging
==================================================================================

 A practical walkthrough of building a production-ready Laravel package — covering service provider design, auto-discovery, config merging, and the subtle pitfalls that trip up even experienced engineers.

 ![](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp) [Mohamed Said](https://www.msaied.com/public#person) Published 22 Jul 2026 · Updated 22 Jul 2026 · 3 min read

ShareCopy linkCopied

 ![Building a Laravel Package: Service Providers, Auto-Discovery, and Config Merging](https://cdn.msaied.com/454/449f0f66a4aebbd744a318fc44906f1c.png) 

  On this page +1. [Why Package Architecture Matters](#why-package-architecture-matters)
2. [Structuring the Service Provider](#structuring-the-service-provider)
3. [mergeConfigFrom — The Subtle Trap](#codemergeconfigfromcode-the-subtle-trap)
4. [Auto-Discovery Done Right](#auto-discovery-done-right)
5. [Testing the Package in Isolation](#testing-the-package-in-isolation)
6. [Takeaways](#takeaways)

 Why Package Architecture Matters
--------------------------------

Most Laravel packages look fine from the outside but become maintenance nightmares once they grow. The root cause is almost always a service provider that does too much, config merging that silently overwrites user values, or auto-discovery that registers things the consuming app never asked for.

This article walks through the decisions that separate a throwaway package from one you'd actually ship to Packagist.

---

Structuring the Service Provider
--------------------------------

A service provider has two jobs: **bind things** in `register()` and **bootstrap side-effects** in `boot()`. Mixing them causes subtle ordering bugs.

```php
namespace Acme\Auditor;

use Illuminate\Support\ServiceProvider;

class AuditorServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->mergeConfigFrom(
            __DIR__.'/../config/auditor.php',
            'auditor'
        );

        $this->app->singleton(AuditLogger::class, function ($app) {
            return new AuditLogger(
                $app['db']->connection(
                    $app['config']['auditor.connection']
                )
            );
        });
    }

    public function boot(): void
    {
        if ($this->app->runningInConsole()) {
            $this->publishes([
                __DIR__.'/../config/auditor.php' => config_path('auditor.php'),
            ], 'auditor-config');

            $this->loadMigrationsFrom(__DIR__.'/../database/migrations');
        }

        $this->loadRoutesFrom(__DIR__.'/../routes/auditor.php');
    }
}

```

Key decisions here:

- `mergeConfigFrom` in `register()` so the config is available when other providers bind against it.
- Migrations and publishable assets are gated behind `runningInConsole()` — no filesystem overhead on every web request.
- Routes are always loaded; gate them behind a config flag if the package is optional.

---

`mergeConfigFrom` — The Subtle Trap
-----------------------------------

`mergeConfigFrom` only does a **shallow merge**. If your config has nested arrays and the user publishes a partial override, nested keys they omit will be dropped.

```php
// Package default
'drivers' => [
    'database' => ['table' => 'audit_logs'],
    'redis'    => ['prefix' => 'audit:'],
],

// User publishes and sets only:
'drivers' => [
    'database' => ['table' => 'my_audits'],
],
// 'redis' key is now gone — mergeConfigFrom won't restore it.

```

The fix is a recursive merge in `register()`:

```php
public function register(): void
{
    $this->app->afterResolving('config', function ($config) {
        $config->set('auditor', array_replace_recursive(
            require __DIR__.'/../config/auditor.php',
            $config->get('auditor', [])
        ));
    });
}

```

This ensures package defaults fill every missing nested key without overwriting user values.

---

Auto-Discovery Done Right
-------------------------

Auto-discovery via `composer.json` is convenient but opt-in should be the default for anything that registers routes, middleware, or commands.

```json
{
    "extra": {
        "laravel": {
            "providers": [
                "Acme\\Auditor\\AuditorServiceProvider"
            ],
            "aliases": {
                "Auditor": "Acme\\Auditor\\Facades\\Auditor"
            }
        }
    }
}

```

For packages that should be explicitly registered (e.g., they alter query behavior globally), document the manual registration path and consider adding a `dont-discover` note in your README. Consumers can always add your provider to `bootstrap/providers.php` in Laravel 11+.

---

Testing the Package in Isolation
--------------------------------

Use `orchestra/testbench` to boot a minimal Laravel application inside your test suite without a full app skeleton.

```php
use Orchestra\Testbench\TestCase;

class AuditLoggerTest extends TestCase
{
    protected function getPackageProviders($app): array
    {
        return [AuditorServiceProvider::class];
    }

    protected function defineEnvironment($app): void
    {
        $app['config']->set('auditor.connection', 'testing');
    }

    public function test_logger_resolves_from_container(): void
    {
        $logger = $this->app->make(AuditLogger::class);

        $this->assertInstanceOf(AuditLogger::class, $logger);
    }
}

```

This pattern lets you assert config merging, binding resolution, and route registration without touching a real application.

---

Takeaways
---------

- Keep `register()` for bindings and config; keep `boot()` for side-effects.
- `mergeConfigFrom` is shallow — use `array_replace_recursive` for nested defaults.
- Gate migrations and publishable assets behind `runningInConsole()`.
- Auto-discovery is a convenience, not a mandate — document manual registration for invasive packages.
- `orchestra/testbench` is non-negotiable for reliable package tests.

- [laravel](https://www.msaied.com/public/articles?search=laravel)
- [packages](https://www.msaied.com/public/articles?search=packages)
- [service-providers](https://www.msaied.com/public/articles?search=service-providers)
- [php](https://www.msaied.com/public/articles?search=php)

 Frequently asked questions 
---------------------------

  Why does my nested config disappear after a user publishes a partial config file?Because `mergeConfigFrom` performs a shallow merge. Nested arrays in the user's published file replace the entire nested array from the package default. Use `array\_replace\_recursive` in `register()` to preserve all nested defaults while still respecting user overrides.

   Should I always enable auto-discovery for my Laravel package?Not necessarily. Auto-discovery is convenient for utility packages, but packages that register global middleware, alter query behavior, or load routes unconditionally should document manual registration so consumers can control when the package is active.

   How do I test a package without creating a full Laravel application?Use `orchestra/testbench`. It boots a minimal Laravel container, lets you declare your service providers via `getPackageProviders()`, and configure the environment via `defineEnvironment()` — all without a real app skeleton.

   ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)About the author
----------------

[Mohamed Said](https://www.msaied.com/public#person)Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

[About](https://www.msaied.com/public#about) [GitHub ↗](https://github.com/EG-Mohamed) [LinkedIn ↗](https://www.linkedin.com/in/msaiedm/) [WhatsApp ↗](https://wa.me/201094619204) [Email Address ↗](mailto:hello@msaied.com) [My CV ↗](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)  

   [Previous articleRouteKey Model Attribute and More: What's New in Laravel 13.21](https://www.msaied.com/public/articles/routekey-model-attribute-and-more-whats-new-in-laravel-1321) [Next articleFilament at Scale: Multi-Panel Auth, Custom Panels, and Table Query Tuning](https://www.msaied.com/public/articles/filament-at-scale-multi-panel-auth-custom-panels-and-table-query-tuning-3)  

   On this page
-------------

1. [Why Package Architecture Matters](#why-package-architecture-matters)
2. [Structuring the Service Provider](#structuring-the-service-provider)
3. [mergeConfigFrom — The Subtle Trap](#codemergeconfigfromcode-the-subtle-trap)
4. [Auto-Discovery Done Right](#auto-discovery-done-right)
5. [Testing the Package in Isolation](#testing-the-package-in-isolation)
6. [Takeaways](#takeaways)

 ###  Have a technical challenge?

 Tell me what you’re building. I reply within two working days.

[Start a conversation](https://www.msaied.com/public#contact) 

   Related articles
-----------------

 [ ![](https://cdn.msaied.com/740/cce86edc21eddcbdd2f2454fadaf9c70.png)  · 3 min read### The Pipeline Pattern in Laravel: Custom Pipelines Beyond Middleware

5 Oct 2026 ](https://www.msaied.com/public/articles/the-pipeline-pattern-in-laravel-custom-pipelines-beyond-middleware-1) [ ![](https://cdn.msaied.com/739/2d6897fdcdcf090613f96f72a64b8a78.png)  · 4 min read### MySQL Full-Text Search in Laravel: Indexes, Relevance Scoring, and Boolean Mode

4 Oct 2026 ](https://www.msaied.com/public/articles/mysql-full-text-search-in-laravel-indexes-relevance-scoring-and-boolean-mode) [ ![](https://cdn.msaied.com/738/073696a3fefe18bec825beec5ac658f5.png)  · 4 min read### Laravel Queue Rate-Limited Middleware: Throttling Jobs Without Losing Work

4 Oct 2026 ](https://www.msaied.com/public/articles/laravel-queue-rate-limited-middleware-throttling-jobs-without-losing-work) 

  Have a technical challenge?
----------------------------

Tell me what you’re building. I reply within two working days.

 [Discuss your project ↗](https://www.msaied.com/public#contact) 

  © 2026 Mohamed Said · Built with Laravel, meant to last.Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

 - [Home](https://www.msaied.com/public)
- [Articles](https://www.msaied.com/public/articles)
- [Certificates](https://www.msaied.com/public/certificates)
- [GitHub](https://github.com/EG-Mohamed)
- [LinkedIn](https://www.linkedin.com/in/msaiedm/)
- [WhatsApp](https://wa.me/201094619204)
- [Email Address](mailto:hello@msaied.com)
- [My CV](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
- [Sitemap](https://www.msaied.com/public/sitemap.xml)
