Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add twig extension #36

Merged
merged 1 commit into from
Oct 25, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 6 additions & 1 deletion composer.json
Expand Up @@ -39,6 +39,7 @@
"roave/infection-static-analysis-plugin": "^1.18",
"squizlabs/php_codesniffer": "^3.4",
"symfony/var-dumper": "^4.2 || ^5.0 || ^6.0",
"twig/twig": "^2.4.4 || ^3.0",
"vimeo/psalm": "^4.4"
},
"autoload": {
Expand Down Expand Up @@ -81,7 +82,11 @@
"pheature/dbal-toggle": "Allows using Dbal toggle management implementation.",
"pheature/inmemory-toggle": "Allows using Inmemory toggle management implementation.",
"pheature/toggle-crud": "Allows using toggle management CRUD implementation.",
"pheature/toggle-crud-psr7-api": "Allows using toggle management CRUD HTTP API."
"pheature/toggle-crud-psr7-api": "Allows using toggle management CRUD HTTP API.",
"twig/twig-bundle": "Allows using the twig extension."
},
"conflict": {
"twig/twig": "<2.4.4"
},
"config": {
"sort-packages": true,
Expand Down
27 changes: 27 additions & 0 deletions src/DependencyInjection/TwigExtensionPass.php
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace Pheature\Community\Symfony\DependencyInjection;

use Pheature\Community\Symfony\Twig\PheatureFlagsExtension;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

final class TwigExtensionPass implements CompilerPassInterface
{
/** @psalm-suppress ReservedWord */
public function process(ContainerBuilder $container): void
kpicaza marked this conversation as resolved.
Show resolved Hide resolved
{
if (!$container->hasDefinition('twig')) {
return;
}

$container->register(PheatureFlagsExtension::class, PheatureFlagsExtension::class);

$container->findDefinition('twig')
->addMethodCall('addExtension', [new Reference(PheatureFlagsExtension::class)])
;
}
}
2 changes: 2 additions & 0 deletions src/PheatureFlagsBundle.php
Expand Up @@ -10,6 +10,7 @@
use Pheature\Community\Symfony\DependencyInjection\SegmentFactoryPass;
use Pheature\Community\Symfony\DependencyInjection\ToggleStrategyFactoryPass;
use Pheature\Community\Symfony\DependencyInjection\ToggleAPIPass;
use Pheature\Community\Symfony\DependencyInjection\TwigExtensionPass;
use Pheature\Model\Toggle\EnableByMatchingIdentityId;
use Pheature\Model\Toggle\EnableByMatchingSegment;
use Pheature\Model\Toggle\IdentitySegment;
Expand Down Expand Up @@ -63,5 +64,6 @@ public function build(ContainerBuilder $container): void
$container->addCompilerPass(new FeatureRepositoryFactoryPass());
$container->addCompilerPass(new FeatureFinderFactoryPass());
$container->addCompilerPass(new ToggleAPIPass());
$container->addCompilerPass(new TwigExtensionPass());
kpicaza marked this conversation as resolved.
Show resolved Hide resolved
}
}
33 changes: 33 additions & 0 deletions src/Twig/PheatureFlagsExtension.php
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace Pheature\Community\Symfony\Twig;

use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
use Twig\TwigTest;

final class PheatureFlagsExtension extends AbstractExtension
{
/**
* @return TwigTest[]
*/
public function getTests(): array
{
return [
new TwigTest('enabled', [PheatureFlagsRuntime::class, 'isEnabled']),
new TwigTest('enabled_for', [PheatureFlagsRuntime::class, 'isEnabled'], ['one_mandatory_argument' => true]),
];
}

/**
* @return TwigFunction[]
*/
public function getFunctions(): array
{
return [
new TwigFunction('is_feature_enabled', [PheatureFlagsRuntime::class, 'isFeatureEnabled']),
];
}
}
41 changes: 41 additions & 0 deletions src/Twig/PheatureFlagsRuntime.php
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace Pheature\Community\Symfony\Twig;

use Pheature\Core\Toggle\Exception\FeatureNotFoundException;
use Pheature\Core\Toggle\Read\ConsumerIdentity;
use Pheature\Core\Toggle\Read\Toggle;
use Pheature\Model\Toggle\Identity;
use Twig\Extension\RuntimeExtensionInterface;

final class PheatureFlagsRuntime implements RuntimeExtensionInterface
{
private Toggle $toggle;

public function __construct(Toggle $toggle)
{
$this->toggle = $toggle;
}

/**
* @throws FeatureNotFoundException
*/
public function isEnabled(string $feature, ConsumerIdentity $consumerIdentity = null): bool
{
return $this->toggle->isEnabled($feature, $consumerIdentity);
}

/**
* @param array<string, mixed> $payload
*
* @throws FeatureNotFoundException
*/
public function isFeatureEnabled(string $feature, ?string $identity = null, array $payload = []): bool
{
$consumerIdentity = is_string($identity) ? new Identity($identity, $payload) : null;

return $this->isEnabled($feature, $consumerIdentity);
}
}
41 changes: 41 additions & 0 deletions test/DependencyInjection/TwigExtensionPassTest.php
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace Pheature\Test\Community\Symfony\DependencyInjection;

use Pheature\Community\Symfony\DependencyInjection\TwigExtensionPass;
use Pheature\Community\Symfony\Twig\PheatureFlagsExtension;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Twig\Environment;
use Twig\Loader\ArrayLoader;

final class TwigExtensionPassTest extends TestCase
{
public function testItShouldNotRegisterTwigExtensionIfTwigServiceIsNotPresent(): void
{
$compilerPass = new TwigExtensionPass();
$container = $containerBuilder = new ContainerBuilder();
$container->addCompilerPass($compilerPass);
$container->compile();

$removedIds = $container->getRemovedIds();
$this->assertArrayNotHasKey(PheatureFlagsExtension::class, $removedIds);
}

public function testItShouldRegisterTwigExtension(): void
{
$compilerPass = new TwigExtensionPass();
$container = $containerBuilder = new ContainerBuilder();
$container->register('twig', Environment::class)
->addArgument(new Definition(ArrayLoader::class))
;
$container->addCompilerPass($compilerPass);
$container->compile();

$removedIds = $container->getRemovedIds();
$this->assertArrayHasKey(PheatureFlagsExtension::class, $removedIds);
}
}
83 changes: 83 additions & 0 deletions test/Twig/PheatureFlagsExtensionTest.php
@@ -0,0 +1,83 @@
<?php

declare(strict_types=1);

namespace Pheature\Test\Community\Symfony\DependencyInjection;

use Pheature\Community\Symfony\Twig\PheatureFlagsExtension;
use Pheature\Community\Symfony\Twig\PheatureFlagsRuntime;
use Pheature\Core\Toggle\Read\ConsumerIdentity;
use Pheature\Core\Toggle\Read\Feature;
use Pheature\Core\Toggle\Read\FeatureFinder;
use PHPUnit\Framework\TestCase;
use Pheature\Core\Toggle\Read\Toggle;
use Pheature\Core\Toggle\Read\ToggleStrategies;
use Pheature\Core\Toggle\Read\ToggleStrategy;
use Twig\Environment;
use Twig\Loader\ArrayLoader;
use Twig\RuntimeLoader\FactoryRuntimeLoader;

final class PheatureFlagsExtensionTest extends TestCase
{
public function testItShouldExposeTwoTests(): void
{
$extension = new PheatureFlagsExtension(new Toggle($this->createStub(FeatureFinder::class)));
$this->assertCount(2, $extension->getTests());
}

public function testItShouldExposeOneFunction(): void
{
$extension = new PheatureFlagsExtension(new Toggle($this->createStub(FeatureFinder::class)));
$this->assertCount(1, $extension->getFunctions());
}

/**
* @dataProvider getTemplates
*/
public function testRendering($template, $variables)
{
$loader = new ArrayLoader(['index' => $template]);
$twig = new Environment($loader, ['debug' => true, 'cache' => false]);
$twig->addExtension(new PheatureFlagsExtension());
$toggle = $this->createAllFeaturesEnabledToggle();
$twig->addRuntimeLoader(new FactoryRuntimeLoader([PheatureFlagsRuntime::class => fn() => new PheatureFlagsRuntime($toggle)]));

$this->assertSame('yes', $twig->load('index')->render($variables));
}

public function getTemplates()
{
return [
['{% if "foo" is enabled %}yes{% endif %}', []],
['{% if "foo" is enabled_for(i) %}yes{% endif %}', ['i' => $this->createStub(ConsumerIdentity::class)]],
['{% if is_feature_enabled("foo") %}yes{% endif %}', []],
];
}

private function createAllFeaturesEnabledToggle(): Toggle
{
$strategyStub = $this->createStub(ToggleStrategy::class);
$strategyStub
->method('isSatisfiedBy')
->willReturn(true)
;

$featureStub = $this->createStub(Feature::class);
$featureStub
->method('isEnabled')
->willReturn(true)
;
$featureStub
->method('strategies')
->willReturn(new ToggleStrategies($strategyStub))
;

$featureFinderStub = $this->createStub(FeatureFinder::class);
$featureFinderStub
->method('get')
->willReturn($featureStub)
;

return new Toggle($featureFinderStub);
}
}
93 changes: 93 additions & 0 deletions test/Twig/PheatureFlagsRuntimeTest.php
@@ -0,0 +1,93 @@
<?php

declare(strict_types=1);

namespace Pheature\Test\Community\Symfony\DependencyInjection;

use Pheature\Community\Symfony\Twig\PheatureFlagsRuntime;
use Pheature\Core\Toggle\Read\ConsumerIdentity;
use Pheature\Core\Toggle\Read\Feature;
use Pheature\Core\Toggle\Read\FeatureFinder;
use PHPUnit\Framework\TestCase;
use Pheature\Core\Toggle\Read\Toggle;
use Pheature\Core\Toggle\Read\ToggleStrategies;
use Pheature\Core\Toggle\Read\ToggleStrategy;

final class PheatureFlagsRuntimeTest extends TestCase
{
public function testIsEnabledMethodDelegatesToToggle(): void
{
$featureMock = $this->createMock(Feature::class);
$featureMock
->expects($this->once())
->method('isEnabled')
->willReturn(false)
;

$featureFinderMock = $this->createMock(FeatureFinder::class);
$featureFinderMock
->expects($this->once())
->method('get')
->with($this->identicalTo('foobar'))
->willReturn($featureMock)
;

$extension = new PheatureFlagsRuntime(new Toggle($featureFinderMock));
self::assertFalse($extension->isEnabled('foobar'));
}

public function testIsFeatureEnabledMethodDelegatesToToggle(): void
{
$featureMock = $this->createMock(Feature::class);
$featureMock
->expects($this->once())
->method('isEnabled')
->willReturn(false)
;

$featureFinderMock = $this->createMock(FeatureFinder::class);
$featureFinderMock
->expects($this->once())
->method('get')
->with($this->identicalTo('foobar'))
->willReturn($featureMock)
;

$extension = new PheatureFlagsRuntime(new Toggle($featureFinderMock));
self::assertFalse($extension->isFeatureEnabled('foobar'));
}

public function testIsFeatureEnabledMethodPassesAConsumerIdentityToStrategies(): void
{
$strategyMock = $this->createMock(ToggleStrategy::class);
$strategyMock
->expects($this->once())
->method('isSatisfiedBy')
->with($this->callback(fn(ConsumerIdentity $identity) => $identity->id() === 'foo' && $identity->payload() === ['foo' => 'bar', 'baz' => 'qux']))
->willReturn(true)
;

$featureMock = $this->createMock(Feature::class);
$featureMock
->expects($this->once())
->method('isEnabled')
->willReturn(true)
;
$featureMock
->expects($this->once())
->method('strategies')
->willReturn(new ToggleStrategies($strategyMock))
;

$featureFinderMock = $this->createMock(FeatureFinder::class);
$featureFinderMock
->expects($this->once())
->method('get')
->with($this->identicalTo('foobar'))
->willReturn($featureMock)
;

$extension = new PheatureFlagsRuntime(new Toggle($featureFinderMock));
self::assertTrue($extension->isFeatureEnabled('foobar', 'foo', ['foo' => 'bar', 'baz' => 'qux']));
}
}