Lines
97.72%
43 / 44
Methods
80.00%
4 / 5
Classes
0.00%
0 / 1
| Name | Lines | Methods | CRAP | ||||
|---|---|---|---|---|---|---|---|
| __construct | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| discover | 100.00% | 33 / 33 | 100.00% | 1 / 1 | 10 | ||
| findClassForFile | 100.00% | 3 / 3 | 100.00% | 1 / 1 | 4 | ||
| isRunnableApp | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 2 | ||
| validateAppId | 83.33% | 5 / 6 | 0.00% | 0 / 1 | 3.04 | ||
| 22 | final readonly class AppDiscovery | |
| 23 | { | |
| 24 | public function __construct( | |
| 25 | private Psr4Map $psr4Map, | |
| 26 | private string $appsDirectory, | |
| 27 | ) {} | |
| 28 | ||
| 29 | /** @throws AppException */ | |
| 30 | public function discover(): DiscoveryResult | |
| 31 | { | |
| 32 | $directory = realpath($this->appsDirectory); | |
| 33 | ||
| 34 | if ($directory === false || !is_dir($directory)) { | |
| 35 | return DiscoveryResult::createEmpty(); | |
| 36 | } | |
| 37 | ||
| 38 | $root = $this->psr4Map->findAutoloadRootContaining($directory) ?? throw AppException::createForReason( | |
| 39 | AppError::DirectoryNotAutoloadable, | |
| 40 | directory: $directory, | |
| 41 | ); | |
| 42 | $apps = []; | |
| 43 | $unmarked = []; | |
| 44 | $unloadable = []; | |
| 45 | $classesByKey = []; | |
| 46 | ||
| 47 | foreach (Finder::create()->files()->in($directory)->name('*.php')->sortByName() as $file) { | |
| 48 | try { | |
| 49 | $reflection = $this->findClassForFile($root, $file); | |
| 50 | } catch (Throwable $e) { | |
| 51 | $unloadable[] = new UnloadableAppFile($file->getRealPath() ?: $file->getPathname(), $e); | |
| 52 | ||
| 53 | continue; | |
| 54 | } | |
| 55 | ||
| 56 | if ($reflection === null) { | |
| 57 | continue; | |
| 58 | } | |
| 59 | ||
| 60 | $automation = $reflection->getAttributes(Automation::class)[0] ?? null; | |
| 61 | ||
| 62 | if ($automation === null) { | |
| 63 | if ($this->isRunnableApp($reflection)) { | |
| 64 | $unmarked[] = new UnmarkedApp($reflection->getName()); | |
| 65 | } | |
| 66 | ||
| 67 | continue; | |
| 68 | } | |
| 69 | ||
| 70 | $app = new DiscoveredApp($reflection->getName(), $this->validateAppId($reflection, $automation->newInstance())); | |
| 71 | $key = $app->id->toEnvironmentKey(); | |
| 72 | ||
| 73 | if (isset($classesByKey[$key])) { | |
| 74 | throw AppException::createForReason(AppError::DuplicateId, appId: $app->id->value, firstClass: $classesByKey[$key], secondClass: $app->class); | |
| 75 | } | |
| 76 | ||
| 77 | $classesByKey[$key] = $app->class; | |
| 78 | $apps[] = $app; | |
| 79 | } | |
| 80 | ||
| 81 | return new DiscoveryResult( | |
| 82 | DiscoveredAppCollection::fromApps($apps), | |
| 83 | UnmarkedAppCollection::fromList($unmarked), | |
| 84 | UnloadableAppFileCollection::fromList($unloadable), | |
| 85 | ); | |
| 86 | } | |
| 87 | ||
| 88 | /** @return ReflectionClass<object>|null */ | |
| 89 | private function findClassForFile(AutoloadRoot $root, SplFileInfo $file): ?ReflectionClass | |
| 90 | { | |
| 91 | $path = $file->getRealPath(); | |
| 92 | $class = $path === false ? null : $root->findClassNameForFile($path); | |
| 93 | ||
| 94 | return $class === null || !class_exists($class) ? null : new ReflectionClass($class); | |
| 95 | } | |
| 96 | ||
| 97 | /** @param ReflectionClass<object> $reflection */ | |
| 98 | private function isRunnableApp(ReflectionClass $reflection): bool | |
| 99 | { | |
| 100 | return $reflection->isInstantiable() && $reflection->implementsInterface(App::class); | |
| 101 | } | |
| 102 | ||
| 103 | /** | |
| 104 | * @param ReflectionClass<object> $reflection | |
| 105 | * @throws AppException | |
| 106 | */ | |
| 107 | private function validateAppId(ReflectionClass $reflection, Automation $automation): AppId | |
| 108 | { | |
| 109 | $class = $reflection->getName(); | |
| 110 | ||
| 111 | if (!$reflection->implementsInterface(App::class)) { | |
| 112 | throw AppException::createForReason(AppError::NotAnApp, className: $class, appInterface: App::class); | |
| 113 | } | |
| 114 | ||
| 115 | if (!$reflection->isInstantiable()) { | |
| 116 | throw AppException::createForReason(AppError::NotInstantiable, className: $class); | |
| 117 | } | |
| 118 | ||
| 119 | return AppId::tryFromString($automation->id) ?? throw AppException::createForReason(AppError::IdInvalid, className: $class, appId: $automation->id); | |
| 120 | } | |
| 121 | } |