From b223377a54134086c5998c24b4d25d6c449ebd57 Mon Sep 17 00:00:00 2001 From: Julien Rosset Date: Tue, 18 Apr 2023 12:20:36 +0200 Subject: [PATCH] New trait for application with commands auto discovery --- composer.json | 3 +- src/CliProgram/AutoDiscoveryApplication.php | 61 +++++++++ src/CliProgram/AutoDiscoverySpot.php | 133 +++++++++++++++++++ src/CliProgram/AutoDiscoverySpotClass.php | 136 ++++++++++++++++++++ src/CliProgram/IAutoDiscoverySpot.php | 17 +++ src/CliProgram/IAutoDiscoverySpotClass.php | 21 +++ src/CliProgram/ICommand.php | 21 +++ src/CliProgram/SafeApplication.php | 4 +- tests/Application.php | 7 + 9 files changed, 401 insertions(+), 2 deletions(-) create mode 100644 src/CliProgram/AutoDiscoveryApplication.php create mode 100644 src/CliProgram/AutoDiscoverySpot.php create mode 100644 src/CliProgram/AutoDiscoverySpotClass.php create mode 100644 src/CliProgram/IAutoDiscoverySpot.php create mode 100644 src/CliProgram/IAutoDiscoverySpotClass.php create mode 100644 src/CliProgram/ICommand.php diff --git a/composer.json b/composer.json index bd976dd..319e12a 100644 --- a/composer.json +++ b/composer.json @@ -6,7 +6,8 @@ "minimum-stability": "stable", "require": { "php": "^7.4 || ^8.0", - "symfony/console": "^6.0" + "symfony/console": "^6.0", + "jrosset/betterphptoken": "^1.0" }, "autoload": { "psr-4": { diff --git a/src/CliProgram/AutoDiscoveryApplication.php b/src/CliProgram/AutoDiscoveryApplication.php new file mode 100644 index 0000000..07bf619 --- /dev/null +++ b/src/CliProgram/AutoDiscoveryApplication.php @@ -0,0 +1,61 @@ +autoDiscoverySpots; + } + /** + * Set the list of discovery spots + * + * @param IAutoDiscoverySpot[] $autoDiscoverySpots The list of discovery spots + * + * @return $this + */ + public function setAutoDiscoverySpots (array $autoDiscoverySpots): self { + $this->autoDiscoverySpots = $autoDiscoverySpots; + return $this; + } + /** + * Add a discovery spot + * + * @param IAutoDiscoverySpot $spot The discovery spot + * @param IAutoDiscoverySpot ...$extraSpots Extra discovery spots to add + * + * @return $this + */ + public function addAutoDiscoverySpots (IAutoDiscoverySpot $spot, IAutoDiscoverySpot ...$extraSpots): self { + $this->autoDiscoverySpots[] = $spot; + foreach ($extraSpots as $extraSpot) { + $this->autoDiscoverySpots[] = $extraSpot; + } + return $this; + } + + /** + * Add discovered commands to application + * + * @return void + */ + public function addDiscoveredCommands (): void { + foreach ($this->getAutoDiscoverySpots() as $autoDiscoverySpot) { + foreach ($autoDiscoverySpot->getCommands() as $command) { + $this->add($command); + } + } + } +} \ No newline at end of file diff --git a/src/CliProgram/AutoDiscoverySpot.php b/src/CliProgram/AutoDiscoverySpot.php new file mode 100644 index 0000000..557b26d --- /dev/null +++ b/src/CliProgram/AutoDiscoverySpot.php @@ -0,0 +1,133 @@ +setDirectoryPath($directoryPath); + $this->setProcessSubDirectories($processSubDirectories); + } + + /** + * The directory path + * + * @return string The directory path + */ + public function getDirectoryPath (): string { + return $this->directoryPath; + } + /** + * Set the directory path + * + * @param string $directoryPath The directory path + * + * @return $this + */ + public function setDirectoryPath (string $directoryPath): self { + $this->directoryPath = $directoryPath; + return $this; + } + + /** + * True if search in subdirectories too, else False + * + * @return bool True if search in subdirectories too, else False + */ + public function isProcessSubDirectories (): bool { + return $this->processSubDirectories; + } + /** + * Set if search in subdirectories too + * + * @param bool $processSubDirectories True if search in subdirectories too, else False + * + * @return $this + */ + public function setProcessSubDirectories (bool $processSubDirectories): self { + $this->processSubDirectories = $processSubDirectories; + return $this; + } + + /** + * @inheritDoc + */ + public function getCommands (): array { + return static::getClassesOfDirectory($this->getDirectoryPath(), $this->isProcessSubDirectories()); + } + /** + * Get spot's classes of a directory + * + * @param string $directoryPath The directory path + * @param bool $processSubDirectories True if search in subdirectories too, else False + * + * @return (ICommand&Command)[] Spot's classes + */ + private static function getClassesOfDirectory (string $directoryPath, bool $processSubDirectories): array { + $classes = []; + + $directoryIterator = new FilesystemIterator($directoryPath); + foreach ($directoryIterator as $fileInfo) { + if ($fileInfo->isDot() || mb_substr($fileInfo->getFilename(), 0, 1) === '.') { + continue; + } + if ($fileInfo->isDir()) { + if ($processSubDirectories) { + /** @noinspection PhpConditionAlreadyCheckedInspection */ + $classes = array_merge( + $classes, + static::getClassesOfDirectory($fileInfo->getPathname(), $processSubDirectories) + ); + } + continue; + } + + $spotClass = AutoDiscoverySpotClass::createFromFile($fileInfo->getPathname()); + $classPath = realpath($spotClass->getName()); + if (array_key_exists($classPath, $classes)) { + continue; + } + + try { + $class = new ReflectionClass($spotClass->getName()); + if ($class->isAbstract() + || $class->isInterface() + || $class->isTrait() + || !$class->implementsInterface(ICommand::class) + || !is_subclass_of($class->getName(), Command::class) + ) { + continue; + } + + $classes[$classPath] = $class->newInstance(); + } + catch (ReflectionException $exception) { + continue; + } + } + + return $classes; + } +} \ No newline at end of file diff --git a/src/CliProgram/AutoDiscoverySpotClass.php b/src/CliProgram/AutoDiscoverySpotClass.php new file mode 100644 index 0000000..a3df848 --- /dev/null +++ b/src/CliProgram/AutoDiscoverySpotClass.php @@ -0,0 +1,136 @@ +setPath($path); + $this->setName($name); + } + + /** + * Create from file path only + * + * Get the first class name of the file + * + * @param string $path The class file path + * + * @return $this + */ + public static function createFromFile (string $path): self { + defined('T_NAME_QUALIFIED') || define('T_NAME_QUALIFIED', 10002); + + $fileHandler = fopen($path, 'r'); + + $class = $namespace = $buffer = ''; + $currTokenGlobal = 0; + while (!feof($fileHandler)) { + $buffer .= fread($fileHandler, 512); + if (mb_strpos($buffer, '{') === false) { + continue; + } + + $tokens = BetterPhpToken::tokenize($buffer); + $nbTokens = count($tokens); + for (; $currTokenGlobal < $nbTokens; $currTokenGlobal++) { + $token = $tokens[$currTokenGlobal]; + if ($token->is(T_NAMESPACE)) { + for ($currTokenSub = $currTokenGlobal + 1; $currTokenSub < $nbTokens; $currTokenSub++) { + $subToken = $tokens[$currTokenSub]; + + if ($subToken->is(T_STRING, T_NAME_QUALIFIED)) { + $namespace .= '\\' . $subToken->getText(); + } + elseif ($subToken->getText() === '{' || $subToken->getText() === ';') { + break; + } + } + } + elseif ($token->is(T_CLASS) && ($currTokenGlobal === 0 || !$tokens[$currTokenGlobal - 1]->is(T_DOUBLE_COLON))) { + for ($currTokenSub = $currTokenGlobal + 1; $currTokenSub < $nbTokens; $currTokenSub++) { + $subToken = $tokens[$currTokenSub]; + if ($subToken->getText() === '{') { + $class = $tokens[$currTokenGlobal + 2]; + } + } + } + } + } + + return new static((mb_strlen($namespace) > 0 ? $namespace . '\\' : '') . $class, $path); + } + + /** + * @inheritDoc + */ + public function getName (): string { + return $this->name; + } + /** + * Set the class name + * + * @param class-string $name The class name + * + * @return $this + */ + public function setName (string $name): self { + $this->name = $name; + return $this; + } + + /** + * @inheritDoc + */ + public function getPath (): string { + return $this->path; + } + /** + * Set the class file path + * + * @param string $path The class file path + * + * @return $this + * + * @throws LogicException If the file path doesn't exist + * @throws RuntimeException If the file path isn't readable + */ + public function setPath (string $path): self { + if (!file_exists($path)) { + throw new LogicException('Invalid class path: file is missing'); + } + if (!is_readable($path)) { + throw new RuntimeException('Invalid class path: file is not readable'); + } + + $this->path = realpath($path); + return $this; + } +} \ No newline at end of file diff --git a/src/CliProgram/IAutoDiscoverySpot.php b/src/CliProgram/IAutoDiscoverySpot.php new file mode 100644 index 0000000..1e04d29 --- /dev/null +++ b/src/CliProgram/IAutoDiscoverySpot.php @@ -0,0 +1,17 @@ +addDiscoveredCommands(); + } } \ No newline at end of file