diff --git a/bin/slim b/bin/slim index 9896594..036fc1c 100755 --- a/bin/slim +++ b/bin/slim @@ -9,7 +9,10 @@ declare(strict_types=1); -use Slim\Console\Application; +use Slim\Console\App; +use Slim\Console\Config\ConfigResolver; + +$cwd = getcwd(); if (file_exists(__DIR__ . '/../../../autoload.php')) { require __DIR__ . '/../../../autoload.php'; @@ -17,5 +20,7 @@ if (file_exists(__DIR__ . '/../../../autoload.php')) { require __DIR__ . '/../vendor/autoload.php'; } -$app = new Application(); +$config = (new ConfigResolver())->resolve($cwd); + +$app = new App($config); $app->run(); diff --git a/composer.json b/composer.json index 1a124ca..c3857f2 100644 --- a/composer.json +++ b/composer.json @@ -13,14 +13,14 @@ } ], "require": { + "ext-json": "*", "php": "^7.2", - "symfony/console": "^5.0", - "symfony/config": "^5.0" + "symfony/console": "^5.0" }, "require-dev": { "adriansuter/php-autoload-override": "^1.0", "phpspec/prophecy": "^1.10", - "phpstan/phpstan": "^0.12.19", + "phpstan/phpstan": "^0.12.23", "phpunit/phpunit": "^8.5", "squizlabs/php_codesniffer": "^3.5" }, diff --git a/src/Application.php b/src/App.php similarity index 70% rename from src/Application.php rename to src/App.php index 5f85abe..2d27487 100644 --- a/src/Application.php +++ b/src/App.php @@ -10,18 +10,31 @@ namespace Slim\Console; +use Slim\Console\Config\Config; use Symfony\Component\Console\Application as SymfonyApplication; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Throwable; -class Application extends SymfonyApplication +class App extends SymfonyApplication { - private const VERSION = '0.1'; + protected const NAME = 'Slim Console'; - public function __construct() + protected const VERSION = '0.1'; + + /** + * @var Config + */ + protected $config; + + /** + * @param Config $config + */ + public function __construct(Config $config) { - parent::__construct('Slim Console', self::VERSION); + parent::__construct(static::NAME, static::VERSION); + + $this->config = $config; } /** @@ -48,4 +61,12 @@ public function doRun(InputInterface $input, OutputInterface $output): int return parent::doRun($input, $output); } + + /** + * @return Config + */ + public function getConfig(): Config + { + return $this->config; + } } diff --git a/src/Command/AbstractCommand.php b/src/Command/AbstractCommand.php new file mode 100644 index 0000000..949c360 --- /dev/null +++ b/src/Command/AbstractCommand.php @@ -0,0 +1,37 @@ +getApplication(); + + if ($app instanceof App === false) { + throw new RuntimeException( + 'Usage of the method `getConfig()` requires the parent application to be a Slim Console Application.' + ); + } + + return $app->getConfig(); + } +} diff --git a/src/Config/Config.php b/src/Config/Config.php new file mode 100644 index 0000000..8a80dd8 --- /dev/null +++ b/src/Config/Config.php @@ -0,0 +1,213 @@ + + */ + protected static $defaults = [ + 'bootstrapDir' => 'app', + 'indexDir' => 'public', + 'indexFile' => 'index.php', + 'sourceDir' => 'src', + 'commandsDir' => 'src' + . DIRECTORY_SEPARATOR + . 'Application' + . DIRECTORY_SEPARATOR + . 'Console' + . DIRECTORY_SEPARATOR + . 'Commands', + ]; + + /** + * @var string + */ + protected $bootstrapDir; + + /** + * @var string + */ + protected $indexDir; + + /** + * @var string + */ + protected $indexFile; + + /** + * @var string + */ + protected $sourceDir; + + /** + * @var string|null + */ + protected $commandsDir; + + /** + * @param string $bootstrapDir + * @param string $indexDir + * @param string $indexFile + * @param string $sourceDir + * @param string|null $commandsDir + */ + protected function __construct( + string $bootstrapDir, + string $indexDir, + string $indexFile, + string $sourceDir, + ?string $commandsDir = null + ) { + $this->bootstrapDir = $bootstrapDir; + $this->indexDir = $indexDir; + $this->indexFile = $indexFile; + $this->sourceDir = $sourceDir; + $this->commandsDir = $commandsDir; + } + + /** + * @return string + */ + public function getBootstrapDir(): string + { + return $this->bootstrapDir; + } + + /** + * @return string + */ + public function getIndexDir(): string + { + return $this->indexDir; + } + + /** + * @return string + */ + public function getIndexFile(): string + { + return $this->indexFile; + } + + /** + * @return string + */ + public function getSourceDir(): string + { + return $this->sourceDir; + } + + /** + * @return string|null + */ + public function getCommandsDir(): ?string + { + return $this->commandsDir; + } + + /** + * @param array $params + * + * @throws InvalidArgumentException + */ + protected static function validate(array $params): void + { + [ + 'bootstrapDir' => $bootstrapDir, + 'indexDir' => $indexDir, + 'indexFile' => $indexFile, + 'sourceDir' => $sourceDir, + 'commandsDir' => $commandsDir, + ] = $params; + + if (!is_string($bootstrapDir) || empty($bootstrapDir) || ctype_space($bootstrapDir)) { + throw new InvalidArgumentException('`bootstrapDir` must be a string.'); + } + + if (!is_string($indexDir) || empty($indexDir) || ctype_space($indexDir)) { + throw new InvalidArgumentException('`indexDir` must be a string.'); + } + + if (!is_string($indexFile) || empty($indexFile) || ctype_space($indexFile)) { + throw new InvalidArgumentException('`indexFile` must be a string.'); + } + + if (!is_string($sourceDir) || empty($sourceDir) || ctype_space($sourceDir)) { + throw new InvalidArgumentException('`sourceDir` must be a string.'); + } + + if (!empty($commandsDir) && (!is_string($commandsDir) || ctype_space($commandsDir))) { + throw new InvalidArgumentException('`commandsDir` must be a string.'); + } + } + + /** + * @param array $params + * + * @return Config + * + * @throws InvalidArgumentException + */ + public static function fromArray(array $params): Config + { + $params = array_merge(self::$defaults, $params); + + self::validate($params); + + return new self( + $params['bootstrapDir'], + $params['indexDir'], + $params['indexFile'], + $params['sourceDir'], + $params['commandsDir'] + ); + } + + /** + * @return Config + * + * @throws InvalidArgumentException + */ + public static function fromEnvironment(): Config + { + return self::fromArray([ + 'bootstrapDir' => (string) getenv(self::SLIM_CONSOLE_BOOTSTRAP_DIR), + 'indexDir' => (string) getenv(self::SLIM_CONSOLE_INDEX_DIR), + 'indexFile' => (string) getenv(self::SLIM_CONSOLE_INDEX_FILE), + 'sourceDir' => (string) getenv(self::SLIM_CONSOLE_SOURCE_DIR), + 'commandsDir' => (string) getenv(self::SLIM_CONSOLE_COMMANDS_DIR), + ]); + } + + /** + * @return Config + */ + public static function fromDefaults(): Config + { + return new self( + self::$defaults['bootstrapDir'], + self::$defaults['indexDir'], + self::$defaults['indexFile'], + self::$defaults['sourceDir'], + self::$defaults['commandsDir'] + ); + } +} diff --git a/src/Config/ConfigResolver.php b/src/Config/ConfigResolver.php new file mode 100644 index 0000000..f779553 --- /dev/null +++ b/src/Config/ConfigResolver.php @@ -0,0 +1,130 @@ +attemptResolvingConfigFromEnvironment(); + } catch (CannotResolveConfigException $e) { + try { + return $this->attemptResolvingConfigFromSupportedFormats($dir); + } catch (CannotResolveConfigException $e) { + return Config::fromDefaults(); + } + } + } + + /** + * @return Config + * + * @throws CannotResolveConfigException + */ + protected function attemptResolvingConfigFromEnvironment(): Config + { + $bootstrapDir = getenv(Config::SLIM_CONSOLE_BOOTSTRAP_DIR); + $commandsDir = getenv(Config::SLIM_CONSOLE_COMMANDS_DIR); + $indexDir = getenv(Config::SLIM_CONSOLE_INDEX_DIR); + $indexFile = getenv(Config::SLIM_CONSOLE_INDEX_FILE); + $sourceDir = getenv(Config::SLIM_CONSOLE_SOURCE_DIR); + + if ( + (is_string($bootstrapDir) && !empty($bootstrapDir) && !ctype_space($bootstrapDir)) + || (is_string($commandsDir) && !empty($commandsDir) && !ctype_space($commandsDir)) + || (is_string($indexDir) && !empty($indexDir) && !ctype_space($indexDir)) + || (is_string($indexFile) && !empty($indexFile) && !ctype_space($indexFile)) + || (is_string($sourceDir) && !empty($sourceDir) && !ctype_space($sourceDir)) + ) { + return Config::fromEnvironment(); + } + + throw new CannotResolveConfigException(); + } + + /** + * @param string|null $dir + * + * @return Config + * + * @throws CannotResolveConfigException + * @throws CannotParseConfigException + * @throws RuntimeException + */ + protected function attemptResolvingConfigFromSupportedFormats(?string $dir = null): Config + { + $dir = $dir ?? getcwd(); + $basePath = $dir . DIRECTORY_SEPARATOR . self::CONFIG_FILENAME; + + foreach ($this->supportedFormats as $format) { + $path = $basePath . ".{$format}"; + if (is_file($path) && is_readable($path)) { + return $this->attemptParsingConfigFromFile($path, $format); + } + } + + throw new CannotResolveConfigException(); + } + + /** + * @param string $path + * @param string $format + * + * @return Config + * + * @throws CannotParseConfigException + * @throws RuntimeException + */ + protected function attemptParsingConfigFromFile(string $path, string $format): Config + { + switch ($format) { + case self::FORMAT_PHP: + return PHPConfigParser::parse($path); + + case self::FORMAT_JSON: + return JSONConfigParser::parse($path); + + default: + throw new RuntimeException("Invalid configuration format `{$format}`."); + } + } +} diff --git a/src/Config/Parser/ConfigParserInterface.php b/src/Config/Parser/ConfigParserInterface.php new file mode 100644 index 0000000..3b11003 --- /dev/null +++ b/src/Config/Parser/ConfigParserInterface.php @@ -0,0 +1,26 @@ +add($mockCommand); + + $inputInterfaceProphecy = $this->prophesize(InputInterface::class); + + $inputInterfaceProphecy + ->hasParameterOption(['--help', '-h'], true) + ->willReturn(false) + ->shouldBeCalledOnce(); + + $inputInterfaceProphecy + ->hasParameterOption(['--help', '-h']) + ->willReturn(true) + ->shouldBeCalledOnce(); + + $inputInterfaceProphecy + ->isInteractive() + ->willReturn(true) + ->shouldBeCalledOnce(); + + $inputInterfaceProphecy + ->hasArgument('command') + ->willReturn(false) + ->shouldBeCalledOnce(); + + $inputInterfaceProphecy + ->validate() + ->shouldBeCalledOnce(); + + $inputInterfaceProphecy + ->hasParameterOption(['--version', '-V'], true) + ->willReturn(false) + ->shouldBeCalledOnce(); + + $inputInterfaceProphecy + ->getFirstArgument() + ->willReturn(MockCommand::getDefaultName()) + ->shouldBeCalledOnce(); + + $inputInterfaceProphecy + ->bind(Argument::any()) + ->shouldBeCalledTimes(2); + + $outputInterfaceProphecy = $this->prophesize(OutputInterface::class); + + $outputInterfaceProphecy + ->writeln($app->getLongVersion()) + ->shouldBeCalledOnce(); + + $outputInterfaceProphecy + ->writeln('') + ->shouldBeCalledOnce(); + + $outputInterfaceProphecy + ->writeln($mockCommand->getMockOutput()) + ->shouldBeCalledOnce(); + + $app->doRun($inputInterfaceProphecy->reveal(), $outputInterfaceProphecy->reveal()); + } + + public function testGetConfig(): void + { + $config = Config::fromDefaults(); + + $app = new App($config); + + $this->assertSame($config, $app->getConfig()); + } +} diff --git a/tests/Command/AbstractCommandTest.php b/tests/Command/AbstractCommandTest.php new file mode 100644 index 0000000..ef9116b --- /dev/null +++ b/tests/Command/AbstractCommandTest.php @@ -0,0 +1,44 @@ +add($mockCommand); + + $this->assertSame($config, $mockCommand->getConfig()); + } + + public function testGetConfigThrowsRuntimeExceptionWithIncompatibleApp(): void + { + $mockCommand = new MockCommand(); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage( + 'Usage of the method `getConfig()` requires the parent application to be a Slim Console Application.' + ); + + $mockCommand->getConfig(); + } +} diff --git a/tests/Config/ConfigResolverTest.php b/tests/Config/ConfigResolverTest.php new file mode 100644 index 0000000..8472f7c --- /dev/null +++ b/tests/Config/ConfigResolverTest.php @@ -0,0 +1,110 @@ +setupEnvConfig(); + + $exampleJsonConfigPath = $this->getExampleConfigPath(ConfigResolver::FORMAT_JSON); + $configResolver = new ConfigResolver(); + + $config = $configResolver->resolve($exampleJsonConfigPath); + + $this->assertSame($this->envParams['bootstrapDir'], $config->getBootstrapDir()); + $this->assertSame($this->envParams['commandsDir'], $config->getCommandsDir()); + $this->assertSame($this->envParams['indexDir'], $config->getIndexDir()); + $this->assertSame($this->envParams['indexFile'], $config->getIndexFile()); + $this->assertSame($this->envParams['sourceDir'], $config->getSourceDir()); + } + + /** + * @dataProvider supportedFormatsProvider + * + * @param string $format + * @param Closure $parser + */ + public function testAttemptResolvingConfigFromSupportedFormats(string $format, Closure $parser): void + { + $configPath = $this->getExampleConfigPath($format) + . DIRECTORY_SEPARATOR + . ConfigResolver::CONFIG_FILENAME . '.' . $format; + $example = $parser($configPath); + + $exampleConfigPath = $this->getExampleConfigPath($format); + $configResolver = new ConfigResolver(); + + $config = $configResolver->resolve($exampleConfigPath); + + $this->assertSame($example['bootstrapDir'], $config->getBootstrapDir()); + $this->assertSame($example['commandsDir'], $config->getCommandsDir()); + $this->assertSame($example['indexDir'], $config->getIndexDir()); + $this->assertSame($example['indexFile'], $config->getIndexFile()); + $this->assertSame($example['sourceDir'], $config->getSourceDir()); + } + + public function testAttemptParsingConfigFromFileThrowsRuntimeException(): void + { + $attemptParsingConfigFromFileMethod = new ReflectionMethod( + ConfigResolver::class, + 'attemptParsingConfigFromFile' + ); + $attemptParsingConfigFromFileMethod->setAccessible(true); + + $invalidFormat = 'invalid'; + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage("Invalid configuration format `{$invalidFormat}`."); + + $configResolver = new ConfigResolver(); + + $attemptParsingConfigFromFileMethod->invoke($configResolver, $this->examplesConfigBasePath, $invalidFormat); + } + + public function testResolveFallbackOnDefaults(): void + { + $defaultsReflection = new ReflectionProperty(Config::class, 'defaults'); + $defaultsReflection->setAccessible(true); + + $defaults = $defaultsReflection->getValue(); + + $configResolver = new ConfigResolver(); + $config = $configResolver->resolve($this->examplesConfigBasePath); + + $this->assertSame($defaults['bootstrapDir'], $config->getBootstrapDir()); + $this->assertSame($defaults['commandsDir'], $config->getCommandsDir()); + $this->assertSame($defaults['indexDir'], $config->getIndexDir()); + $this->assertSame($defaults['indexFile'], $config->getIndexFile()); + $this->assertSame($defaults['sourceDir'], $config->getSourceDir()); + } +} diff --git a/tests/Config/ConfigTest.php b/tests/Config/ConfigTest.php new file mode 100644 index 0000000..90c9c3b --- /dev/null +++ b/tests/Config/ConfigTest.php @@ -0,0 +1,112 @@ + $bootstrapDir, + 'commandsDir' => $commandsDir, + 'indexDir' => $indexDir, + 'indexFile' => $indexFile, + 'sourceDir' => $sourceDir, + ]); + + $this->assertSame($bootstrapDir, $config->getBootstrapDir()); + $this->assertSame($commandsDir, $config->getCommandsDir()); + $this->assertSame($indexDir, $config->getIndexDir()); + $this->assertSame($indexFile, $config->getIndexFile()); + $this->assertSame($sourceDir, $config->getSourceDir()); + } + + public function testFromEnvironment(): void + { + $this->setupEnvConfig(); + + $config = Config::fromEnvironment(); + + $this->assertSame($this->envParams['bootstrapDir'], $config->getBootstrapDir()); + $this->assertSame($this->envParams['commandsDir'], $config->getCommandsDir()); + $this->assertSame($this->envParams['indexDir'], $config->getIndexDir()); + $this->assertSame($this->envParams['indexFile'], $config->getIndexFile()); + $this->assertSame($this->envParams['sourceDir'], $config->getSourceDir()); + } + + public function testFromDefaults(): void + { + $defaultsReflection = new ReflectionProperty(Config::class, 'defaults'); + $defaultsReflection->setAccessible(true); + + $defaults = $defaultsReflection->getValue(); + $config = Config::fromDefaults(); + + $this->assertSame($defaults['bootstrapDir'], $config->getBootstrapDir()); + $this->assertSame($defaults['commandsDir'], $config->getCommandsDir()); + $this->assertSame($defaults['indexDir'], $config->getIndexDir()); + $this->assertSame($defaults['indexFile'], $config->getIndexFile()); + $this->assertSame($defaults['sourceDir'], $config->getSourceDir()); + } + + public function invalidDataProvider(): array + { + return [ + ['bootstrapDir', ''], + ['bootstrapDir', ' '], + ['commandsDir', ' '], + ['indexDir', ''], + ['indexDir', ' '], + ['indexFile', ''], + ['indexFile', ' '], + ['sourceDir', ''], + ['sourceDir', ' '], + ]; + } + + /** + * @dataProvider invalidDataProvider + * + * @param string $param + * @param string $value + */ + public function testValidate(string $param, string $value): void + { + $configReflection = new ReflectionClass(Config::class); + + $defaultsReflection = $configReflection->getProperty('defaults'); + $defaultsReflection->setAccessible(true); + + $defaults = $defaultsReflection->getValue(); + + $validateMethod = $configReflection->getMethod('validate'); + $validateMethod->setAccessible(true); + + $params = array_merge($defaults, [$param => $value]); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("`{$param}` must be a string."); + + $validateMethod->invoke(null, $params); + } +} diff --git a/tests/Config/Parser/JSONConfigParserTest.php b/tests/Config/Parser/JSONConfigParserTest.php new file mode 100644 index 0000000..c192dff --- /dev/null +++ b/tests/Config/Parser/JSONConfigParserTest.php @@ -0,0 +1,62 @@ +getExampleConfigPath(ConfigResolver::FORMAT_JSON); + $jsonConfigPath = $exampleJsonConfigPath + . DIRECTORY_SEPARATOR + . ConfigResolver::CONFIG_FILENAME . '.' . ConfigResolver::FORMAT_JSON; + $jsonConfig = json_decode(file_get_contents($jsonConfigPath), true); + + $config = JSONConfigParser::parse($jsonConfigPath); + + $this->assertSame($jsonConfig['bootstrapDir'], $config->getBootstrapDir()); + $this->assertSame($jsonConfig['commandsDir'], $config->getCommandsDir()); + $this->assertSame($jsonConfig['indexDir'], $config->getIndexDir()); + $this->assertSame($jsonConfig['indexFile'], $config->getIndexFile()); + $this->assertSame($jsonConfig['sourceDir'], $config->getSourceDir()); + } + + /** + * @dataProvider invalidConfigurationProvider + * + * @param string $fileName + * @param string $expectedExceptionMessage + */ + public function testParseThrowsInvalidArgumentException(string $fileName, string $expectedExceptionMessage): void + { + $invalidJsonConfigPath = $this->examplesConfigBasePath + . DIRECTORY_SEPARATOR . 'invalid-json' + . DIRECTORY_SEPARATOR . $fileName; + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage($expectedExceptionMessage); + + JSONConfigParser::parse($invalidJsonConfigPath); + } +} diff --git a/tests/Config/Parser/PHPConfigParserTest.php b/tests/Config/Parser/PHPConfigParserTest.php new file mode 100644 index 0000000..27e2bb6 --- /dev/null +++ b/tests/Config/Parser/PHPConfigParserTest.php @@ -0,0 +1,48 @@ +getExampleConfigPath(ConfigResolver::FORMAT_PHP); + $phpConfigPath = $exampleJsonConfigPath + . DIRECTORY_SEPARATOR + . ConfigResolver::CONFIG_FILENAME . '.' . ConfigResolver::FORMAT_PHP; + $phpConfig = require $phpConfigPath; + + $config = PHPConfigParser::parse($phpConfigPath); + + $this->assertSame($phpConfig['bootstrapDir'], $config->getBootstrapDir()); + $this->assertSame($phpConfig['commandsDir'], $config->getCommandsDir()); + $this->assertSame($phpConfig['indexDir'], $config->getIndexDir()); + $this->assertSame($phpConfig['indexFile'], $config->getIndexFile()); + $this->assertSame($phpConfig['sourceDir'], $config->getSourceDir()); + } + + public function testParseThrowsExceptionWithInvalidConfigFormat(): void + { + $invalidJsonConfigPath = $this->examplesConfigBasePath + . DIRECTORY_SEPARATOR . 'invalid-php' + . DIRECTORY_SEPARATOR . 'invalid-format.php'; + + $this->expectException(CannotParseConfigException::class); + $this->expectExceptionMessage('Slim Console configuration should be an array.'); + + PHPConfigParser::parse($invalidJsonConfigPath); + } +} diff --git a/tests/Mocks/MockCommand.php b/tests/Mocks/MockCommand.php new file mode 100644 index 0000000..b1bfc72 --- /dev/null +++ b/tests/Mocks/MockCommand.php @@ -0,0 +1,47 @@ +writeln($this->getMockOutput()); + return 1; + } + + /** + * @return string + */ + public function getMockOutput(): string + { + return $this->output; + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..ac97c02 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,61 @@ +examplesConfigBasePath . DIRECTORY_SEPARATOR . $format; + } + + /** + * @var string[] + */ + protected $envParams = [ + 'bootstrapDir' => 'envCustomDir', + 'commandsDir' => 'envCustomCommands', + 'indexDir' => 'envCustomIndexDir', + 'indexFile' => 'envCustom.php', + 'sourceDir' => 'envCustomSrc', + ]; + + protected function setupEnvConfig(): void + { + putenv(Config::SLIM_CONSOLE_BOOTSTRAP_DIR . '=' . $this->envParams['bootstrapDir']); + putenv(Config::SLIM_CONSOLE_COMMANDS_DIR . '=' . $this->envParams['commandsDir']); + putenv(Config::SLIM_CONSOLE_INDEX_DIR . '=' . $this->envParams['indexDir']); + putenv(Config::SLIM_CONSOLE_INDEX_FILE . '=' . $this->envParams['indexFile']); + putenv(Config::SLIM_CONSOLE_SOURCE_DIR . '=' . $this->envParams['sourceDir']); + } +} diff --git a/tests/examples/invalid-json/invalid-format.json b/tests/examples/invalid-json/invalid-format.json new file mode 100644 index 0000000..50a269a --- /dev/null +++ b/tests/examples/invalid-json/invalid-format.json @@ -0,0 +1 @@ +"invalid" diff --git a/tests/examples/invalid-json/invalid-syntax.json b/tests/examples/invalid-json/invalid-syntax.json new file mode 100644 index 0000000..cee1564 --- /dev/null +++ b/tests/examples/invalid-json/invalid-syntax.json @@ -0,0 +1,3 @@ +{ + invalid: 'json', +} diff --git a/tests/examples/invalid-php/invalid-format.php b/tests/examples/invalid-php/invalid-format.php new file mode 100644 index 0000000..ce6a0b9 --- /dev/null +++ b/tests/examples/invalid-php/invalid-format.php @@ -0,0 +1,11 @@ + 'customBootstrapDir', + 'commandsDir' => 'customCommandsDir', + 'indexDir' => 'customIndexDir', + 'indexFile' => 'customIndexFile.php', + 'sourceDir' => 'customSourceDir', +];