Skip to content

Commit 2c449d6

Browse files
committed
Introduce #[IsNotEnabled] attribute to complement #[IsEnabled]
Signed-off-by: Quentin Devos <4972091+Okhoshi@users.noreply.github.com>
1 parent 7034fc8 commit 2c449d6

File tree

5 files changed

+124
-14
lines changed

5 files changed

+124
-14
lines changed

README.md

+24-4
Original file line numberDiff line numberDiff line change
@@ -55,16 +55,15 @@ class MyService
5555

5656
## Controller attribute
5757

58-
You can also check for feature flag using an `#[IsEnabled]` attribute on a controller. You can use it on the whole
59-
controller class as well as on a concrete method.
58+
You can also check for feature flag using `#[IsEnabled]` and `#[IsNotEnabled]` attributes on a controller. You can use
59+
it on the whole controller class as well as on a concrete method.
6060

6161
```php
6262
<?php
6363
6464
use Unleash\Client\Bundle\Attribute\IsEnabled;
6565
use Symfony\Component\HttpFoundation\Response;
6666
use Symfony\Component\Routing\Annotation\Route;
67-
use Symfony\Component\HttpFoundation\Response;
6867
6968
#[IsEnabled('my_awesome_feature')]
7069
final class MyController
@@ -88,12 +87,33 @@ In the example above the user on `/my-route` needs both `my_awesome_feature` and
8887
(because of one attribute on the class and another attribute on the method) while the `/other-route` needs only
8988
`my_awesome_feature` enabled (because of class attribute).
9089

90+
```php
91+
<?php
92+
93+
use Unleash\Client\Bundle\Attribute\IsNotEnabled;
94+
use Symfony\Component\HttpFoundation\Response;
95+
use Symfony\Component\Routing\Annotation\Route;
96+
97+
#[IsNotEnabled('kill_switch')]
98+
final class MyHeavyController
99+
{
100+
#[Route('/my-route')]
101+
public function myRoute(): Response
102+
{
103+
// todo
104+
}
105+
}
106+
```
107+
108+
In the second example, `/my-route` route is only enabled if `kill_switch` is **not** enabled.
109+
91110
You can also notice that one of the attributes specifies a second optional parameter with status code. The supported
92111
status codes are:
93112
- `404` - `NotFoundHttpException`
94113
- `403` - `AccessDeniedHttpException`
95114
- `400` - `BadRequestHttpException`
96-
- `401` - `UnauthorizedHttpException` with message "Unauthorized".
115+
- `401` - `UnauthorizedHttpException` with message "Unauthorized".
116+
- `503` - `ServiceUnavailableHttpException`
97117

98118
The default status code is `404`. If you use an unsupported status code `InvalidValueException` will be thrown.
99119

src/Attribute/ControllerAttribute.php

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
namespace Unleash\Client\Bundle\Attribute;
4+
5+
use JetBrains\PhpStorm\ExpectedValues;
6+
use Symfony\Component\HttpFoundation\Response;
7+
8+
interface ControllerAttribute
9+
{
10+
public function getFeatureName(): string;
11+
12+
#[ExpectedValues([
13+
Response::HTTP_NOT_FOUND,
14+
Response::HTTP_FORBIDDEN,
15+
Response::HTTP_BAD_REQUEST,
16+
Response::HTTP_UNAUTHORIZED,
17+
Response::HTTP_SERVICE_UNAVAILABLE,
18+
])]
19+
public function getErrorCode(): int;
20+
}

src/Attribute/IsEnabled.php

+19-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
use Symfony\Component\HttpFoundation\Response;
88

99
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)]
10-
final class IsEnabled
10+
final class IsEnabled implements ControllerAttribute
1111
{
1212
public function __construct(
1313
public string $featureName,
@@ -16,8 +16,26 @@ public function __construct(
1616
Response::HTTP_FORBIDDEN,
1717
Response::HTTP_BAD_REQUEST,
1818
Response::HTTP_UNAUTHORIZED,
19+
Response::HTTP_SERVICE_UNAVAILABLE,
1920
])]
2021
public int $errorCode = Response::HTTP_NOT_FOUND,
2122
) {
2223
}
24+
25+
public function getFeatureName(): string
26+
{
27+
return $this->featureName;
28+
}
29+
30+
#[ExpectedValues([
31+
Response::HTTP_NOT_FOUND,
32+
Response::HTTP_FORBIDDEN,
33+
Response::HTTP_BAD_REQUEST,
34+
Response::HTTP_UNAUTHORIZED,
35+
Response::HTTP_SERVICE_UNAVAILABLE,
36+
])]
37+
public function getErrorCode(): int
38+
{
39+
return $this->errorCode;
40+
}
2341
}

src/Attribute/IsNotEnabled.php

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
namespace Unleash\Client\Bundle\Attribute;
4+
5+
use Attribute;
6+
use JetBrains\PhpStorm\ExpectedValues;
7+
use Symfony\Component\HttpFoundation\Response;
8+
9+
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)]
10+
final class IsNotEnabled implements ControllerAttribute
11+
{
12+
public function __construct(
13+
public string $featureName,
14+
#[ExpectedValues([
15+
Response::HTTP_NOT_FOUND,
16+
Response::HTTP_FORBIDDEN,
17+
Response::HTTP_BAD_REQUEST,
18+
Response::HTTP_UNAUTHORIZED,
19+
Response::HTTP_SERVICE_UNAVAILABLE,
20+
])]
21+
public int $errorCode = Response::HTTP_NOT_FOUND,
22+
) {
23+
}
24+
25+
public function getFeatureName(): string
26+
{
27+
return $this->featureName;
28+
}
29+
30+
#[ExpectedValues([
31+
Response::HTTP_NOT_FOUND,
32+
Response::HTTP_FORBIDDEN,
33+
Response::HTTP_BAD_REQUEST,
34+
Response::HTTP_UNAUTHORIZED,
35+
Response::HTTP_SERVICE_UNAVAILABLE,
36+
])]
37+
public function getErrorCode(): int
38+
{
39+
return $this->errorCode;
40+
}
41+
}

src/Listener/ControllerAttributeResolver.php

+20-9
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,14 @@
1212
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
1313
use Symfony\Component\HttpKernel\Exception\HttpException;
1414
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
15+
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
1516
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
1617
use Symfony\Component\HttpKernel\KernelEvents;
1718
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
1819
use Throwable;
20+
use Unleash\Client\Bundle\Attribute\ControllerAttribute;
1921
use Unleash\Client\Bundle\Attribute\IsEnabled;
22+
use Unleash\Client\Bundle\Attribute\IsNotEnabled;
2023
use Unleash\Client\Bundle\Event\BeforeExceptionThrownForAttributeEvent;
2124
use Unleash\Client\Bundle\Event\UnleashEvents;
2225
use Unleash\Client\Exception\InvalidValueException;
@@ -61,36 +64,44 @@ public function onControllerResolved(ControllerEvent $event): void
6164
$reflectionClass = new ReflectionClass($class);
6265
$reflectionMethod = $reflectionClass->getMethod($method);
6366

64-
/** @var array<ReflectionAttribute<IsEnabled>> $attributes */
67+
/** @var array<ReflectionAttribute<ControllerAttribute>> $attributes */
6568
$attributes = [
66-
...$reflectionClass->getAttributes(IsEnabled::class),
67-
...$reflectionMethod->getAttributes(IsEnabled::class),
69+
...$reflectionClass->getAttributes(ControllerAttribute::class, ReflectionAttribute::IS_INSTANCEOF),
70+
...$reflectionMethod->getAttributes(ControllerAttribute::class, ReflectionAttribute::IS_INSTANCEOF),
6871
];
6972

7073
foreach ($attributes as $attribute) {
7174
$attribute = $attribute->newInstance();
72-
assert($attribute instanceof IsEnabled);
73-
if (!$this->unleash->isEnabled($attribute->featureName)) {
75+
assert($attribute instanceof ControllerAttribute);
76+
77+
$isFeatureEnabled = $this->unleash->isEnabled($attribute->getFeatureName());
78+
$throwException = match ($attribute::class) {
79+
IsEnabled::class => !$isFeatureEnabled,
80+
IsNotEnabled::class => $isFeatureEnabled,
81+
default => false,
82+
};
83+
if ($throwException) {
7484
throw $this->getException($attribute);
7585
}
7686
}
7787
}
7888

79-
private function getException(IsEnabled $attribute): HttpException|Throwable
89+
private function getException(ControllerAttribute $attribute): HttpException|Throwable
8090
{
81-
$event = new BeforeExceptionThrownForAttributeEvent($attribute->errorCode);
91+
$event = new BeforeExceptionThrownForAttributeEvent($attribute->getErrorCode());
8292
$this->eventDispatcher->dispatch($event, UnleashEvents::BEFORE_EXCEPTION_THROWN_FOR_ATTRIBUTE);
8393
$exception = $event->getException();
8494
if ($exception !== null) {
8595
return $exception;
8696
}
8797

88-
return match ($attribute->errorCode) {
98+
return match ($attribute->getErrorCode()) {
8999
Response::HTTP_BAD_REQUEST => new BadRequestHttpException(),
90100
Response::HTTP_UNAUTHORIZED => new UnauthorizedHttpException('Unauthorized'),
91101
Response::HTTP_FORBIDDEN => new AccessDeniedHttpException(),
92102
Response::HTTP_NOT_FOUND => new NotFoundHttpException(),
93-
default => throw new InvalidValueException("Unsupported status code: {$attribute->errorCode}"),
103+
Response::HTTP_SERVICE_UNAVAILABLE => new ServiceUnavailableHttpException(),
104+
default => throw new InvalidValueException("Unsupported status code: {$attribute->getErrorCode()}"),
94105
};
95106
}
96107
}

0 commit comments

Comments
 (0)