New trait for application with commands auto discovery

2.x
Julien Rosset 2 years ago
parent 5948f89fa2
commit b223377a54

@ -6,7 +6,8 @@
"minimum-stability": "stable", "minimum-stability": "stable",
"require": { "require": {
"php": "^7.4 || ^8.0", "php": "^7.4 || ^8.0",
"symfony/console": "^6.0" "symfony/console": "^6.0",
"jrosset/betterphptoken": "^1.0"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {

@ -0,0 +1,61 @@
<?php
namespace jrosset\CliProgram;
/**
* An application with command auto discovery
*/
trait AutoDiscoveryApplication {
/**
* @var IAutoDiscoverySpot[] The list of discovery spots
*/
private array $autoDiscoverySpots = [];
/**
* The list of discovery spots
*
* @return IAutoDiscoverySpot[] The list of discovery spots
*/
public function getAutoDiscoverySpots (): array {
return $this->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);
}
}
}
}

@ -0,0 +1,133 @@
<?php
namespace jrosset\CliProgram;
use FilesystemIterator;
use ReflectionClass;
use ReflectionException;
use Symfony\Component\Console\Command\Command;
/**
* A spot for command auto discovery
*/
class AutoDiscoverySpot implements IAutoDiscoverySpot {
/**
* @var string The directory path
*/
private string $directoryPath;
/**
* @var bool True if search in subdirectories too, else False
*/
private bool $processSubDirectories;
/**
* Initialization
*
* @param string $directoryPath The directory path
* @param bool $processSubDirectories True if search in subdirectories too, else False
*/
public function __construct (string $directoryPath, bool $processSubDirectories = true) {
$this->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;
}
}

@ -0,0 +1,136 @@
<?php
namespace jrosset\CliProgram;
use jrosset\BetterPhpToken\BetterPhpToken;
use LogicException;
use ReflectionClass;
use ReflectionException;
use RuntimeException;
use Throwable;
/**
* A command auto discovery spot class
*/
class AutoDiscoverySpotClass implements IAutoDiscoverySpotClass {
/**
* @var class-string The class name
*/
private string $name;
/**
* @var string The class file path
*/
private string $path;
/**
* Initialization
*
* @param string $name The class name
* @param string $path The class file path
*
* @throws LogicException If the file path doesn't exist
* @throws RuntimeException If the file path isn't readable
*/
public function __construct (string $name, string $path) {
$this->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;
}
}

@ -0,0 +1,17 @@
<?php
namespace jrosset\CliProgram;
use Symfony\Component\Console\Command\Command;
/**
* Interface for a command auto discovery spot
*/
interface IAutoDiscoverySpot {
/**
* Spot's commands
*
* @return (ICommand&Command)[] Spot's commands
*/
public function getCommands (): array;
}

@ -0,0 +1,21 @@
<?php
namespace jrosset\CliProgram;
/**
* Interface for a command auto discovery spot class
*/
interface IAutoDiscoverySpotClass {
/**
* The class name
*
* @return class-string The class name
*/
public function getName (): string;
/**
* The class file path
*
* @return string The class file path
*/
public function getPath(): string;
}

@ -0,0 +1,21 @@
<?php
namespace jrosset\CliProgram;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Throwable;
/**
* Interface for all commands
*/
interface ICommand {
/**
* Runs the command.
*
* @return int The command exit code
*
* @throws Throwable If an error occurs
*/
public function run (InputInterface $input, OutputInterface $output): int;
}

@ -8,7 +8,9 @@ use Symfony\Component\Console\Output\OutputInterface;
use Throwable; use Throwable;
/** /**
* A “safe” application: runtime exception are treated by {@see SafeApplication::processRuntimeException()} * A “safe” application: without runtime exception
*
* Runtime exception are treated by {@see SafeApplication::processRuntimeException()}
*/ */
trait SafeApplication { trait SafeApplication {
/** /**

@ -1,7 +1,14 @@
<?php <?php
use jrosset\CliProgram\AutoDiscoveryApplication;
use jrosset\CliProgram\SafeApplication; use jrosset\CliProgram\SafeApplication;
class Application extends \Symfony\Component\Console\Application { class Application extends \Symfony\Component\Console\Application {
use SafeApplication; use SafeApplication;
use AutoDiscoveryApplication;
public function __construct (string $name = 'UNKNOWN', string $version = 'UNKNOWN') {
parent::__construct($name, $version);
$this->addDiscoveredCommands();
}
} }
Loading…
Cancel
Save