Przejdź do treści

Zadanie 6. Migracja danych

Implementacja wysyłania danych do serwisu rekomendacji

Pojawia się potrzeba biznesowa, aby zbierać informacje za każdym razem, kiedy ktoś chce pobrać szczegóły produktu. Każdy request po produkt ma wysyłać do serwisu rekomendacji taką informację, z pomocą metody createImpression.

Moglibyśmy zaimplementować taką zmianę bezpośrednio w naszym kontrolerze, ale zdecydowaliśmy się wyseparować ten kontekst, bo nasza aplikacja wspiera stosowanie tzw. middleware.

Zdecydowaliśmy, że aby nie zaciemniać kontrolera, stworzymy middleware, który uruchomi się w łańcuchu razem z naszym kontrolerem.

Zastosowanie nowego middleware uzależnimy od nowej flagi.

1. Nowa flaga

Do tablicy w main/src/Flags.php dodaj klucz

1
2
3
4
5
<?php
return array(
    'show_recommendations_on_product_lookup' => false, 
    'create_impression_on_product_lookup' => false
);

2. Wstępna implementacja middleware

2.1. Utwórz plik main/src/Products/ProductImpressionMiddleware.php

Do konstruktora dodaj zależność od RecommendationsServiceInterface.

<?php

namespace Tbd\Main\Products;

use Psr\Http\Message\ServerRequestInterface;
use Tbd\Main\Recommendations\RecommendationsServiceInterface;

class ProductImpressionMiddleware
{
    private $service;

    public function __construct(RecommendationsServiceInterface $service){
        $this->service = $service;
    }

    public function __invoke(ServerRequestInterface $request, callable $next)
    {
        $response = $next($request);
        return $response;
    }
}

Taka implementacja sprawi, że gdy nasz middleware zostanie uruchomiony, będzie miał możliwość uruchomić następny callback w łańcuchu i zmanipulować odpowiedź lub wykonać dodatkową logikę.

Wykorzystamy to, by uruchomić nasz dotychczasowy kontroler i w zależności od odpowiedzi, wysłać informacje o wyświetleniu do serwisu rekomendacji.

2.2. Zmodyfikuj klasę Application, by w zależności od stanu flagi uwzględniała nasz nowy middleware.

<?php
/* ... */
if(FeatureFlag::isEnabled('create_impression_on_product_lookup')) {
    $this->app->get('/products/{id}', Products\ProductImpressionMiddleware::class, Products\ProductLookupController::class);
}else{
    $this->app->get('/products/{id}', Products\ProductLookupController::class);
}
W zależności od stanu flagi dodaj do kontenera DI informacje o serwisie rekomendacji i naszym middleware.

<?php
/*...*/
$diArray = [
    Products\ProductsListController::class => function (ProductRepository $repository) {
        return new Products\ProductsListController($repository);
    },
    Products\ProductLookupController::class => function (ProductRepository $repository) {
        return new Products\ProductLookupController($repository);
    }
];

if(FeatureFlag::isEnabled('create_impression_on_product_lookup')) {
    $diArray[RecommendationsService::class] = function(){
        $address = getenv('RECOMMENDATIONS_SERVICE_URL');
        return new RecommendationsService($address);
    };
    $diArray[Products\ProductImpressionMiddleware::class] = function (RecommendationsService $service) {
        return new Products\ProductImpressionMiddleware($service);
    };
}

$container = new Container($diArray);

3. Testy automatyczne

3.1. Stwórz klasę testów ProductImpressionMiddlewareTest, kopiując ProductLookupControllerTest.

3.2. Zmodyfikuj wszystkie testy tak, by oznaczały się jako pominięte, jeśli flaga create_impression_on_product_lookup jest wyłączona

3.3. Zmodyfikuj testy tak, by uruchamiały middleware ProductImpressionMiddleware w łańcuchu z ProductLookupController.

3.4. Uruchom pipeline.sh, by mieć pewność, że wszystko jest ok.

3.5. Commit.

Rozwiązanie
<?php

namespace Tbd\Main\Tests\Products;

use Tbd\Main\FeatureFlags\FeatureFlag;
use Tbd\Main\Products\Product;
use Tbd\Main\Products\ProductImpressionMiddleware;
use Tbd\Main\Products\ProductLookupController;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ResponseInterface;
use React\Http\Message\ServerRequest;
use Tbd\Main\Products\ProductRepositoryInterface;
use Tbd\Main\Recommendations\RecommendationsServiceInterface;

class ProductImpressionMiddlewareTest extends TestCase
{
    public function testControllerReturnsValidResponseWithRecommendationsDisabled()
    {
        if(!FeatureFlag::isEnabled('create_impression_on_product_lookup')){
            $this->markTestSkipped("Flag create_impression_on_product_lookup is disabled");
        }

        if(FeatureFlag::isEnabled('show_recommendations_on_product_lookup')){
            $this->markTestSkipped("Flag show_recommendations_on_product_lookup is enabled");
        }

        $request = new ServerRequest('GET', 'http://example.com/products/3');
        $request = $request->withAttribute("id", "3");

        $product = new Product(3, 'test', 'description', 100);

        $stub = $this->createMock(ProductRepositoryInterface::class);
        $stub->method('findProduct')
            ->will($this->returnValueMap([["3", $product]]));

        $controller = new ProductLookupController($stub);

        $recoStub = $this->createMock(RecommendationsServiceInterface::class);
        $recoStub->method('createImpression')
            ->will($this->returnValueMap([[3, true]]));
        $recoStub->expects($this->once())
            ->method('createImpression');

        $middleware = new ProductImpressionMiddleware($recoStub);

        $response = $middleware($request, $controller);

        $this->assertInstanceOf(ResponseInterface::class, $response);
        $this->assertEquals(200, $response->getStatusCode());
        $this->assertEquals('application/json', $response->getHeaderLine('Content-Type'));

        $output='{
    "name": "test",
    "description": "description",
    "price": 100.0
}';
        $this->assertEquals($output, (string) trim($response->getBody()));
    }

    public function testControllerReturnsValidResponseWithRecommendationsEnabled()
    {
        if(!FeatureFlag::isEnabled('create_impression_on_product_lookup')){
            $this->markTestSkipped("Flag create_impression_on_product_lookup is disabled");
        }

        if(!FeatureFlag::isEnabled('show_recommendations_on_product_lookup')){
            $this->markTestSkipped("Flag show_recommendations_on_product_lookup is disabled");
        }

        $request = new ServerRequest('GET', 'http://example.com/products/3');
        $request = $request->withAttribute("id", "3");

        $product = new Product(3, 'test', 'description', 100);

        $stub = $this->createMock(ProductRepositoryInterface::class);
        $stub->method('findProduct')
            ->will($this->returnValueMap([["3", $product]]));

        $controller = new ProductLookupController($stub);

        $recoStub = $this->createMock(RecommendationsServiceInterface::class);
        $recoStub->method('getRecommendations')
            ->will($this->returnValueMap([[3, [1]]]));
        $recoStub->method('createImpression')
            ->will($this->returnValueMap([[3, true]]));
        $recoStub->expects($this->once())
            ->method('createImpression');

        $controller->getDataProvider()->getImplementation()->setService($recoStub);

        $middleware = new ProductImpressionMiddleware($recoStub);

        $response = $middleware($request, $controller);

        $this->assertInstanceOf(ResponseInterface::class, $response);
        $this->assertEquals(200, $response->getStatusCode());
        $this->assertEquals('application/json', $response->getHeaderLine('Content-Type'));

        $output='{
    "name": "test",
    "description": "description",
    "price": 100.0,
    "recommendations": [
        1
    ]
}';
        $this->assertEquals($output, (string) trim($response->getBody()));
    }

    public function testControllerReturns404Response()
    {
        if(!FeatureFlag::isEnabled('create_impression_on_product_lookup')){
            $this->markTestSkipped("Flag create_impression_on_product_lookup is disabled");
        }

        $request = new ServerRequest('GET', 'http://example.com/products/3');
        $request = $request->withAttribute("id", "3");

        $stub = $this->createMock(ProductRepositoryInterface::class);
        $stub->method('findProduct')
            ->will($this->returnValueMap([["3", null]]));

        $recoStub = $this->createMock(RecommendationsServiceInterface::class);
        $recoStub->method('createImpression')
            ->will($this->returnValueMap([[3, true]]));
        $recoStub->expects($this->never())
            ->method('createImpression');

        $controller = new ProductLookupController($stub);
        $middleware = new ProductImpressionMiddleware($recoStub);

        $response = $middleware($request, $controller);

        $this->assertInstanceOf(ResponseInterface::class, $response);
        $this->assertEquals(404, $response->getStatusCode());
        $this->assertEquals('text/plain; charset=utf-8', $response->getHeaderLine('Content-Type'));

        $output='Product not found';
        $this->assertEquals($output, (string) trim($response->getBody()));
    }
}

4. Implementacja rozwiązania

4.1 Zmodyfikuj ProductImpressionMiddleware, tak by w razie poprawnej odpowiedzi z kontrolera ProductLookupController wysłał informacje o wyświetleniu do serwisu rekomendacji.

4.2. Przetestuj swoje rozwiązanie za pomocą testów automatycznych, uruchamiając je ze zmienną środowiskową, która włączy flagę

FEATURE_FLAG_CREATE_IMPRESSION_ON_PRODUCT_LOOKUP=1 composer run tests

4.3 Zmodyfikuj pipeline.sh, by uruchamiał testy kolejny raz, ale z włączoną flagą

4.4. Uruchom pipeline.sh, by mieć pewność, że wszystko jest ok.

4.5. Commit.

5. Wdrożenie

5.1. Zmodyfikuj plik docker-compose.yml i ustaw zmienną środowiskową FEATURE_FLAG_CREATE_IMPRESSION_ON_PRODUCT_LOOKUP na 1.

5.2. Uruchom ponownie usługę

docker-compose build
docker-compose stop
docker-compose create
docker-compose start

lub jeśli masz to uruchomione w terminalu, to

docker-compose up

5.3. Sprawdź, czy wszystko działa i nic nie wybucha

http://127.0.0.1:8181/products/1

5.4. Sprawdź, czy serwis rekomendacji zwraca dane

http://127.0.0.1:8182/recommendations/2

5.5. Zmodyfikuj stan flagi create_impression_on_product_lookup w main/src/Flags.php, ustawiając wartość na true

5.6. Uruchom pipeline.sh, by mieć pewność, że wszystko jest ok.

5.7. Commit.

W idealnym świecie, po tym wdrożeniu dane zaczynają się zbierać, ale nic ich jeszcze nie wyświetla. Przygotowana przez nas integracja z poprzedniego ćwiczenia ciągle nie jest włączona.

Dodatkowy skrypt migrujący istniejące dane

Po wielu analizach ktoś doszukał się historycznych danych, którymi musimy zasilić nasz serwis rekomendacji, zanim zaczniemy go używać i pokażemy rekomendacje klientom.

Id Impressions
1 120
2 200
3 333

1. Implementacja rozwiązania

1.1. Utwórz plik main/public/migrate-reco.php

1.2. Zainicjalizuj autoloader i obsługę Feature Flag.

1.3. Zakończ działanie skryptu, jeśli flaga create_impression_on_product_lookup jest wyłączona.

1.4. Wykorzystując klasę \Tbd\Main\Recommendations\RecommendationsService napisz rozwiązanie, które wyśle odpowiednią liczbę zdarzeń do systemu.

Serwis jest dostępny pod adresem http://127.0.0.1:8182.

1.5. Uruchom skrypt.

php migrate-reco.php

W idealnym świecie, dane z produkcji zbierają się od ostatniego wdrożenia, teraz dodatkowo uzupełniamy dane o informacje historyczne. Serwis jest w tym momencie gotowy.

Wdrożenie wszystkich funkcji

1. Zwracanie rekomendacji

1.1. Przetestuj swoje rozwiązanie za pomocą testów automatycznych, uruchamiając je ze zmienną środowiskową, która włączy obie flagi

FEATURE_FLAG_CREATE_IMPRESSION_ON_PRODUCT_LOOKUP=1 FEATURE_FLAG_SHOW_RECOMMENDATIONS_ON_PRODUCT_LOOKUP=1 composer run tests

1.2. Zmodyfikuj pipeline.sh, by dodatkowo uruchamiał testy w konfiguracjach:

  • obie flagi włączone
  • tylko flaga FEATURE_FLAG_SHOW_RECOMMENDATIONS_ON_PRODUCT_LOOKUP włączona
  • tylko flaga FEATURE_FLAG_CREATE_IMPRESSION_ON_PRODUCT_LOOKUP włączona
  • obie flagi wyłączone

1.3. Uruchom pipeline.sh, by mieć pewność, że wszystko jest ok.

1.4. Zmodyfikuj plik docker-compose.yml i ustaw zmienną środowiskową FEATURE_FLAG_SHOW_RECOMMENDATIONS_ON_PRODUCT_LOOKUP na 1.

1.5. Uruchom ponownie usługę

docker-compose build
docker-compose stop
docker-compose create
docker-compose start

lub jeśli masz to uruchomione w terminalu, to

docker-compose up

1.6. Sprawdź, czy wszystko działa i nic nie wybucha

http://127.0.0.1:8181/products/1

1.7. Sprawdź, czy serwis rekomendacji zwraca dane

http://127.0.0.1:8182/recommendations/2

1.8. Zmodyfikuj stan flagi show_recommendations_on_product_lookup w main/src/Flags.php, ustawiając wartość na true

1.9. Uruchom pipeline.sh, by mieć pewność, że wszystko jest ok.

1.10. Commit.