diff --git a/README.md b/README.md index f5306e7..f67bcab 100644 --- a/README.md +++ b/README.md @@ -186,6 +186,40 @@ class MyController extends Controller } ``` +### API Controller + +The bundle provides an API controller that exposes feature flags via REST endpoints: + +- `GET /features` - Get all feature flags +- `GET /features/{key}` - Get a specific feature flag by key + +To enable the API controller, you need to register its routes in your routing configuration: + +```yaml +# config/routes.yaml (or app/config/routing.yml for older Symfony versions) +feature_api: + resource: '@NovawayFeatureFlagBundle/Resources/config/routing.yml' +``` + +You can also register endpoints manually: + +```yaml +features_all: + path: /features + defaults: + _controller: Novaway\Bundle\FeatureFlagBundle\Controller\FeatureApiController::all + methods: [GET] + +feature_get: + path: /features/{key} + defaults: + _controller: Novaway\Bundle\FeatureFlagBundle\Controller\FeatureApiController::get + requirements: + key: '[a-zA-Z0-9_\-]+' + methods: [GET] +``` +The API returns JSON responses with feature flag data including key, description, and enabled status. + ### Implement your own storage provider 1. First your need to create your storage provider class which implement the `Novaway\Bundle\FeatureFlagBundle\Storage\StorageInterface` interface diff --git a/src/Controller/FeatureApiController.php b/src/Controller/FeatureApiController.php new file mode 100644 index 0000000..cfb6afc --- /dev/null +++ b/src/Controller/FeatureApiController.php @@ -0,0 +1,74 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Novaway\Bundle\FeatureFlagBundle\Controller; + +use Novaway\Bundle\FeatureFlagBundle\Model\FeatureInterface; +use Novaway\Bundle\FeatureFlagBundle\Storage\FeatureUndefinedException; +use Novaway\Bundle\FeatureFlagBundle\Storage\StorageInterface; +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\Response; + +class FeatureApiController +{ + /** @var StorageInterface */ + private $storage; + + public function __construct(StorageInterface $storage) + { + $this->storage = $storage; + } + + public function all(): Response + { + /** @var array> $features */ + $features = []; + + foreach ($this->storage->all() as $feature) { + $features[$feature->getKey()] = $this->featureAsArray($feature); + } + + return new JsonResponse($features); + } + + public function get(string $key): Response + { + try { + $feature = $this->storage->get($key); + } catch (FeatureUndefinedException $e) { + return new JsonResponse( + [ + 'type' => 'undefined-feature', + 'title' => $e->getMessage(), + ], + Response::HTTP_NOT_FOUND + ); + } + + return new JsonResponse($this->featureAsArray($feature)); + } + + /** + * @return array + */ + private function featureAsArray(FeatureInterface $feature): array + { + if (method_exists($feature, 'toArray')) { + return $feature->toArray(); + } + + return [ + 'key' => $feature->getKey(), + 'description' => $feature->getDescription(), + 'enabled' => $feature->isEnabled(), + ]; + } +} diff --git a/src/Resources/config/routing.yml b/src/Resources/config/routing.yml new file mode 100644 index 0000000..13fd0ba --- /dev/null +++ b/src/Resources/config/routing.yml @@ -0,0 +1,13 @@ +novaway_feature_flag_api_all: + path: /features + defaults: + _controller: Novaway\Bundle\FeatureFlagBundle\Controller\FeatureApiController::all + methods: [GET] + +novaway_feature_flag_api_get: + path: /features/{key} + defaults: + _controller: Novaway\Bundle\FeatureFlagBundle\Controller\FeatureApiController::get + requirements: + key: '[a-zA-Z0-9_\-]+' + methods: [GET] diff --git a/src/Resources/config/services.yml b/src/Resources/config/services.yml index 733ea6e..f56773d 100644 --- a/src/Resources/config/services.yml +++ b/src/Resources/config/services.yml @@ -3,6 +3,10 @@ services: arguments: ['@novaway_feature_flag.storage'] tags: ['console.command'] + Novaway\Bundle\FeatureFlagBundle\Controller\FeatureApiController: + arguments: [ '@novaway_feature_flag.storage' ] + tags: ['controller.service_arguments'] + Novaway\Bundle\FeatureFlagBundle\Manager\DefaultFeatureManager: arguments: ['@Novaway\Bundle\FeatureFlagBundle\Storage\ArrayStorage'] Novaway\Bundle\FeatureFlagBundle\Manager\LegacyFeatureManager: diff --git a/tests/Fixtures/App/config/routing.yml b/tests/Fixtures/App/config/routing.yml index 7e4c247..4d8d7a7 100644 --- a/tests/Fixtures/App/config/routing.yml +++ b/tests/Fixtures/App/config/routing.yml @@ -1,3 +1,7 @@ +feature_api: + resource: '@NovawayFeatureFlagBundle/Resources/config/routing.yml' + prefix: /api + features: path: /features defaults: { _controller: 'Novaway\Bundle\FeatureFlagBundle\Tests\Fixtures\App\TestBundle\Controller\DefaultController::features' } diff --git a/tests/Functional/Controller/FeatureApiControllerTest.php b/tests/Functional/Controller/FeatureApiControllerTest.php new file mode 100644 index 0000000..a33c9cf --- /dev/null +++ b/tests/Functional/Controller/FeatureApiControllerTest.php @@ -0,0 +1,85 @@ + + * 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\Controller; + +use Novaway\Bundle\FeatureFlagBundle\Tests\Functional\WebTestCase; + +final class FeatureApiControllerTest extends WebTestCase +{ + public function testApiGetAllFeatures(): void + { + self::$client->request('GET', '/api/features'); + + static::assertTrue(static::$client->getResponse()->isSuccessful()); + static::assertJsonStringEqualsJsonString(<<getResponse()->getContent()); + } + + public function testApiGetOneFeature(): void + { + self::$client->request('GET', '/api/features/foo'); + + static::assertTrue(static::$client->getResponse()->isSuccessful()); + static::assertJsonStringEqualsJsonString(<<getResponse()->getContent()); + } + + public function testApiGetUnknownFeatureReturnAnError(): void + { + self::$client->request('GET', '/api/features/unknow-feature'); + + static::assertSame(404, static::$client->getResponse()->getStatusCode()); + static::assertJsonStringEqualsJsonString(<<getResponse()->getContent()); + } +}