diff --git a/.github/workflows/cs-tests.yml b/.github/workflows/cs-tests.yml
new file mode 100644
index 0000000..3da9965
--- /dev/null
+++ b/.github/workflows/cs-tests.yml
@@ -0,0 +1,46 @@
+on:
+ - push
+
+name: Run phpcs checks
+
+jobs:
+ mutation:
+ name: PHP ${{ matrix.php }}-${{ matrix.os }}
+
+ runs-on: ${{ matrix.os }}
+
+ strategy:
+ matrix:
+ os:
+ - ubuntu-latest
+
+ php:
+ - "8.1"
+ - "8.2"
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+
+ - name: Install PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: "${{ matrix.php }}"
+ tools: composer:v2, cs2pr
+ coverage: none
+
+ - name: Determine composer cache directory
+ run: echo "COMPOSER_CACHE_DIR=$(composer config cache-dir)" >> $GITHUB_ENV
+
+ - name: Cache dependencies installed with composer
+ uses: actions/cache@v3
+ with:
+ path: ${{ env.COMPOSER_CACHE_DIR }}
+ key: php${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }}
+ restore-keys: |
+ php${{ matrix.php }}-composer-
+ - name: Install dependencies with composer
+ run: composer update --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi
+
+ - name: Run phpcs checks
+ run: vendor/bin/phpcs
diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml
new file mode 100644
index 0000000..74550fc
--- /dev/null
+++ b/.github/workflows/static-analysis.yml
@@ -0,0 +1,46 @@
+on:
+ - push
+
+name: Run static analysis
+
+jobs:
+ mutation:
+ name: PHP ${{ matrix.php }}-${{ matrix.os }}
+
+ runs-on: ${{ matrix.os }}
+
+ strategy:
+ matrix:
+ os:
+ - ubuntu-latest
+
+ php:
+ - "8.1"
+ - "8.2"
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+
+ - name: Install PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: "${{ matrix.php }}"
+ tools: composer:v2, cs2pr
+ coverage: none
+
+ - name: Determine composer cache directory
+ run: echo "COMPOSER_CACHE_DIR=$(composer config cache-dir)" >> $GITHUB_ENV
+
+ - name: Cache dependencies installed with composer
+ uses: actions/cache@v3
+ with:
+ path: ${{ env.COMPOSER_CACHE_DIR }}
+ key: php${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }}
+ restore-keys: |
+ php${{ matrix.php }}-composer-
+ - name: Install dependencies with composer
+ run: composer update --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi
+
+ - name: Run static analysis
+ run: vendor/bin/psalm --no-cache --output-format=github --show-info=false --threads=4
diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml
new file mode 100644
index 0000000..d2ab8e7
--- /dev/null
+++ b/.github/workflows/unit-tests.yml
@@ -0,0 +1,47 @@
+on:
+ - push
+
+name: Run PHPUnit tests
+
+jobs:
+ mutation:
+ name: PHP ${{ matrix.php }}-${{ matrix.os }}
+
+ runs-on: ${{ matrix.os }}
+
+ strategy:
+ matrix:
+ os:
+ - ubuntu-latest
+
+ php:
+ - "8.1"
+ - "8.2"
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+
+ - name: Install PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: "${{ matrix.php }}"
+ tools: composer:v2, cs2pr
+ coverage: none
+
+ - name: Determine composer cache directory
+ run: echo "COMPOSER_CACHE_DIR=$(composer config cache-dir)" >> $GITHUB_ENV
+
+ - name: Cache dependencies installed with composer
+ uses: actions/cache@v3
+ with:
+ path: ${{ env.COMPOSER_CACHE_DIR }}
+ key: php${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }}
+ restore-keys: |
+ php${{ matrix.php }}-composer-
+
+ - name: Install dependencies with composer
+ run: composer install --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi
+
+ - name: Run PHPUnit tests
+ run: vendor/bin/phpunit --colors=always
diff --git a/.gitignore b/.gitignore
index f3e6777..be50580 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,8 @@
### Composer template
composer.phar
/vendor/
+.phpcs-cache
+.phpunit.result.cache
# Commit your application's lock file http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file
# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file
diff --git a/OSSMETADATA b/OSSMETADATA
index 6c7e106..b96d4a4 100644
--- a/OSSMETADATA
+++ b/OSSMETADATA
@@ -1 +1 @@
-osslifecycle=active
\ No newline at end of file
+osslifecycle=active
diff --git a/README.md b/README.md
index 75aefe7..8bbe150 100644
--- a/README.md
+++ b/README.md
@@ -1,33 +1,38 @@
# dot-cli

-
+
[](https://github.com/dotkernel/dot-cli/issues)
[](https://github.com/dotkernel/dot-cli/network)
[](https://github.com/dotkernel/dot-cli/stargazers)
[](https://github.com/dotkernel/dot-cli/blob/3.0/LICENSE)
+[](https://github.com/dotkernel/dot-cli/actions/workflows/static-analysis.yml)
+
+[](https://insight.symfony.com/projects/b9489f03-14e3-441f-aefd-e3b549b4917e)
+
DotKernel component to build console applications based on [laminas-cli](https://github.com/laminas/laminas-cli).
### Requirements
-- PHP >= 7.4
-- laminas/laminas-servicemanager >= 3.6,
-- laminas/laminas-cli >= 1.0
+- PHP >= 8.1
+- laminas/laminas-servicemanager >= 3.11,
+- laminas/laminas-cli >= 1.4
### Setup
#### 1. Install package
Run the following command in your application's root directory:
-```bash
-$ composer require dotkernel/dot-cli
-```
+
+ composer require dotkernel/dot-cli
#### 2. Register ConfigProvider
-Open your application's `config/config.php` and add `Dot\Cli\ConfigProvider::class,` under the _DK packages_ comment.
+Open your application's `config/config.php` and the following line under the _DK packages_ comment:
+
+ Dot\Cli\ConfigProvider::class,
#### 3. Copy bootstrap file
-Locate in this package the following file `bin/cli.php` then copy it to your application's `bin/` directory.
+Locate file `bin/cli.php` in this package, then copy it to your application's `bin/` directory.
This is the bootstrap file you will use to execute your commands with.
#### 4. Copy config file
@@ -37,9 +42,9 @@ This is the config file you will add your commands to.
### Testing
Using the command line, go to your application's root directory, then type the following command:
-```bash
-$ php /bin/cli.php
-```
+
+ php /bin/cli.php
+
The output should look similar to this, containing information on how to start using dot-cli:
```text
DotKernel CLI 1.0.0
diff --git a/composer.json b/composer.json
index 73ef0d8..8bfbb7d 100644
--- a/composer.json
+++ b/composer.json
@@ -17,18 +17,42 @@
"email": "team@dotkernel.com"
}
],
+ "config": {
+ "sort-packages": true,
+ "allow-plugins": {
+ "dealerdirect/phpcodesniffer-composer-installer": true
+ }
+ },
"require": {
- "php": "~7.4.0 || ~8.0.0 || ~8.1.0",
+ "php": "~8.1.0 || ~8.2.0",
"laminas/laminas-cli": "^1.4.0",
"laminas/laminas-servicemanager": "^3.11.1"
},
- "require-dev": {
- "phpunit/phpunit": "^9.5",
- "squizlabs/php_codesniffer": "^3.5"
- },
"autoload": {
"psr-4": {
"Dot\\Cli\\": "src"
}
+ },
+ "require-dev": {
+ "laminas/laminas-coding-standard": "^2.5",
+ "mikey179/vfsstream": "^1.6",
+ "phpunit/phpunit": "^10.2",
+ "vimeo/psalm": "^5.13"
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "DotTest\\Cli\\": "test/"
+ }
+ },
+ "scripts": {
+ "check": [
+ "@cs-check",
+ "@test"
+ ],
+ "cs-check": "phpcs",
+ "cs-fix": "phpcbf",
+ "test": "phpunit --colors=always",
+ "test-coverage": "phpunit --colors=always --coverage-clover clover.xml",
+ "static-analysis": "psalm --shepherd --stats"
}
}
diff --git a/config/autoload/cli.global.php b/config/autoload/cli.global.php
index 864af0e..b5e83bf 100644
--- a/config/autoload/cli.global.php
+++ b/config/autoload/cli.global.php
@@ -1,21 +1,20 @@
[
- 'version' => '1.0.0',
- 'name' => 'DotKernel CLI',
+ 'dot_cli' => [
+ 'version' => '1.0.0',
+ 'name' => 'DotKernel CLI',
'commands' => [
DemoCommand::getDefaultName() => DemoCommand::class,
- ]
+ ],
],
FileLockerInterface::class => [
'enabled' => true,
'dirPath' => getcwd() . '/data/lock',
- ]
+ ],
];
diff --git a/phpcs.xml b/phpcs.xml
new file mode 100644
index 0000000..c294658
--- /dev/null
+++ b/phpcs.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ bin
+ config
+ src
+ test
+
+
+
+
diff --git a/phpunit.xml b/phpunit.xml
new file mode 100644
index 0000000..4a1bf00
--- /dev/null
+++ b/phpunit.xml
@@ -0,0 +1,17 @@
+
+
+
+
+ ./test
+
+
+
+
+
+ ./src
+
+
+
diff --git a/psalm-baseline.xml b/psalm-baseline.xml
new file mode 100644
index 0000000..9352203
--- /dev/null
+++ b/psalm-baseline.xml
@@ -0,0 +1,13 @@
+
+
+
+
+ new TerminateListener($config)
+ new ContainerCommandLoader($container, $config['commands'])
+
+
+ new TerminateListener($config)
+ new ContainerCommandLoader($container, $config['commands'])
+
+
+
diff --git a/psalm.xml b/psalm.xml
new file mode 100644
index 0000000..9dd8f07
--- /dev/null
+++ b/psalm.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/Application.php b/src/Application.php
index e516af3..aed2143 100644
--- a/src/Application.php
+++ b/src/Application.php
@@ -5,23 +5,15 @@
namespace Dot\Cli;
use Exception;
+use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Throwable;
-/**
- * Class Application
- * @package Dot\Cli
- */
class Application extends \Symfony\Component\Console\Application
{
private FileLockerInterface $fileLocker;
- /**
- * Application constructor.
- * @param FileLockerInterface $fileLocker
- * @param array $config
- */
public function __construct(FileLockerInterface $fileLocker, array $config)
{
parent::__construct($config['name'] ?? 'UNKNOWN', $config['version'] ?? 'UNKNOWN');
@@ -29,18 +21,12 @@ public function __construct(FileLockerInterface $fileLocker, array $config)
$this->fileLocker = $fileLocker;
}
- /**
- * Application destructor.
- */
public function __destruct()
{
$this->fileLocker->unlock();
}
/**
- * @param InputInterface $input
- * @param OutputInterface $output
- * @return int
* @throws Throwable
*/
public function doRun(InputInterface $input, OutputInterface $output): int
@@ -51,7 +37,7 @@ public function doRun(InputInterface $input, OutputInterface $output): int
$this->fileLocker->lock();
} catch (Exception $exception) {
$output->writeln($exception->getMessage());
- return 0;
+ return Command::FAILURE;
}
return parent::doRun($input, $output);
diff --git a/src/Command/DemoCommand.php b/src/Command/DemoCommand.php
index 99130bc..88b47a2 100644
--- a/src/Command/DemoCommand.php
+++ b/src/Command/DemoCommand.php
@@ -5,47 +5,23 @@
namespace Dot\Cli\Command;
use Symfony\Component\Console\Command\Command;
-use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
-use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
-/**
- * Class DemoCommand
- * @package Dot\Cli\Command
- */
class DemoCommand extends Command
{
+ /** @var string $defaultName */
protected static $defaultName = 'demo:command';
- /**
- * @return void
- */
protected function configure(): void
{
$this
->setName(self::$defaultName)
- ->setDescription('Demo command description.')
- ->addOption('opt_required', 'r', InputOption::VALUE_REQUIRED, 'Required parameter')
- ->addOption('opt_optional', 'o', InputOption::VALUE_OPTIONAL, 'Optional parameter')
- ->addArgument('arg_required', InputArgument::REQUIRED, 'Required argument')
- ->addArgument('arg_optional', InputArgument::OPTIONAL, 'Optional argument', 'arg')
- ->addArgument('arg_array', InputArgument::IS_ARRAY, 'Array argument');
+ ->setDescription('Demo command description.');
}
- /**
- * @param InputInterface $input
- * @param OutputInterface $output
- * @return int
- */
protected function execute(InputInterface $input, OutputInterface $output): int
{
- $output->writeln('opt_required=' . $input->getOption('opt_required'));
- $output->writeln('opt_optional=' . $input->getOption('opt_optional'));
- $output->writeln('arg_required=' . $input->getArgument('arg_required'));
- $output->writeln('arg_optional=' . $input->getArgument('arg_optional'));
- $output->writeln('arg_array=[' . implode(', ', $input->getArgument('arg_array')) . ']');
-
- return 0;
+ return Command::SUCCESS;
}
}
diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php
index 18b3ed3..c1762be 100644
--- a/src/ConfigProvider.php
+++ b/src/ConfigProvider.php
@@ -7,15 +7,8 @@
use Dot\Cli\Factory\ApplicationFactory;
use Dot\Cli\Factory\FileLockerFactory;
-/**
- * Class ConfigProvider
- * @package Dot\Cli
- */
class ConfigProvider
{
- /**
- * @return array
- */
public function __invoke(): array
{
return [
@@ -23,19 +16,16 @@ public function __invoke(): array
];
}
- /**
- * @return string[][]
- */
public function getDependencyConfig(): array
{
return [
- 'aliases' => [
- FileLockerInterface::class => FileLocker::class
+ 'aliases' => [
+ FileLockerInterface::class => FileLocker::class,
],
'factories' => [
Application::class => ApplicationFactory::class,
- FileLocker::class => FileLockerFactory::class
- ]
+ FileLocker::class => FileLockerFactory::class,
+ ],
];
}
}
diff --git a/src/Factory/ApplicationFactory.php b/src/Factory/ApplicationFactory.php
index 99c4717..f7cc7f7 100644
--- a/src/Factory/ApplicationFactory.php
+++ b/src/Factory/ApplicationFactory.php
@@ -5,51 +5,69 @@
namespace Dot\Cli\Factory;
use Dot\Cli\FileLockerInterface;
+use Exception;
use Laminas\Cli\ContainerCommandLoader;
use Laminas\Cli\Listener\TerminateListener;
-use Composer\InstalledVersions;
+use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
+use Psr\Container\NotFoundExceptionInterface;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\ConsoleEvents;
use Symfony\Component\EventDispatcher\EventDispatcher;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
-use Webmozart\Assert\Assert;
-/**
- * Class ApplicationFactory
- */
+use function array_key_exists;
+use function is_array;
+
class ApplicationFactory
{
+ public const MESSAGE_MISSING_CONFIG = 'Unable to find config.';
+ public const MESSAGE_MISSING_PACKAGE_CONFIG = 'Unable to find dot-cli config.';
+ public const MESSAGE_MISSING_CONFIG_COMMANDS = 'Unable to find dot-cli config: commands.';
+
/**
- * @param ContainerInterface $container
- * @return Application
+ * @throws ContainerExceptionInterface
+ * @throws NotFoundExceptionInterface
+ * @throws Exception
*/
public function __invoke(ContainerInterface $container): Application
{
- $config = $container->get('config')['dot_cli'] ?? [];
- Assert::isMap($config);
+ if (! $container->has('config')) {
+ throw new Exception(self::MESSAGE_MISSING_CONFIG);
+ }
+ $config = $container->get('config');
- $version = InstalledVersions::getPrettyVersion('laminas/laminas-cli');
- Assert::string($version);
+ if (
+ ! array_key_exists('dot_cli', $config)
+ || ! is_array($config['dot_cli'])
+ || empty($config['dot_cli'])
+ ) {
+ throw new Exception(self::MESSAGE_MISSING_PACKAGE_CONFIG);
+ }
+ $config = $config['dot_cli'];
- $commands = $config['commands'] ?? [];
- Assert::isMap($commands);
- Assert::allString($commands);
+ if (
+ ! array_key_exists('commands', $config)
+ || ! is_array($config['commands'])
+ || empty($config['commands'])
+ ) {
+ throw new Exception(self::MESSAGE_MISSING_CONFIG_COMMANDS);
+ }
$eventDispatcherServiceName = __NAMESPACE__ . '\SymfonyEventDispatcher';
$dispatcher = $container->has($eventDispatcherServiceName)
? $container->get($eventDispatcherServiceName)
: new EventDispatcher();
- Assert::isInstanceOf($dispatcher, EventDispatcherInterface::class);
-
$dispatcher->addListener(ConsoleEvents::TERMINATE, new TerminateListener($config));
- $fileLocker = $container->get(FileLockerInterface::class);
- Assert::isInstanceOf($fileLocker, FileLockerInterface::class);
+ if (! $container->has(FileLockerInterface::class)) {
+ throw new Exception(FileLockerFactory::MESSAGE_MISSING_FILE_LOCKER);
+ }
- $application = new \Dot\Cli\Application($fileLocker, $config);
- // phpcs:ignore WebimpressCodingStandard.PHP.CorrectClassNameCase
- $application->setCommandLoader(new ContainerCommandLoader($container, $commands));
+ $application = new \Dot\Cli\Application(
+ $container->get(FileLockerInterface::class),
+ $config
+ );
+ $application->setCommandLoader(new ContainerCommandLoader($container, $config['commands']));
$application->setDispatcher($dispatcher);
$application->setAutoExit(false);
diff --git a/src/Factory/FileLockerFactory.php b/src/Factory/FileLockerFactory.php
index 7970e1e..34d1649 100644
--- a/src/Factory/FileLockerFactory.php
+++ b/src/Factory/FileLockerFactory.php
@@ -6,30 +6,61 @@
use Dot\Cli\FileLocker;
use Dot\Cli\FileLockerInterface;
+use Exception;
+use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
-use Webmozart\Assert\Assert;
+use Psr\Container\NotFoundExceptionInterface;
+
+use function array_key_exists;
+use function is_array;
+use function is_bool;
+use function is_string;
-/**
- * Class FileLockerFactory
- * @package Dot\Cli\Factory
- */
class FileLockerFactory
{
+ public const MESSAGE_MISSING_FILE_LOCKER = 'Unable to find FileLocker service.';
+ public const MESSAGE_MISSING_FILE_LOCKER_CONFIG = 'Missing/invalid dot-cli FileLocker config.';
+ public const MESSAGE_MISSING_FILE_LOCKER_CONFIG_ENABLED = 'Missing/invalid dot-cli FileLocker config: enabled';
+ public const MESSAGE_MISSING_FILE_LOCKER_CONFIG_DIR_PATH = 'Missing/invalid dot-cli FileLocker config: dirPath';
+
/**
- * @param ContainerInterface $container
- * @return FileLockerInterface
+ * @throws ContainerExceptionInterface
+ * @throws NotFoundExceptionInterface
+ * @throws Exception
*/
public function __invoke(ContainerInterface $container): FileLockerInterface
{
- $config = $container->get('config')[FileLockerInterface::class] ?? [];
- Assert::isMap($config);
+ if (! $container->has('config')) {
+ throw new Exception(ApplicationFactory::MESSAGE_MISSING_CONFIG);
+ }
+ $config = $container->get('config');
+
+ if (
+ ! array_key_exists(FileLockerInterface::class, $config)
+ || ! is_array($config[FileLockerInterface::class])
+ || empty($config[FileLockerInterface::class])
+ ) {
+ throw new Exception(self::MESSAGE_MISSING_FILE_LOCKER_CONFIG);
+ }
+ $config = $config[FileLockerInterface::class];
- $dirPath = $config['dirPath'];
- Assert::stringNotEmpty($dirPath);
+ if (
+ ! array_key_exists('enabled', $config)
+ || ! is_bool($config['enabled'])
+ ) {
+ throw new Exception(self::MESSAGE_MISSING_FILE_LOCKER_CONFIG_ENABLED);
+ }
- $fileLocker = new FileLocker();
- $fileLocker->setEnabled($config['enabled'] ?? false)->setDirPath($dirPath);
+ if (
+ ! array_key_exists('dirPath', $config)
+ || ! is_string($config['dirPath'])
+ || empty($config['dirPath'])
+ ) {
+ throw new Exception(self::MESSAGE_MISSING_FILE_LOCKER_CONFIG_DIR_PATH);
+ }
- return $fileLocker;
+ return (new FileLocker())
+ ->setEnabled($config['enabled'])
+ ->setDirPath($config['dirPath']);
}
}
diff --git a/src/FileLocker.php b/src/FileLocker.php
index bf232e2..b1337c5 100644
--- a/src/FileLocker.php
+++ b/src/FileLocker.php
@@ -7,48 +7,61 @@
use Exception;
use function fclose;
-use function file_exists;
use function flock;
use function fopen;
-use function is_dir;
-use function mkdir;
use function rtrim;
use function sprintf;
-use function unlink;
+use function str_replace;
+
+use const LOCK_EX;
+use const LOCK_NB;
+use const LOCK_UN;
-/**
- * Class FileLocker
- * @package Dot\Cli
- */
class FileLocker implements FileLockerInterface
{
private bool $enabled;
private ?string $dirPath;
private ?string $commandName;
- private $lockFile;
-
- /**
- * @return $this
- */
+ private mixed $lockFile;
+
+ public function __construct(
+ bool $enabled = false,
+ ?string $dirPath = null,
+ ?string $commandName = null,
+ mixed $lockFile = null,
+ ) {
+ $this->enabled = $enabled;
+ $this->dirPath = $dirPath;
+ $this->commandName = $commandName;
+ $this->lockFile = $lockFile;
+ }
+
public function initLockFile(): self
{
$this->lockFile = fopen($this->getLockFilePath(), 'w+');
return $this;
}
-
- /**
- * @return bool
- */
+
+ public function enable(): self
+ {
+ $this->enabled = true;
+
+ return $this;
+ }
+
+ public function disable(): self
+ {
+ $this->enabled = false;
+
+ return $this;
+ }
+
public function isEnabled(): bool
{
return $this->enabled;
}
- /**
- * @param bool $enabled
- * @return $this
- */
public function setEnabled(bool $enabled): self
{
$this->enabled = $enabled;
@@ -56,39 +69,25 @@ public function setEnabled(bool $enabled): self
return $this;
}
- /**
- * @return string|null
- */
public function getDirPath(): ?string
{
return $this->dirPath;
}
- /**
- * @param string|null $dirPath
- * @return $this
- */
public function setDirPath(?string $dirPath): self
{
- $dirPath = rtrim($dirPath, '/');
- $dirPath = rtrim($dirPath, '\\');
+ $dirPath = rtrim($dirPath, '/');
+ $dirPath = rtrim($dirPath, '\\');
$this->dirPath = $dirPath;
return $this;
}
- /**
- * @return string|null
- */
public function getCommandName(): ?string
{
return $this->commandName;
}
- /**
- * @param string|null $commandName
- * @return $this
- */
public function setCommandName(?string $commandName): self
{
$this->commandName = $commandName;
@@ -96,41 +95,33 @@ public function setCommandName(?string $commandName): self
return $this;
}
- /**
- * @return false|resource
- */
- public function getLockFile(): bool
+ public function getLockFile(): mixed
{
return $this->lockFile;
}
- /**
- * @param bool $lockFile
- * @return $this
- */
- public function setLockFile(bool $lockFile): self
+ public function setLockFile(mixed $lockFile): self
{
$this->lockFile = $lockFile;
-
+
return $this;
}
- /**
- * @return string
- */
public function getLockFilePath(): string
{
- $commandName = str_replace(':', '-', $this->commandName);
- return sprintf('%s/command-%s.lock', $this->dirPath, $commandName);
+ return sprintf(
+ '%s/command-%s.lock',
+ $this->dirPath,
+ str_replace(':', '-', $this->commandName)
+ );
}
/**
- * @throws Exception
- * @return void
+ * @inheritDoc
*/
public function lock(): void
{
- if (!$this->enabled) {
+ if (! $this->enabled) {
return;
}
@@ -140,19 +131,16 @@ public function lock(): void
$this->initLockFile();
- if (!flock($this->lockFile, LOCK_EX|LOCK_NB, $wouldBlock)) {
+ if (! flock($this->lockFile, LOCK_EX | LOCK_NB, $wouldBlock)) {
if ($wouldBlock) {
- throw new \Exception('Another process holds the lock!');
+ throw new Exception('The file lock is being held by a different process');
}
}
}
- /**
- * @return void
- */
public function unlock(): void
{
- if (!$this->enabled) {
+ if (! $this->enabled) {
return;
}
diff --git a/src/FileLockerInterface.php b/src/FileLockerInterface.php
index 58bada8..f8b47e1 100644
--- a/src/FileLockerInterface.php
+++ b/src/FileLockerInterface.php
@@ -4,57 +4,38 @@
namespace Dot\Cli;
-/**
- * Interface FileLocker
- * @package Dot\Cli
- */
+use Exception;
+
interface FileLockerInterface
{
- /**
- * @return bool
- */
+ public function initLockFile(): self;
+
+ public function enable(): self;
+
+ public function disable(): self;
+
public function isEnabled(): bool;
- /**
- * @param bool $enabled
- * @return self
- */
public function setEnabled(bool $enabled): self;
- /**
- * @return string|null
- */
public function getDirPath(): ?string;
- /**
- * @param string|null $dirPath
- * @return $this
- */
public function setDirPath(?string $dirPath): self;
- /**
- * @return string|null
- */
public function getCommandName(): ?string;
- /**
- * @param string|null $commandName
- * @return $this
- */
public function setCommandName(?string $commandName): self;
- /**
- * @return string
- */
+ public function getLockFile(): mixed;
+
+ public function setLockFile(mixed $lockFile): self;
+
public function getLockFilePath(): string;
/**
- * @return void
+ * @throws Exception
*/
public function lock(): void;
- /**
- * @return void
- */
public function unlock(): void;
}
diff --git a/test/ApplicationTest.php b/test/ApplicationTest.php
new file mode 100644
index 0000000..316ae82
--- /dev/null
+++ b/test/ApplicationTest.php
@@ -0,0 +1,38 @@
+getConfig();
+
+ $fileLocker = new FileLocker(
+ $config[FileLockerInterface::class]['enabled'],
+ $config[FileLockerInterface::class]['dirPath']
+ );
+ $input = new StringInput('list');
+ $output = new BufferedOutput();
+
+ $application = new Application($fileLocker, $this->getConfig()['dot_cli']);
+ $result = $application->doRun($input, $output);
+ $this->assertSame($result, Command::SUCCESS);
+ }
+}
diff --git a/test/Command/DemoCommandTest.php b/test/Command/DemoCommandTest.php
new file mode 100644
index 0000000..1987287
--- /dev/null
+++ b/test/Command/DemoCommandTest.php
@@ -0,0 +1,41 @@
+assertInstanceOf(DemoCommand::class, $command);
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ public function testCommandWillExecute(): void
+ {
+ $command = new DemoCommand();
+ $reflection = new ReflectionMethod(DemoCommand::class, 'execute');
+
+ $result = $reflection->invoke(
+ $command,
+ new ArgvInput(),
+ new BufferedOutput()
+ );
+ $this->assertSame($result, Command::SUCCESS);
+ }
+}
diff --git a/test/CommonTrait.php b/test/CommonTrait.php
new file mode 100644
index 0000000..fb1589e
--- /dev/null
+++ b/test/CommonTrait.php
@@ -0,0 +1,51 @@
+fileSystem = vfsStream::setup('root', 0644, [
+ 'data' => [
+ 'lock' => [],
+ ],
+ ]);
+ $this->config = $this->generateConfig(
+ sprintf('%s/data/lock', $this->fileSystem->url())
+ );
+ }
+
+ protected function getConfig(): array
+ {
+ return $this->config;
+ }
+
+ protected function generateConfig(string $dirPath): array
+ {
+ return [
+ 'dot_cli' => [
+ 'version' => '1.0.0',
+ 'name' => 'DotKernel CLI',
+ 'commands' => [
+ 'test' => 'test',
+ ],
+ ],
+ FileLockerInterface::class => [
+ 'enabled' => true,
+ 'dirPath' => $dirPath,
+ ],
+ ];
+ }
+}
diff --git a/test/ConfigProviderTest.php b/test/ConfigProviderTest.php
new file mode 100644
index 0000000..07d36b1
--- /dev/null
+++ b/test/ConfigProviderTest.php
@@ -0,0 +1,39 @@
+config = (new ConfigProvider())();
+ }
+
+ public function testHasDependencies(): void
+ {
+ $this->assertArrayHasKey('dependencies', $this->config);
+ }
+
+ public function testDependenciesHasFactories(): void
+ {
+ $this->assertArrayHasKey('factories', $this->config['dependencies']);
+ $this->assertArrayHasKey(Application::class, $this->config['dependencies']['factories']);
+ $this->assertArrayHasKey(FileLocker::class, $this->config['dependencies']['factories']);
+ }
+
+ public function testDependenciesHasAliases(): void
+ {
+ $this->assertArrayHasKey('aliases', $this->config['dependencies']);
+ $this->assertArrayHasKey(FileLockerInterface::class, $this->config['dependencies']['aliases']);
+ }
+}
diff --git a/test/Factory/ApplicationFactoryTest.php b/test/Factory/ApplicationFactoryTest.php
new file mode 100644
index 0000000..04cb48b
--- /dev/null
+++ b/test/Factory/ApplicationFactoryTest.php
@@ -0,0 +1,144 @@
+createMock(ContainerInterface::class);
+
+ $container->expects($this->once())
+ ->method('has')
+ ->with('config')
+ ->willReturn(false);
+
+ $this->expectException(\Exception::class);
+ $this->expectExceptionMessage(ApplicationFactory::MESSAGE_MISSING_CONFIG);
+ (new ApplicationFactory())($container);
+ }
+
+ /**
+ * @throws Exception
+ * @throws ContainerExceptionInterface
+ * @throws NotFoundExceptionInterface
+ */
+ public function testWillNotCreateApplicationWithoutPackageConfig(): void
+ {
+ $container = $this->createMock(ContainerInterface::class);
+
+ $container->expects($this->once())
+ ->method('has')
+ ->with('config')
+ ->willReturn(true);
+
+ $container->expects($this->once())
+ ->method('get')
+ ->with('config')
+ ->willReturn([
+ 'test',
+ ]);
+
+ $this->expectException(\Exception::class);
+ $this->expectExceptionMessage(ApplicationFactory::MESSAGE_MISSING_PACKAGE_CONFIG);
+ (new ApplicationFactory())($container);
+ }
+
+ /**
+ * @throws Exception
+ * @throws ContainerExceptionInterface
+ * @throws NotFoundExceptionInterface
+ */
+ public function testWillNotCreateApplicationWithoutConfigCommands(): void
+ {
+ $container = $this->createMock(ContainerInterface::class);
+
+ $container->expects($this->once())
+ ->method('has')
+ ->with('config')
+ ->willReturn(true);
+
+ $container->expects($this->once())
+ ->method('get')
+ ->with('config')
+ ->willReturn([
+ 'dot_cli' => [
+ 'test',
+ ],
+ ]);
+
+ $this->expectException(\Exception::class);
+ $this->expectExceptionMessage(ApplicationFactory::MESSAGE_MISSING_CONFIG_COMMANDS);
+ (new ApplicationFactory())($container);
+ }
+
+ /**
+ * @throws Exception
+ * @throws ContainerExceptionInterface
+ * @throws NotFoundExceptionInterface
+ */
+ public function testWillNotCreateApplicationWithoutFileLockerService(): void
+ {
+ $container = $this->createMock(ContainerInterface::class);
+
+ $container->method('has')->willReturnMap([
+ ['config', true],
+ ['Dot\Cli\Factory\SymfonyEventDispatcher', false],
+ [FileLockerInterface::class, false],
+ ]);
+
+ $container->expects($this->once())
+ ->method('get')
+ ->with('config')
+ ->willReturn($this->getConfig());
+
+ $this->expectException(\Exception::class);
+ $this->expectExceptionMessage(FileLockerFactory::MESSAGE_MISSING_FILE_LOCKER);
+ (new ApplicationFactory())($container);
+ }
+
+ /**
+ * @throws ContainerExceptionInterface
+ * @throws NotFoundExceptionInterface
+ * @throws Exception
+ */
+ public function testWillCreateApplication(): void
+ {
+ $container = $this->createMock(ContainerInterface::class);
+ $fileLocker = $this->createMock(FileLockerInterface::class);
+
+ $container->method('has')->willReturnMap([
+ ['config', true],
+ ['Dot\Cli\Factory\SymfonyEventDispatcher', false],
+ [FileLockerInterface::class, true],
+ ]);
+
+ $container->method('get')->willReturnMap([
+ ['config', $this->getConfig()],
+ [FileLockerInterface::class, $fileLocker],
+ ]);
+
+ $application = (new ApplicationFactory())($container);
+ $this->assertInstanceOf(Application::class, $application);
+ }
+}
diff --git a/test/Factory/FileLockerFactoryTest.php b/test/Factory/FileLockerFactoryTest.php
new file mode 100644
index 0000000..0e2442f
--- /dev/null
+++ b/test/Factory/FileLockerFactoryTest.php
@@ -0,0 +1,145 @@
+createMock(ContainerInterface::class);
+
+ $container->expects($this->once())
+ ->method('has')
+ ->with('config')
+ ->willReturn(false);
+
+ $this->expectException(\Exception::class);
+ $this->expectExceptionMessage(ApplicationFactory::MESSAGE_MISSING_CONFIG);
+ (new FileLockerFactory())($container);
+ }
+
+ /**
+ * @throws Exception
+ * @throws ContainerExceptionInterface
+ * @throws NotFoundExceptionInterface
+ */
+ public function testWillNotCreateServiceWithoutFileLockerConfig(): void
+ {
+ $container = $this->createMock(ContainerInterface::class);
+
+ $container->expects($this->once())
+ ->method('has')
+ ->with('config')
+ ->willReturn(true);
+
+ $container->expects($this->once())
+ ->method('get')
+ ->with('config')
+ ->willReturn([
+ 'test',
+ ]);
+
+ $this->expectException(\Exception::class);
+ $this->expectExceptionMessage(FileLockerFactory::MESSAGE_MISSING_FILE_LOCKER_CONFIG);
+ (new FileLockerFactory())($container);
+ }
+
+ /**
+ * @throws Exception
+ * @throws ContainerExceptionInterface
+ * @throws NotFoundExceptionInterface
+ */
+ public function testWillNotCreateServiceWithoutFileLockerConfigEnabled(): void
+ {
+ $container = $this->createMock(ContainerInterface::class);
+
+ $container->expects($this->once())
+ ->method('has')
+ ->with('config')
+ ->willReturn(true);
+
+ $container->expects($this->once())
+ ->method('get')
+ ->with('config')
+ ->willReturn([
+ FileLockerInterface::class => [
+ 'dirPath' => 'test',
+ ],
+ ]);
+
+ $this->expectException(\Exception::class);
+ $this->expectExceptionMessage(FileLockerFactory::MESSAGE_MISSING_FILE_LOCKER_CONFIG_ENABLED);
+ (new FileLockerFactory())($container);
+ }
+
+ /**
+ * @throws Exception
+ * @throws ContainerExceptionInterface
+ * @throws NotFoundExceptionInterface
+ */
+ public function testWillNotCreateServiceWithoutFileLockerConfigDirPath(): void
+ {
+ $container = $this->createMock(ContainerInterface::class);
+
+ $container->expects($this->once())
+ ->method('has')
+ ->with('config')
+ ->willReturn(true);
+
+ $container->expects($this->once())
+ ->method('get')
+ ->with('config')
+ ->willReturn([
+ FileLockerInterface::class => [
+ 'enabled' => false,
+ ],
+ ]);
+
+ $this->expectException(\Exception::class);
+ $this->expectExceptionMessage(FileLockerFactory::MESSAGE_MISSING_FILE_LOCKER_CONFIG_DIR_PATH);
+ (new FileLockerFactory())($container);
+ }
+
+ /**
+ * @throws Exception
+ * @throws ContainerExceptionInterface
+ * @throws NotFoundExceptionInterface
+ */
+ public function testCanCreateService(): void
+ {
+ $container = $this->createMock(ContainerInterface::class);
+
+ $container->expects($this->once())
+ ->method('has')
+ ->with('config')
+ ->willReturn(true);
+
+ $container->expects($this->once())
+ ->method('get')
+ ->with('config')
+ ->willReturn($this->getConfig());
+
+ $service = (new FileLockerFactory())($container);
+ $this->assertInstanceOf(FileLocker::class, $service);
+ }
+}
diff --git a/test/FileLockerTest.php b/test/FileLockerTest.php
new file mode 100644
index 0000000..dde3fe0
--- /dev/null
+++ b/test/FileLockerTest.php
@@ -0,0 +1,162 @@
+fileSystem = vfsStream::setup('root', 0644, [
+ 'data' => [
+ 'lock' => [],
+ ],
+ ]);
+ $this->config = $this->generateConfig(
+ sprintf('%s/data/lock', $this->fileSystem->url())
+ );
+ }
+
+ public function testAccessors(): void
+ {
+ $fileLocker = new FileLocker();
+ $this->assertFalse($fileLocker->isEnabled());
+ $fileLocker->enable();
+ $this->assertInstanceOf(FileLocker::class, $fileLocker);
+ $this->assertTrue($fileLocker->isEnabled());
+ $fileLocker->disable();
+ $this->assertInstanceOf(FileLocker::class, $fileLocker);
+ $this->assertFalse($fileLocker->isEnabled());
+ $fileLocker->setEnabled(true);
+ $this->assertInstanceOf(FileLocker::class, $fileLocker);
+ $this->assertTrue($fileLocker->isEnabled());
+ $fileLocker->setEnabled(false);
+ $this->assertInstanceOf(FileLocker::class, $fileLocker);
+ $this->assertFalse($fileLocker->isEnabled());
+ $this->assertNull($fileLocker->getDirPath());
+ $fileLocker->setDirPath('test');
+ $this->assertInstanceOf(FileLocker::class, $fileLocker);
+ $this->assertSame('test', $fileLocker->getDirPath());
+ $this->assertNull($fileLocker->getCommandName());
+ $fileLocker->setCommandName('test');
+ $this->assertInstanceOf(FileLocker::class, $fileLocker);
+ $this->assertSame('test', $fileLocker->getCommandName());
+ $this->assertNull($fileLocker->getLockFile());
+ $fileLocker->setLockFile('test');
+ $this->assertInstanceOf(FileLocker::class, $fileLocker);
+ $this->assertSame('test', $fileLocker->getLockFile());
+ $this->assertSame('test/command-test.lock', $fileLocker->getLockFilePath());
+ }
+
+ public function testWillInitLockFile(): void
+ {
+ $config = $this->getConfig();
+
+ $fileLocker = new FileLocker(true, $config['dirPath'], 'test');
+ $this->assertNull($fileLocker->getLockFile());
+ $fileLocker->initLockFile();
+ $this->assertInstanceOf(FileLocker::class, $fileLocker);
+ $this->assertIsResource($fileLocker->getLockFile());
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function testWillNotLockWhenDisabled(): void
+ {
+ $config = $this->getConfig();
+
+ $fileLocker = new FileLocker(false, $config['dirPath'], 'test');
+ $this->assertNull($fileLocker->getLockFile());
+ $fileLocker->lock();
+ $this->assertInstanceOf(FileLocker::class, $fileLocker);
+ $this->assertNull($fileLocker->getLockFile());
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function testWillNotLockWithoutValidCommandName(): void
+ {
+ $config = $this->getConfig();
+
+ $fileLocker = new FileLocker(true, $config['dirPath']);
+ $this->assertNull($fileLocker->getLockFile());
+ $fileLocker->lock();
+ $this->assertInstanceOf(FileLocker::class, $fileLocker);
+ $this->assertNull($fileLocker->getLockFile());
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function testWillLockWhenLockedAndEnabledAndHasValidCommandName(): void
+ {
+ $config = $this->getConfig();
+
+ $fileLocker = new FileLocker(true, $config['dirPath'], 'test');
+ $this->assertNull($fileLocker->getLockFile());
+ $fileLocker->lock();
+ $this->assertInstanceOf(FileLocker::class, $fileLocker);
+ $this->assertIsResource($fileLocker->getLockFile());
+ $this->assertFileExists($fileLocker->getLockFilePath());
+ }
+
+ public function testWillNotUnlockWhenDisabled(): void
+ {
+ $config = $this->getConfig();
+
+ $fileLocker = new FileLocker(false, $config['dirPath'], 'test');
+ $this->assertNull($fileLocker->getLockFile());
+ $fileLocker->unlock();
+ $this->assertInstanceOf(FileLocker::class, $fileLocker);
+ $this->assertNull($fileLocker->getLockFile());
+ }
+
+ public function testWillNotUnlockWithoutValidCommandName(): void
+ {
+ $config = $this->getConfig();
+
+ $fileLocker = new FileLocker(false, $config['dirPath']);
+ $this->assertNull($fileLocker->getLockFile());
+ $fileLocker->unlock();
+ $this->assertInstanceOf(FileLocker::class, $fileLocker);
+ $this->assertNull($fileLocker->getLockFile());
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function testWillUnlockWhenLockedAndEnabledAndHasValidCommandName(): void
+ {
+ $config = $this->getConfig();
+
+ $fileLocker = new FileLocker(true, $config['dirPath'], 'test');
+ $fileLocker->lock();
+ $this->assertInstanceOf(FileLocker::class, $fileLocker);
+ $this->assertIsResource($fileLocker->getLockFile());
+ $this->assertFileExists($fileLocker->getLockFilePath());
+ $fileLocker->unlock();
+ $this->assertInstanceOf(FileLocker::class, $fileLocker);
+ $this->assertFileIsReadable($fileLocker->getLockFilePath());
+ $this->assertFileIsWritable($fileLocker->getLockFilePath());
+ }
+
+ protected function generateConfig(string $dirPath): array
+ {
+ return [
+ 'enabled' => true,
+ 'dirPath' => $dirPath,
+ ];
+ }
+}