New trait for application with commands auto discovery
parent
5948f89fa2
commit
b223377a54
@ -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;
|
||||||
|
}
|
@ -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…
Reference in New Issue