Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
74 changes: 74 additions & 0 deletions src/Controller/FeatureApiController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?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\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<string, array<string, mixed>> $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<string, mixed>
*/
private function featureAsArray(FeatureInterface $feature): array
{
if (method_exists($feature, 'toArray')) {
return $feature->toArray();
}

return [
'key' => $feature->getKey(),
'description' => $feature->getDescription(),
'enabled' => $feature->isEnabled(),
];
}
}
13 changes: 13 additions & 0 deletions src/Resources/config/routing.yml
Original file line number Diff line number Diff line change
@@ -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]
4 changes: 4 additions & 0 deletions src/Resources/config/services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 4 additions & 0 deletions tests/Fixtures/App/config/routing.yml
Original file line number Diff line number Diff line change
@@ -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' }
Expand Down
85 changes: 85 additions & 0 deletions tests/Functional/Controller/FeatureApiControllerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?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\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(<<<JSON
{
"bar": {
"description": "Bar feature description",
"enabled": false,
"key": "bar",
"options": {
"foo": "bar",
"tableau": {
"test": "zaza"
}
}
},
"env_var": {
"description": "",
"enabled": false,
"key": "env_var",
"options": []
},
"foo": {
"description": "",
"enabled": true,
"key": "foo",
"options": []
},
"override": {
"description": "",
"enabled": false,
"key": "override",
"options": []
}
}
JSON, static::$client->getResponse()->getContent());
}

public function testApiGetOneFeature(): void
{
self::$client->request('GET', '/api/features/foo');

static::assertTrue(static::$client->getResponse()->isSuccessful());
static::assertJsonStringEqualsJsonString(<<<JSON
{
"description": "",
"enabled": true,
"key": "foo",
"options": []
}
JSON, static::$client->getResponse()->getContent());
}

public function testApiGetUnknownFeatureReturnAnError(): void
{
self::$client->request('GET', '/api/features/unknow-feature');

static::assertSame(404, static::$client->getResponse()->getStatusCode());
static::assertJsonStringEqualsJsonString(<<<JSON
{
"type": "undefined-feature",
"title": "Feature 'unknow-feature' not exists."
}
JSON, static::$client->getResponse()->getContent());
}
}
Loading