skip to content

In a Laravel package's service provider, what do loadRoutesFrom() and loadViewsFrom() do, and how can an app override one of the package's views?

level: middleimportance: should knowfreq 35%

answer

  1. routes skipped when already cached
  2. a view namespace like audit-trail::
  3. resources/views/vendor/{namespace} checked first
  4. publish the views or add one file
  5. no copy needed until you customise

basics

~10 s

loadRoutesFrom() requires the package's route file unless routes are cached; loadViewsFrom() registers a view namespace such as audit-trail::. Laravel checks resources/views/vendor/audit-trail first, so a file placed there overrides the package's view.

solid answer

~40 s

`loadRoutesFrom(__DIR__.'/../routes/web.php')` simply `require`s the package's route file during boot, **unless** the application's routes are cached, in which case the package routes are already inside the cache. `loadViewsFrom(__DIR__.'/../resources/views', 'audit-trail')` registers a **view namespace**, so the package and the app render `view('audit-trail::index')`. When it registers the namespace, Laravel first adds `resources/views/vendor/audit-trail` (for each configured view path, if that directory exists) and only then the package's own directory. So an app overrides one template by creating `resources/views/vendor/audit-trail/index.blade.php`; the rest still come from the package. Publishing the views with a `publishes()` tag copies them all into that directory as a starting point. Both calls belong in the provider's `boot()` method.

code

php · 12 lines
php
<?php

// routes/web.php inside the audit-trail package
use Acme\AuditTrail\Http\Controllers\AuditLogController;
use Illuminate\Support\Facades\Route;

Route::middleware(['web', 'auth'])
    ->prefix(config('audit-trail.path', 'audit'))
    ->name('audit-trail.')
    ->group(function () {
        Route::get('/', [AuditLogController::class, 'index'])->name('index');
    });

go deeper

for a junior

Know that package views are referenced as namespace::view and that a copy under resources/views/vendor/namespace overrides the package's version.

for a middle

Explain loadRoutesFrom skipping when routes are cached and how loadViewsFrom registers the vendor override directory before the package path.

for a senior

Keep package routes configurable (prefix, middleware) and advise apps to override individual views instead of publishing everything.

for a principal

Decide how much UI an in-house package should own versus leave to each app, balancing shared fixes against local customisation.

## Two resources, two strategies A Laravel package often ships screens: an audit-trail package might add a `/audit` page listing recent changes, rendered with its own Blade templates. The service provider tells Laravel where these live, usually in `boot()`: ```php public function boot(): void { $this->loadRoutesFrom(__DIR__.'/../routes/web.php'); $this->loadViewsFrom(__DIR__.'/../resources/views', 'audit-trail'); } ``` Neither call copies anything into the app. Routes and views are **loaded from the package** on every request, so a package update changes them immediately. ## `loadRoutesFrom()` The method is short in source: if the application supports route caching and its routes **are cached**, it does nothing; otherwise it `require`s the file. The reason is that a route cache already contains every route that was registered when it was built, package routes included. Loading the file again would register duplicates or waste time. Points worth knowing: - The package route file is plain route registration; it can wrap its routes in a group with a prefix, a name prefix and middleware such as `web` and `auth`. - Letting the app configure the prefix through the package's config (`config('audit-trail.path', 'audit')`) avoids URL clashes between the three apps that use it. - Because the file runs inside the provider, route definitions depend on the package being registered; an app that excludes the package from discovery and never registers it gets no routes. ## `loadViewsFrom()` and view namespaces `loadViewsFrom($path, 'audit-trail')` registers a **namespace hint** with the view factory. Views are then referenced as `namespace::name`: ```php return view('audit-trail::index', ['entries' => $entries]); ``` The override mechanism is built into this call. When the view factory is resolved, the method: 1. Loops over the app's configured view paths (normally just `resources/views`). 2. For each, if `<path>/vendor/audit-trail` exists, adds it to the `audit-trail` namespace. 3. Adds the package's own directory **last**. Lookups try the locations in order, so an app file wins over the package file of the same name. ## Overriding one view | Goal | Action | |---|---| | Change one template | create `resources/views/vendor/audit-trail/index.blade.php` | | Start from the package's templates | publish them with the package's view tag, then edit | | Go back to the package version | delete the app's copy | The view directory must exist when the view factory is first resolved in a request; for a normal request cycle that is simply "exists on disk". Publishing views is optional: the package typically registers `publishes([views => resource_path('views/vendor/audit-trail')], 'audit-trail-views')`, and running that tag copies every template into the override directory. Publishing everything means the app no longer receives template fixes from package updates for any copied file, so copy only what you change. ## Anonymous components from packages Blade components placed in the package's `components` view folder are reachable with the namespace too, for example `<x-audit-trail::badge />`. Class-based components need registration in the provider, which is a Blade topic of its own. ## Translations work the same way The same pattern exists for language files: `loadTranslationsFrom($path, 'audit-trail')` registers a namespace, strings are read as `__('audit-trail::messages.recorded')`, and an app can override them from its `lang/vendor/audit-trail` directory. The consistent idea across views and translations is **namespace plus an app-level vendor directory that wins**. ## Choosing what the package should own For the audit-trail package, the team keeps the list screen in the package (all three apps get fixes at once) but lets each app override the layout template to match its own navigation. That is one file per app under `resources/views/vendor/audit-trail`, and every other template keeps coming from the package. ## Mistakes interviewers look for - Expecting package routes to change on a server whose route cache was built before the update. - Calling `view('index')` inside the package instead of `view('audit-trail::index')`, which searches the app's own views. - Publishing every view by default and then missing package fixes. - Assuming `loadViewsFrom` copies files; it only registers locations.

  • The package added a new route in an update, but it returns 404 in production. Why?
    Production probably serves a route cache built before the update. `loadRoutesFrom()` skips the package's file when routes are cached, so only routes present at cache time exist. Rebuilding the route cache in the deploy picks up the new route.
  • How does Laravel know to look in resources/views/vendor/audit-trail before the package's views?
    `loadViewsFrom()` itself registers that directory: for each configured view path it adds `<path>/vendor/<namespace>` to the namespace when the directory exists, then adds the package path last. Lookups try locations in order, so the app's copy wins.

saying these in an interview costs you the question

  • loadViewsFrom copies the package's views into resources/views
  • Package routes load even when the route cache is in use
  • Overriding one package view requires publishing all of them
  • Package views are referenced without their namespace prefix
  • loadRoutesFrom registers routes lazily on first request to the prefix