Laravel Package Development: Providers &amp; Config | Mohamed Said        [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://msaied.com) [ Home ](https://msaied.com) [ Projects ](https://msaied.com/projects) [ Articles  ](https://msaied.com/articles) [ Certificates ](https://msaied.com/certificates) [ Contact ](https://msaied.com#contact-section) 

       [  ](https://github.com/EG-Mohamed)       

 [ Home ](https://msaied.com) [ Projects ](https://msaied.com/projects) [ Articles ](https://msaied.com/articles) [ Certificates ](https://msaied.com/certificates) [ Contact ](https://msaied.com#contact-section) 

  [ home ](https://msaied.com)    [ articles ](https://msaied.com/articles)    Building a Laravel Package: Service Providers, Auto-Discovery, and Config Merging        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)

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

  #laravel   #packages   #service-providers   #php  

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

     22 Jul 2026      3 min read    ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said  

       Table of contents

1. [  01   Why Package Architecture Matters  ](#why-package-architecture-matters)
2. [  02   Structuring the Service Provider  ](#structuring-the-service-provider)
3. [  03   mergeConfigFrom — The Subtle Trap  ](#codemergeconfigfromcode-the-subtle-trap)
4. [  04   Auto-Discovery Done Right  ](#auto-discovery-done-right)
5. [  05   Testing the Package in Isolation  ](#testing-the-package-in-isolation)
6. [  06   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.

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fbuilding-a-laravel-package-service-providers-auto-discovery-and-config-merging-2&text=Building+a+Laravel+Package%3A+Service+Providers%2C+Auto-Discovery%2C+and+Config+Merging) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fbuilding-a-laravel-package-service-providers-auto-discovery-and-config-merging-2) 

 Frequently Asked Questions 
----------------------------

  3 questions  

     Q01  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. 

      Q02  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. 

      Q03  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. 

  Continue reading

 More Articles 
---------------

 [ View all    ](https://msaied.com/articles) 

 [ ![Modular Monolith in Laravel: Enforcing Bounded Contexts Without a Microservices Tax](https://cdn.msaied.com/451/014ea4597c12e3ba1ce7a4f4a33d299e.png) laravel architecture ddd 

### Modular Monolith in Laravel: Enforcing Bounded Contexts Without a Microservices Tax

Learn how to carve a Laravel application into cohesive bounded contexts using modules, internal contracts, and...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 21 Jul 2026     3 min read  

  Read    

 ](https://msaied.com/articles/modular-monolith-in-laravel-enforcing-bounded-contexts-without-a-microservices-tax-3) [ ![DDD Value Objects and DTOs in Laravel Without the Bloat](https://cdn.msaied.com/450/41688537085b86f102fd8c219a35319f.png) laravel ddd php 

### DDD Value Objects and DTOs in Laravel Without the Bloat

Learn how to implement domain-driven value objects and data transfer objects in Laravel using PHP 8.3 readonly...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 21 Jul 2026     1 min read  

  Read    

 ](https://msaied.com/articles/ddd-value-objects-and-dtos-in-laravel-without-the-bloat) [ ![Laravel Time Machine: Profile Every Stage of a Request Lifecycle](https://cdn.msaied.com/453/308c1bf5c3ea88a09ccda212f98bc409.png) Laravel Performance Profiling Developer Tools 

### Laravel Time Machine: Profile Every Stage of a Request Lifecycle

Laravel Time Machine is a request lifecycle profiler that records millisecond timings for every phase — bootst...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 20 Jul 2026     3 min read  

  Read    

 ](https://msaied.com/articles/laravel-time-machine-profile-every-stage-of-a-request-lifecycle) 

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

Explore

- [Home](https://msaied.com)
- [Projects](https://msaied.com/projects)
- [Articles](https://msaied.com/articles)
- [Certificates](https://msaied.com/certificates)
- [Contact](https://msaied.com#contact-section)

Connect

- [   hello@msaied.com ](mailto:hello@msaied.com)
- [   +20 109 461 9204 ](tel:+201094619204)

© 2026 Mohamed Said. All rights reserved.

 [  ](https://github.com/EG-Mohamed) [  ](https://www.linkedin.com/in/msaiedm/) [  ](https://wa.me/201094619204) [  ](mailto:hello@msaied.com) [  ](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
