Transform ratings service to Symfony MicroKernel application
The ratings service contained a PHP 4 style code. This change introduces a framework for processing the http-requests so we dont have to do it on our own. It'll also showcase how Instana is usable with modern PHP.
This commit is contained in:
2
.env
2
.env
@@ -1,3 +1,3 @@
|
||||
# environment file for docker-compose
|
||||
REPO=robotshop
|
||||
TAG=0.4.20
|
||||
TAG=0.5.0
|
||||
|
@@ -59,6 +59,8 @@ services:
|
||||
build:
|
||||
context: ratings
|
||||
image: ${REPO}/rs-ratings:${TAG}
|
||||
environment:
|
||||
APP_ENV: prod
|
||||
networks:
|
||||
- robot-shop
|
||||
depends_on:
|
||||
|
@@ -24,3 +24,11 @@ COPY --from=build /app/vendor/ /var/www/html/vendor/
|
||||
|
||||
COPY html/ /var/www/html
|
||||
|
||||
# This is important. Symfony needs write permissions and we
|
||||
# dont know the context in which the container will run, i.e.
|
||||
# which user will be forced from the outside so better play
|
||||
# safe for this simple demo.
|
||||
RUN rm -Rf /var/www/var/*
|
||||
RUN chown -R www-data /var/www
|
||||
RUN chmod -R 777 /var/www
|
||||
|
||||
|
@@ -1,5 +1,23 @@
|
||||
{
|
||||
"require": {
|
||||
"monolog/monolog": "^1.24.0"
|
||||
"php": "^7.3",
|
||||
"ext-curl": "*",
|
||||
"ext-json": "*",
|
||||
"ext-pdo": "*",
|
||||
"psr/log": "*",
|
||||
"monolog/monolog": "^1.24.0",
|
||||
"symfony/config": "^5.0",
|
||||
"symfony/http-kernel": "^5.0",
|
||||
"symfony/http-foundation": "^5.0",
|
||||
"symfony/routing": "^5.0",
|
||||
"symfony/dependency-injection": "^5.0",
|
||||
"symfony/framework-bundle": "^5.0",
|
||||
"doctrine/annotations": "^1.10",
|
||||
"symfony/monolog-bundle": "^3.5"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Instana\\RobotShop\\Ratings\\": "src/"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,9 @@
|
||||
DirectoryIndex index.php
|
||||
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
RewriteCond %{ENV:REDIRECT_STATUS} =""
|
||||
RewriteRule ^index\.php(?:/(.*)|$) %{ENV:BASE}/$1 [R=301,L]
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteRule api/(.*)$ api.php?request=$1 [QSA,NC,L]
|
||||
RewriteRule ^ %{ENV:BASE}/index.php [L]
|
||||
</IfModule>
|
||||
|
@@ -1,106 +0,0 @@
|
||||
<?php
|
||||
// load composer installed files
|
||||
require_once(__DIR__.'/vendor/autoload.php');
|
||||
use Monolog\Logger;
|
||||
use Monolog\Handler\StreamHandler;
|
||||
|
||||
abstract class API {
|
||||
protected $method = '';
|
||||
|
||||
protected $endpoint = '';
|
||||
|
||||
protected $verb = '';
|
||||
|
||||
protected $args = array();
|
||||
|
||||
protected $file = Null;
|
||||
|
||||
protected $logger = Null;
|
||||
|
||||
protected $logHandler = Null;
|
||||
|
||||
public function __construct($request) {
|
||||
// Logging
|
||||
$this->logHandler = new StreamHandler('php://stdout', Logger::INFO);
|
||||
|
||||
// CORS
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: *');
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$this->args = explode('/', rtrim($request, '/'));
|
||||
$this->endpoint = array_shift($this->args);
|
||||
|
||||
if(array_key_exists(0, $this->args) && !is_numeric($this->args[0])) {
|
||||
$this->verb = array_shift($this->args);
|
||||
}
|
||||
|
||||
$this->method = $_SERVER['REQUEST_METHOD'];
|
||||
if($this->method == 'POST' && array_key_exists('HTTP_X_METHOD', $_SERVER)) {
|
||||
if($_SERVER['HTTP_X_HTTP_METHOD'] == 'DELETE') {
|
||||
$this->method = 'DELETE';
|
||||
} else if($_SERVER['HTTP_X_HTTP_METHOD'] == 'PUT') {
|
||||
$this->method = 'PUT';
|
||||
} else {
|
||||
throw new Exception('Unexpected header');
|
||||
}
|
||||
}
|
||||
|
||||
switch($this->method) {
|
||||
case 'DELETE':
|
||||
case 'POST':
|
||||
$this->request = $this->_cleanInputs($_POST);
|
||||
break;
|
||||
case 'GET':
|
||||
$this->request = $this->_cleanInputs($_GET);
|
||||
break;
|
||||
case 'PUT':
|
||||
$this->request = $this->_cleanInputs($_GET);
|
||||
$this->file = file_get_contents('php://input');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public function processAPI() {
|
||||
if(method_exists($this, $this->endpoint)) {
|
||||
try {
|
||||
$result = $this->{$this->endpoint}();
|
||||
return $this->_response($result, 200);
|
||||
} catch (Exception $e) {
|
||||
return $this->_response($e->getMessage(), $e->getCode());
|
||||
}
|
||||
}
|
||||
return $this->_response("No endpoint: $this->endpoint", 404);
|
||||
}
|
||||
|
||||
private function _response($data, $status = 200) {
|
||||
header('HTTP/1.1 ' . $status . ' ' . $this->_requestStatus($status));
|
||||
return json_encode($data);
|
||||
}
|
||||
|
||||
private function _cleanInputs($data) {
|
||||
$clean_input = array();
|
||||
|
||||
if(is_array($data)) {
|
||||
foreach($data as $k => $v) {
|
||||
$clean_input[$k] = $this->_cleanInputs($v);
|
||||
}
|
||||
} else {
|
||||
$clean_input = trim(strip_tags($data));
|
||||
}
|
||||
|
||||
return $clean_input;
|
||||
}
|
||||
|
||||
private function _requestStatus($code) {
|
||||
$status = array(
|
||||
200 => 'OK',
|
||||
400 => 'Bad Request',
|
||||
404 => 'Not Found',
|
||||
405 => 'Method Not Allowed',
|
||||
500 => 'Internal Server Error');
|
||||
|
||||
return (array_key_exists("$code", $status) ? $status["$code"] : $status['500']);
|
||||
}
|
||||
}
|
||||
?>
|
@@ -1,179 +0,0 @@
|
||||
<?php
|
||||
require_once 'API.class.php';
|
||||
|
||||
use Monolog\Logger;
|
||||
|
||||
class RatingsAPI extends API {
|
||||
public function __construct($request, $origin) {
|
||||
parent::__construct($request);
|
||||
// Logging
|
||||
$this->logger = new Logger('RatingsAPI');
|
||||
$this->logger->pushHandler($this->logHandler);
|
||||
}
|
||||
|
||||
protected function health() {
|
||||
$this->logger->info('health OK');
|
||||
return 'OK';
|
||||
}
|
||||
|
||||
protected function dump() {
|
||||
$data = array();
|
||||
$data['method'] = $this->method;
|
||||
$data['verb'] = $this->verb;
|
||||
$data = array_merge($data, array('args' => $this->args));
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
// ratings/fetch/sku
|
||||
protected function fetch() {
|
||||
if($this->method == 'GET' && isset($this->verb) && count($this->args) == 0) {
|
||||
$sku = $this->verb;
|
||||
if(! $this->_checkSku($sku)) {
|
||||
throw new Exception("$sku not found", 404);
|
||||
}
|
||||
$data = $this->_getRating($sku);
|
||||
return $data;
|
||||
} else {
|
||||
$this->logger->warn('fetch rating - bad request');
|
||||
throw new Exception('Bad request', 400);
|
||||
}
|
||||
}
|
||||
|
||||
// ratings/rate/sku/score
|
||||
protected function rate() {
|
||||
if($this->method == 'PUT' && isset($this->verb) && count($this->args) == 1) {
|
||||
$sku = $this->verb;
|
||||
$score = intval($this->args[0]);
|
||||
$score = min(max(1, $score), 5);
|
||||
|
||||
if(! $this->_checkSku($sku)) {
|
||||
throw new Exception("$sku not found", 404);
|
||||
}
|
||||
|
||||
$rating = $this->_getRating($sku);
|
||||
if($rating['avg_rating'] == 0) {
|
||||
// not rated yet
|
||||
$this->_insertRating($sku, $score);
|
||||
} else {
|
||||
// iffy maths
|
||||
$newAvg = (($rating['avg_rating'] * $rating['rating_count']) + $score) / ($rating['rating_count'] + 1);
|
||||
$this->_updateRating($sku, $newAvg, $rating['rating_count'] + 1);
|
||||
}
|
||||
} else {
|
||||
$this->logger->warn('set rating - bad request');
|
||||
throw new Exception('Bad request', 400);
|
||||
}
|
||||
|
||||
return 'OK';
|
||||
}
|
||||
|
||||
private function _getRating($sku) {
|
||||
$db = $this->_dbConnect();
|
||||
if($db) {
|
||||
$stmt = $db->prepare('select avg_rating, rating_count from ratings where sku = ?');
|
||||
if($stmt->execute(array($sku))) {
|
||||
$data = $stmt->fetch();
|
||||
if($data) {
|
||||
// for some reason avg_rating is return as a string
|
||||
$data['avg_rating'] = floatval($data['avg_rating']);
|
||||
return $data;
|
||||
} else {
|
||||
// nicer to return an empty record than throw 404
|
||||
return array('avg_rating' => 0, 'rating_count' => 0);
|
||||
}
|
||||
} else {
|
||||
$this->logger->error('failed to query data');
|
||||
throw new Exception('Failed to query data', 500);
|
||||
}
|
||||
} else {
|
||||
$this->logger->error('database connection error');
|
||||
throw new Exception('Database connection error', 500);
|
||||
}
|
||||
}
|
||||
|
||||
private function _updateRating($sku, $score, $count) {
|
||||
$db = $this->_dbConnect();
|
||||
if($db) {
|
||||
$stmt = $db->prepare('update ratings set avg_rating = ?, rating_count = ? where sku = ?');
|
||||
if(! $stmt->execute(array($score, $count, $sku))) {
|
||||
$this->logger->error('failed to update rating');
|
||||
throw new Exception('Failed to update data', 500);
|
||||
}
|
||||
} else {
|
||||
$this->logger->error('database connection error');
|
||||
throw new Exception('Database connection error', 500);
|
||||
}
|
||||
}
|
||||
|
||||
private function _insertRating($sku, $score) {
|
||||
$db = $this->_dbConnect();
|
||||
if($db) {
|
||||
$stmt = $db->prepare('insert into ratings(sku, avg_rating, rating_count) values(?, ?, ?)');
|
||||
if(! $stmt->execute(array($sku, $score, 1))) {
|
||||
$this->logger->error('failed to insert data');
|
||||
throw new Exception('Failed to insert data', 500);
|
||||
}
|
||||
} else {
|
||||
$this->logger->error('database connection error');
|
||||
throw new Exception('Database connection error', 500);
|
||||
}
|
||||
}
|
||||
|
||||
private function _dbConnect() {
|
||||
$dsn = getenv('PDO_URL') ? getenv('PDO_URL') : 'mysql:host=mysql;dbname=ratings;charset=utf8mb4';
|
||||
$opt = array(
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false
|
||||
);
|
||||
|
||||
$db = false;
|
||||
try {
|
||||
$db = new PDO($dsn, 'ratings', 'iloveit', $opt);
|
||||
} catch (PDOException $e) {
|
||||
$msg = $e->getMessage();
|
||||
$this->logger->error("Database error $msg");
|
||||
$db = false;
|
||||
}
|
||||
|
||||
return $db;
|
||||
}
|
||||
|
||||
// check sku exists in product catalogue
|
||||
private function _checkSku($sku) {
|
||||
$url = getenv('CATALOGUE_URL') ? getenv('CATALOGUE_URL') : 'http://catalogue:8080/';
|
||||
$url = $url . 'product/' . $sku;
|
||||
|
||||
$opt = array(
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
);
|
||||
$curl = curl_init($url);
|
||||
curl_setopt_array($curl, $opt);
|
||||
|
||||
$data = curl_exec($curl);
|
||||
if(! $data) {
|
||||
$this->logger->error('failed to connect to catalogue');
|
||||
throw new Exception('Failed to connect to catalogue', 500);
|
||||
}
|
||||
$status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
|
||||
$this->logger->info("catalogue status $status");
|
||||
|
||||
curl_close($curl);
|
||||
|
||||
return $status == 200;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if(!array_key_exists('HTTP_ORIGIN', $_SERVER)) {
|
||||
$_SERVER['HTTP_ORIGIN'] = $_SERVER['SERVER_NAME'];
|
||||
}
|
||||
|
||||
try {
|
||||
$API = new RatingsAPI($_REQUEST['request'], $_SERVER['HTTP_ORIGIN']);
|
||||
echo $API->processAPI();
|
||||
} catch(Exception $e) {
|
||||
echo json_encode(Array('error' => $e->getMessage()));
|
||||
}
|
||||
?>
|
15
ratings/html/index.php
Normal file
15
ratings/html/index.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require __DIR__.'/vendor/autoload.php';
|
||||
|
||||
use Instana\RobotShop\Ratings\Kernel;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
$env = getenv('APP_ENV') ?: 'dev';
|
||||
$kernel = new Kernel($env, true);
|
||||
$request = Request::createFromGlobals();
|
||||
$response = $kernel->handle($request);
|
||||
$response->send();
|
||||
$kernel->terminate($request, $response);
|
@@ -1 +1,3 @@
|
||||
<?php phpinfo(); ?>
|
||||
<?php
|
||||
|
||||
phpinfo();
|
||||
|
46
ratings/html/src/Controller/HealthController.php
Normal file
46
ratings/html/src/Controller/HealthController.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Instana\RobotShop\Ratings\Controller;
|
||||
|
||||
use Instana\RobotShop\Ratings\Service\HealthCheckService;
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerAwareTrait;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* @Route("/_health")
|
||||
*/
|
||||
class HealthController implements LoggerAwareInterface
|
||||
{
|
||||
use LoggerAwareTrait;
|
||||
|
||||
/**
|
||||
* @var HealthCheckService
|
||||
*/
|
||||
private $healthCheckService;
|
||||
|
||||
public function __construct(HealthCheckService $healthCheckService)
|
||||
{
|
||||
$this->healthCheckService = $healthCheckService;
|
||||
}
|
||||
|
||||
public function __invoke(Request $request)
|
||||
{
|
||||
$checks = [];
|
||||
try {
|
||||
$this->healthCheckService->checkConnectivity();
|
||||
$checks['pdo_connectivity'] = true;
|
||||
} catch (\PDOException $e) {
|
||||
$checks['pdo_connectivity'] = false;
|
||||
}
|
||||
|
||||
$this->logger->info('Health-Check', $checks);
|
||||
|
||||
return new JsonResponse($checks, $checks['pdo_connectivity'] ? Response::HTTP_OK : Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
}
|
90
ratings/html/src/Controller/RatingsApiController.php
Normal file
90
ratings/html/src/Controller/RatingsApiController.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Instana\RobotShop\Ratings\Controller;
|
||||
|
||||
use Instana\RobotShop\Ratings\Service\CatalogueService;
|
||||
use Instana\RobotShop\Ratings\Service\RatingsService;
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerAwareTrait;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* @Route("/api")
|
||||
*/
|
||||
class RatingsApiController implements LoggerAwareInterface
|
||||
{
|
||||
use LoggerAwareTrait;
|
||||
|
||||
/**
|
||||
* @var RatingsService
|
||||
*/
|
||||
private $ratingsService;
|
||||
|
||||
/**
|
||||
* @var CatalogueService
|
||||
*/
|
||||
private $catalogueService;
|
||||
|
||||
public function __construct(CatalogueService $catalogueService, RatingsService $ratingsService)
|
||||
{
|
||||
$this->ratingsService = $ratingsService;
|
||||
$this->catalogueService = $catalogueService;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/rate/{sku}/{score}", methods={"PUT"})
|
||||
*/
|
||||
public function put(Request $request, string $sku, int $score): Response
|
||||
{
|
||||
$score = min(max(1, $score), 5);
|
||||
|
||||
try {
|
||||
if (false === $this->catalogueService->checkSKU($sku)) {
|
||||
throw new NotFoundHttpException("$sku not found");
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
throw new HttpException(500, $e->getMessage(), $e);
|
||||
}
|
||||
|
||||
try {
|
||||
$rating = $this->ratingsService->ratingBySku($sku);
|
||||
if (0 === $rating['avg_rating']) {
|
||||
// not rated yet
|
||||
$this->ratingsService->addRatingForSKU($sku, $score);
|
||||
} else {
|
||||
// iffy maths
|
||||
$newAvg = (($rating['avg_rating'] * $rating['rating_count']) + $score) / ($rating['rating_count'] + 1);
|
||||
$this->ratingsService->updateRatingForSKU($sku, $newAvg, $rating['rating_count'] + 1);
|
||||
}
|
||||
|
||||
return new JsonResponse([
|
||||
'success' => true,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
throw new HttpException(500, 'Unable to update rating', $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/fetch/{sku}", methods={"GET"})
|
||||
*/
|
||||
public function get(Request $request, string $sku): Response
|
||||
{
|
||||
try {
|
||||
if (!$this->ratingsService->ratingBySku($sku)) {
|
||||
throw new NotFoundHttpException("$sku not found");
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
throw new HttpException(500, $e->getMessage(), $e);
|
||||
}
|
||||
|
||||
return new JsonResponse($this->ratingsService->ratingBySku($sku));
|
||||
}
|
||||
}
|
55
ratings/html/src/Database.php
Normal file
55
ratings/html/src/Database.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Instana\RobotShop\Ratings;
|
||||
|
||||
use PDO;
|
||||
use PDOException;
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerAwareTrait;
|
||||
|
||||
class Database implements LoggerAwareInterface
|
||||
{
|
||||
use LoggerAwareTrait;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $dsn;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $user;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $password;
|
||||
|
||||
public function __construct(string $dsn, string $user, string $password)
|
||||
{
|
||||
$this->dsn = $dsn;
|
||||
$this->user = $user;
|
||||
$this->password = $password;
|
||||
}
|
||||
|
||||
public function getConnection(): PDO
|
||||
{
|
||||
$opt = [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
];
|
||||
|
||||
try {
|
||||
return new PDO($this->dsn, $this->user, $this->password, $opt);
|
||||
} catch (PDOException $e) {
|
||||
$msg = $e->getMessage();
|
||||
$this->logger->error("Database error $msg");
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Instana\RobotShop\Ratings\Integration;
|
||||
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpKernel\Event\FinishRequestEvent;
|
||||
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
use Symfony\Contracts\Service\ResetInterface;
|
||||
|
||||
class InstanaHeadersLoggingProcessor implements EventSubscriberInterface, ResetInterface
|
||||
{
|
||||
private $routeData;
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
KernelEvents::REQUEST => ['addHeaderData', 1],
|
||||
KernelEvents::FINISH_REQUEST => ['removeHeaderData', 1],
|
||||
];
|
||||
}
|
||||
|
||||
public function __invoke(array $records): array
|
||||
{
|
||||
if ($this->routeData && !isset($records['extra']['requests'])) {
|
||||
$records['extra']['instana'] = array_values($this->routeData);
|
||||
}
|
||||
|
||||
return $records;
|
||||
}
|
||||
|
||||
public function addHeaderData(RequestEvent $event): void
|
||||
{
|
||||
if ($event->isMasterRequest()) {
|
||||
$this->reset();
|
||||
}
|
||||
|
||||
$request = $event->getRequest();
|
||||
if (0 === $request->headers->get('X-INSTANA-L', 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$currentTraceHeaders = [
|
||||
'l' => $request->headers->get('X-INSTANA-L', 'n/a'),
|
||||
's' => $request->headers->get('X-INSTANA-S', 'n/a'),
|
||||
't' => $request->headers->get('X-INSTANA-T', 'n/a'),
|
||||
];
|
||||
|
||||
if (0 !== $request->headers->get('X-INSTANA-SYNTHETIC', 0)) {
|
||||
$currentTraceHeaders['sy'] = $request->headers->get('X-INSTANA-SYNTHETIC', 0);
|
||||
}
|
||||
|
||||
$this->routeData[spl_object_id($request)] = $currentTraceHeaders;
|
||||
}
|
||||
|
||||
public function reset(): void
|
||||
{
|
||||
$this->routeData = [];
|
||||
}
|
||||
|
||||
public function removeHeaderData(FinishRequestEvent $event): void
|
||||
{
|
||||
$requestId = spl_object_id($event->getRequest());
|
||||
unset($this->routeData[$requestId]);
|
||||
}
|
||||
}
|
123
ratings/html/src/Kernel.php
Normal file
123
ratings/html/src/Kernel.php
Normal file
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Instana\RobotShop\Ratings;
|
||||
|
||||
use Instana\RobotShop\Ratings\Controller\HealthController;
|
||||
use Instana\RobotShop\Ratings\Controller\RatingsApiController;
|
||||
use Instana\RobotShop\Ratings\Integration\InstanaHeadersLoggingProcessor;
|
||||
use Instana\RobotShop\Ratings\Service\CatalogueService;
|
||||
use Instana\RobotShop\Ratings\Service\RatingsService;
|
||||
use Monolog\Formatter\LineFormatter;
|
||||
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
|
||||
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
|
||||
use Symfony\Bundle\MonologBundle\MonologBundle;
|
||||
use Symfony\Component\Config\Loader\LoaderInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpKernel\Event\ResponseEvent;
|
||||
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
use Symfony\Component\Routing\RouteCollectionBuilder;
|
||||
|
||||
class Kernel extends BaseKernel implements EventSubscriberInterface
|
||||
{
|
||||
use MicroKernelTrait;
|
||||
|
||||
public function registerBundles()
|
||||
{
|
||||
return [
|
||||
new FrameworkBundle(),
|
||||
new MonologBundle(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function getSubscribedEvents()
|
||||
{
|
||||
return [
|
||||
KernelEvents::RESPONSE => 'corsResponseFilter',
|
||||
];
|
||||
}
|
||||
|
||||
public function corsResponseFilter(ResponseEvent $event)
|
||||
{
|
||||
$response = $event->getResponse();
|
||||
|
||||
$response->headers->add([
|
||||
'Access-Control-Allow-Origin' => '*',
|
||||
'Access-Control-Allow-Methods: *',
|
||||
]);
|
||||
}
|
||||
|
||||
protected function configureContainer(ContainerBuilder $c, LoaderInterface $loader): void
|
||||
{
|
||||
$c->loadFromExtension('framework', [
|
||||
'secret' => 'S0ME_SECRET',
|
||||
]);
|
||||
|
||||
$c->loadFromExtension('monolog', [
|
||||
'handlers' => [
|
||||
'stdout' => [
|
||||
'type' => 'stream',
|
||||
'level' => 'info',
|
||||
'path' => 'php://stdout',
|
||||
'channels' => ['!request'],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$c->setParameter('catalogueUrl', getenv('CATALOGUE_URL') ?: 'http://catalogue:8080');
|
||||
$c->setParameter('pdo_dsn', getenv('PDO_URL') ?: 'mysql:host=mysql;dbname=ratings;charset=utf8mb4');
|
||||
$c->setParameter('pdo_user', 'ratings');
|
||||
$c->setParameter('pdo_password', 'iloveit');
|
||||
$c->setParameter('logger.name', 'RatingsAPI');
|
||||
|
||||
$c->register(InstanaHeadersLoggingProcessor::class)
|
||||
->addTag('kernel.event_subscriber')
|
||||
->addTag('monolog.processor');
|
||||
|
||||
$c->register('monolog.formatter.instana_headers', LineFormatter::class)
|
||||
->addArgument('[%%datetime%%] [%%extra.token%%] %%channel%%.%%level_name%%: %%message%% %%context%% %%extra%%\n');
|
||||
|
||||
$c->register(Database::class)
|
||||
->addArgument($c->getParameter('pdo_dsn'))
|
||||
->addArgument($c->getParameter('pdo_user'))
|
||||
->addArgument($c->getParameter('pdo_password'))
|
||||
->addMethodCall('setLogger', [new Reference('logger')])
|
||||
->setAutowired(true);
|
||||
|
||||
$c->register(CatalogueService::class)
|
||||
->addArgument($c->getParameter('catalogueUrl'))
|
||||
->addMethodCall('setLogger', [new Reference('logger')])
|
||||
->setAutowired(true);
|
||||
|
||||
$c->register('database.connection', \PDO::class)
|
||||
->setFactory([new Reference(Database::class), 'getConnection']);
|
||||
|
||||
$c->setAlias(\PDO::class, 'database.connection');
|
||||
|
||||
$c->register(RatingsService::class)
|
||||
->addMethodCall('setLogger', [new Reference('logger')])
|
||||
->setAutowired(true);
|
||||
|
||||
$c->register(HealthController::class)
|
||||
->addMethodCall('setLogger', [new Reference('logger')])
|
||||
->addTag('controller.service_arguments')
|
||||
->setAutowired(true);
|
||||
|
||||
$c->register(RatingsApiController::class)
|
||||
->addMethodCall('setLogger', [new Reference('logger')])
|
||||
->addTag('controller.service_arguments')
|
||||
->setAutowired(true);
|
||||
}
|
||||
|
||||
protected function configureRoutes(RouteCollectionBuilder $routes)
|
||||
{
|
||||
$routes->import(__DIR__.'/Controller/', '/', 'annotation');
|
||||
}
|
||||
}
|
48
ratings/html/src/Service/CatalogueService.php
Normal file
48
ratings/html/src/Service/CatalogueService.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Instana\RobotShop\Ratings\Service;
|
||||
|
||||
use Exception;
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerAwareTrait;
|
||||
|
||||
class CatalogueService implements LoggerAwareInterface
|
||||
{
|
||||
use LoggerAwareTrait;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $catalogueUrl;
|
||||
|
||||
public function __construct(string $catalogueUrl)
|
||||
{
|
||||
$this->catalogueUrl = $catalogueUrl;
|
||||
}
|
||||
|
||||
public function checkSKU(string $sku): bool
|
||||
{
|
||||
$url = sprintf('%s/product/%s', $this->catalogueUrl, $sku);
|
||||
|
||||
$opt = [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
];
|
||||
$curl = curl_init($url);
|
||||
curl_setopt_array($curl, $opt);
|
||||
|
||||
$data = curl_exec($curl);
|
||||
if (!$data) {
|
||||
$this->logger->error('failed to connect to catalogue');
|
||||
throw new Exception('Failed to connect to catalogue');
|
||||
}
|
||||
|
||||
$status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
|
||||
$this->logger->info("catalogue status $status");
|
||||
|
||||
curl_close($curl);
|
||||
|
||||
return 200 === $status;
|
||||
}
|
||||
}
|
25
ratings/html/src/Service/HealthCheckService.php
Normal file
25
ratings/html/src/Service/HealthCheckService.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Instana\RobotShop\Ratings\Service;
|
||||
|
||||
use PDO;
|
||||
|
||||
class HealthCheckService
|
||||
{
|
||||
/**
|
||||
* @var PDO
|
||||
*/
|
||||
private $pdo;
|
||||
|
||||
public function __construct(PDO $pdo)
|
||||
{
|
||||
$this->pdo = $pdo;
|
||||
}
|
||||
|
||||
public function checkConnectivity()
|
||||
{
|
||||
return $this->pdo->prepare('SELECT 1 + 1 FROM DUAL;')->execute();
|
||||
}
|
||||
}
|
67
ratings/html/src/Service/RatingsService.php
Normal file
67
ratings/html/src/Service/RatingsService.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Instana\RobotShop\Ratings\Service;
|
||||
|
||||
use PDO;
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerAwareTrait;
|
||||
|
||||
class RatingsService implements LoggerAwareInterface
|
||||
{
|
||||
private const QUERY_RATINGS_BY_SKU = 'select avg_rating, rating_count from ratings where sku = ?';
|
||||
private const QUERY_UPDATE_RATINGS_BY_SKU = 'update ratings set avg_rating = ?, rating_count = ? where sku = ?';
|
||||
private const QUERY_INSERT_RATING = 'insert into ratings(sku, avg_rating, rating_count) values(?, ?, ?)';
|
||||
|
||||
use LoggerAwareTrait;
|
||||
|
||||
/**
|
||||
* @var PDO
|
||||
*/
|
||||
private $connection;
|
||||
|
||||
public function __construct(PDO $connection)
|
||||
{
|
||||
$this->connection = $connection;
|
||||
}
|
||||
|
||||
public function ratingBySku(string $sku): array
|
||||
{
|
||||
$stmt = $this->connection->prepare(self::QUERY_RATINGS_BY_SKU);
|
||||
if (false === $stmt->execute([$sku])) {
|
||||
$this->logger->error('failed to query data');
|
||||
|
||||
throw new \Exception('Failed to query data', 500);
|
||||
}
|
||||
|
||||
$data = $stmt->fetch();
|
||||
if ($data) {
|
||||
// for some reason avg_rating is return as a string
|
||||
$data['avg_rating'] = (float) $data['avg_rating'];
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
// nicer to return an empty record than throw 404
|
||||
return ['avg_rating' => 0, 'rating_count' => 0];
|
||||
}
|
||||
|
||||
public function updateRatingForSKU(string $sku, $score, int $count): void
|
||||
{
|
||||
$stmt = $this->connection->prepare(self::QUERY_UPDATE_RATINGS_BY_SKU);
|
||||
if (!$stmt->execute([$score, $count, $sku])) {
|
||||
$this->logger->error('failed to update rating');
|
||||
throw new \Exception('Failed to update data', 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function addRatingForSKU($sku, $rating): void
|
||||
{
|
||||
$stmt = $this->connection->prepare(self::QUERY_INSERT_RATING);
|
||||
if (!$stmt->execute([$sku, $rating, 1])) {
|
||||
$this->logger->error('failed to insert data');
|
||||
throw new \Exception('Failed to insert data', 500);
|
||||
}
|
||||
}
|
||||
}
|
0
ratings/html/var/cache/.gitkeep
vendored
Normal file
0
ratings/html/var/cache/.gitkeep
vendored
Normal file
0
ratings/html/var/log/.gitkeep
Normal file
0
ratings/html/var/log/.gitkeep
Normal file
Reference in New Issue
Block a user