basePath = $basePath ?? base_path(); $this->appPath = $appPath ?? app_path(); $this->routesPath = $routesPath ?? base_path('routes/web.php'); $this->adminPanelProviderPath = $adminPanelProviderPath ?? app_path('Providers/Filament/AdminPanelProvider.php'); } /** * @return array */ public function discover(): array { $adminScopedClasses = $this->discoverAdminScopedClasses(); /** @var array $components */ $components = []; foreach ($this->resourceFiles() as $path) { $className = $this->classNameFromPath($path); $components[$className] = new ActionSurfaceDiscoveredComponent( className: $className, componentType: ActionSurfaceComponentType::Resource, panelScopes: $this->panelScopesFor($className, $adminScopedClasses), ); } foreach ($this->pageFiles() as $path) { $className = $this->classNameFromPath($path); $components[$className] = new ActionSurfaceDiscoveredComponent( className: $className, componentType: ActionSurfaceComponentType::Page, panelScopes: $this->panelScopesFor($className, $adminScopedClasses), ); } foreach ($this->relationManagerFiles() as $path) { $className = $this->classNameFromPath($path); $components[$className] = new ActionSurfaceDiscoveredComponent( className: $className, componentType: ActionSurfaceComponentType::RelationManager, panelScopes: $this->panelScopesFor($className, $adminScopedClasses), ); } ksort($components); return array_values($components); } /** * @param array $adminScopedClasses * @return array */ private function panelScopesFor(string $className, array $adminScopedClasses): array { $scopes = [ActionSurfacePanelScope::Tenant]; if (in_array($className, $adminScopedClasses, true)) { $scopes[] = ActionSurfacePanelScope::Admin; } return $scopes; } /** * @return array */ private function resourceFiles(): array { return $this->collectPhpFiles($this->appPath.'/Filament/Resources', function (string $path): bool { if (! str_ends_with($path, 'Resource.php')) { return false; } if (str_contains($path, '/Pages/')) { return false; } if (str_contains($path, '/RelationManagers/')) { return false; } return true; }); } /** * @return array */ private function pageFiles(): array { return $this->collectPhpFiles($this->appPath.'/Filament/Pages', static function (string $path): bool { return str_ends_with($path, '.php'); }); } /** * @return array */ private function relationManagerFiles(): array { return $this->collectPhpFiles($this->appPath.'/Filament/Resources', function (string $path): bool { if (! str_contains($path, '/RelationManagers/')) { return false; } return str_ends_with($path, 'RelationManager.php'); }); } /** * @param callable(string): bool $filter * @return array */ private function collectPhpFiles(string $directory, callable $filter): array { if (! is_dir($directory)) { return []; } $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($directory, RecursiveDirectoryIterator::SKIP_DOTS), ); $paths = []; /** @var SplFileInfo $file */ foreach ($iterator as $file) { if (! $file->isFile()) { continue; } $path = str_replace('\\', '/', $file->getPathname()); if (! $filter($path)) { continue; } $paths[] = $path; } sort($paths); return $paths; } /** * @return array */ private function discoverAdminScopedClasses(): array { $classes = array_merge( $this->parseFilamentClassReferences($this->adminPanelProviderPath), $this->parseFilamentClassReferences($this->routesPath), ); $classes = array_values(array_unique(array_filter($classes, static function (string $className): bool { return str_starts_with($className, 'App\\Filament\\'); }))); sort($classes); return $classes; } /** * @return array */ private function parseFilamentClassReferences(string $filePath): array { if (! is_file($filePath)) { return []; } $contents = file_get_contents($filePath); if (! is_string($contents) || $contents === '') { return []; } $imports = $this->parseUseStatements($contents); preg_match_all('/\\\\?([A-Z][A-Za-z0-9_\\\\]*)::(?:class|registerRoutes)\b/', $contents, $matches); $classes = []; foreach ($matches[1] as $token) { $resolved = $this->resolveClassToken($token, $imports); if ($resolved === null) { continue; } $classes[] = $resolved; } return $classes; } /** * @return array */ private function parseUseStatements(string $contents): array { preg_match_all('/^use\s+([^;]+);/m', $contents, $matches); $imports = []; foreach ($matches[1] as $importExpression) { $normalized = trim($importExpression); if (! str_contains($normalized, '\\')) { continue; } $parts = preg_split('/\s+as\s+/i', $normalized); $fqcn = ltrim($parts[0], '\\'); $alias = $parts[1] ?? null; if (! is_string($alias) || trim($alias) === '') { $segments = explode('\\', $fqcn); $alias = end($segments); } if (! is_string($alias) || trim($alias) === '') { continue; } $imports[trim($alias)] = $fqcn; } return $imports; } /** * @param array $imports */ private function resolveClassToken(string $token, array $imports): ?string { $token = ltrim(trim($token), '\\'); if ($token === '') { return null; } if (str_contains($token, '\\')) { return $token; } return $imports[$token] ?? null; } private function classNameFromPath(string $path): string { $normalizedPath = str_replace('\\', '/', $path); $normalizedAppPath = str_replace('\\', '/', $this->appPath); $relative = ltrim(substr($normalizedPath, strlen($normalizedAppPath)), '/'); return 'App\\'.str_replace('/', '\\', substr($relative, 0, -4)); } }