Skip to main content

Appendix A - The Hidden APIs of the First-Party Packages

Every chapter so far has asked the same question of Laravel's own core: which public, realistically usable method or class never made it into the official documentation. This appendix asks the same question of ten first-party packages built around that core: Sanctum, Pennant, Scout, Pulse, Horizon, Telescope, Passport, Fortify, Folio, and Socialite. Each of them ships and versions on its own schedule, independent of the framework itself, so this appendix is not meant to be read start to finish the way Chapters 1 through 18 or Appendix B are. Read it in whatever order is useful. Every package section below is self-contained: none of them continues the one before it, and none assumes any other section has been read first. Where the rest of this book is pinned throughout to laravel/framework v13.22.0, every entry here instead carries its own package version and its own verification date, tracked independently, package by package.

Each entry below also carries four short labels, defined once here rather than re-explained ten times. Audience states whether the method addresses an application developer, the book's default reader, or, less often, a package or subsystem author instead. Alias flags a method that is a trivial rename of one already documented elsewhere, with no behavior of its own worth learning separately. Stability warns that the package an entry belongs to has a history of changing its public API across minor versions, and that the entry should be re-verified against the installed version before any production use. Case type states whether an entry is an undocumented method inside an otherwise-documented class, an entire undocumented class, or a class whose own documentation page covers only some of its methods.

PackageMethod(s)Description
SanctumPersonalAccessToken::cant()Checks a token's own abilities directly, not only the current request's token
PennantFeature::getAllMissing()Resolves a feature across several scopes in one call, skipping already-cached ones
ScoutwithScoutMetadata(), syncWithSearchUsingQueue()Attaches transient hit metadata, and routes a model's own sync jobs to a queue
PulsePulse::ignore(), Pulse::ignoreRoutes(), Pulse::rescue(), Pulse::lazy()Suppresses recording, skips its own dashboard route, contains exceptions, defers a closure to ingest time
HorizonHorizon::night(), Horizon::use()A deprecated dark-theme toggle and a programmatic Redis connection switch
TelescopewithoutRecording(), hideRequestParameters(), hideResponseParameters()Suspends recording for a block, redacts named request/response fields
PassportwithCookieSerialization(), withCookieEncryption(), ignoreCsrfToken()Configures its own cookie-based SPA authentication guard
FortifyloginThrough(), confirmPasswordsUsing()A documented-mechanism alias for its login pipeline, and a custom password-confirmation check
FolioFolio::data(), Folio::middlewareFor(), Folio::renderUsing()Reads back the last-matched page's own data, reports a URL's middleware, overrides rendering globally
SocialiteenablePKCE()Switches an OAuth2 provider's authorization flow to PKCE

Sanctum

Verified against laravel/sanctum v4.3.3 (2026-08-04).

A Sanctum personal access token can check its own abilities directly:

$token->cant('orders:write');

The documented way to check a token's abilities looks similar but is not the same method. Sanctum documents tokenCan()/tokenCant() on the authenticated user, not on the token itself: $request->user()->tokenCant('orders:write'). That documented pair only ever answers for the token authenticating the current request, because HasApiTokens::tokenCan() reads $this->accessToken, the token Sanctum resolved for this request and no other. PersonalAccessToken::cant() has no such restriction: it works on any token instance at hand, including one that belongs to a different user and has nothing to do with the request being served.

That gap is exactly what a token-auditing endpoint needs. SanctumTokenAuditController looks up a specific token by its own id, unrelated to whoever is making the request, and reports whether it lacks a named ability:

class SanctumTokenAuditController extends Controller
{
// Deliberately unauthenticated for this appendix's minimal, isolated scenario: a real
// application would gate who may audit a token that does not belong to the requester.
// The point demonstrated here is `PersonalAccessToken::cant()` itself, not an access
// control layer around it.
public function missingAbility(Request $request, PersonalAccessToken $token)
{
$validated = $request->validate([
'ability' => ['required', 'string'],
]);

return response()->json([
'missing' => $token->cant($validated['ability']),
]);
}
}

GET /api/tokens/{token}/missing-ability?ability=orders:write binds {token} straight to a PersonalAccessToken model through route-model binding, so the token being audited is never the one authenticating the call. The test suite makes that explicit by authenticating as one user while auditing a token that belongs to another:

$owner = User::factory()->create();
$token = $owner->createToken('limited', ['orders:read']);

$auditor = User::factory()->create();
Sanctum::actingAs($auditor, ['*']);

$response = $this->getJson("/api/tokens/{$token->accessToken->id}/missing-ability?ability=orders:write");

$response->assertOk()->assertJson(['missing' => true]);

Flags: audience, application developer, no shift toward package authorship. Alias, not an alias - cant() is not interchangeable with tokenCant(), since only cant() can be pointed at a token other than the one authenticating the current request. Stability, no known history of breaking changes to this method. Case type, an undocumented method defined directly on PersonalAccessToken, a class the official documentation never addresses directly - only its effects, reached exclusively through HasApiTokens::tokenCan()/tokenCant() on the user model, are documented.

Pennant

Verified against laravel/pennant v1.24.0 (2026-08-04).

Pennant resolves a feature's value for several scopes in one call, only for whichever ones are not already cached:

Feature::getAllMissing(['new-checkout' => [$userA, $userB, $userC]]);

The documented equivalent, Feature::loadMissing(), does the same thing for exactly one scope at a time: Feature::for($user)->loadMissing(['new-checkout']) attaches the single scope already fixed by the fluent builder, then calls the very method used above internally. getAllMissing() skips that one-scope limitation entirely. Its $features argument accepts a feature name mapped to a list of scopes, resolving every one of them not already cached in a single call, backed by a single query rather than one per scope. Already-cached scopes are silently dropped from both the resolution step and the return value.

PennantFeatureAuditController puts this to use warming a feature's cache for a batch of users at once, skipping whichever are already resolved:

class PennantFeatureAuditController extends Controller
{
public function warmMissing(Request $request)
{
$users = User::query()
->whereIn('id', explode(',', $request->string('user_ids')->toString()))
->get();

$resolved = Feature::getAllMissing(['new-checkout' => $users->all()]);

return response()->json(['resolved' => $resolved]);
}
}

The test suite calls GET /api/pennant/new-checkout/warm-missing twice for the same three users: once with nothing cached, where all three come back resolved, and once after resolving the first user directly outside the batch call, where only the remaining two come back - proving the already-cached one was skipped, not silently re-resolved:

Feature::for($users->first())->active('new-checkout');

$response = $this->getJson('/api/pennant/new-checkout/warm-missing?user_ids='.$users->pluck('id')->implode(','));

expect($response->json('resolved.new-checkout'))->toHaveCount(2);

Flags: audience, application developer. Alias, not an alias - getAllMissing() is the batching primitive loadMissing() itself delegates to for its own single-scope case; calling it directly is what unlocks resolving several scopes in one call, a capability the documented method cannot express on its own. Stability, Pennant is a comparatively young package, and getAllMissing() in particular lives on an internal driver-wrapper class with no signature declared anywhere on the Feature facade itself, so a future refactor could rename or remove it without the deprecation cycle a documented method would get. Case type, an undocumented method on an internal class, Laravel\Pennant\Drivers\Decorator, that the documented Feature::loadMissing() reaches only for a single scope, through a second internal class, PendingScopedFeatureInteraction.

Scout

Verified against laravel/scout v11.5.0 (2026-08-04).

A searchable model can carry transient, display-only data on a single instance without it ever becoming one of its own attributes:

$article->withScoutMetadata('highlight', $snippet);

$article->scoutMetadata()['highlight'];

And a model can send its own sync jobs to a queue of its choosing, regardless of the application-wide default:

public function syncWithSearchUsingQueue()
{
return 'scout-sync';
}

withScoutMetadata()/scoutMetadata() are a setter/getter pair, not a hook to override: Scout's own engines call the setter automatically. Algolia's engine, for instance, attaches every underscore-prefixed field a search hit comes back with (its own highlighting and ranking data) onto the hydrated model this same way, before handing it back to application code. Nothing stops application code from calling it directly for its own purposes, as ScoutArticleController does below. syncWithSearchUsingQueue() is read by Scout's own queueMakeSearchable() when it dispatches a model's sync job; overriding it per model, as SearchableArticle does, sends that one model's own sync jobs to a dedicated queue no matter what the global scout.queue configuration says for everything else.

class SearchableArticle extends Model
{
use Searchable;

public function syncWithSearchUsingQueue()
{
return 'scout-sync';
}
}

ScoutArticleController::search() attaches a computed snippet to each hit as metadata, kept separate from the hit's own attributes:

public function search(Request $request)
{
$results = SearchableArticle::search($request->string('q')->toString())->get();

return response()->json($results->map(function (SearchableArticle $article) {
$article->withScoutMetadata('highlight', Str::limit($article->body, 40));

return [
'id' => $article->id,
'title' => $article->title,
'highlight' => $article->scoutMetadata()['highlight'],
];
}));
}

The test suite confirms the queue override by faking the queue, enabling scout.queue, and creating a new article through store():

Queue::fake();
config(['scout.queue' => true]);

$response = $this->postJson('/api/scout/articles', [
'title' => 'Dedicated queue article',
'body' => 'This article syncs through its own overridden queue name.',
]);

Queue::assertPushedOn('scout-sync', MakeSearchable::class);

Flags: audience, application developer. Alias, neither method is an alias - withScoutMetadata() has no documented equivalent at all, and syncWithSearchUsingQueue() overrides, per model, behavior the documentation only exposes as one application-wide configuration option. Stability, no known history of breaking changes to either method. Case type, two undocumented methods inside an otherwise partially-documented trait, Searchable - most of its lifecycle (search(), toSearchableArray(), searchable()) is documented, these two are not.

Pulse

Verified against laravel/pulse v1.8.0 (2026-08-04).

Pulse can suppress its own recording for the duration of a callback, stop registering its own dashboard route, contain a callback's exceptions, and defer a callback until its own ingest cycle:

Pulse::ignore(fn () => /* nothing recorded in here */ null);
Pulse::ignoreRoutes();
Pulse::rescue(fn () => /* exceptions here never propagate */ null);
Pulse::lazy(fn () => /* runs later, at ingest time */ null);

None of the four behave quite like their names suggest at first read. ignore() takes no pattern to match against - it is a runtime wrapper that flips an internal flag off for the duration of the callback, so anything recorded inside it, of any kind, is skipped entirely; Pulse uses this on itself so that writing its own buffered entries to storage does not generate new entries about that write. ignoreRoutes() takes no arguments at all: it does not filter which routes get recorded, it tells Pulse not to register its own /pulse dashboard route in the first place. rescue() matches its likely first impression closely: it catches anything a callback throws and hands it to whatever was registered through the documented Pulse::handleExceptionsUsing() - rescue() itself is the undocumented half of a pair whose other half is documented. lazy() does not toggle a "lazy mode": it queues a closure to run later, only once Pulse's own ingest cycle actually happens, not at the point it was called.

Audience shift, for two of these four: lazy() and rescue() are not something application code calls to observe Pulse - they are the same building blocks Pulse's own bundled recorders call internally. Every recorder shipped with the package (SlowJobs, Servers, SlowOutgoingRequests, Exceptions, Queues, SlowQueries, CacheInteractions, UserJobs) calls $this->pulse->lazy() and/or $this->pulse->rescue() on itself; nothing else in the framework does. An application developer who only wants to react to a caught exception reaches for the documented Pulse::handleExceptionsUsing() instead, exactly as AppendixAServiceProvider does below - rescue() and lazy() matter directly only to whoever is writing a custom Pulse Recorder of their own, the same package/subsystem-author shift Chapter 3 gave Manager and MultipleInstanceManager. ignore() and ignoreRoutes() stay ordinary application-developer calls.

AppendixAServiceProvider wires three of the four together:

public function register(): void
{
// Pulse: must run during the register phase, before any provider's boot() - including
// PulseServiceProvider's own, which is what actually checks registersRoutes() to decide
// whether to add the /pulse dashboard route in the first place.
Pulse::ignoreRoutes();
}

public function boot(): void
{
// Pulse: route caught internal-recording exceptions to a static log a test can inspect.
Pulse::handleExceptionsUsing(fn (Throwable $e) => PulseFailureLog::record($e));
}

The test suite proves each behavior directly. ignoreRoutes() leaves no dashboard route to hit at all:

$this->get('/pulse')->assertNotFound();

ignore() and lazy() both need Pulse::startRecording() first, since this project's own phpunit.xml sets PULSE_ENABLED=false for the whole suite, which makes Pulse stop recording by default during tests:

Pulse::startRecording();

Pulse::record('appendix-a-test', 'not-ignored', 1);
Pulse::ignore(fn () => Pulse::record('appendix-a-test', 'ignored', 1));

expect(Pulse::ingest())->toBe(1);

And rescue() confirms the exception never propagates, reaching the configured handler instead:

$result = Pulse::rescue(fn () => throw new RuntimeException('boom'));

expect($result)->toBeNull()
->and(PulseFailureLog::$caught)->toContain('boom');

Flags: audience, application developer for ignore()/ignoreRoutes(); a package/subsystem-author shift for lazy()/rescue(), per the note above - confirmed against every bundled Recorder's own source. Alias, none of the four are aliases. rescue() and the documented handleExceptionsUsing() are a matched pair, one undocumented primitive and one documented configuration point; ignore() is related to, but distinct from, the documented Pulse::filter() - filter() drops an entry after it has already been captured, ignore() prevents capture from happening at all. Stability, no known history of breaking changes to these four; Pulse itself is a comparatively young package, worth a mild verify-before-production note. Case type, four undocumented methods inside an otherwise partially-documented Pulse class - filter(), handleExceptionsUsing(), and the dashboard's own authorization gate are documented, these four are not.

Horizon

Verified against laravel/horizon v5.48.2 (2026-08-04).

Horizon can switch its dashboard to a dark theme and point itself at a different Redis connection than its own default:

Horizon::night();
Horizon::use('appendix-a-queue');

night() carries an @deprecated tag directly in the installed version's own source, with no replacement found anywhere in config/horizon.php - most likely because the dashboard now follows the browser's own color-scheme preference automatically, though nothing in the source confirms that directly. It is included here because it still passes every selection criterion, but it should not be reached for in new code. use($connection) is not deprecated and holds up well: its entire effect is copying whatever Redis connection config is registered under the given name into database.redis.horizon, the connection name Horizon always reads from internally. The documentation covers only the config-file equivalent, 'use' => 'default' in config/horizon.php

  • calling the method directly is what makes that choice programmatic, for instance to vary it by environment, rather than fixed in a config file.

A cross-reference to Chapter 8 (the binding lifecycle) was considered for use(), since both pick which underlying resource a subsystem resolves against, but declined: Chapter 8 is specifically about the container's own binding resolution, and use() never touches the container at all - it only rewrites a plain config array. The parallel is real but not close enough to state without misleading.

AppendixAServiceProvider defines a self-contained Redis connection and applies both calls, without ever opening an actual Redis socket:

config(['database.redis.appendix-a-queue' => [
'host' => '127.0.0.1',
'port' => 6379,
]]);

Horizon::night();
Horizon::use('appendix-a-queue');

The test suite confirms use()'s effect directly through config, and night()'s only through reflection on its own protected static property, since a dashboard theme has nothing else to assert against:

expect(config('database.redis.horizon.host'))->toBe('127.0.0.1')
->and(config('database.redis.horizon.port'))->toBe(6379);

Flags: audience, application developer. Alias, neither is an alias - use()'s only documented equivalent is a config file key, not a method. Stability, night() is marked deprecated in the verified version itself; state this plainly rather than folding it into a generic verify-before-production note. Case type, two undocumented methods on an otherwise partially-documented Horizon class - most of what horizon.md covers is expressed through the config file rather than this class's own static methods.

Telescope

Verified against laravel/telescope v5.22.0 (2026-08-04).

Telescope can suspend its own recording for a block of code and redact named fields from what it stores, without disabling recording altogether:

Telescope::withoutRecording(fn () => /* nothing recorded in here */ null);
Telescope::hideRequestParameters(['password']);
Telescope::hideResponseParameters(['token']);

withoutRecording() flips a static flag off for the callback's duration, restored afterward - every record*() method funnels through a common path that checks this flag first, so anything recorded inside the callback, of any kind, never queues at all. Neither hiding method is mentioned by exact name on the documentation page, but hideRequestParameters() is not entirely a stranger to it: the page's own code sample calls a method named hideSensitiveRequestDetails() without ever explaining what is inside it, and the real TelescopeServiceProvider stub Laravel generates for every project that installs Telescope fills that method with Telescope::hideRequestParameters(['_token']); and a matching call for headers. The primitive is already running in every Telescope installation from day one; the docs just never call it by name. hideResponseParameters() has no such scaffold reference at all - not even an indirect one.

A cross-reference to Chapter 6 is worth drawing here: that chapter's dontTruncateExceptions() section carries its own security note about Authorization header leakage in verbose diagnostic HTTP client output. hideRequestHeaders()/hideRequestParameters() is Telescope's own answer to exactly that class of risk, applied to a first-party observability tool instead of an HTTP client. A second candidate, Chapter 15's Log::listen()/withoutContext(), was considered and declined - both sit under a broad "observability control" theme, but log interception and context clearing do not mirror suppression and redaction closely enough to state as a real parallel.

TelescopeDemoController puts both ideas to work. runInternalTask() wraps a sensitive operation so Telescope never sees it, while a second, unwrapped call stays visible:

public function runInternalTask(Request $request)
{
Telescope::withoutRecording(function () {
Telescope::recordRequest(IncomingEntry::make(['note' => 'internal-task']));
});

Telescope::recordRequest(IncomingEntry::make(['note' => 'visible-task']));

return response()->json(['done' => true]);
}

processPayment() exercises Telescope's own request watcher directly, so the redaction runs through its real, unmodified code path:

public function processPayment(Request $request)
{
$response = response()->json(['token' => 'secret-token-value', 'status' => 'ok']);

(new RequestWatcher)->recordRequest(new RequestHandled($request, $response));

return $response;
}

The test suite confirms both: the wrapped note never appears in the queue while the unwrapped one does, and a submitted password alongside a returned token both come back as '********' while an unrelated field passes through untouched:

expect($entry->content['payload']['password'])->toBe('********')
->and($entry->content['payload']['card_holder'])->toBe('A. Tester')
->and($entry->content['response']['token'])->toBe('********');

Flags: audience, application developer. Alias, none of the three are aliases - only hideRequestParameters() has even an indirect documentation trail, through the generated scaffolding's own use of it, never through the docs page's prose. Stability, no known history of breaking changes to these three. Case type, three undocumented methods inside an otherwise partially-documented Telescope class - filter()/filterBatch(), the enabled flag, and the dashboard's own authorization gate are documented, these three are not.

Passport

Verified against laravel/passport v13.7.5 (2026-08-04).

Stability warning: this entry replaces Passport::hashClientSecrets(), planned originally, after re-verification found it removed outright in Passport v13 (client secrets are now always hashed, with no opt-in toggle left to document). Passport has a real history of removing, not just changing, previously undocumented methods between major versions - re-verify this entry again against whatever version is actually installed before relying on any of it in production.

Passport can decrypt its own authentication cookie, expect it in a given serialized form, and skip its embedded CSRF check:

Passport::withCookieEncryption();
Passport::withCookieSerialization();
Passport::ignoreCsrfToken();

All three configure the exact same mechanism: Guards\TokenGuard::getTokenViaCookie() and decodeJwtTokenCookie(), the code path that authenticates a request through Passport's already-documented SPA cookie flow (the laravel_token cookie CreateFreshApiToken sets). withCookieEncryption()/withoutCookieEncryption() decide whether Passport decrypts that cookie itself, independent of the framework's own EncryptCookies middleware, which the api middleware group does not include. withCookieSerialization()/withoutCookieSerialization() decide whether the decrypted payload is treated as PHP-serialized data. ignoreCsrfToken() skips comparing the cookie's embedded csrf claim against the request's own CSRF token. None of the three appear by exact name in the documentation, which covers only the cookie flow's existence, not these configuration knobs into it.

The test suite builds a cookie exactly the way Passport's own ApiTokenCookieFactory and the framework's EncryptCookies middleware would together, then flips each flag mid-test against a route guarded by auth:api:

function buildPassportCookie(int $userId, string $csrfToken): string
{
$jwt = app(ApiTokenCookieFactory::class)->make($userId, $csrfToken)->getValue();

$prefixed = CookieValuePrefix::create(Passport::cookie(), app('encrypter')->getKey()).$jwt;

return app('encrypter')->encrypt($prefixed, Passport::$unserializesCookies);
}
$this->withCredentials()->withUnencryptedCookie(Passport::cookie(), $cookie)
->withHeaders(['X-CSRF-TOKEN' => $csrf])
->getJson('/api/passport-demo/whoami')
->assertOk();

Auth::forgetGuards();
Passport::withoutCookieEncryption();

$this->withCredentials()->withUnencryptedCookie(Passport::cookie(), $cookie)
->withHeaders(['X-CSRF-TOKEN' => $csrf])
->getJson('/api/passport-demo/whoami')
->assertStatus(401);

Auth::forgetGuards() matters here for a reason worth stating plainly: within one test, the authentication guard and its resolved user stay cached across multiple requests, unlike separate real HTTP requests - without it, a second call would silently reuse the first call's already authenticated user instead of re-evaluating the cookie against the newly flipped flag.

Flags: audience, application developer. Alias, none of the three are aliases. Stability, explicit and prominent, per the warning above - re-verify before production use. Case type, three undocumented methods configuring an already-documented mechanism (Passport's cookie-based SPA authentication flow), none of them named on the documentation page itself.

Fortify

Verified against laravel/fortify v1.37.3 (2026-08-04).

Fortify can swap its own login pipeline for a custom one and replace the default password-confirmation check:

Fortify::loginThrough(fn ($request) => [/* pipe classes */]);
Fortify::confirmPasswordsUsing(fn ($user, $password) => /* custom check */ true);

loginThrough() is a trivial alias: its entire body is static::authenticateThrough($callback);, and authenticateThrough() is documented, by exact name, in the "Customizing the Authentication Pipeline" section, worked example included. loginThrough() itself never appears on the docs page, but the mechanism it reaches is not new - stated here plainly rather than presented as a discovery of its own. confirmPasswordsUsing() is not an alias: it sets a public static callback that Actions\ConfirmPassword reads directly, falling back to the guard's own username-and-password check only when nothing has been registered.

AppendixAServiceProvider uses both. The login pipeline reuses Fortify's own real pipe classes and appends one custom pipe:

Fortify::loginThrough(fn ($request) => array_filter([
config('fortify.limiters.login') ? EnsureLoginIsNotThrottled::class : null,
AttemptToAuthenticate::class,
PrepareAuthenticatedSession::class,
RecordLoginAttempt::class,
]));

Fortify::confirmPasswordsUsing(fn ($user, $password) => Hash::check($password, $user->password)
|| $password === self::FORTIFY_RECOVERY_CODE);

RecordLoginAttempt is the custom pipe, proving the pipeline that ran was the custom one:

class RecordLoginAttempt
{
public static int $count = 0;

public function handle($request, $next)
{
self::$count++;

return $next($request);
}
}

The test suite confirms both entries directly - a real login through the custom pipeline, and password confirmation accepted via the recovery code alone:

$this->post('/login', ['email' => $user->email, 'password' => 'correct-password']);

$this->assertAuthenticatedAs($user);
expect(RecordLoginAttempt::$count)->toBe(1);
$this->actingAs($user)
->post('/user/confirm-password', ['password' => AppendixAServiceProvider::FORTIFY_RECOVERY_CODE])
->assertSessionHasNoErrors();

Flags: audience, application developer. Alias, loginThrough() is a trivial, one-line delegation to the documented authenticateThrough() - not a new mechanism; confirmPasswordsUsing() is not an alias. Stability, no known history of breaking changes to either. Case type, confirmPasswordsUsing() is an undocumented method inside an otherwise documented password-confirmation flow; loginThrough() is an undocumented name for an already-documented mechanism reached through a different, documented name.

Folio

Verified against laravel/folio v1.1.19 (2026-08-04).

Folio can read back data from whichever page it last matched, report which middleware a URL would run under, and override how every matched page becomes a response:

Folio::data('article');
Folio::middlewareFor('/admin/dashboard');
Folio::renderUsing(fn ($request, $matchedView) => /* custom response */ null);

data() and middlewareFor() are not what their names suggest at first read. data() does not share new data with pages; its entire body is Arr::get($this->lastMatchedView?->data ?: [], $key, $default); - it reads a piece of data, typically a route parameter, from whichever view Folio most recently matched, usable from outside that page entirely. middlewareFor() does not assign middleware to a path; it looks up, for a given URI, what middleware the already-documented Folio::path(...)->middleware([...]) call would apply there, and returns that list - a read-only lookup, not a configuration point. Its argument is relative to the mount's own base URI, not the full application path. renderUsing() matches its likely first impression, with one narrower detail: its scope is global, overriding every Folio-matched page across every mount, not a single page's own rendering.

AppendixAServiceProvider mounts an isolated page directory and registers both a middleware pattern and the global render override:

Folio::path(resource_path('views/pages/appendix-a-folio'))
->uri('/appendix-a-folio')
->middleware([
'admin/*' => ['auth'],
]);

Folio::renderUsing(function ($request, $matchedView) {
$response = response(View::file($matchedView->path, $matchedView->data));
$response->headers->set('X-Appendix-A-Rendered-By', 'custom');

return $response;
});

The article page reads its own route parameter back through data() instead of the page's own automatically injected Blade variable:

{{ Folio::data('article') }}

The test suite confirms middlewareFor()'s report is truthful, not just self-consistent - the same pattern that answers 'auth' also genuinely blocks an unauthenticated request:

expect(Folio::middlewareFor('/admin/dashboard'))->toContain('auth')
->and(Folio::middlewareFor('/articles/hello-world'))->not->toContain('auth');

$this->get('/appendix-a-folio/admin/dashboard')->assertRedirect('/login');

Flags: audience, application developer. Alias, none of the three are aliases - state the corrected read-only direction for data() and middlewareFor() plainly, since both read easily as assignment methods on first encounter. Stability, no known history of breaking changes to these three. Case type, three undocumented methods inside an otherwise partially-documented Folio/FolioManager - the assignment side of data sharing, middleware, and rendering is documented through a page's own inline render() closure and the Folio::path()->middleware() chain; the introspection and global-override side covered here is not.

Socialite

Verified against laravel/socialite v5.29.0 (2026-08-04).

A Socialite provider can switch its authorization-code flow to PKCE with a single call:

Socialite::driver('github')->enablePKCE()->redirect();

enablePKCE() sets a protected flag read at three points: the authorization request gains code_challenge and code_challenge_method parameters; redirect() generates a random verifier and stores it in the session; token exchange pulls that verifier back out and sends it along. Nothing about PKCE appears anywhere in the documentation, not even in passing.

class SocialiteRedirectController extends Controller
{
public function redirect()
{
return Socialite::driver('github')->stateless()->enablePKCE()->redirect();
}
}

The test suite confirms the generated authorization URL actually carries both parameters:

parse_str(parse_url($response->headers->get('Location'), PHP_URL_QUERY), $query);

expect($query)->toHaveKey('code_challenge')
->and($query['code_challenge_method'])->toBe('S256');

and that a provider instance which never called enablePKCE() carries neither, checked in its own separate test since Socialite's manager caches a driver instance by name for the lifetime of one test - a provider already switched to PKCE earlier in the same test would still report it afterward otherwise.

Flags: audience, application developer. Alias, not an alias. Stability, no known history of breaking changes. Case type, an undocumented method on an otherwise well-documented Two\AbstractProvider - the OAuth2 flow itself, scopes, and stateless mode are documented, PKCE is not mentioned at all.

Closing

Ten packages is not an exhaustive survey of the Laravel ecosystem's first-party packages, and even these ten were not searched exhaustively for every undocumented method they carry - each section above covers what its own scenario needed, not everything each package's source contains. Appendix B (Ideas for Future Contributions) picks up from here, with specific sibling methods already found along the way and areas of the ecosystem not yet touched at all.