Saltar al contenido principal

Qué es un paquete

Un paquete en Laravel es un paquete de Composer que añade funcionalidad a tu aplicación. Existen dos grandes tipos.
  • Paquetes stand-alone — bibliotecas PHP genéricas que no dependen de Laravel (por ejemplo, Carbon, Pest).
  • Paquetes Laravel — paquetes con funcionalidad integrada con Laravel: rutas, controladores, vistas, configuración, etc.
Esta guía trata el segundo caso: paquetes específicos para Laravel. El desarrollo de paquetes requiere entender a fondo la estructura interna de Laravel: service providers, facades, publicación de archivos de configuración, etc.
Para escribir tests de tu paquete, usa Orchestra Testbench. Podrás escribir los tests como si estuvieras dentro de una aplicación Laravel normal.

Autodiscovery de paquetes

Al instalar un paquete, Laravel lee la sección extra.laravel de composer.json y registra automáticamente el service provider y los facades.
"extra": {
    "laravel": {
        "providers": [
            "Acme\\Courier\\CourierServiceProvider"
        ],
        "aliases": {
            "Courier": "Acme\\Courier\\Facades\\Courier"
        }
    }
}
Con esta configuración, el paquete se carga automáticamente sin que el usuario tenga que editar bootstrap/providers.php.

Desactivar el autodiscovery

Si el usuario quiere desactivar el autodiscovery de un paquete concreto, lo configura en el composer.json de la aplicación.
"extra": {
    "laravel": {
        "dont-discover": [
            "acme/courier"
        ]
    }
}

Rol del service provider

El service provider es el punto de entrada del paquete. En él centralizas el registro en Laravel de vistas, configuración, migraciones, rutas y demás recursos. Un service provider extiende Illuminate\Support\ServiceProvider y tiene dos métodos principales: register y boot.
<?php

namespace Acme\Courier;

use Illuminate\Support\ServiceProvider;

class CourierServiceProvider extends ServiceProvider
{
    /**
     * Registra los servicios del paquete
     */
    public function register(): void
    {
        // Los bindings al contenedor van aquí
        $this->mergeConfigFrom(
            __DIR__.'/../config/courier.php', 'courier'
        );

        $this->app->singleton(CourierManager::class, function ($app) {
            return new CourierManager($app['config']['courier']);
        });
    }

    /**
     * Bootea los servicios del paquete
     */
    public function boot(): void
    {
        // Los registros de recursos van aquí
        $this->loadRoutesFrom(__DIR__.'/../routes/web.php');
        $this->loadViewsFrom(__DIR__.'/../resources/views', 'courier');
        $this->loadTranslationsFrom(__DIR__.'/../lang', 'courier');

        $this->publishesMigrations([
            __DIR__.'/../database/migrations' => database_path('migrations'),
        ]);

        $this->publishes([
            __DIR__.'/../config/courier.php' => config_path('courier.php'),
        ], 'courier-config');

        $this->publishes([
            __DIR__.'/../resources/views' => resource_path('views/vendor/courier'),
        ], 'courier-views');
    }
}
No registres event listeners, rutas o vistas dentro del método register. Podrías acabar utilizando por error servicios de otro provider aún no cargado. Todo lo que no sean bindings debe hacerse en boot.

Publicación del archivo de configuración

publishes() — publicar archivos

Al llamar a publishes() desde boot, el usuario puede copiar el archivo de configuración a su aplicación con el comando vendor:publish.
public function boot(): void
{
    $this->publishes([
        __DIR__.'/../config/courier.php' => config_path('courier.php'),
    ]);
}
Los valores publicados se leen igual que cualquier otra configuración.
$value = config('courier.option');

mergeConfigFrom() — mezclar con valores por defecto

Con mergeConfigFrom() en register, se aplican los valores por defecto del paquete aunque el usuario no haya publicado el archivo.
public function register(): void
{
    $this->mergeConfigFrom(
        __DIR__.'/../config/courier.php', 'courier'
    );
}
mergeConfigFrom() no mezcla en profundidad los arrays anidados. En configuraciones con arrays multinivel, si el usuario define solo parte de las opciones, el resto puede no mezclarse.

Separar grupos de publicación con tags

Con el segundo argumento de publishes() puedes indicar una etiqueta para que el usuario publique solo los recursos que necesite.
public function boot(): void
{
    $this->publishes([
        __DIR__.'/../config/courier.php' => config_path('courier.php'),
    ], 'courier-config');

    $this->publishesMigrations([
        __DIR__.'/../database/migrations/' => database_path('migrations'),
    ], 'courier-migrations');
}
# Publica solo el archivo de configuración
php artisan vendor:publish --tag=courier-config

# Publica todos los archivos que ofrece el provider
php artisan vendor:publish --provider="Acme\Courier\CourierServiceProvider"

Registro de rutas

Con loadRoutesFrom() cargas el archivo de rutas. Si la caché de rutas de la aplicación está activa, se salta automáticamente.
public function boot(): void
{
    $this->loadRoutesFrom(__DIR__.'/../routes/web.php');
}
En el archivo de rutas, apunta a los controladores del paquete.
// routes/web.php
use Acme\Courier\Http\Controllers\TrackingController;
use Illuminate\Support\Facades\Route;

Route::prefix('courier')->group(function () {
    Route::get('/track/{id}', [TrackingController::class, 'show'])
        ->name('courier.track');
});

Publicación de migraciones

Con publishesMigrations() puedes publicar los archivos de migración. Al publicarlos, Laravel actualiza automáticamente el timestamp.
public function boot(): void
{
    $this->publishesMigrations([
        __DIR__.'/../database/migrations' => database_path('migrations'),
    ]);
}

Publicación de vistas

loadViewsFrom() — registrar vistas

Con loadViewsFrom() registras un directorio de vistas. El segundo argumento es el namespace con el que referencias las vistas usando paquete::vista.
public function boot(): void
{
    $this->loadViewsFrom(__DIR__.'/../resources/views', 'courier');
}
Tras el registro, referencia las vistas con el namespace del paquete.
Route::get('/dashboard', function () {
    return view('courier::dashboard');
});
Laravel busca las vistas en dos ubicaciones. Primero mira resources/views/vendor/courier de la aplicación y, si no existe, usa el directorio de vistas del paquete. Esto permite al usuario personalizarlas.

Publicar las vistas

public function boot(): void
{
    $this->loadViewsFrom(__DIR__.'/../resources/views', 'courier');

    $this->publishes([
        __DIR__.'/../resources/views' => resource_path('views/vendor/courier'),
    ], 'courier-views');
}

Registrar componentes de Blade

Si el paquete incluye componentes, regístralos en boot.
use Illuminate\Support\Facades\Blade;
use Acme\Courier\View\Components\AlertComponent;

public function boot(): void
{
    Blade::component('courier-alert', AlertComponent::class);
}
También puedes registrarlos en bloque usando un namespace de componente.
use Illuminate\Support\Facades\Blade;

public function boot(): void
{
    Blade::componentNamespace('Acme\\Courier\\View\\Components', 'courier');
}
{{-- Con registro individual --}}
<x-courier-alert />

{{-- Con registro por namespace --}}
<x-courier::alert />

Publicación de traducciones

Con loadTranslationsFrom() registras los archivos de traducción. Se referencian como paquete::archivo.clave.
public function boot(): void
{
    $this->loadTranslationsFrom(__DIR__.'/../lang', 'courier');

    $this->publishes([
        __DIR__.'/../lang' => $this->app->langPath('vendor/courier'),
    ]);
}
// Uso de la traducción
echo trans('courier::messages.welcome');
Si usas archivos de traducción en JSON, utiliza loadJsonTranslationsFrom().
public function boot(): void
{
    $this->loadJsonTranslationsFrom(__DIR__.'/../lang');
}

Registro de comandos

Los comandos Artisan del paquete se registran con el método commands(). Es habitual registrarlos solo en entorno de consola.
use Acme\Courier\Console\Commands\InstallCommand;
use Acme\Courier\Console\Commands\SyncCommand;

public function boot(): void
{
    if ($this->app->runningInConsole()) {
        $this->commands([
            InstallCommand::class,
            SyncCommand::class,
        ]);
    }
}

Integración con el comando optimize

Si el paquete tiene su propia caché, puedes integrarla con php artisan optimize y php artisan optimize:clear mediante optimizes().
public function boot(): void
{
    if ($this->app->runningInConsole()) {
        $this->optimizes(
            optimize: 'courier:cache',
            clear: 'courier:clear-cache',
        );
    }
}

Añadir información al comando about

Para añadir información del paquete a la salida de php artisan about, usa AboutCommand::add().
use Illuminate\Foundation\Console\AboutCommand;

public function boot(): void
{
    AboutCommand::add('Courier Package', fn () => ['Version' => '1.0.0']);
}

Creación de una facade

Los facades permiten invocar bindings del contenedor como métodos estáticos.
1

Crea la clase de servicio

<?php

namespace Acme\Courier;

class CourierManager
{
    public function __construct(
        protected array $config,
    ) {}

    public function send(string $to, string $message): bool
    {
        // Envío del mensaje
        return true;
    }

    public function track(string $id): array
    {
        // Obtiene la información de seguimiento
        return ['status' => 'delivered'];
    }
}
2

Crea la clase facade

Extiende Illuminate\Support\Facades\Facade y devuelve la clave del binding del contenedor en getFacadeAccessor().
<?php

namespace Acme\Courier\Facades;

use Illuminate\Support\Facades\Facade;

/**
 * @method static bool send(string $to, string $message)
 * @method static array track(string $id)
 *
 * @see \Acme\Courier\CourierManager
 */
class Courier extends Facade
{
    protected static function getFacadeAccessor(): string
    {
        return \Acme\Courier\CourierManager::class;
    }
}
3

Enlaza en el service provider

public function register(): void
{
    $this->app->singleton(\Acme\Courier\CourierManager::class, function ($app) {
        return new \Acme\Courier\CourierManager($app['config']['courier']);
    });
}
4

Regístralo en composer.json

"extra": {
    "laravel": {
        "providers": [
            "Acme\\Courier\\CourierServiceProvider"
        ],
        "aliases": {
            "Courier": "Acme\\Courier\\Facades\\Courier"
        }
    }
}
Añadiendo anotaciones @method en el docblock de la facade activas el autocompletado en el IDE.
// Invocación a través de la facade
use Acme\Courier\Facades\Courier;

Courier::send('[email protected]', 'Ha llegado tu paquete');
$status = Courier::track('ABC-123');

DeferrableProvider — carga diferida

Los providers que solo hacen bindings al contenedor pueden implementar la interfaz DeferrableProvider para cargarse de forma diferida. El provider no se carga hasta que el servicio se necesita realmente, lo que mejora el rendimiento de la aplicación.
<?php

namespace Acme\Courier;

use Illuminate\Contracts\Support\DeferrableProvider;
use Illuminate\Support\ServiceProvider;

class CourierServiceProvider extends ServiceProvider implements DeferrableProvider
{
    public function register(): void
    {
        $this->app->singleton(CourierManager::class, function ($app) {
            return new CourierManager($app['config']['courier']);
        });
    }

    /**
     * Devuelve los servicios que provee este provider
     *
     * @return array<int, string>
     */
    public function provides(): array
    {
        return [CourierManager::class];
    }
}
Laravel compila y almacena la lista de servicios que proveen los providers diferidos. El provider solo se carga cuando se resuelve algún servicio de provides().
No uses DeferrableProvider en providers que necesitan registrar recursos (vistas, rutas, event listeners, etc.). Si se cargan de forma diferida, esos recursos quedarán sin registrar.

Pruebas del paquete

Para testear el paquete de forma aislada, usa Orchestra Testbench. Podrás escribir los tests como si estuvieras dentro de una app Laravel normal.
composer require --dev orchestra/testbench
En tu test case, sobrescribe getPackageProviders() para registrar el service provider del paquete.
<?php

namespace Acme\Courier\Tests;

use Acme\Courier\CourierServiceProvider;
use Orchestra\Testbench\TestCase as BaseTestCase;

class TestCase extends BaseTestCase
{
    /**
     * Registra los service providers del paquete
     */
    protected function getPackageProviders($app): array
    {
        return [
            CourierServiceProvider::class,
        ];
    }

    /**
     * Registra los alias de facade del paquete
     */
    protected function getPackageAliases($app): array
    {
        return [
            'Courier' => \Acme\Courier\Facades\Courier::class,
        ];
    }

    /**
     * Configuración del entorno de pruebas
     */
    protected function defineEnvironment($app): void
    {
        $app['config']->set('courier.api_key', 'test-key');
    }
}
<?php

namespace Acme\Courier\Tests\Feature;

use Acme\Courier\Facades\Courier;
use Acme\Courier\Tests\TestCase;

class CourierTest extends TestCase
{
    public function test_can_send_message(): void
    {
        $result = Courier::send('[email protected]', 'Mensaje de prueba');

        $this->assertTrue($result);
    }
}

Publicación en Composer

Buenas prácticas para publicar tu paquete en Packagist. Configuración básica del composer.json
{
    "name": "acme/courier",
    "description": "A Laravel courier package",
    "type": "library",
    "license": "MIT",
    "require": {
        "php": "^8.2",
        "illuminate/support": "^11.0||^12.0||^13.0"
    },
    "require-dev": {
        "orchestra/testbench": "^9.0||^10.0",
        "phpunit/phpunit": "^11.0"
    },
    "autoload": {
        "psr-4": {
            "Acme\\Courier\\": "src/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "Acme\\Courier\\Tests\\": "tests/"
        }
    },
    "extra": {
        "laravel": {
            "providers": [
                "Acme\\Courier\\CourierServiceProvider"
            ],
            "aliases": {
                "Courier": "Acme\\Courier\\Facades\\Courier"
            }
        }
    },
    "minimum-stability": "stable",
    "prefer-stable": true
}
Al depender de illuminate/support, incluyes solo los componentes de Laravel que necesita el paquete en lugar de illuminate/framework completo. Mantén pequeño el árbol de dependencias.
Ejemplo de estructura de directorios
acme/courier/
├── config/
│   └── courier.php
├── database/
│   └── migrations/
│       └── 2024_01_01_000000_create_courier_logs_table.php
├── lang/
│   └── es/
│       └── messages.php
├── resources/
│   └── views/
│       └── dashboard.blade.php
├── routes/
│   └── web.php
├── src/
│   ├── Console/
│   │   └── Commands/
│   │       └── InstallCommand.php
│   ├── Facades/
│   │   └── Courier.php
│   ├── Http/
│   │   └── Controllers/
│   │       └── TrackingController.php
│   ├── CourierManager.php
│   └── CourierServiceProvider.php
├── tests/
│   ├── Feature/
│   └── TestCase.php
├── composer.json
└── README.md

Páginas relacionadas

Service providers

Detalles sobre los métodos register y boot de los service providers y los providers diferidos.

Gestión de compatibilidad de versiones

Estrategia para reaccionar ante actualizaciones mayores de Laravel y PHP, y configuración de la matriz de pruebas en GitHub Actions.
Última modificación el 13 de julio de 2026