discoverEventClasses(); // Guard against a vacuous pass: if discovery silently finds nothing (e.g. a // broken base path), the loop below would assert nothing. The pilot ships ten // contract classes in Tickets, so discovery must find them. $this->assertContains( \Leantime\Domain\Tickets\Events\TicketUpdated::class, $discovered, 'Event class discovery found nothing — the vocabulary check would pass vacuously.' ); $violations = []; foreach ($discovered as $class) { $implements = class_implements($class); $shortName = substr($class, strrpos($class, '\\') + 1); if (in_array(LeantimeFilter::class, $implements, true)) { if (! str_ends_with($shortName, 'Filter')) { $violations[] = "$class — filter classes must be named {Thing}Filter"; } continue; } if (in_array(LeantimeEvent::class, $implements, true)) { $endsWithVerb = false; foreach (EventVerb::cases() as $verb) { if (str_ends_with($shortName, $verb->name)) { $endsWithVerb = true; break; } } if (! $endsWithVerb) { $violations[] = "$class — event classes must be named {Entity}{Verb} with a verb from EventVerb"; } } } $this->assertSame([], $violations, "Event vocabulary violations:\n".implode("\n", $violations)); } /** * Finds all classes in Domain and Core Events/ folders that implement one of the * class-based hook contracts. * * @return array */ private function discoverEventClasses(): array { // Anchor on the canonical app-root constant rather than a brittle relative // dirname() hop, so the scan can't silently miss the Events folders. $appRoot = defined('APP_ROOT') ? APP_ROOT : dirname(__DIR__, 5); $files = array_merge( glob($appRoot.'/app/Domain/*/Events/*.php') ?: [], glob($appRoot.'/app/Core/*/Events/*.php') ?: [], ); $classes = []; foreach ($files as $file) { $relative = str_replace([$appRoot.'/app/', '/', '.php'], ['', '\\', ''], $file); $class = 'Leantime\\'.$relative; if (! class_exists($class)) { continue; } $implements = class_implements($class) ?: []; if (in_array(LeantimeEvent::class, $implements, true) || in_array(LeantimeFilter::class, $implements, true)) { $classes[] = $class; } } return $classes; } }