From b6909c8ed8a493b9b7e1d7a19a0a78bf2d096bda Mon Sep 17 00:00:00 2001 From: Imani Manyara Date: Mon, 5 Oct 2026 07:00:09 -0400 Subject: [PATCH 1/2] Declare the Illuminate components src/ uses directly Testbench installs the whole framework, so a component src/ uses but require does not name passes every test and fails only for a consumer without it. require now names each illuminate/* component src/ reaches by import, facade or helper, plus laravel/framework where Illuminate\Foundation or its global helpers are used. Framework replaces every split, so an application installs nothing new. DeclaredRequirementsTest keeps it that way. --- CHANGELOG.md | 9 ++ composer.json | 3 + tests/Unit/DeclaredRequirementsTest.php | 140 ++++++++++++++++++++++++ 3 files changed, 152 insertions(+) create mode 100644 tests/Unit/DeclaredRequirementsTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index dc4941a..393cd70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,15 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and ## [Unreleased] +### Changed + +- `require` declares what `src/` uses directly: `illuminate/filesystem` and `illuminate/log` + (the `File` and `Log` facades in `FileChannel` and `LogChannel`), and `laravel/framework`, for + `Illuminate\Foundation\Bus\Dispatchable` and the `config_path()`/`now()` helpers, none of which + has a split package. `laravel/framework` replaces every `illuminate/*` split, so an application installs nothing + new. `tests/Unit/DeclaredRequirementsTest.php` now fails when `src/` uses an Illuminate component, + facade or global helper that `require` does not name. + ## [0.1.0] - 2026-08-15 ### Changed diff --git a/composer.json b/composer.json index 638ff78..a1d7b7e 100644 --- a/composer.json +++ b/composer.json @@ -40,10 +40,13 @@ "illuminate/cache": "^13.0", "illuminate/contracts": "^13.0", "illuminate/database": "^13.0", + "illuminate/filesystem": "^13.0", "illuminate/http": "^13.0", + "illuminate/log": "^13.0", "illuminate/mail": "^13.0", "illuminate/queue": "^13.0", "illuminate/support": "^13.0", + "laravel/framework": "^13.0", "psr/log": "^3.0" }, "require-dev": { diff --git a/tests/Unit/DeclaredRequirementsTest.php b/tests/Unit/DeclaredRequirementsTest.php new file mode 100644 index 0000000..4beb784 --- /dev/null +++ b/tests/Unit/DeclaredRequirementsTest.php @@ -0,0 +1,140 @@ +\...` name, imported or fully qualified; + * - a facade from `Illuminate\Support\Facades`, mapped to the component behind it; + * - a global helper. Every global helper (`config()`, `app()`, `__()`, `now()`, `report()`, ...) is + * defined in `Illuminate/Foundation/helpers.php`, which only `laravel/framework` autoloads, so a + * helper call needs `laravel/framework` as well as the component it reaches. + * + * `Illuminate\Foundation` has no split package, so its declaration is `laravel/framework`, which + * "replaces" every `illuminate/*` split and so is consistent with requiring them too (estate-followups + * decision D3). A line carrying a `class_exists()`-style guard is skipped: that use is optional. + * Measured 2026-10-05 (tooling-hygiene audit H6). + */ + +/** @var array facade => package */ +const NOTIFICATIONS_FACADES = [ + 'App' => 'illuminate/container', 'Artisan' => 'illuminate/console', 'Auth' => 'illuminate/auth', + 'Blade' => 'illuminate/view', 'Broadcast' => 'illuminate/broadcasting', 'Bus' => 'illuminate/bus', + 'Cache' => 'illuminate/cache', 'Config' => 'illuminate/config', 'Context' => 'illuminate/log', + 'Cookie' => 'illuminate/cookie', 'Crypt' => 'illuminate/encryption', 'Date' => 'illuminate/support', + 'DB' => 'illuminate/database', 'Event' => 'illuminate/events', 'Facade' => 'illuminate/support', + 'File' => 'illuminate/filesystem', 'Gate' => 'illuminate/auth', 'Hash' => 'illuminate/hashing', + 'Http' => 'illuminate/http', 'Lang' => 'illuminate/translation', 'Log' => 'illuminate/log', + 'Mail' => 'illuminate/mail', 'Notification' => 'illuminate/notifications', 'Password' => 'illuminate/auth', + 'Pipeline' => 'illuminate/pipeline', 'Process' => 'illuminate/process', 'Queue' => 'illuminate/queue', + 'RateLimiter' => 'illuminate/cache', 'Redirect' => 'illuminate/routing', 'Redis' => 'illuminate/redis', + 'Request' => 'illuminate/http', 'Response' => 'illuminate/routing', 'Route' => 'illuminate/routing', + 'Schedule' => 'illuminate/console', 'Schema' => 'illuminate/database', 'Session' => 'illuminate/session', + 'Storage' => 'illuminate/filesystem', 'URL' => 'illuminate/routing', 'Validator' => 'illuminate/validation', + 'View' => 'illuminate/view', 'Vite' => 'laravel/framework', +]; + +/** @var array helper => package it reaches (null: Foundation itself) */ +const NOTIFICATIONS_HELPERS = [ + 'abort' => null, 'abort_if' => null, 'abort_unless' => null, 'app' => 'illuminate/container', + 'app_path' => null, 'auth' => 'illuminate/auth', 'base_path' => null, 'bcrypt' => 'illuminate/hashing', + 'cache' => 'illuminate/cache', 'config' => 'illuminate/config', 'config_path' => null, + 'database_path' => null, 'dispatch' => 'illuminate/bus', 'event' => 'illuminate/events', + 'info' => 'illuminate/log', 'lang_path' => null, 'logger' => 'illuminate/log', 'now' => 'illuminate/support', + 'public_path' => null, 'redirect' => 'illuminate/routing', 'report' => null, 'request' => 'illuminate/http', + 'rescue' => null, 'resolve' => 'illuminate/container', 'resource_path' => null, + 'response' => 'illuminate/routing', 'route' => 'illuminate/routing', 'session' => 'illuminate/session', + 'storage_path' => null, 'to_route' => 'illuminate/routing', 'today' => 'illuminate/support', + 'trans' => 'illuminate/translation', 'trans_choice' => 'illuminate/translation', '__' => 'illuminate/translation', + 'url' => 'illuminate/routing', 'validator' => 'illuminate/validation', 'view' => 'illuminate/view', +]; + +/** @return array> package => evidence ("file:line") */ +function notificationsUsedIlluminatePackages(): array +{ + $root = dirname(__DIR__, 2); + $used = []; + $files = 0; + + $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($root . '/src', FilesystemIterator::SKIP_DOTS)); + + foreach ($iterator as $file) { + if ($file->getExtension() !== 'php') { + continue; + } + + $files++; + $relative = substr($file->getPathname(), strlen($root) + 1); + + // Comments carry names that are not uses; strip them, keeping line numbers. + $code = ''; + + foreach (token_get_all((string) file_get_contents($file->getPathname())) as $token) { + $code .= is_array($token) && in_array($token[0], [T_COMMENT, T_DOC_COMMENT], true) + ? str_repeat("\n", substr_count($token[1], "\n")) + : (is_array($token) ? $token[1] : $token); + } + + foreach (explode("\n", $code) as $index => $line) { + if (preg_match('/\b(class|interface|trait|function)_exists\s*\(/', $line) === 1) { + continue; + } + + $where = $relative . ':' . ($index + 1); + + preg_match_all('/Illuminate\\\\([A-Za-z]+)\\\\(?:Facades\\\\([A-Za-z]+))?/', $line, $names, PREG_SET_ORDER); + + foreach ($names as $name) { + if ($name[1] === 'Support' && ($name[2] ?? '') !== '') { + $package = NOTIFICATIONS_FACADES[$name[2]] ?? 'unmapped facade ' . $name[2]; + } elseif ($name[1] === 'Foundation') { + $package = 'laravel/framework'; + } else { + $package = 'illuminate/' . strtolower($name[1]); + } + + $used[$package][] = $where; + } + + $bare = (string) preg_replace(["/'(?:\\\\.|[^'\\\\])*'/", '/"(?:\\\\.|[^"\\\\])*"/'], "''", $line); + preg_match_all('/(?:$\\\\])(?toBeGreaterThanOrEqual(20) + ->and(count($used))->toBeGreaterThanOrEqual(10); + + return $used; +} + +it('declares every illuminate component that src/ uses', function (): void { + $composer = json_decode((string) file_get_contents(dirname(__DIR__, 2) . '/composer.json'), true, flags: JSON_THROW_ON_ERROR); + + $missing = []; + + foreach (notificationsUsedIlluminatePackages() as $package => $where) { + if (! array_key_exists($package, $composer['require'])) { + $missing[$package] = $where[0]; + } + } + + expect($missing)->toBe([]); +}); From fc4e8cf896f32684495c06d39105b8bdfe0f7a37 Mon Sep 17 00:00:00 2001 From: Imani Manyara Date: Mon, 5 Oct 2026 08:25:27 -0400 Subject: [PATCH 2/2] Use a plain comment for the requirements test header phpcs (PSR12.Files.FileHeader) reads a docblock after declare() as a misplaced file docblock. --- tests/Unit/DeclaredRequirementsTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Unit/DeclaredRequirementsTest.php b/tests/Unit/DeclaredRequirementsTest.php index 4beb784..6d75025 100644 --- a/tests/Unit/DeclaredRequirementsTest.php +++ b/tests/Unit/DeclaredRequirementsTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -/** +/* * Every Illuminate component that src/ uses is declared in `require`. * * Testbench installs the whole framework, so an undeclared component passes every test here and