Building a Laravel Package: Providers &amp; Auto-Discovery | 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 Structure Matters ](#why-package-structure-matters)
2. [  The Service Provider Skeleton ](#the-service-provider-skeleton)
3. [  Why mergeConfigFrom and Not Just config()-&gt;set() ](#why-codemergeconfigfromcode-and-not-just-codeconfig-gtsetcode)
4. [  Auto-Discovery via composer.json ](#auto-discovery-via-codecomposerjsoncode)
5. [  Config Merging in Depth ](#config-merging-in-depth)
6. [  Testing the Package in Isolation ](#testing-the-package-in-isolation)
7. [  Takeaways ](#takeaways)

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

  #laravel   #packages   #service-providers   #php  

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

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

       Table of contents

1. [  01   Why Package Structure Matters  ](#why-package-structure-matters)
2. [  02   The Service Provider Skeleton  ](#the-service-provider-skeleton)
3. [  03   Why mergeConfigFrom and Not Just config()-&gt;set()  ](#why-codemergeconfigfromcode-and-not-just-codeconfig-gtsetcode)
4. [  04   Auto-Discovery via composer.json  ](#auto-discovery-via-codecomposerjsoncode)
5. [  05   Config Merging in Depth  ](#config-merging-in-depth)
6. [  06   Testing the Package in Isolation  ](#testing-the-package-in-isolation)
7. [  07   Takeaways  ](#takeaways)

 Why Package Structure Matters
-----------------------------

Dropping logic into a `src/` folder and registering a service provider is easy. Doing it in a way that survives upgrades, plays nicely with `config:cache`, and lets consumers override every default — that takes deliberate design.

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

---

The Service Provider Skeleton
-----------------------------

Start with a provider that separates *registration* from *booting*. Registration is for binding; booting is for everything that reads those bindings.

```php
namespace Acme\Beacon;

use Illuminate\Support\ServiceProvider;

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

        $this->app->singleton(BeaconClient::class, function ($app) {
            return new BeaconClient(
                config('beacon.endpoint'),
                config('beacon.timeout'),
            );
        });
    }

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

            $this->commands([
                Commands\BeaconPingCommand::class,
            ]);
        }

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

```

### Why `mergeConfigFrom` and Not Just `config()->set()`

`mergeConfigFrom` only runs when the key doesn't already exist in the config repository. That means a published and customised `config/beacon.php` wins automatically — no extra logic needed. It also survives `config:cache` because the merge happens at registration time, before the cache is written.

---

Auto-Discovery via `composer.json`
----------------------------------

Laravel reads the `extra.laravel` key during `composer require` and registers providers and facades without any manual step.

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

```

Consumers who want to opt out add the provider to the `dont-discover` array in their own `composer.json`. Design your package so manual registration still works identically — don't rely on discovery order.

---

Config Merging in Depth
-----------------------

A common mistake is publishing a flat config and then adding nested keys in a later version. Consumers who published early now have a stale file and silently miss new keys.

Two mitigations:

1. **Namespace every key under a single top-level array** — `beacon.http.timeout` rather than `beacon.timeout`. Adding `beacon.http.retries` later won't break existing published files because `mergeConfigFrom` does a shallow merge at the top level only.
2. **Document the merge limitation.** For deeply nested defaults, consider a `ConfigFactory` that reads the published file and fills gaps explicitly:

```php
final class BeaconConfig
{
    public static function fromArray(array $raw): self
    {
        return new self(
            endpoint: $raw['endpoint'] ?? 'https://beacon.example.com',
            timeout: $raw['http']['timeout'] ?? 5,
            retries: $raw['http']['retries'] ?? 3,
        );
    }
}

```

This makes defaults explicit and testable without relying on merge behaviour.

---

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

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

```php
use Orchestra\Testbench\TestCase;
use Acme\Beacon\BeaconServiceProvider;

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

    protected function defineEnvironment($app): void
    {
        $app['config']->set('beacon.endpoint', 'https://test.local');
    }

    public function test_client_is_bound(): void
    {
        $client = $this->app->make(BeaconClient::class);
        $this->assertInstanceOf(BeaconClient::class, $client);
    }
}

```

This pattern lets you assert that your provider registers bindings correctly, that config defaults apply, and that publishable assets exist at the expected paths — all without a host application.

---

Takeaways
---------

- Use `mergeConfigFrom` in `register()`, not `boot()` — it must run before bindings that read config.
- Guard console-only registration (`commands`, `publishes`) behind `runningInConsole()` to avoid overhead on every request.
- Auto-discovery is a convenience, not a contract — always support manual registration.
- Shallow config merging is a known limitation; compensate with explicit factory classes for nested defaults.
- `orchestra/testbench` gives you a real Laravel container in tests; use `defineEnvironment` to override config per test.

 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-1&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-1) 

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

  3 questions  

     Q01  Does `mergeConfigFrom` work after `config:cache` has been run?        Yes. `mergeConfigFrom` runs during the `register` phase, which happens before the config cache is written. Once the cache exists, Laravel loads it directly and skips the merge — but by then the merged values are already baked in. 

      Q02  Should I load migrations automatically or require consumers to publish them?        Use `loadMigrationsFrom` for packages where the schema is an implementation detail consumers shouldn't modify. If consumers are likely to need custom columns or indexes, publish the migrations instead so they own the files. 

      Q03  How do I prevent my package's service provider from being auto-discovered in a specific app?        The consuming application adds the provider class to the `extra.laravel.dont-discover` array in its own `composer.json`. Your package needs no special handling — Laravel's discovery mechanism respects that list automatically. 

  Continue reading

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

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

 [ ![HTTP Query Method Support in Laravel 13.19](https://cdn.msaied.com/394/4c917aad559beddb5eeb9a7a1e107f4c.png) Laravel Laravel 13 HTTP Client 

### HTTP Query Method Support in Laravel 13.19

Laravel 13.19 adds Http::query() for the HTTP QUERY verb, query/queryJson testing helpers, a reduceInto() coll...

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

 8 Jul 2026     3 min read  

  Read    

 ](https://msaied.com/articles/http-query-method-support-in-laravel-1319) [ ![Blackfire & Xdebug Profiling in Laravel: Finding Real Bottlenecks in Production-Like Environments](https://cdn.msaied.com/393/a8dfc4ec52c4febc3ef41a491eb452ea.png) laravel performance profiling 

### Blackfire &amp; Xdebug Profiling in Laravel: Finding Real Bottlenecks in Production-Like Environments

Stop guessing where your Laravel app is slow. This guide walks through practical Blackfire and Xdebug profilin...

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

 8 Jul 2026     4 min read  

  Read    

 ](https://msaied.com/articles/blackfire-xdebug-profiling-in-laravel-finding-real-bottlenecks-in-production-like-environments) [ ![Laravel Cloud Security Defaults Behind Every Deploy](https://cdn.msaied.com/395/28df8f542fbe81ecee3ba4ad897f36d6.png) Laravel Cloud Security PHP 

### Laravel Cloud Security Defaults Behind Every Deploy

Laravel Cloud ships WAF rules, DDoS mitigation, automatic PHP patching, SOC 2 Type II compliance, and deploy-t...

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

 8 Jul 2026     4 min read  

  Read    

 ](https://msaied.com/articles/laravel-cloud-security-defaults-behind-every-deploy) 

   [  ![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)
