Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Building Web Apps with Laravel  Module 17 – Architecture, multi-tenancy and packages

Building and publishing a Laravel package

Turn validation rules shared by several Laravel apps into one Composer package with a service provider, config and translations, then test and release it.

  • Advanced
  • 15 minutes
  • Examples run with PHP 8.5.11, Composer 2.10.3, Laravel 13.34.0 and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 5 of 15 minutes free
Free: One key in composer.json makes it a Laravel package · A fresh app installs the package without one line of setup. With a pass: 2 in-depth parts and 3 interview answers, about 10 minutes.

What you will learn

  • Structure a Composer package with a service provider, publishable config, translations and an Artisan command
  • Test a package with Orchestra Testbench on Laravel 12 and Laravel 13 in a CI matrix
  • Choose illuminate constraints and the semantic version number of each release
  • Publish a package on Packagist once composer validate and composer audit have run
  • Add AI guidelines and a skill for Laravel Boost to a package

Before you start

What you’ll learn · Before you start

What you will learn

  • Structure a Composer package with a service provider, publishable config, translations and an Artisan command
  • Test a package with Orchestra Testbench on Laravel 12 and Laravel 13 in a CI matrix
  • Choose illuminate constraints and the semantic version number of each release
  • Publish a package on Packagist once composer validate and composer audit have run
  • Add AI guidelines and a skill for Laravel Boost to a package

Before you start

On this page

Pathshala runs three Laravel apps, for admissions, fees and parents, and each kept its own copy of the checks for PAN, IFSC and PIN code values. A fix to the PIN code check reached one copy and missed the other two. Building a Laravel package turns the three copies into one Composer package that every app requires, and publishing a Laravel package makes each fix one release that every app takes with composer update.

A Laravel package differs from a plain Composer: PHP's dependency manager. It reads composer.json, downloads the packages a project needs from Packagist and installs them into the vendor folder. More in “Install PHP, Composer and Laravel 13” package by one key in its composer.json: extra.laravel names its service provider: A class that sets one part of the application up. Its register method binds services into the container, and its boot method uses them once every provider has registered. More in “How Laravel handles a request”, and Laravel loads that provider when an app installs the package, with its config, messages and Artisan commands, without a line of setup in the app. Here you build pathshala/indian-rules, four validation rules with English and Hindi messages, and install it into a fresh app.

One key in composer.json makes it a Laravel package

Building the package starts with a folder that has its own composer.json. Under extra.laravel, providers lists the service provider and aliases the facade: A class that names one entry of Laravel's container and forwards every static call on it to the object stored there, as Cache::get() forwards to the cache manager. More in “Service providers, facades and contracts”; the rest of the folder is ordinary Composer and PHP:

Path What it holds
composer.json name, constraints, extra.laravel
src/ the provider, a service, four rules, a facade, a command
config/indian-rules.php defaults an app may copy and edit
lang/en, lang/hi the messages
resources/boost/ notes for coding assistants
tests/ the package’s own tests

The provider is the package’s entry point. Its register() merges the default config under the key indian-rules and binds the service the facade reaches. Its boot() calls loadTranslationsFrom() with the namespace indian-rules, so a rule fails with the message key indian-rules::validation.pan; publishes() with a publish tag: A name a package gives to one group of files in publishes(), so php artisan vendor:publish --tag=<name> copies only that group, such as the config or the language files, into the app. for each group of files an app may copy; and commands() for the Artisan command. Read the eight files that matter:

The package's composer.json, provider, service, a rule, its command, config and messages

install/packages/indian-rules/composer.json

{
    "name": "pathshala/indian-rules",
    "description": "Validation rules for Indian identifiers (PAN, GSTIN, IFSC, PIN code) for Laravel, with English and Hindi messages.",
    "type": "library",
    "license": "MIT",
    "keywords": ["laravel", "validation", "india"],
    "require": {
        "php": "^8.3",
        "composer-runtime-api": "^2.2",
        "illuminate/console": "^12.0 || ^13.0",
        "illuminate/contracts": "^12.0 || ^13.0",
        "illuminate/support": "^12.0 || ^13.0",
        "illuminate/validation": "^12.0 || ^13.0"
    },
    "require-dev": {
        "orchestra/testbench": "^10.12 || ^11.3"
    },
    "autoload": {
        "psr-4": {
            "Pathshala\\IndianRules\\": "src/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "Pathshala\\IndianRules\\Tests\\": "tests/"
        }
    },
    "extra": {
        "laravel": {
            "providers": [
                "Pathshala\\IndianRules\\IndianRulesServiceProvider"
            ],
            "aliases": {
                "IndianRules": "Pathshala\\IndianRules\\Facades\\IndianRules"
            }
        }
    },
    "scripts": {
        "test": "phpunit"
    },
    "config": {
        "sort-packages": true
    }
}

Service provider · install/packages/indian-rules/src/IndianRulesServiceProvider.php

<?php

namespace Pathshala\IndianRules;

use Composer\InstalledVersions;
use Illuminate\Foundation\Console\AboutCommand;
use Illuminate\Support\ServiceProvider;
use Pathshala\IndianRules\Console\CheckCommand;

class IndianRulesServiceProvider extends ServiceProvider
{
    /**
     * Bind the package's services and merge its default config under the app's own copy.
     */
    public function register(): void
    {
        $this->mergeConfigFrom(__DIR__.'/../config/indian-rules.php', 'indian-rules');

        $this->app->singleton(IndianRules::class);
    }

    /**
     * Tell Laravel where the translations are, what an app may publish, and what the package adds to Artisan.
     */
    public function boot(): void
    {
        $this->loadTranslationsFrom(__DIR__.'/../lang', 'indian-rules');

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

        $this->publishes([
            __DIR__.'/../lang' => $this->app->langPath('vendor/indian-rules'),
        ], 'indian-rules-lang');

        if ($this->app->runningInConsole()) {
            $this->commands([CheckCommand::class]);

            AboutCommand::add('Indian rules', fn () => [
                'Version' => InstalledVersions::getPrettyVersion('pathshala/indian-rules'),
                'Normalise input' => config('indian-rules.normalise') ? 'on' : 'off',
            ]);
        }
    }
}

install/packages/indian-rules/src/IndianRules.php

<?php

namespace Pathshala\IndianRules;

use Illuminate\Contracts\Config\Repository;

/**
 * The checks the package's rule classes share, and the object the IndianRules facade reaches.
 */
class IndianRules
{
    private const BASE36 = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';

    public function __construct(private Repository $config)
    {
    }

    /**
     * The value as the rules read it: in capitals and without spaces, unless the app turned that off.
     */
    public function normalise(string $value): string
    {
        if (! $this->config->get('indian-rules.normalise')) {
            return $value;
        }

        return strtoupper(str_replace(' ', '', $value));
    }

    /**
     * Ten characters, in the shape this package accepts: five letters, four digits and a letter.
     */
    public function isPan(string $value): bool
    {
        return preg_match('/^[A-Z]{5}[0-9]{4}[A-Z]$/', $this->normalise($value)) === 1;
    }

    /**
     * Eleven characters: four letters for the bank, a zero, then six letters or digits for the branch.
     */
    public function isIfsc(string $value): bool
    {
        return preg_match('/^[A-Z]{4}0[A-Z0-9]{6}$/', $this->normalise($value)) === 1;
    }

    /**
     * Six digits; "411 001" is read as 411001 while normalising is on.
     */
    public function isPincode(string $value): bool
    {
        return preg_match('/^[0-9]{6}$/', $this->normalise($value)) === 1;
    }

    /**
     * What is wrong with a 15-character tax number: 'shape', 'state', 'check', or null when nothing is.
     * The shape: a two-digit state code, a PAN, a two-character entity code and one check character.
     */
    public function gstinProblem(string $value): ?string
    {
        $value = $this->normalise($value);

        if (preg_match('/^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][0-9A-Z]{2}[0-9A-Z]$/', $value) !== 1) {
            return 'shape';
        }

        $states = $this->config->get('indian-rules.gstin.states');
        if (is_array($states) && ! in_array(substr($value, 0, 2), $states, true)) {
            return 'state';
        }

        if ($this->config->get('indian-rules.gstin.check_character') && $value[14] !== $this->checkCharacter(substr($value, 0, 14))) {
            return 'check';
        }

        return null;
    }

    /**
     * The check character of the first fourteen characters, computed as Luhn mod 36 over their base-36 values.
     * The rules that define the number say there is one check character, not how it is computed: compare this
     * with the issuer's published method before a live form refuses numbers with it.
     */
    public function checkCharacter(string $first14): string
    {
        $sum = 0;
        $factor = 2;

        for ($i = 13; $i >= 0; $i--) {
            $addend = $factor * strpos(self::BASE36, $first14[$i]);
            $factor = $factor === 2 ? 1 : 2;
            $sum += intdiv($addend, 36) + $addend % 36;
        }

        return self::BASE36[(36 - $sum % 36) % 36];
    }
}

Rules/Pan.php · install/packages/indian-rules/src/Rules/Pan.php

<?php

namespace Pathshala\IndianRules\Rules;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Pathshala\IndianRules\IndianRules;

class Pan implements ValidationRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        if (! is_string($value) || ! app(IndianRules::class)->isPan($value)) {
            $fail('indian-rules::validation.pan')->translate();
        }
    }
}

Console/CheckCommand.php · install/packages/indian-rules/src/Console/CheckCommand.php

<?php

namespace Pathshala\IndianRules\Console;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\Validator;
use Pathshala\IndianRules\Rules\Gstin;
use Pathshala\IndianRules\Rules\Ifsc;
use Pathshala\IndianRules\Rules\Pan;
use Pathshala\IndianRules\Rules\Pincode;

class CheckCommand extends Command
{
    protected $signature = 'indian-rules:check
        {rule : pan, gstin, ifsc or pincode}
        {value : the value to check}
        {--locale= : the language of the messages, such as hi}';

    protected $description = 'Check one value with one of the indian-rules rules';

    private const RULES = ['pan' => Pan::class, 'gstin' => Gstin::class, 'ifsc' => Ifsc::class, 'pincode' => Pincode::class];

    public function handle(): int
    {
        $rule = self::RULES[$this->argument('rule')] ?? null;

        if ($rule === null) {
            $this->error('Unknown rule: use pan, gstin, ifsc or pincode.');

            return self::INVALID;
        }

        if ($this->option('locale')) {
            $this->laravel->setLocale($this->option('locale'));
        }

        $validator = Validator::make(['value' => $this->argument('value')], ['value' => [new $rule()]]);

        if ($validator->passes()) {
            $this->line('valid');

            return self::SUCCESS;
        }

        foreach ($validator->errors()->all() as $message) {
            $this->line($message);
        }

        return self::FAILURE;
    }
}

config · install/packages/indian-rules/config/indian-rules.php

<?php

return [

    /*
     * Upper-case the value and drop spaces before checking, so a number typed as "abcpe 1234 f" is checked as
     * ABCPE1234F. Set it to false to check exactly what was sent.
     */
    'normalise' => true,

    /*
     * The fifteen-character tax number: which state codes the app accepts (null for every code) and whether the
     * last character is checked against the first fourteen.
     */
    'gstin' => [
        'states' => null,
        'check_character' => true,
    ],

];

lang/en · install/packages/indian-rules/lang/en/validation.php

<?php

return [
    'pan' => 'The :attribute field must be a 10-character PAN, such as ABCPE1234F.',
    'ifsc' => 'The :attribute field must be an 11-character IFSC, such as ABCD0123456.',
    'pincode' => 'The :attribute field must be a PIN code of six digits.',
    'gstin' => [
        'shape' => 'The :attribute field must be a 15-character number, such as 29ABCDE1234F1ZW.',
        'state' => 'The :attribute field starts with a state code this app does not accept.',
        'check' => 'The last character of :attribute does not match the other fourteen.',
    ],
];

lang/hi · install/packages/indian-rules/lang/hi/validation.php

<?php

return [
    'pan' => ':attribute दस वर्णों का PAN होना चाहिए, जैसे ABCPE1234F।',
    'ifsc' => ':attribute ग्यारह वर्णों का IFSC होना चाहिए, जैसे ABCD0123456।',
    'pincode' => ':attribute छह अंकों का पिन कोड होना चाहिए।',
    'gstin' => [
        'shape' => ':attribute 29ABCDE1234F1ZW जैसी 15 वर्णों की संख्या होनी चाहिए।',
        'state' => ':attribute ऐसे राज्य कोड से शुरू होता है जिसे यह ऐप स्वीकार नहीं करता।',
        'check' => ':attribute का अंतिम वर्ण बाकी चौदह वर्णों से मेल नहीं खाता।',
    ],
];

The constraints say which versions of PHP and of the framework’s illuminate/* components the package works with: ^12.0 || ^13.0 allows any 12.x or 13.x release, and the double pipe is the logical OR that Composer’s documentation recommends.

The identifiers the rules check

Rule 10 of the CGST Rules builds the 15-character GSTIN from a two-character state code, a ten-character PAN or TAN, a two-character entity code and one checksum character; the package accepts numbers built on a PAN only. The RBI describes a branch’s IFSC as 11 characters with 0 in fifth place, and the package checks a PIN code as six digits.

A fresh app installs the package without one line of setup

While the package has no release, an app can require it from a folder beside it through a path repository: A Composer repository of type path that points at a folder on the same disk, so an app can require a package that has no release yet; Composer links or copies that folder into vendor/.. Composer then links the folder into vendor/, and Laravel’s package discovery: Laravel's step after every Composer autoload dump that reads each installed package's extra.laravel section and records its service providers and facade aliases in bootstrap/cache/packages.php, so an app needs no setup to load them. picks up the provider from extra.laravel. Step through what happens between composer require and the first check:

Interactive · stage by stage From composer require to the first check Step through one install, from composer require to the first check the app runs, and see which file each step writes.
A fresh app installs pathshala/indian-rules, step 8 of 8. Showing composer require, Autoload dump, package:discover, packages.php, register(), boot(), vendor:publish, The app calls it. In focus: The app calls it.composer requirefrom a folder beside the appAutoload dumpsrc/ mapped for PSR-4package:discoverreads extra.laravelpackages.phpprovider and alias keptregister()defaults merged, boundboot()messages, publish, commandvendor:publishthe app gets its copyThe app calls itrule, facade, command

The drawing shows the whole scene; its 8 steps are listed below.

Every step as text
  1. The package is required. composer require found pathshala/indian-rules in a path repository, a folder beside the app, and linked it into vendor/ as version 1.0.0. The app's composer.json now asks for ^1.0.
  2. Autoloading is rebuilt. Composer regenerated its autoloader, so the namespace Pathshala\IndianRules\ now loads from the package's src/ folder, then it ran the script that Laravel's skeleton lists after every autoload dump.
  3. Discovery reads extra.laravel. That script is php artisan package:discover. It reads the extra.laravel section of every installed package; the recorded run lists seven packages, the last one pathshala/indian-rules.
  4. The manifest is written. bootstrap/cache/packages.php now holds the provider class and the IndianRules alias. The app's own bootstrap/providers.php was not edited.
  5. register() runs first. On the next command, Laravel reads the manifest and calls register() on the provider: it merges the default config under the key indian-rules and binds the IndianRules service once for the whole app.
  6. boot() runs after register(). Once all providers are registered, boot() names the folder of the English and Hindi messages, the files an app may publish and the Artisan command the package adds.
  7. The app takes its own copy. php artisan vendor:publish --tag=indian-rules-config copied the defaults to config/indian-rules.php. The app edits that copy; the file in the package stays the default.
  8. The checks work. The command said valid for abcpe 1234 f, refused a tax number with a wrong last character in English and in Hindi, and the facade worked out W, with no setup written in the app.

The same install, recorded in a new Pathshala app. Two checks are meant to fail, so the page shows the message a wrong check character gets:

A fresh app requires the package, publishes its config and runs its checks Python · install_session.py
from session import run

# A fresh Pathshala app installs pathshala/indian-rules from a folder beside it (a path repository), as a team does
# while the package has no release yet. Package discovery writes the provider and the alias into
# bootstrap/cache/packages.php; vendor:publish copies the config; the command, the Hindi messages and the facade
# then work without one line of setup in the app. Two checks are meant to fail (lines with "!"): they show the
# message a wrong check character gets.
run(
    """
export COMPOSER_FUND=0
composer config repositories.indian-rules '{"type": "path", "url": "packages/indian-rules", "options": {"versions": {"pathshala/indian-rules": "1.0.0"}}}'
composer require pathshala/indian-rules
grep -A11 "'pathshala/indian-rules'" bootstrap/cache/packages.php
php artisan vendor:publish --tag=indian-rules-config
php artisan indian-rules:check pan "abcpe 1234 f"
! php artisan indian-rules:check gstin 29ABCDE1234F1Z0
! php artisan indian-rules:check gstin 29ABCDE1234F1Z0 --locale=hi
php artisan tinker --execute 'echo IndianRules::checkCharacter("29ABCDE1234F1Z"), PHP_EOL;'
php artisan about --only=indian_rules
""",
    app=True,
    overlay=("install",),
)

Output

$ export COMPOSER_FUND=0
$ composer config repositories.indian-rules '{"type": "path", "url": "packages/indian-rules", "options": {"versions": {"pathshala/indian-rules": "1.0.0"}}}'
$ composer require pathshala/indian-rules
./composer.json has been updated
Running composer update pathshala/indian-rules
Loading composer repositories with package information
Updating dependencies
Lock file operations: 1 install, 0 updates, 0 removals
  - Locking pathshala/indian-rules (1.0.0)
Writing lock file
Installing dependencies from lock file (including require-dev)
Package operations: 1 install, 0 updates, 0 removals
  - Installing pathshala/indian-rules (1.0.0): Symlinking from packages/indian-rules
Generating optimized autoload files
> Illuminate\Foundation\ComposerScripts::postAutoloadDump
> @php artisan package:discover --ansi

   INFO  Discovering packages.

  laravel/pail .......................................................... DONE
  laravel/pao ........................................................... DONE
  laravel/tinker ........................................................ DONE
  nesbot/carbon ......................................................... DONE
  nunomaduro/collision .................................................. DONE
  nunomaduro/termwind ................................................... DONE
  pathshala/indian-rules ................................................ DONE

> @php artisan vendor:publish --tag=laravel-assets --ansi --force

   INFO  No publishable resources for tag [laravel-assets].

No security vulnerability advisories found.
Using version ^1.0 for pathshala/indian-rules
$ grep -A11 "'pathshala/indian-rules'" bootstrap/cache/packages.php
  'pathshala/indian-rules' =>
  array (
    'providers' =>
    array (
      0 => 'Pathshala\\IndianRules\\IndianRulesServiceProvider',
    ),
    'aliases' =>
    array (
      'IndianRules' => 'Pathshala\\IndianRules\\Facades\\IndianRules',
    ),
  ),
);
$ php artisan vendor:publish --tag=indian-rules-config
   INFO  Publishing [indian-rules-config] assets.

  Copying file [packages/indian-rules/config/indian-rules.php] to [config/indian-rules.php]  DONE
$ php artisan indian-rules:check pan "abcpe 1234 f"
valid
$ php artisan indian-rules:check gstin 29ABCDE1234F1Z0
The last character of value does not match the other fourteen.
[exit status 1]
$ php artisan indian-rules:check gstin 29ABCDE1234F1Z0 --locale=hi
value का अंतिम वर्ण बाकी चौदह वर्णों से मेल नहीं खाता।
[exit status 1]
$ php artisan tinker --execute 'echo IndianRules::checkCharacter("29ABCDE1234F1Z"), PHP_EOL;'
W
$ php artisan about --only=indian_rules
  Indian rules ...............................................................
  Normalise input ......................................................... on
  Version .............................................................. 1.0.0

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 install_session.py

Besides composer.lock, the app’s own files changed in two places only: composer.json gained the repository and ^1.0, and vendor:publish added config/indian-rules.php. Discovery listed the package beside six others and wrote its provider and alias into bootstrap/cache/packages.php. The command accepted abcpe 1234 f because the default config upper-cases the value and drops spaces before checking. The Hindi message came from the package’s lang/hi file, and about showed the version that Composer installed.

Key idea

The app requires the package and copies only the files it wants to change; the provider named in extra.laravel does every other step, which is what makes a Composer package a Laravel package.

Checkpoint

Check: What makes a Composer package a Laravel package · Installing a package and package discovery

  1. Question 1 of 3 A package's composer.json maps its namespace under autoload.psr-4 but has no extra section. An app requires it. What happens?

    Choose one answer.

    Show the answer: Question 1 of 3

    Answer: Its classes load, but its service provider is not registered until the app adds it to bootstrap/providers.php

    extra.laravel.providers is the key that makes Laravel load a package's provider on its own. Without it the package is still a Composer package: its classes autoload, and the app must register the provider itself.

  2. Question 2 of 3 After composer require pathshala/indian-rules, which file lists the package's provider and its IndianRules alias for Laravel?

    Choose one answer.

    Show the answer: Question 2 of 3

    Answer: bootstrap/cache/packages.php

    php artisan package:discover reads the extra.laravel section of every installed package and writes the providers and aliases it finds into bootstrap/cache/packages.php, which Laravel reads on every request and command.

  3. Question 3 of 3 An app ran php artisan vendor:publish --tag=indian-rules-config and then edited config/indian-rules.php. Later, composer update installs a new release of the package. Which file holds the app's settings now?

    Choose one answer.

    Show the answer: Question 3 of 3

    Answer: The app's edited config/indian-rules.php

    vendor:publish copies a tagged group of files into the app once. The app edits its copy, and the package's own file stays the default that the provider merges under it.

Four more things decide whether other teams can depend on the package. Orchestra Testbench runs the package’s tests inside a small Laravel application it builds for each test. A CI matrix: A CI job definition that runs once for every combination of the values it lists, for example three PHP versions times two Laravel versions: six jobs. runs those tests once for each PHP and Laravel version the constraints allow. semantic versioning: A way of numbering releases as major.minor.patch: a new major version may break code written for the one before, a new minor adds features without breaking anything, and a patch only fixes bugs. More in “What Laravel is and when to use it” tells an app which releases can break it, and publishing a Laravel package on Packagist: The default package repository for Composer. Composer asks it where to download a package such as laravel/framework. More in “Install PHP, Composer and Laravel 13” turns each git tag into a version. Laravel Boost reads guidelines and skills that a package ships, so coding assistants use the package as intended.

Testing the package with Testbench on Laravel 12 and 13 Pro: opens with a Pro, Premium or Ultimate pass

A package has no application of its own to run its tests in, yet it promises to work in every app on every Laravel version its constraints allow. Two recorded runs show what that takes: a Testbench suite that catches a config bug only an app with a published config would meet, and a two-leg matrix that catches a change only Laravel 12 refuses.

In this part:

  • A test case that boots the package the way an app would
  • Why a published config with one key lost the other
  • A matrix of PHP and Laravel versions, and the failure only one leg catches

4 runnable examples (.github/workflows/tests.yml, CheckCommand.php with attributes, CommandTest.php, ConfigTest.php, MessagesTest.php, Python, RulesTest.php, phpunit.xml.dist, tests/TestCase.php) · about 6 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Versions, release checks, Packagist and Boost Pro: opens with a Pro, Premium or Ultimate pass

A release is a promise to every app that requires the package, so its number and its constraints have to say exactly what changed and what was tested. The runs below check a release before its tag, and the rules turn each kind of change into a version number.

In this part:

  • Constraints that claim only what the matrix proves
  • Which change needs a major, a minor or a patch number
  • The checks before a tag: validate, the root version and audit
  • Publishing on Packagist, one tag per version
  • Notes for coding assistants: Boost guidelines and skills
  • A rubric for a package another team can depend on

3 runnable examples (Python, guidelines/core.blade.php, skills/indian-rules-validation/SKILL.md) · about 9 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Practise

In your own Pathshala app, make a folder packages/indian-rules with the eight files above, add it as a path repository and require it. Then add a fifth rule, BatchCode, for Pathshala’s own batch codes, three capital letters, a hyphen and four digits such as MAT-0412, with an English and a Hindi message, and check it with php artisan indian-rules:check before and after you publish the language files.

Practice that adapts to you

Practise this lesson

7 questions about 7 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 7 Which composer.json key makes Laravel load a package's service provider without any setup in the app?

    Choose one answer.

    Show the answer: Question 1 of 7

    Answer: extra.laravel.providers

    Composer keeps the extra section for other tools to read, and Laravel reads extra.laravel: providers for the service providers and aliases for the facades.

  2. Question 2 of 7 A package's provider must tell Laravel where its lang folder is and which files an app may publish. Where do those calls go?

    Choose one answer.

    Show the answer: Question 2 of 7

    Answer: In boot(), which runs once every provider is registered

    Laravel calls register() on every provider first and boot() on every provider after that, so boot() can rely on the whole application, including the translator and Artisan.

  3. Question 3 of 7 The provider calls loadTranslationsFrom(__DIR__.'/../lang', 'indian-rules'). Which key reaches the pincode line of lang/en/validation.php?

    Choose one answer.

    Show the answer: Question 3 of 7

    Answer: indian-rules::validation.pincode

    loadTranslationsFrom() registers the folder under a namespace, so the package's messages never clash with the app's own lines, and switching the locale to hi picks lang/hi/validation.php.

  4. Question 4 of 7 One app wants the Hindi message of the PIN code rule worded differently, and the other apps must keep the package's wording. What should it do?

    Choose one answer.

    Show the answer: Question 4 of 7

    Answer: Publish the language files with the indian-rules-lang tag and edit its copy in lang/vendor/indian-rules

    publishes() takes a tag per group of files, so an app copies only what it wants to change: here the lang group, which the provider publishes to lang/vendor/indian-rules.

  5. Question 5 of 7 In the recorded install, what ran php artisan package:discover?

    Choose one answer.

    Show the answer: Question 5 of 7

    Answer: The app's post-autoload-dump script, after Composer regenerated its autoloader

    Every Composer autoload dump in a Laravel app ends with package:discover, so requiring, updating or removing a package rewrites bootstrap/cache/packages.php without any step from the developer.

  6. Question 6 of 7 The recorded app accepted abcpe 1234 f as a PAN. What would php artisan indian-rules:check pan "abcpe 1234 f" print after the app sets 'normalise' => false in its published config?

    Choose one answer.

    Show the answer: Question 6 of 7

    Answer: The PAN message: the value field must be a 10-character PAN, such as ABCPE1234F

    The app's published copy is what the package reads, so changing one key there changes how every rule of the package behaves in that app.

  7. Question 7 of 7 An app wants to register IndianRulesServiceProvider itself, and only in some environments. What must it change?

    Choose one answer.

    Show the answer: Question 7 of 7

    Answer: List pathshala/indian-rules under extra.laravel.dont-discover in the app's composer.json, then register the provider itself

    Discovery is the default, and the app's own composer.json can turn it off for one package, or for all of them with *, when the app wants to decide where a provider runs.

Interview questions

Warm-up (fresher to 2–5 years): which file does Laravel’s package discovery write, and what does it hold?

Package discovery writes bootstrap/cache/packages.php, a list of each installed package’s service providers and facade aliases. The Laravel skeleton runs php artisan package:discover after every Composer autoload dump; it reads the extra.laravel section of every package in vendor/composer/installed.json and writes that file. Laravel registers those providers on every request and command, so the app’s own bootstrap/providers.php is never edited. An app that wants to register a package’s provider itself lists the package under extra.laravel.dont-discover in its own composer.json.

Three longer questions build on this lesson:

  1. Mid to senior

    Three Laravel apps of one company each hold a copy of the same validation rules and a trait that applies them. A teammate proposes a private Composer package instead. When is the package the right move rather than the copied trait, and what does it cost the team?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to staff

    You maintain a Laravel package that supports Laravel 12 and 13 on PHP 8.3 to 8.5. Design its CI: what runs on every pull request, how the jobs are built, and how you read a failure that appears on one job only.

    Model answer, Pro. First step and follow-ups

  3. Mid to senior

    Your Laravel package is at 2.3.1. Name the kinds of change that force 3.0.0, beyond renaming or removing a PHP class, and say how you would ship one of them without stranding the apps that depend on you.

    Model answer, Pro. First step and follow-ups

Flashcards · 5 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. What makes a Composer package a Laravel package?

    One key in its composer.json: extra.laravel, whose providers list names the service provider (and aliases the facade). Laravel's package discovery loads that provider when an app installs the package, with no setup in the app.

  2. What does the package's service provider do in register() and boot()?

    register() merges the default config under the package's key and binds the service the facade reaches. boot() loads the translations under a namespace, so messages read as indian-rules::validation.pan, registers publishes() tags for files an app may copy, and adds the Artisan command.

  3. What does a constraint of ^12.0 || ^13.0 on illuminate components say?

    That the package works with any 12.x or any 13.x release of the framework's components; the double pipe is the logical OR that Composer's documentation recommends.

  4. How can an app install a package that has no release yet?

    Through a path repository pointing at the package's folder: Composer links the folder into vendor/, and package discovery writes its provider and alias into bootstrap/cache/packages.php. vendor:publish then copies only the files the app wants to change, such as its config.

  5. Which four things decide whether other teams can depend on a package?

    Orchestra Testbench, which runs its tests inside a small Laravel app built for each test; a CI matrix over every PHP and Laravel version the constraints allow; semantic versioning, so apps know which releases can break them; and publishing on Packagist, where each git tag becomes a version.

Key takeaways

  • A Laravel package is a Composer package whose composer.json names its service provider and facade under extra.laravel.
  • Package discovery runs after every autoload dump and writes the provider and the alias into bootstrap/cache/packages.php, so the app needs no setup.
  • The provider merges its default config and binds its services in register(), and loads messages, declares publishable files and adds commands in boot().
  • An app copies only the files it wants to change, with vendor:publish and the package’s tag, and edits that copy.

Terms in this lesson

CI matrix
A CI job definition that runs once for every combination of the values it lists, for example three PHP versions times two Laravel versions: six jobs.
Orchestra Testbench
A development dependency of Laravel packages that builds a small Laravel application for each test, so a package's tests run without a host app.
package discovery
Laravel's step after every Composer autoload dump that reads each installed package's extra.laravel section and records its service providers and facade aliases in bootstrap/cache/packages.php, so an app needs no setup to load them.
path repository
A Composer repository of type path that points at a folder on the same disk, so an app can require a package that has no release yet; Composer links or copies that folder into vendor/.
publish tag
A name a package gives to one group of files in publishes(), so php artisan vendor:publish --tag=<name> copies only that group, such as the config or the language files, into the app.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.