Package Development and Ecosystem

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Laravel 13.x (13.34.0) on PHP 8.3 to 8.5, checked on 2026-10-05. A Laravel package is a Composer package with a service provider. Laravel discovers the provider from composer.json, and the provider registers bindings, configuration, views, commands and routes. Bookshelf’s ISBN code is pulled out into bookshelf/isbn below, and the second half of the page lists the packages most applications choose from. Composer packages in general, with versioning, autoloading and publishing to Packagist, are on Composer and Packages.

A package

The package lives next to the application, in ../packages/isbn:

packages/isbn/
├── composer.json
├── config/isbn.php
├── resources/views/isbn.blade.php
├── src/Isbn.php                       validation, normalisation, ISBN-13 conversion, fixed-width formatting
├── src/IsbnServiceProvider.php
├── src/Facades/Isbn.php
├── src/View/IsbnComponent.php         <x-isbn value="…" />
├── src/Console/CheckIsbnCommand.php   php artisan isbn:check
├── tests/TestCase.php
└── tests/IsbnTest.php

extra.laravel in composer.json is what discovery reads:

{
    "name": "bookshelf/isbn",
    "require": {
        "php": "^8.3",
        "illuminate/console": "^12.0|^13.0",
        "illuminate/support": "^12.0|^13.0",
        "illuminate/view": "^12.0|^13.0"
    },
    "require-dev": { "orchestra/testbench": "^11.3", "phpunit/phpunit": "^12.5" },
    "autoload": { "psr-4": { "Bookshelf\\Isbn\\": "src/" } },
    "extra": {
        "laravel": {
            "providers": ["Bookshelf\\Isbn\\IsbnServiceProvider"],
            "aliases": { "Isbn": "Bookshelf\\Isbn\\Facades\\Isbn" }
        }
    }
}

The service provider does the wiring:

<?php
// src/IsbnServiceProvider.php
namespace Bookshelf\Isbn;

use Illuminate\Support\Facades\Blade;
use Illuminate\Support\ServiceProvider;

class IsbnServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->mergeConfigFrom(__DIR__.'/../config/isbn.php', 'isbn');      // the app's file overrides keys, not the whole array
        $this->app->singleton(Isbn::class, fn ($app) => new Isbn($app['config']->get('isbn.separator')));
    }

    public function boot(): void
    {
        $this->loadViewsFrom(__DIR__.'/../resources/views', 'isbn');        // isbn::isbn, overridable in resources/views/vendor/isbn
        Blade::component('isbn', View\IsbnComponent::class);                // <x-isbn value="…" />

        if ($this->app->runningInConsole()) {
            $this->commands([Console\CheckIsbnCommand::class]);
            $this->publishes([__DIR__.'/../config/isbn.php' => config_path('isbn.php')], 'isbn-config');
            $this->publishes([__DIR__.'/../resources/views' => resource_path('views/vendor/isbn')], 'isbn-views');
        }

        if (class_exists(\Illuminate\Foundation\Console\AboutCommand::class)) {   // laravel/framework, not illuminate/*
            \Illuminate\Foundation\Console\AboutCommand::add('ISBN', fn () => ['Separator' => config('isbn.separator')]);
        }
    }
}

Configuration is merged in register(), so other providers can read it during boot. Views, components, commands and publishing belong in boot(). Publishing is only needed in the console, which the runningInConsole() check reflects.

The require section must match what the code uses. The first version required only illuminate/support, like many packages. AboutCommand lives in Illuminate\Foundation, which exists only in laravel/framework, and the Blade component needs illuminate/view. In a full application that does not show, because the framework provides everything, but the package would break when installed with the illuminate/* components alone. The fix is to require the illuminate/* components the code uses and to guard the Foundation-only call.

Installing it with a path repository

During development the application installs the package from the directory, as a symlink, so changes are live:

$ composer config repositories.isbn '{"type": "path", "url": "../packages/isbn", "options": {"symlink": true}}'
$ composer require bookshelf/isbn:@dev
  - Installing bookshelf/isbn (dev-main): Symlinking from ../packages/isbn
 bookshelf/isbn ................................................ DONE
$ php -r '$m = require "bootstrap/cache/packages.php"; echo json_encode($m["bookshelf/isbn"]);'
{"providers":["Bookshelf\\Isbn\\IsbnServiceProvider"],"aliases":{"Isbn":"Bookshelf\\Isbn\\Facades\\Isbn"}}

Nothing was added to bootstrap/providers.php. Discovery wrote the provider into bootstrap/cache/packages.php, next to the 28 other discovered packages. Then:

$ php artisan isbn:check 978-0-8070-8305-4 0807083054 978-0-8070-8305-5 0-441-17271-X
+-------------------+-------+-------------------+
| Input             | Valid | ISBN-13           |
+-------------------+-------+-------------------+
| 978-0-8070-8305-4 | no    | -                 |
| 0807083054        | yes   | 978-0-8070-8305-5 |
| 978-0-8070-8305-5 | yes   | 978-0-8070-8305-5 |
| 0-441-17271-X     | no    | -                 |
+-------------------+-------+-------------------+
$ php artisan about --only=isbn
 ISBN ..........................................................
 Separator ................................................... -
$ php artisan vendor:publish --tag=isbn-config
 Copying file [../packages/isbn/config/isbn.php] to [config/isbn.php] ...... DONE
# with 'separator' => ' ' in config/isbn.php:
Blade::render('<x-isbn value="0807083054" />')   <span class="isbn" title="ISBN-13">978 0 8070 8305 5</span>
Isbn::format('0807083054')                       978 0 8070 8305 5

The first line is a finding about Bookshelf’s own data. All four ISBNs that CatalogueSeeder writes on Migrations, Seeders and Factories, including Kindred's 978-0-8070-8305-4 used in the examples on earlier pages, fail the checksum, while a known valid number such as 978-0-14-143947-1 passes. The application’s own Isbn13 rule rejects them too. The seeder wrote them directly, so no validation ran. A data-quality command like isbn:check, run over the whole table, finds such rows.

Testing with Testbench

Orchestra Testbench boots a minimal Laravel application around the package, so its provider, configuration, views and commands can be tested without a host application:

<?php
// tests/TestCase.php
namespace Bookshelf\Isbn\Tests;

use Bookshelf\Isbn\IsbnServiceProvider;
use Orchestra\Testbench\TestCase as Orchestra;

abstract class TestCase extends Orchestra
{
    protected function getPackageProviders($app): array
    {
        return [IsbnServiceProvider::class];
    }

    protected function defineEnvironment($app): void
    {
        $app['config']->set('isbn.separator', '-');
    }
}
<?php
// tests/IsbnTest.php
namespace Bookshelf\Isbn\Tests;

use Bookshelf\Isbn\Facades\Isbn;
use Illuminate\Support\Facades\Blade;
use PHPUnit\Framework\Attributes\DataProvider;

class IsbnTest extends TestCase
{
    public static function isbns(): array
    {
        return [
            'ISBN-10' => ['0807083054', true],
            'ISBN-10 with X' => ['080442957X', true],
            'ISBN-13' => ['978-0-8070-8305-5', true],
            'wrong check digit' => ['978-0-8070-8305-4', false],
            'too short' => ['12345', false],
        ];
    }

    #[DataProvider('isbns')]
    public function test_it_validates_lengths_and_checksums(string $isbn, bool $valid): void
    {
        $this->assertSame($valid, Isbn::isValid($isbn));
    }

    public function test_it_formats_with_the_configured_separator(): void
    {
        $this->assertSame('978-0-8070-8305-5', Isbn::format('0807083054'));

        config(['isbn.separator' => ' ']);
        $this->app->forgetInstance(\Bookshelf\Isbn\Isbn::class);
        Isbn::clearResolvedInstance(\Bookshelf\Isbn\Isbn::class);
        $this->assertSame('978 0 8070 8305 5', Isbn::format('0807083054'));
    }

    public function test_it_renders_the_blade_component(): void
    {
        $this->assertStringContainsString('978-0-8070-8305-5', Blade::render('<x-isbn value="0807083054" />'));
    }

    public function test_the_command_reports_invalid_input(): void
    {
        $this->artisan('isbn:check', ['isbn' => ['978-0-8070-8305-4']])->expectsOutputToContain('no')->assertSuccessful();
    }
}
$ vendor/bin/phpunit
OK (8 tests, 10 assertions)

A normal composer install in the package directory would give it its own vendor/. Here that install filled the disk (it clones the whole framework with --prefer-source), so the tests ran with Bookshelf’s vendor/, which had Testbench 11.5 added, through a small bootstrap file. Pest needs a project root with its own vendor/, so the tests use PHPUnit classes. In CI, the package is tested against each Laravel version it claims, with a matrix over illuminate/* versions and --prefer-lowest.

spatie/laravel-package-tools removes most of the provider boilerplate (->hasConfigFile()->hasViews()->hasCommand(…)), and many published packages use it.

The ecosystem

Versions from Packagist on 2026-10-05, all supporting Laravel 13 unless noted:

Package Version Use

First-party

laravel/cashier (Stripe), laravel/cashier-paddle

16.8.0, 2.8.1

Subscriptions, invoices and webhooks for Stripe or Paddle

laravel/pennant

1.26.0

Feature flags (Processes, Concurrency and Context)

laravel/pint

1.32.1

Code style on PHP-CS-Fixer, with a Laravel preset

laravel/envoy

2.12.2

Task runner for deploy scripts over SSH, with Blade syntax

laravel/folio

1.2.0

Page-based routing from files in resources/views/pages

Precognition

in the framework

Live validation from front-end forms using the server’s rules (Validation and Form Requests)

laravel/scout

11.8.0

Full-text search drivers (Redis, MongoDB and Search)

laravel/socialite

5.31.0

OAuth login with GitHub, Google and others (Authentication)

Nova, Spark

commercial

Admin panel, and SaaS billing scaffolding. Licensed, not on Packagist

Third-party

filament/filament

5.9.0

Admin panels, forms and tables on Livewire

spatie/laravel-permission

8.3.0

Roles and permissions in the database

spatie/laravel-medialibrary

11.23.9

File attachments for models, with conversions

spatie/laravel-backup, spatie/laravel-activitylog, spatie/laravel-data

10.3.3, 5.1.1, 4.23.0

Backups, audit logs, typed data objects

larastan/larastan

3.12.3

PHPStan extension that understands Laravel’s magic

nuwave/lighthouse

6.71.0

GraphQL server (Building APIs and API Resources)

barryvdh/laravel-debugbar

4.4.4

In-page debug toolbar

Legacy

Mix, Homestead, Valet

maintained

Replaced in the documentation by Vite, Sail or Herd

Breeze, Jetstream

2.4.2, 5.5.3 (frozen)

Replaced by the starter kits (Inertia and Starter Kits)

Before adding a package: check that it supports the current Laravel version (the illuminate/* constraints), that releases are recent, how many maintainers it has, and what composer audit says. A package that only saves a few lines is a dependency for the life of the application.

Where older books and tutorials are outdated

  • Registering every package’s provider and facade in config/app.php (Stauffer ch. 11) is unnecessary since package discovery in Laravel 5.5. bootstrap/providers.php holds only the application’s own providers.

  • illuminate/support as the only dependency works for a package that uses only the support classes. Packages that touch Foundation must say so.

  • Testing a package inside a host application is replaced by Testbench.

  • Laravel Mix, Homestead and Valet still work. Vite, Sail and Herd are what the documentation describes.