Zadanie 2. Obsługa Feature Flag
Implementacja Feature Flag
Dodamy do naszej aplikacji obsługę feature flag, tworząc klasy, które pozwolą nam na:
statyczny dostęp do stanu flag z dowolnego miejsca w kodzie
niestatyczny dostęp do stanu flag tam, gdzie będziemy chcieli mieć implementację jako zależność
obsługę stanu domyślnego feature flag z poziomu kodu
obsługę stanu feature flag z pomocą zmiennych środowiskowych
Na początek interfejs
main/src/FeatureFlags/FeatureFlagsInterface.php <?php
namespace Tbd\Main\FeatureFlags ;
interface FeatureFlagsInterface
{
public function setEnabled ( string $flag , bool $enabled = true ) : void ;
public function isEnabled ( string $flag ) : bool ;
}
Prosta implementacja naszego interfejsu
main/src/FeatureFlags/InMemoryFeatureFlags.php <?php
namespace Tbd\Main\FeatureFlags ;
class InMemoryFeatureFlags implements FeatureFlagsInterface
{
private $flags = [];
public function __construct ( array $flags = []){
$this -> flags = $flags ;
}
public function setEnabled ( string $flag , bool $enabled = true ) : void
{
$this -> flags [ $flag ] = $enabled ;
}
public function isEnabled ( string $flag ) : bool
{
return isset ( $this -> flags [ $flag ]) ? $this -> flags [ $flag ] : false ;
}
}
Źródło stanu flag
Statyczny helper
main/src/FeatureFlags/FeatureFlag.php <?php
namespace Tbd\Main\FeatureFlags ;
class FeatureFlag
{
private static FeatureFlagsInterface $instance ;
public static function setFeatureFlags ( FeatureFlagsInterface $featureFlags ) : void
{
self :: $instance = $featureFlags ;
}
public static function isEnabled ( string $flag ) : bool
{
if ( isset ( self :: $instance )) {
return self :: $instance -> isEnabled ( $flag );
}
return false ;
}
}
Połączmy całość
main/public/index.php <?php
use Tbd\Main\FeatureFlags\FeatureFlag ;
use Tbd\Main\FeatureFlags\InMemoryFeatureFlags ;
require __DIR__ . '/../vendor/autoload.php' ;
$initialFlags = require __DIR__ . '/../src/Flags.php' ;
$featureFlags = new InMemoryFeatureFlags ( $initialFlags );
FeatureFlag :: setFeatureFlags ( $featureFlags );
$app = new \Tbd\Main\Application ();
$app -> run ();
Możliwość nadpisania stanu flag zmiennymi środowiskowymi
main/src/FeatureFlags/EnvOverrider.php <?php
namespace Tbd\Main\FeatureFlags ;
class EnvOverrider
{
public function overrideFlags ( array $flags ) : array
{
foreach ( $flags as $flag => $value ){
$env = getenv ( "FEATURE_FLAG_" . strtoupper ( $flag ));
if ( $env !== false ){
$flags [ $flag ] = ( bool ) $env ;
}
}
return $flags ;
}
}
Połączmy całość
main/public/index.php <?php
use Tbd\Main\FeatureFlags\EnvOverrider ;
use Tbd\Main\FeatureFlags\FeatureFlag ;
use Tbd\Main\FeatureFlags\InMemoryFeatureFlags ;
require __DIR__ . '/../vendor/autoload.php' ;
$initialFlags = require __DIR__ . '/../src/Flags.php' ;
$envOverrider = new EnvOverrider ();
$featureFlags = new InMemoryFeatureFlags ( $envOverrider -> overrideFlags ( $initialFlags ));
FeatureFlag :: setFeatureFlags ( $featureFlags );
$app = new \Tbd\Main\Application ();
$app -> run ();
Obsługa flag w testach automatycznych
main/tests/bootstrap.php <?php
use Tbd\Main\FeatureFlags\EnvOverrider ;
use Tbd\Main\FeatureFlags\FeatureFlag ;
use Tbd\Main\FeatureFlags\InMemoryFeatureFlags ;
require_once __DIR__ . '/../vendor/autoload.php' ;
$initialFlags = require __DIR__ . '/../src/Flags.php' ;
$envOverrider = new EnvOverrider ();
$featureFlags = new InMemoryFeatureFlags ( $envOverrider -> overrideFlags ( $initialFlags ));
FeatureFlag :: setFeatureFlags ( $featureFlags );
Sprawdźmy czy nic nie zepsuliśmy
Pierwsze zastosowanie Feature Flag
Wykorzystamy naszą implementację, by wprowadzić pierwszą zmianę w działaniu aplikacji.
Chcemy, aby kontroler ProductsListController zwracał również opis i cenę produktu, ale zmianę chcemy zamknąć w Feature Flagę.
Response ma dodatkowo zwracać klucze description i price.
1. Nowa flaga
Do tablicy w main/src/Flags.php dodaj klucz
<?php
return array (
'show_product_details_on_list' => false
);
2. Testy automatyczne
2.1. Zmień nazwę testu ProductListControllerTest::testControllerReturnsValidResponse na ProductListControllerTest::testControllerReturnsValidResponseWithDetailsDisabled.
2.2. Dopisz kod sprawiający, by test został pominięty, gdy flaga jest włączona.
Podpowiedź
<?php
/* ... */
if ( FeatureFlag :: isEnabled ( 'show_product_details_on_list' )){
$this -> markTestSkipped ( "Flag show_product_details_on_list is enabled" );
}
2.3. Uruchom pipeline.sh, by mieć pewność, że wszystko jest ok.
2.4. Commit.
2.5. Stwórz test ProductListControllerTest::testControllerReturnsValidResponseWithDetailsEnabled, który testuje zachowanie aplikacji, gdy flaga jest włączona. Dopisz do niego kod, który pominie test, gdy flaga jest wyłączona.
2.6. Uruchom pipeline.sh, by mieć pewność, że wszystko jest ok.
2.7. Commit.
Rozwiązanie
main/tests/Products/ProductListControllerTest.php
<?php
namespace Tbd\Main\Tests\Products ;
use Tbd\Main\FeatureFlags\FeatureFlag ;
use Tbd\Main\Products\Product ;
use PHPUnit\Framework\TestCase ;
use Psr\Http\Message\ResponseInterface ;
use React\Http\Message\ServerRequest ;
use Tbd\Main\Products\ProductRepositoryInterface ;
use Tbd\Main\Products\ProductsListController ;
class ProductListControllerTest extends TestCase
{
public function testControllerReturnsValidResponseWithDetailsDisabled ()
{
if ( FeatureFlag :: isEnabled ( 'show_product_details_on_list' )){
$this -> markTestSkipped ( "Flag show_product_details_on_list is enabled" );
}
$request = new ServerRequest ( 'GET' , 'http://example.com/products/' );
$product1 = new Product ( 1 , 'test' , 'description' , 100 );
$product2 = new Product ( 2 , 'test2' , 'description2' , 200 );
$stub = $this -> createMock ( ProductRepositoryInterface :: class );
$stub -> method ( 'listProducts' )
-> willReturn ([ $product1 , $product2 ]);
$controller = new ProductsListController ( $stub );
$response = $controller ( $request );
$this -> assertInstanceOf ( ResponseInterface :: class , $response );
$this -> assertEquals ( 200 , $response -> getStatusCode ());
$this -> assertEquals ( 'application/json' , $response -> getHeaderLine ( 'Content-Type' ));
$output = '[
{
"id": 1,
"name": "test"
},
{
"id": 2,
"name": "test2"
}
]' ;
$this -> assertEquals ( $output , ( string ) trim ( $response -> getBody ()));
}
public function testControllerReturnsValidResponseWithDetailsEnabled ()
{
if ( ! FeatureFlag :: isEnabled ( 'show_product_details_on_list' )){
$this -> markTestSkipped ( "Flag show_product_details_on_list is disabled" );
}
$request = new ServerRequest ( 'GET' , 'http://example.com/products/' );
$product1 = new Product ( 1 , 'test' , 'description' , 100 );
$product2 = new Product ( 2 , 'test2' , 'description2' , 200 );
$stub = $this -> createMock ( ProductRepositoryInterface :: class );
$stub -> method ( 'listProducts' )
-> willReturn ([ $product1 , $product2 ]);
$controller = new ProductsListController ( $stub );
$response = $controller ( $request );
$this -> assertInstanceOf ( ResponseInterface :: class , $response );
$this -> assertEquals ( 200 , $response -> getStatusCode ());
$this -> assertEquals ( 'application/json' , $response -> getHeaderLine ( 'Content-Type' ));
$output = '[
{
"id": 1,
"name": "test",
"description": "description",
"price": 100.0
},
{
"id": 2,
"name": "test2",
"description": "description2",
"price": 200.0
}
]' ;
$this -> assertEquals ( $output , ( string ) trim ( $response -> getBody ()));
}
public function testControllerReturnsEmptyResponse ()
{
$request = new ServerRequest ( 'GET' , 'http://example.com/products/' );
$stub = $this -> createMock ( ProductRepositoryInterface :: class );
$stub -> method ( 'listProducts' )
-> willReturn ([]);
$controller = new ProductsListController ( $stub );
$response = $controller ( $request );
$this -> assertInstanceOf ( ResponseInterface :: class , $response );
$this -> assertEquals ( 200 , $response -> getStatusCode ());
$this -> assertEquals ( 'application/json' , $response -> getHeaderLine ( 'Content-Type' ));
$output = '[]' ;
$this -> assertEquals ( $output , ( string ) trim ( $response -> getBody ()));
}
}
3. Implementacja rozwiązania
3.1. Stwórz kod, który zaimplementuje wymagane zachowanie w aplikacji, który uruchomi się, gdy flaga jest włączona.
3.2. Przetestuj swoje rozwiązanie za pomocą testów automatycznych, uruchamiając je ze zmienną środowiskową, która włączy flagę
FEATURE_FLAG_SHOW_PRODUCT_DETAILS_ON_LIST = 1 composer run tests
3.3 Zmodyfikuj pipeline.sh, by uruchamiał testy drugi raz, ale z włączoną flagą
3.4. Uruchom pipeline.sh, by mieć pewność, że wszystko jest ok.
3.5. Commit.