Every publishable resource is registered under a tag built from your package's short name, a separator, and a group name:
{short-name}{separator}{group}
The default separator is ::, so a package named Blog publishes under blog::config,
blog::views, and so on.
php artisan vendor:publish --tag=blog::configphp artisan vendor:publish --tag=blog::views --forcephp artisan vendor:publish --provider="Acme\Blog\BlogServiceProvider"
Publishing is registered during boot(), and only when the application is running in the console —
a web request never builds the publish map.
Every tag
| Tag | Declared by | Destination |
|---|---|---|
blog::assets |
hasAssets() |
public/vendor/blog/ |
laravel-assets |
hasAssets() |
public/vendor/blog/ |
blog::config |
hasConfig() |
config/{filename} |
blog::factories |
hasFactories() |
database/factories/{Name}.php |
blog::migrations |
hasMigrations() |
database/migrations/ |
blog::providers |
hasProviders() |
app/Providers/{Name}.php |
blog::routes |
hasRoutes() |
routes/vendor/blog/{filename} |
blog::seeders |
hasSeeders() |
database/seeders/{Name}.php |
blog::stubs |
hasStubs() |
stubs/blog/{filename} |
blog::translations |
hasTranslations() |
lang/vendor/blog/ |
blog::view-components |
hasComponents() |
app/View/Components/blog/{Dir}/ |
blog::view-component-namespaces |
hasComponentNamespaces() |
app/View/Components/blog/{Dir}/ |
blog::views |
hasViews() |
resources/views/vendor/blog/ |
Broadcast channels are the one declared resource with no tag — see why they are not publishable.
The laravel-assets tag
Assets register under two tags. The second is Laravel's own convention, and it is what the
application skeleton runs from Composer's post-update-cmd:
php artisan vendor:publish --tag=laravel-assets --ansi --force
One command republishes every installed package's assets after every composer update — Horizon,
Telescope and Nova all rely on it. Laravel accumulates publish groups per path, so both tags publish
the same files, and an untagged vendor:publish is unaffected.
Which override points are real
Publishing means different things for different resources, and it is worth being precise, because "publish it and edit it" is only true for half of this table.
| Resource | After publishing |
|---|---|
| Views | Overrides. Laravel checks views/vendor/blog/ first, per view. |
| Translations | Overrides. Same, per key. |
| Config | Merges. The published file wins per top-level key; your defaults fill the rest. |
| Migrations | Replaces, if you were not also loading them. |
| Seeders, factories, stubs | Copies. Ready to use; nothing overrides anything. |
| Routes | Copies, inert. Your package still loads its own — see Routes. |
| Providers | Copies, inert. The consumer must register it. |
| View components | Forks. Laravel has no lookup order for component classes. |
| Assets | Copies to the same place the mirror uses. |
Changing the tag separator
public function hasPublishTagSeparator(string|array $separator): static
Some ecosystems prefer the flat package-group form. Pass a different separator:
$packager ->name('Blog') ->hasPublishTagSeparator('-') ->hasConfig();
php artisan vendor:publish --tag=blog-config
Registering both forms
Pass an array, and every group registers under each separator:
$packager->hasPublishTagSeparator(['::', '-']);
# Both work, and publish the same files.php artisan vendor:publish --tag=blog::configphp artisan vendor:publish --tag=blog-config
The first separator is the primary one: it is what the install command uses when it builds the tags it publishes. Put the form you consider canonical first.
This is the migration path for a package changing its convention — ship both for a release, document the new one, drop the old one in the next major.
Publishing everything
# By tag — one group.php artisan vendor:publish --tag=blog::config # By provider — every group the package registered.php artisan vendor:publish --provider="Acme\Blog\BlogServiceProvider" # Overwrite files that already exist.php artisan vendor:publish --tag=blog::views --force
--force is the one to warn consumers about: it overwrites their edits without asking. It is also
what a deploy script needs for assets, which is why the laravel-assets hook uses it.
Publishing from the install command
The install command publishes tags on the consumer's behalf, with progress output and a summary:
use NyonCode\LaravelPackageToolkit\Commands\InstallCommand; $packager->hasInstallCommand(function (InstallCommand $command) { $command ->publishConfig() ->publishMigrations() ->publishViews();});
Checking what is registered
use Illuminate\Support\ServiceProvider; ServiceProvider::pathsToPublish(null, 'blog::config');// ['/…/vendor/acme/blog/config/blog.php' => '/…/config/blog.php'] ServiceProvider::publishableGroups();// every registered tag, from every package
Useful in tests, and useful when a tag mysteriously publishes nothing:
test('the config group is registered', function () { expect(ServiceProvider::pathsToPublish(null, 'blog::config'))->not->toBeEmpty();});
A tag that publishes nothing still exits 0
vendor:publish with an unknown tag succeeds silently. So does the install command's step for it.
If a publish appears to do nothing, assert on pathsToPublish() rather than on the exit code.
Naming conventions
The short name is the whole namespace you get. Two rules follow:
- Prefix anything that lands in a shared directory. Seeders, factories and config files publish
flat into directories the application owns.
BlogPostSeeder, notPostSeeder;blog.php, notsettings.php. - Do not change the short name after release. Every tag, path and namespace moves with it, and consumers' published overrides stop being found. See Short name.