Skip to content

Commit

Permalink
Merge pull request #41 from novaway/feature-listing-command
Browse files Browse the repository at this point in the history
Add a command to list features state
  • Loading branch information
jdecool authored May 2, 2023
2 parents 32a1a37 + b85c636 commit b547991
Show file tree
Hide file tree
Showing 9 changed files with 310 additions and 2 deletions.
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
],
"require": {
"php": ">= 7.3",
"ext-json": "*",
"doctrine/annotations": "^1.12|^2.0",
"symfony/framework-bundle": "~4.4|~5.0|~6.0",
"symfony/yaml": "~4.4|~5.0|~6.0"
Expand Down
3 changes: 2 additions & 1 deletion phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ parameters:
level: max
paths:
- src
checkGenericClassInNonGenericObjectType: false
checkGenericClassInNonGenericObjectType: false
checkMissingIterableValueType: false
2 changes: 1 addition & 1 deletion phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@
<directory suffix=".php">src</directory>
</include>
</coverage>
</phpunit>
</phpunit>
126 changes: 126 additions & 0 deletions src/Command/ListFeatureCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<?php

declare(strict_types=1);

/*
* This file is part of the NovawayFeatureFlagBundle package.
* (c) Novaway <https://github.com/novaway/NovawayFeatureFlagBundle>
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Novaway\Bundle\FeatureFlagBundle\Command;

use Novaway\Bundle\FeatureFlagBundle\Model\Feature;
use Novaway\Bundle\FeatureFlagBundle\Storage\StorageInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

final class ListFeatureCommand extends Command
{
private const FORMAT_CSV = 'csv';
private const FORMAT_JSON = 'json';
private const FORMAT_TABLE = 'table';

/** @var StorageInterface */
private $storage;

public function __construct(StorageInterface $storage)
{
parent::__construct('novaway:feature-flag:list');

$this->storage = $storage;
}

protected function configure(): void
{
$this->setDescription('List all features with their state');
$this->addOption('format', 'f', InputOption::VALUE_OPTIONAL, 'Output format', self::FORMAT_TABLE);
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$features = $this->storage->all();

switch ($input->getOption('format')) {
case self::FORMAT_CSV:
$this->renderCsv($output, $features);
break;

case self::FORMAT_JSON:
$this->renderJson($output, $features);
break;

case self::FORMAT_TABLE:
$this->renderTable($output, $features);
break;

default:
/* @phpstan-ignore-next-line */
$output->writeln("<error>Invalid format: {$input->getOption('format')}</error>");

return 1;
}

return 0;
}

private function renderTable(OutputInterface $output, array $features): void
{
$table = new Table($output);
$table->setHeaders(['Name', 'Enabled', 'Description']);
foreach ($features as $feature) {
$table->addRow([
$feature->getKey(),
$feature->isEnabled() ? 'Yes' : 'No',
$feature->getDescription(),
]);
}

$table->render();
}

private function renderJson(OutputInterface $output, array $features): void
{
$json = json_encode(
array_map(static function (Feature $feature): array {
return $feature->toArray();
}, $features),
JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR
);

$output->writeln($json);
}

private function renderCsv(OutputInterface $output, array $features): void
{
$output->writeln($this->getCsvLine(['Name', 'Enabled', 'Description']));

foreach ($features as $feature) {
$output->writeln($this->getCsvLine($feature->toArray()));
}
}

private function getCsvLine(array $columns): string
{
$fp = fopen('php://temp', 'w+');
if (false === $fp) {
throw new \RuntimeException('Unable to open temporary file');
}

fputcsv($fp, $columns);

rewind($fp);
$data = fread($fp, 1048576); // 1MB
if (false === $data) {
throw new \RuntimeException('Unable to read temporary file');
}

fclose($fp);

return rtrim($data, PHP_EOL);
}
}
9 changes: 9 additions & 0 deletions src/Model/Feature.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,13 @@ public function isEnabled(): bool
{
return $this->enabled;
}

public function toArray(): array
{
return [
'key' => $this->key,
'enabled' => $this->enabled,
'description' => $this->description,
];
}
}
4 changes: 4 additions & 0 deletions src/Resources/config/services.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
services:
Novaway\Bundle\FeatureFlagBundle\Command\ListFeatureCommand:
arguments: ['@novaway_feature_flag.storage']
tags: ['console.command']

Novaway\Bundle\FeatureFlagBundle\Manager\DefaultFeatureManager:
arguments: ['@Novaway\Bundle\FeatureFlagBundle\Storage\ArrayStorage']
Novaway\Bundle\FeatureFlagBundle\Manager\LegacyFeatureManager:
Expand Down
2 changes: 2 additions & 0 deletions src/Storage/ArrayStorage.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ class ArrayStorage extends AbstractStorage
*/
public function __construct(array $features = [])
{
ksort($features);

$this->features = [];
foreach ($features as $key => $feature) {
$this->features[$key] = new Feature($key, $feature['enabled'], $feature['description'] ?? '');
Expand Down
27 changes: 27 additions & 0 deletions tests/Functional/Command/ListFeatureCommandTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

/*
* This file is part of the NovawayFeatureFlagBundle package.
* (c) Novaway <https://github.com/novaway/NovawayFeatureFlagBundle>
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Novaway\Bundle\FeatureFlagBundle\Tests\Functional\Command;

use Novaway\Bundle\FeatureFlagBundle\Tests\Functional\WebTestCase;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Console\Command\Command;

final class ListFeatureCommandTest extends WebTestCase
{
public function testCommandIsAvailable(): void
{
$application = new Application(static::bootKernel());
$command = $application->find('novaway:feature-flag:list');

static::assertInstanceOf(Command::class, $command);
}
}
138 changes: 138 additions & 0 deletions tests/Unit/Command/ListFeatureCommandTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
<?php

declare(strict_types=1);

/*
* This file is part of the NovawayFeatureFlagBundle package.
* (c) Novaway <https://github.com/novaway/NovawayFeatureFlagBundle>
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Novaway\Bundle\FeatureFlagBundle\Tests\Unit\Command;

use Novaway\Bundle\FeatureFlagBundle\Command\ListFeatureCommand;
use Novaway\Bundle\FeatureFlagBundle\Storage\ArrayStorage;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Tester\CommandTester;

final class ListFeatureCommandTest extends TestCase
{
private const TEST_DATA = [
'empty-features' => [
'features' => [],
'output' => [
'table' => <<<OUTPUT
+------+---------+-------------+
| Name | Enabled | Description |
+------+---------+-------------+
OUTPUT,
'json' => <<<JSON
[]
JSON,
'csv' => <<<CSV
Name,Enabled,Description
CSV,
],
],
'with-features' => [
'features' => [
'feature1' => [
'enabled' => true,
'description' => 'Feature 1 description',
],
'feature2' => [
'enabled' => false,
'description' => 'Feature 2 description',
],
'feature3' => [
'enabled' => true,
'description' => 'Feature 3 description',
],
],
'output' => [
'table' => <<<OUTPUT
+----------+---------+-----------------------+
| Name | Enabled | Description |
+----------+---------+-----------------------+
| feature1 | Yes | Feature 1 description |
| feature2 | No | Feature 2 description |
| feature3 | Yes | Feature 3 description |
+----------+---------+-----------------------+
OUTPUT,
'json' => <<<JSON
{
"feature1": {
"key": "feature1",
"enabled": true,
"description": "Feature 1 description"
},
"feature2": {
"key": "feature2",
"enabled": false,
"description": "Feature 2 description"
},
"feature3": {
"key": "feature3",
"enabled": true,
"description": "Feature 3 description"
}
}
JSON,
'csv' => <<<CSV
Name,Enabled,Description
feature1,1,"Feature 1 description"
feature2,,"Feature 2 description"
feature3,1,"Feature 3 description"
CSV,
],
],
];

public function testAnErrorOccuredIfInvalidFormatIsProvided(): void
{
$commandTester = $this->createCommandTester();
$commandTester->execute(['--format' => 'invalid']);

static::assertNotSame(0, $commandTester->getStatusCode());
static::assertSame(<<<OUTPUT
Invalid format: invalid
OUTPUT, $commandTester->getDisplay());
}

/**
* @dataProvider featuresProvider
*/
public function testConfiguredFeaturesAreDisplayedInAskedFormat(array $features, string $outputFormat, string $expectedOutput): void
{
$commandTester = $this->createCommandTester($features);
$commandTester->execute(['--format' => $outputFormat]);

static::assertSame(0, $commandTester->getStatusCode());
static::assertSame($expectedOutput, $commandTester->getDisplay());
}

public function featuresProvider(): iterable
{
foreach (self::TEST_DATA as $caseDescription => $testData) {
foreach ($testData['output'] as $format => $expectedOutput) {
yield "$caseDescription in $format format" => [$testData['features'], $format, $expectedOutput];
}
}
}

private function createCommandTester(array $features = []): CommandTester
{
$storage = new ArrayStorage($features);
$command = new ListFeatureCommand($storage);

return new CommandTester($command);
}
}

0 comments on commit b547991

Please sign in to comment.