*/ public static function fromSource(string $source): array { $constructs = []; try { $sequence = \token_get_all( $source, \TOKEN_PARSE, ); } catch (\ParseError $exception) { throw Exception\ParseError::fromParseError($exception); } $count = \count($sequence); $namespacePrefix = ''; $namespaceSegmentOrNamespaceTokens = [ \T_STRING, \T_NAME_QUALIFIED, ]; $classyTokens = [ \T_CLASS, \T_INTERFACE, \T_TRAIT, ]; // https://wiki.php.net/rfc/enumerations if (\PHP_VERSION_ID >= 80100 && \defined('T_ENUM')) { $classyTokens = [ \T_CLASS, \T_ENUM, \T_INTERFACE, \T_TRAIT, ]; } for ($index = 0; $index < $count; ++$index) { $token = $sequence[$index]; // collect namespace name if (\is_array($token) && \T_NAMESPACE === $token[0]) { $namespaceSegments = []; // collect namespace segments for ($index = self::significantAfter($index, $sequence, $count); $index < $count; ++$index) { $token = $sequence[$index]; if (\is_array($token) && !\in_array($token[0], $namespaceSegmentOrNamespaceTokens, true)) { continue; } $content = self::content($token); if (\in_array($content, ['{', ';'], true)) { break; } $namespaceSegments[] = $content; } $namespace = \implode('\\', $namespaceSegments); $namespacePrefix = $namespace . '\\'; } // skip non-classy tokens if (!\is_array($token) || !\in_array($token[0], $classyTokens, true)) { continue; } // skip anonymous classes if (\T_CLASS === $token[0]) { $current = self::significantBefore($index, $sequence); $token = $sequence[$current]; // if significant token before T_CLASS is T_NEW, it's an instantiation of an anonymous class if (\is_array($token) && \T_NEW === $token[0]) { continue; } } $index = self::significantAfter($index, $sequence, $count); $token = $sequence[$index]; $constructs[] = Construct::fromName($namespacePrefix . self::content($token)); } \usort($constructs, static function (Construct $a, Construct $b): int { return \strcmp( $a->name(), $b->name(), ); }); return $constructs; } /** * Returns an array of constructs defined in a directory. * * @throws Exception\DirectoryDoesNotExist * @throws Exception\MultipleDefinitionsFound * * @return array */ public static function fromDirectory(string $directory): array { if (!\is_dir($directory)) { throw Exception\DirectoryDoesNotExist::fromDirectory($directory); } $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator( $directory, \RecursiveDirectoryIterator::FOLLOW_SYMLINKS, )); $constructs = []; foreach ($iterator as $fileInfo) { /** @var \SplFileInfo $fileInfo */ if (!$fileInfo->isFile()) { continue; } if ($fileInfo->getBasename('.php') === $fileInfo->getBasename()) { continue; } /** @var string $fileName */ $fileName = $fileInfo->getRealPath(); /** @var string $source */ $source = \file_get_contents($fileName); try { $constructsFromFile = self::fromSource($source); } catch (Exception\ParseError $exception) { throw Exception\ParseError::fromFileNameAndParseError( $fileName, $exception, ); } if (0 === \count($constructsFromFile)) { continue; } foreach ($constructsFromFile as $construct) { $name = $construct->name(); if (\array_key_exists($name, $constructs)) { $construct = $constructs[$name]; } $constructs[$name] = $construct->definedIn($fileName); } } \usort($constructs, static function (Construct $a, Construct $b): int { return \strcmp( $a->name(), $b->name(), ); }); $constructsWithMultipleDefinitions = \array_filter($constructs, static function (Construct $construct): bool { return 1 < \count($construct->fileNames()); }); if (0 < \count($constructsWithMultipleDefinitions)) { throw Exception\MultipleDefinitionsFound::fromConstructs($constructsWithMultipleDefinitions); } return \array_values($constructs); } /** * Returns the index of the significant token after the index. * * @param array $sequence */ private static function significantAfter(int $index, array $sequence, int $count): int { for ($current = $index + 1; $current < $count; ++$current) { $token = $sequence[$current]; if (\is_array($token) && \in_array($token[0], [\T_COMMENT, \T_DOC_COMMENT, \T_WHITESPACE], true)) { continue; } return $current; } throw Exception\ShouldNotHappen::create(); } /** * Returns the index of the significant token after the index. * * @param array $sequence */ private static function significantBefore( int $index, array $sequence, ): int { for ($current = $index - 1; -1 < $current; --$current) { $token = $sequence[$current]; if (\is_array($token) && \in_array($token[0], [\T_COMMENT, \T_DOC_COMMENT, \T_WHITESPACE], true)) { continue; } return $current; } throw Exception\ShouldNotHappen::create(); } /** * Returns the string content of a token. * * @param array{0: int, 1: string, 2: int}|string $token */ private static function content($token): string { if (\is_array($token)) { return $token[1]; } return $token; } }