mycmdev/nova-modules
mycmdev/nova-modules brings modular architecture to Laravel Nova, letting you register, organize, and toggle feature modules that ship their own resources, tools, and navigation, keeping large Nova apps clean, maintainable, and easy to extend.
Treat the package as a normal Composer dependency and get it configured before writing any feature code.
composer require vendor/package
php artisan vendor:publish --provider="Vendor\Package\PackageServiceProvider"
Laravel's package discovery registers the service provider and any facades automatically, so there is usually no manual config/app.php edit. Confirm this by running php artisan about or php artisan package:discover after install — if the provider is not listed, check composer.json for a dont-discover entry before adding it by hand.
Where to look first, in order:
config/package.php) — this is the real API surface for most packages and the fastest way to understand defaults.vendor/vendor/package/tests/ — packages often ship their own tests, which are the most precise usage documentation available.First use case: configure credentials or a driver via .env, publish and run any migrations, then make a single read-only call from php artisan tinker to prove the round trip works before wiring it into application code.
// tinker
app(\Vendor\Package\Client::class)->ping();
Keep that smoke test in a feature test so a future minor release breaking the wiring is caught by CI rather than in production.
Resolve from the container, don't instantiate. Inject the package's client or manager into constructors and let the container resolve it. This keeps configuration centralized and makes the class swappable in tests.
final class SyncOrders
{
public function __construct(private readonly \Vendor\Package\Client $client) {}
public function __invoke(Order $order): void
{
$this->client->orders()->push($order->toPayload());
}
}
Bind to the contract, not the concrete class. Where the package publishes an interface, bind it in a dedicated service provider. That single binding is the seam you use for swapping drivers later.
$this->app->bind(\Vendor\Package\Contracts\Transport::class, fn ($app) =>
$app->make(\Vendor\Package\Transports\HttpTransport::class)
);
Wrap outbound calls in queued jobs. Nearly every package call is I/O. Putting it behind ShouldQueue gives retries, backoff and rate limiting for free, and keeps request latency predictable.
final class PushOrder implements ShouldQueue
{
use Queueable, InteractsWithQueue;
public int $tries = 5;
public array $backoff = [10, 30, 60, 120];
public function __construct(public readonly Order $order) {}
}
Use events to decouple. If the package dispatches events, subscribe with listeners rather than editing the call site. This survives package upgrades far better than overriding internals.
Mirror the package's config into config/services.php-style env indirection. Never read env() outside config files — it returns null the moment config is cached.
// config/package.php
'api_key' => env('PACKAGE_API_KEY'),
Testing. Prefer the package's own fake/Http::fake() over mocking internals. If no fake ships, bind a hand-rolled test double against the contract and assert on the calls.
$this->mock(\Vendor\Package\Contracts\Transport::class)
->shouldReceive('send')->once()->andReturnTrue();
Publish only what you intend to own. --tag=config alone for most projects. Publishing views or migrations means you inherit maintenance for them across upgrades.
php artisan config:cache is the number one source of "it works locally" bugs. Any new env var requires php artisan config:clear. If a value silently ignores .env in staging but not locally, this is almost always the cause.
Published config is frozen at publish time. vendor:publish does not overwrite existing files, so new config keys added in a minor release will be missing from your copy and will read as null. Diff vendor/vendor/package/config/package.php against config/package.php after every upgrade, or let the package's mergeConfigFrom defaults cover you by only overriding keys you genuinely change.
Published migrations are your responsibility. The package's future migration fixes will not reach you. Prefer running package migrations from vendor/ where the package supports it, and record the decision in the upgrade notes.
Singleton state leaks between tests. If the provider registers a singleton holding a connection or buffer, use RefreshDatabase plus an explicit $this->app->forgetInstance(...) in teardown, or the second test inherits the first test's state.
Facade calls mask dependencies. They are convenient in controllers and terrible in domain classes. If you find yourself unable to unit test a class without booting Laravel, replace the facade call with constructor injection.
Version-pin minor releases in composer.json ("vendor/package": "^1.3" is fine, "*" is not) and read the changelog for anything touching config defaults or the signature of a method you call. Packages frequently ship behavior changes in patch releases under the banner of "fixes."
Debugging order: php artisan package:discover → confirm the binding resolves (app()->bound(...)) → dump the merged config (config('package')) → enable the package's own logging channel if it has one. Doing this in order avoids hours lost to a stale config cache.
Extension points to prefer, in order of durability: config drivers → published contracts/interface bindings → events/listeners → macros. Avoid extending concrete package classes; they are the most likely thing to change shape in a minor release, and subclassing a package class pins you to its internal constructor signature.
Log the package's request/response at debug level behind a config flag. When something fails in production, having a toggle you can flip beats redeploying with added dump() calls.
How can I help you explore Laravel packages today?