Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Package Tools Laravel Package

beyondcode/laravel-package-tools

Laravel Package Tools streamlines package development with a fluent PackageServiceProvider base class that auto-registers config, migrations, commands, views, translations, routes, and assets. Cut boilerplate and ship Laravel packages faster with less setup code.

View on GitHub
Deep Wiki
Context7

Getting Started

Install (with a caveat first). Technically: composer require beyondcode/laravel-package-tools. Practically: the repo is archived, last released 2019-03-05, and predates Laravel 7–11 and PHP 8.x. For anything new, vendor the base class into your own package (it is ~150 lines) or use the maintained successor with the near-identical API. If you do install it, pin it exactly, add it to your dependency-review allowlist with an owner, and treat it as frozen.

Read the base class before you write a provider. Open src/PackageServiceProvider.php and src/Package.php and read both end to end — that is an afternoon, and it is the entire public surface. The whole value proposition is that the base class translates declarations into Laravel's native calls: mergeConfigFrom, loadViewsFrom, loadTranslationsFrom, loadMigrationsFrom, loadRoutesFrom, publishes, and console command registration. You cannot debug it productively without knowing which primitive each declaration maps to.

Scaffold one provider, extending the base:

namespace Acme\Blog;

use BeyondCode\LaravelPackageTools\Package;
use BeyondCode\LaravelPackageTools\PackageServiceProvider;

class BlogServiceProvider extends PackageServiceProvider
{
    public function configurePackage(Package $package): void
    {
        $package
            ->name('acme-blog')          // declare this first - it drives paths and tags
            ->hasConfigFile()
            ->hasViews()
            ->hasTranslations()
            ->hasMigrations(['create_acme_blog_posts_table'])
            ->hasRoute('web')
            ->hasCommands([
                Commands\PublishPost::class,
            ]);
    }
}

Depending on the tagged version you pin, some of these may be declared as public properties on the provider rather than fluently inside configurePackage(). The base class source tells you in thirty seconds; don't guess, and don't mix both styles in one codebase.

Adopt the layout the conventions assume — the base class derives paths from name(), so deviating costs you more than it saves:

config/acme-blog.php
database/migrations/
resources/views/
resources/lang/
routes/web.php
src/Commands/

Register the provider in your package's composer.json so consumers get it via auto-discovery:

"extra": {
    "laravel": {
        "providers": ["Acme\\Blog\\BlogServiceProvider"]
    }
}

Your first use case should be a migration, not a greenfield package: find a package whose provider already does the mergeConfigFrom + loadViewsFrom + publishes + $this->commands([...]) dance by hand and collapse it into the declarative block. That is a small, reviewable diff, it exercises nearly every feature at once, and it proves out the publish tags in a real app before you standardize on the pattern.

Implementation Patterns

Declare, don't wire. Treat the provider as configuration, not code. If you find yourself writing $this->app->mergeConfigFrom(...) next to a configurePackage() block, you have defeated the point and created two sources of truth for the same path.

Use the lifecycle hooks for everything that isn't declaration. The base class gives you packageRegistered() and packageBooted(), which run inside the correctly-ordered register()/boot().

  • packageRegistered() — container bindings, singletons, interface→implementation mapping, config-dependent bindings.
  • packageBooted() — anything that needs other providers up: view composers, Blade::component(), route model bindings, event listeners, macros.

This split is the single most valuable habit to enforce in review, because it is the mistake people make when migrating from a hand-written provider.

Guard console-only work. Command registration and publish groups live in the console. Wrap anything that shells out or does filesystem work in if ($this->app->runningInConsole()) — otherwise php artisan commands, queue workers, and config:cache runs start doing work in web requests.

Publish tags are the contract with your users. With name('acme-blog'), tags fall out as acme-blog-config, acme-blog-migrations, acme-blog-views, acme-blog-assets. Document those tags in your README verbatim, because consumers script them in deploy pipelines and CI. Renaming the package silently renames the tags and breaks every downstream deploy — a rename is a breaking change, not a cosmetic one.

Migrations: pick one model per package and be consistent.

  • Loadable (hasMigrations([...])) — consumers never publish, migrations run from the vendor path, upgrades ship new migrations automatically. Best for internal/shared packages.
  • Publish-only — consumers own the files and their timestamps. Better for packages where customers will edit the schema.

Doing both double-registers and can produce duplicate-table errors on fresh installs. If you are loadable, expose an acme-blog.runs_migrations config toggle (and honour it in packageRegistered()) so consumers can opt out — this is the one escape hatch almost everybody ends up needing.

Routes: know which middleware you're inheriting. A hasRoute('web') file loads under whatever the base class applies. If your package needs its own prefix, domain, or middleware stack, wrap the routes in a route group inside routes/web.php rather than expecting the provider to do it.

Namespacing is automatic and should stay that way. view('acme-blog::posts.index'), trans('acme-blog::messages.welcome'), and config('acme-blog.driver') should all "just work" with zero manual registration. If you are writing loadViewsFrom by hand, you have a declaration you forgot to add.

One base class, many packages. The real payoff is convention uniformity: every internal package has configurePackage(), packageRegistered(), and packageBooted() in that order, with declaration-only content in the first. That makes a new package reviewable in five minutes and diffable across the fleet. Write it into your package scaffolding template and lint for it — the template is the durable artifact; the library is incidental.

Test the behaviour, not the wiring. Use orchestra/testbench and boot the provider as a consumer would. The wiring is the library's job (and if you vendor it, your job once). Your tests should assert that a publish tag exists, that a migration runs on a fresh install, that the route resolves, that the command is discoverable — a fresh-install smoke test per package, because these are exactly the things that break silently across framework upgrades.

Gotchas and Tips

name() must come first. Paths and publish tags derive from it, and the chain reads the value at call time. Declaring it last, or relying on a default, produces confusing runtime behaviour rather than a clear error. It is a one-line review checklist item — enforce it in the template.

Config merging is shallow. Laravel's mergeConfigFrom is a top-level array_merge, so a consumer publishing config/acme-blog.php with only ['driver' => 'x'] replaces your entire nested connections array. Two mitigations: keep your config file flat where possible, and deep-merge explicitly in packageRegistered() if you genuinely need nested defaults. Add a config test that boots with a partial config and asserts the nested defaults survive — this bug appears only in consumer apps, never in your test suite that uses the full file.

php artisan config:cache will make you doubt your own code. Changes to mergeConfigFrom behaviour won't show up until you clear the cache. When something "isn't picked up," php artisan config:clear before you start debugging.

Laravel major-version drift is the real risk, not security. The library performs no I/O and no network access, but it sits on the boot path of every package that extends it. Watch specifically for changes to publish-group semantics, loadMigrationsFrom behaviour, and boot-order timing of the package hooks. Stale Composer constraints in the archived composer.json are the most likely way this blocks a framework upgrade org-wide — verify the constraint range against your target PHP × Laravel matrix before you commit to it.

PHP 8.x noise. Expect deprecation notices from untyped/dynamically created properties if you are on 8.2+. They're harmless but they pollute logs and CI output; if you vendor the base class, add the property declarations and typed signatures while you're in there.

Debugging checklist, in order:

  1. composer dump-autoload — the provider class not being found is almost always this.
  2. Is the provider actually registered? php artisan about (Laravel 9+) or app()->getLoadedProviders() in tinker.
  3. Did the hook fire? Log::info() at the top of packageRegistered() / packageBooted() — boot-order problems become obvious immediately.
  4. Which tags exist? php artisan vendor:publish --list (or --provider="Acme\Blog\BlogServiceProvider" and read the output) before you assume your tag is wrong.
  5. Is it a config-cache artifact? config:clear and retry.

Never run two base classes in one codebase. If you migrate — to a vendored base or to the maintained successor — migrate all packages in defined waves and close each wave out. Two base classes coexisting produces silent behavioural drift in publish tags, migration loading, and boot-order hook timing, which is strictly worse than either end state. Gate it: add the new base to the template, freeze new usage of the old one in CI, and track the remaining providers as a checklist.

Extension points, ranked by safety. Override packageRegistered() / packageBooted() and call parent:: first — that's the supported seam. Overriding register() / boot() is legal but means you now reimplement the ordering guarantees you adopted the library for; do it only when you must, always with parent::, and leave a comment explaining why. Never modify the vendored base class in vendor/ — if you need a change there, you have decided to vendor it, so do that properly.

vendor:publish --force overwrites consumer edits without warning. Do not put it in a deploy script. Publish config once, on setup, and let the consumer diff on upgrade — and if you ship a config file that changes shape, document the migration rather than assuming --force.

Don't let the base class accumulate application logic. The failure mode for this kind of helper is a provider that starts as ten lines of declaration and becomes a second application bootstrap. If a package needs real registration logic, put it in a dedicated class the provider resolves, and keep the provider's job to declaring capabilities and delegating.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle