fix(permissions): resolve module policy paths via composer PSR-4 prefixes
Modules do not share a single layout: some map their namespace to an app/ subdirectory while others fall back to the Modules\ => modules root. The previous modules/<class> hardcode dropped the app/ segment and wrote policies to the module root. Resolve the file path through the registered Composer PSR-4 prefixes, honouring the longest match.
This commit is contained in:
parent
0827b15345
commit
49d348aed8
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Console\Commands;
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
use App\Services\PermissionRegistry;
|
use App\Services\PermissionRegistry;
|
||||||
|
use Composer\Autoload\ClassLoader;
|
||||||
use Illuminate\Console\Attributes\Description;
|
use Illuminate\Console\Attributes\Description;
|
||||||
use Illuminate\Console\Attributes\Signature;
|
use Illuminate\Console\Attributes\Signature;
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
@ -72,12 +73,45 @@ class GeneratePolicies extends Command
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (str_starts_with($class, 'Modules\\')) {
|
if (str_starts_with($class, 'Modules\\')) {
|
||||||
return base_path('modules/'.str_replace('\\', '/', Str::after($class, 'Modules\\')).'.php');
|
return $this->modulePathForClass($class);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a module class to its file path via the registered Composer
|
||||||
|
* PSR-4 prefixes, honouring the most specific (longest) match. Modules do
|
||||||
|
* not share a single layout: some map Modules\Foo\ to modules/Foo/app,
|
||||||
|
* while others fall back to the Modules\ => modules root. Hardcoding
|
||||||
|
* modules/<class> would drop the app/ segment for the former and write the
|
||||||
|
* policy to the module root instead of app/Policies.
|
||||||
|
*/
|
||||||
|
protected function modulePathForClass(string $class): ?string
|
||||||
|
{
|
||||||
|
/** @var ClassLoader $loader */
|
||||||
|
$loader = require base_path('vendor/autoload.php');
|
||||||
|
|
||||||
|
$bestPrefix = null;
|
||||||
|
$bestDir = null;
|
||||||
|
|
||||||
|
foreach ($loader->getPrefixesPsr4() as $prefix => $directories) {
|
||||||
|
if (str_starts_with($class, $prefix) && ($bestPrefix === null || strlen($prefix) > strlen($bestPrefix))) {
|
||||||
|
$bestPrefix = $prefix;
|
||||||
|
$bestDir = $directories[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($bestPrefix === null || $bestDir === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$dir = realpath($bestDir) ?: $bestDir;
|
||||||
|
$relative = str_replace('\\', '/', Str::after($class, $bestPrefix));
|
||||||
|
|
||||||
|
return rtrim($dir, '/').'/'.$relative.'.php';
|
||||||
|
}
|
||||||
|
|
||||||
protected function stub(string $policyClass, string $subject): string
|
protected function stub(string $policyClass, string $subject): string
|
||||||
{
|
{
|
||||||
$namespace = Str::beforeLast($policyClass, '\\');
|
$namespace = Str::beforeLast($policyClass, '\\');
|
||||||
|
|||||||
@ -2,8 +2,21 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Console\Commands\GeneratePolicies;
|
||||||
|
use Composer\Autoload\ClassLoader;
|
||||||
use Illuminate\Support\Facades\File;
|
use Illuminate\Support\Facades\File;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoke the protected modulePathForClass resolver on a fresh command.
|
||||||
|
*/
|
||||||
|
function resolveModulePath(string $class): ?string
|
||||||
|
{
|
||||||
|
$command = new GeneratePolicies();
|
||||||
|
$method = (new ReflectionMethod($command, 'modulePathForClass'));
|
||||||
|
|
||||||
|
return $method->invoke($command, $class);
|
||||||
|
}
|
||||||
|
|
||||||
it('generates a thin policy for a resource model that lacks one', function (): void {
|
it('generates a thin policy for a resource model that lacks one', function (): void {
|
||||||
$path = app_path('Policies/Filament/FlightBundlePolicy.php');
|
$path = app_path('Policies/Filament/FlightBundlePolicy.php');
|
||||||
$original = File::exists($path) ? File::get($path) : null;
|
$original = File::exists($path) ? File::get($path) : null;
|
||||||
@ -40,3 +53,20 @@ it('regenerates a valid stub with --force', function (): void {
|
|||||||
expect($content)->toContain('extends BasePolicy');
|
expect($content)->toContain('extends BasePolicy');
|
||||||
expect($content)->toContain("\$subject = 'award'");
|
expect($content)->toContain("\$subject = 'award'");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('resolves a module policy path via the most specific PSR-4 prefix', function (): void {
|
||||||
|
/** @var ClassLoader $loader */
|
||||||
|
$loader = require base_path('vendor/autoload.php');
|
||||||
|
|
||||||
|
// A module that maps its namespace to an app/ subdirectory must keep the
|
||||||
|
// app/ segment; the generic Modules\ => modules fallback must not win.
|
||||||
|
$loader->addPsr4('Modules\\Foo\\', base_path('modules/Foo/app'));
|
||||||
|
|
||||||
|
try {
|
||||||
|
$path = resolveModulePath('Modules\\Foo\\Policies\\Filament\\BarPolicy');
|
||||||
|
|
||||||
|
expect($path)->toEndWith('modules/Foo/app/Policies/Filament/BarPolicy.php');
|
||||||
|
} finally {
|
||||||
|
$loader->setPsr4('Modules\\Foo\\', []);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user