Skip to content

Commit

Permalink
Add twig extension
Browse files Browse the repository at this point in the history
  • Loading branch information
ajgarlag committed Oct 25, 2022
1 parent 4f89019 commit 27d4128
Show file tree
Hide file tree
Showing 9 changed files with 326 additions and 1 deletion.
7 changes: 6 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
{
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
Original file line number Diff line number Diff line change
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());
}
}
33 changes: 33 additions & 0 deletions src/Twig/PheatureFlagsExtension.php
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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);
}
}
Empty file removed test.sqlite
Empty file.
41 changes: 41 additions & 0 deletions test/DependencyInjection/TwigExtensionPassTest.php
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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']));
}
}

0 comments on commit 27d4128

Please sign in to comment.