...
 
Commits (2)
<?php
namespace Minds\Api;
abstract class Api implements \Minds\Interfaces\Api
{
protected $accessControlAllowHeaders = [];
public function __construct()
{
$this->sendAccessControlHeaders();
}
protected function sendAccessControlHeaders(): void
{
header('Access-Control-Allow-Origin: *', false);
//header("Access-Control-Allow-Headers: " . $this->accessControlAllowHeaders(), false);
//header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");
}
protected function accessControlAllowHeaders(): string
{
$output = "";
$lastHeader = end($this->accessControlAllowHeaders);
foreach ($this->accessControlAllowHeaders as $header) {
$output .= $header;
if ($header !== $lastHeader)
$output .= ",";
}
return $output;
}
protected function setResponseCode(int $code = 200): int
{
switch ($code) {
case 100:
$text = 'Continue';
break;
case 101:
$text = 'Switching Protocols';
break;
case 200:
$text = 'OK';
break;
case 201:
$text = 'Created';
break;
case 202:
$text = 'Accepted';
break;
case 203:
$text = 'Non-Authoritative Information';
break;
case 204:
$text = 'No Content';
break;
case 205:
$text = 'Reset Content';
break;
case 206:
$text = 'Partial Content';
break;
case 300:
$text = 'Multiple Choices';
break;
case 301:
$text = 'Moved Permanently';
break;
case 302:
$text = 'Moved Temporarily';
break;
case 303:
$text = 'See Other';
break;
case 304:
$text = 'Not Modified';
break;
case 305:
$text = 'Use Proxy';
break;
case 400:
$text = 'Bad Request';
break;
case 401:
$text = 'Unauthorized';
break;
case 402:
$text = 'Payment Required';
break;
case 403:
$text = 'Forbidden';
break;
case 404:
$text = 'Not Found';
break;
case 405:
$text = 'Method Not Allowed';
break;
case 406:
$text = 'Not Acceptable';
break;
case 407:
$text = 'Proxy Authentication Required';
break;
case 408:
$text = 'Request Time-out';
break;
case 409:
$text = 'Conflict';
break;
case 410:
$text = 'Gone';
break;
case 411:
$text = 'Length Required';
break;
case 412:
$text = 'Precondition Failed';
break;
case 413:
$text = 'Request Entity Too Large';
break;
case 414:
$text = 'Request-URI Too Large';
break;
case 415:
$text = 'Unsupported Media Type';
break;
case 500:
$text = 'Internal Server Error';
break;
case 501:
$text = 'Not Implemented';
break;
case 502:
$text = 'Bad Gateway';
break;
case 503:
$text = 'Service Unavailable';
break;
case 504:
$text = 'Gateway Time-out';
break;
case 505:
$text = 'HTTP Version not supported';
break;
default:
exit('Unknown http status code "' . htmlentities($code) . '"');
break;
}
$protocol = (isset($_SERVER['SERVER_PROTOCOL']) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0');
header("${protocol} ${code} ${text}");
return $code;
}
protected function sendArrayOfObjects($array, int $code = 200): void
{
$this->send(array_values($array), $code);
}
protected function send($responseArray, int $code = 200, $jsonOptions = 0): void
{
$returnString = json_encode($responseArray, $jsonOptions);
$this->sendJsonString($returnString, $code);
}
protected function sendJsonString(string $jsonString, int $code = 200): void
{
header('Content-Type: application/json');
header('Content-Length:' . strlen($jsonString));
$this->setResponseCode($code);
echo $jsonString;
}
protected function sendInternalServerError(string $message = 'Internal Server Error'): void
{
$this->sendError($message, 500);
}
protected function sendBadRequest(): void
{
$this->sendError('Bad Request', 400);
}
protected function sendNotImplemented(): void
{
$this->sendError('Not Implemented', 501);
}
protected function sendNotModified(string $message = 'Not Modified'): void
{
$this->sendError($message, 304);
}
protected function sendNotAcceptable(string $message = 'Not Acceptable'): void
{
$this->sendError($message, 406);
}
protected function sendUnauthorised(): void
{
$this->sendError('Unauthorised', 401);
}
protected function sendSuccess(): void
{
$this->send(['status' => 'success']);
}
protected function sendError(string $message, int $code = 406): void
{
$this->send($this->buildError($message), $code);
}
protected function buildError(string $message): array
{
return ["message" => $message];
}
public function get($pages): void
{
$this->sendNotImplemented();
}
public function post($pages): void
{
$this->sendNotImplemented();
}
public function put($pages): void
{
$this->sendNotImplemented();
}
public function delete($pages): void
{
$this->sendNotImplemented();
}
}
......@@ -27,7 +27,6 @@ class Factory
// Helpers\RequestMetrics::increment('api');
//} catch (\Exception $e) {
//}
$method = strtolower($_SERVER['REQUEST_METHOD']);
$route = implode('\\', $segments);
......
......@@ -168,7 +168,7 @@ class Analytics extends Cli\Controller implements Interfaces\CliControllerInterf
$from = $this->getOpt('from') ?: strtotime('-10 days');
$to = $this->getOpt('to') ?: strtotime('-1 day');
$boostViews = new Core\Analytics\EntityCentric\BoostViewsData();
$boostViews = new Core\Analytics\EntityCentric\BoostViewsDaily();
$data = $boostViews->getDataSetForDateRange($from, $to);
print_r($data);
}
......
<?php
namespace Minds\Controllers\api\v2\boost\campaigns;
use Minds\Api\Api;
use Minds\Core\Analytics\EntityCentric\BoostViewsDaily;
class analytics extends Api
{
public function get($pages): void
{
switch ($pages[0]) {
case 'rate':
// Get current boost rate
$avgRate = (new BoostViewsDaily())->lastSevenDays()->getAvg();
$this->send(['rate' => $avgRate]);
break;
case 'days':
$days = (new BoostViewsDaily())->lastSevenDays()->getAll();
$this->send(['days' => $days]);
break;
default:
$this->sendBadRequest();
}
}
}
......@@ -7,53 +7,75 @@ use Minds\Core\Data\ElasticSearch\Prepared\Search;
use Minds\Core\Di\Di;
use Minds\Core\Time;
class BoostViewsData
class BoostViewsDaily
{
/**
* @var Client
*/
/** @var Client */
protected $es;
/** @var array */
protected $dailyViews = [];
/** @var int */
protected $totalViews = 0;
/** @var int */
protected $startDayMs;
/** @var int */
protected $endDayMs;
public function __construct(Client $esClient = null)
{
$this->es = $esClient ?: Di::_()->get('Database\ElasticSearch');
$this->lastSevenDays();
}
protected function clearData(): void
{
$this->dailyViews = [];
$this->totalViews = 0;
}
public function lastSevenDays(): self
{
return $this->dateRange(strtotime('yesterday -1 week'), strtotime('yesterday'));
}
public function dateRange(int $start, int $end): self
{
$this->clearData();
$this->startDayMs = Time::toInterval($start, Time::ONE_DAY) * 1000;
$this->endDayMs = Time::toInterval($end, Time::ONE_DAY) * 1000;
return $this;
}
/**
* {
* "query": {
* "range" : {
* "views::boosted" : {
* "gt" : 0
* }
* }
* },
* "size": 0,
* "aggs" : {
* "boost_views" : { "sum" : { "field" : "views::boosted" } },
* "boost_views_daily" : {
* "date_histogram": {
* "field": "@timestamp",
* "interval": "1d"
* },
* "aggs" : {
* "boost_views" : { "sum" : { "field" : "views::boosted" } }
* }
* }
* }
* }
*/
public function getDataSetForDateRange(int $start, int $end): array
protected function query(): void
{
$startDayMs = Time::toInterval($start, Time::ONE_DAY) * 1000;
$endDayMs = Time::toInterval($end, Time::ONE_DAY) * 1000;
if (!empty($this->dailyViews))
return;
$prepared = new Search();
$prepared->query($this->buildQuery());
$response = $this->es->request($prepared);
if (isset($response['aggregations']['boost_views_total'])) {
$this->totalViews = $response['aggregations']['boost_views_total']['value'];
}
if (isset($response['aggregations']['boost_views_daily']['buckets'])) {
foreach ($response['aggregations']['boost_views_daily']['buckets'] as $bucket) {
$this->dailyViews[$bucket['key']] = $bucket['boost_views']['value'];
}
}
}
protected function buildQuery(): array
{
$must = [
'range' => [
'@timestamp' => [
'gte' => $startDayMs,
'lte' => $endDayMs,
'gte' => $this->startDayMs,
'lte' => $this->endDayMs,
]
]
];
......@@ -90,25 +112,30 @@ class BoostViewsData
]
];
$prepared = new Search();
$prepared->query($query);
$response = $this->es->request($prepared);
return $query;
}
$dataset = [
'total' => 0,
'daily' => []
];
public function getAll(): array
{
$this->query();
return $this->dailyViews;
}
if (isset($response['aggregations']['boost_views_total'])) {
$dataset['total'] = $response['aggregations']['boost_views_total']['value'];
}
public function getTotal(): int
{
$this->query();
return $this->totalViews;
}
if (isset($response['aggregations']['boost_views_daily']['buckets'])) {
foreach ($response['aggregations']['boost_views_daily']['buckets'] as $bucket) {
$dataset['daily'][$bucket['key']] = $bucket['boost_views']['value'];
}
}
public function getMax(): int
{
$this->query();
return max($this->dailyViews);
}
return $dataset;
public function getAvg(): float
{
$this->query();
return !empty($this->dailyViews) ? array_sum($this->dailyViews) / count($this->dailyViews) : 0;
}
}
<?php
namespace Spec\Minds\Core\Analytics\EntityCentric;
use Minds\Core\Analytics\EntityCentric\BoostViewsDaily;
use Minds\Core\Data\ElasticSearch\Client;
use Minds\Core\Data\ElasticSearch\Prepared\Search;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class BoostViewsDailySpec extends ObjectBehavior
{
/** @var Client */
protected $esClient;
/** @var array */
protected $response;
public function let(Client $esClient)
{
$this->beConstructedWith($esClient);
$this->esClient = $esClient;
$this->response = [
'aggregations' => [
'boost_views_total' => [
'value' => 1887
],
'boost_views_daily' => [
'buckets' => [
['key' => '1570060800', 'boost_views' => ['value' => 242]],
['key' => '1570147200', 'boost_views' => ['value' => 256]],
['key' => '1570233600', 'boost_views' => ['value' => 287]],
['key' => '1570320000', 'boost_views' => ['value' => 267]],
['key' => '1570406400', 'boost_views' => ['value' => 249]],
['key' => '1570492800', 'boost_views' => ['value' => 290]],
['key' => '1570579200', 'boost_views' => ['value' => 296]]
]
]
]
];
}
public function it_is_initializable()
{
$this->shouldHaveType(BoostViewsDaily::class);
}
public function it_should_set_last_seven_days_range()
{
$this->lastSevenDays()->shouldReturn($this);
}
public function it_should_set_date_range()
{
$start = strtotime('yesterday -1 day');
$end = strtotime('yesterday');
$this->dateRange($start, $end)->shouldReturn($this);
}
public function it_should_return_array_of_daily_views()
{
$this->esClient->request(Argument::type(Search::class))->shouldBeCalled()->willReturn($this->response);
$this->getAll()->shouldReturn([
'1570060800' => 242,
'1570147200' => 256,
'1570233600' => 287,
'1570320000' => 267,
'1570406400' => 249,
'1570492800' => 290,
'1570579200' => 296
]);
}
public function it_should_return_total_views()
{
$this->esClient->request(Argument::type(Search::class))->shouldBeCalled()->willReturn($this->response);
$this->getTotal()->shouldReturn(1887);
}
public function it_should_return_max_views()
{
$this->esClient->request(Argument::type(Search::class))->shouldBeCalled()->willReturn($this->response);
$this->getMax()->shouldReturn(296);
}
public function it_should_return_avg_views()
{
$this->esClient->request(Argument::type(Search::class))->shouldBeCalled()->willReturn($this->response);
$this->getAvg()->shouldBeApproximately(269.57, 0.01);
}
}