Building a Laravel Package: Providers &amp; Auto-Discovery | 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 structure, auto-discovery via composer.json, config merging, and testable defaults that don't fight the framework.

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

ShareCopy linkCopied

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

  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)

 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.

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

  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.

   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.

   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.

   ![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 articleModular Monolith in Laravel: Enforcing Bounded Contexts Without a Microservices Tax](https://www.msaied.com/public/articles/modular-monolith-in-laravel-enforcing-bounded-contexts-without-a-microservices-tax-2) [Next articleLivewire v3 Internals: Morph Markers, JS Hooks, and Alpine Integration](https://www.msaied.com/public/articles/livewire-v3-internals-morph-markers-js-hooks-and-alpine-integration-2)  

   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)

 ###  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)
