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
|
||||
|
||||
use jrosset\CliProgram\AutoDiscoveryApplication;
|
||||
use jrosset\CliProgram\SafeApplication;
|
||||
|
||||
class Application extends \Symfony\Component\Console\Application {
|
||||
use SafeApplication;
|
||||
use AutoDiscoveryApplication;
|
||||
|
||||
public function __construct (string $name = 'UNKNOWN', string $version = 'UNKNOWN') {
|
||||
parent::__construct($name, $version);
|
||||
$this->addDiscoveredCommands();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue