Fixing stats page, replacing ob/highcharts-bundle with own implementation based on ghunti/highcharts-php. Some refactoring.

This commit is contained in:
Alexey Skobkin 2023-08-18 20:20:26 +03:00
parent 3a69565ecb
commit 4c33ce9d84
No known key found for this signature in database
GPG key ID: 5D5CEF6F221278E7
11 changed files with 225 additions and 123 deletions

View file

@ -13,6 +13,7 @@
"doctrine/doctrine-fixtures-bundle": "^3.4", "doctrine/doctrine-fixtures-bundle": "^3.4",
"doctrine/doctrine-migrations-bundle": "^3.2", "doctrine/doctrine-migrations-bundle": "^3.2",
"doctrine/orm": "^2.14", "doctrine/orm": "^2.14",
"ghunti/highcharts-php": "^5.0",
"jms/serializer-bundle": "^5.2", "jms/serializer-bundle": "^5.2",
"knplabs/knp-paginator-bundle": "^6.2", "knplabs/knp-paginator-bundle": "^6.2",
"league/commonmark": "^2.4", "league/commonmark": "^2.4",

56
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "6088d0629768085da296aa5598445c4f", "content-hash": "274812d219a3d19c925c0e67b5f86926",
"packages": [ "packages": [
{ {
"name": "dflydev/dot-access-data", "name": "dflydev/dot-access-data",
@ -1635,6 +1635,60 @@
}, },
"time": "2022-05-23T21:33:49+00:00" "time": "2022-05-23T21:33:49+00:00"
}, },
{
"name": "ghunti/highcharts-php",
"version": "v5.0.0",
"source": {
"type": "git",
"url": "https://github.com/ghunti/HighchartsPHP.git",
"reference": "7bccba4278fcc5d3a50cf6aa35975b0943a03cad"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ghunti/HighchartsPHP/zipball/7bccba4278fcc5d3a50cf6aa35975b0943a03cad",
"reference": "7bccba4278fcc5d3a50cf6aa35975b0943a03cad",
"shasum": ""
},
"require": {
"php": ">=8.0"
},
"require-dev": {
"phpunit/phpunit": "^9.5"
},
"type": "library",
"autoload": {
"psr-4": {
"Ghunti\\HighchartsPHP\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"GPL-3.0"
],
"authors": [
{
"name": "Gonçalo Queirós",
"email": "mail@goncaloqueiros.net",
"homepage": "http://goncaloqueiros.net",
"role": "Developer"
}
],
"description": "A php wrapper for highcharts and highstock javascript libraries",
"homepage": "https://goncaloqueiros.net/highcharts.php",
"keywords": [
"charts",
"highcharts",
"highstock",
"javascript",
"php"
],
"support": {
"email": "mail@goncaloqueiros.net",
"issues": "https://github.com/ghunti/HighchartsPHP/issues",
"source": "https://github.com/ghunti/HighchartsPHP"
},
"time": "2023-04-26T21:37:31+00:00"
},
{ {
"name": "jms/metadata", "name": "jms/metadata",
"version": "2.8.0", "version": "2.8.0",

View file

@ -26,7 +26,7 @@ user_show:
statistics: statistics:
path: /statistics path: /statistics
defaults: { _controller: App\Controller\UserController::top } defaults: { _controller: App\Controller\StatsController::show }
methods: [GET] methods: [GET]
events_last: events_last:

View file

@ -89,7 +89,7 @@ class UserRepositoryTest extends KernelTestCase
public function testGetTopUsers() public function testGetTopUsers()
{ {
$topUsers = $this->userRepo->getTopUsers(); $topUsers = $this->userRepo->getTopUsersBySubscribersCount();
$this->assertCount(3, $topUsers, 'Found not exactly 3 top users'); $this->assertCount(3, $topUsers, 'Found not exactly 3 top users');

View file

@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace App\Chart;
use App\DTO\DailyEventsDTO;
use App\DTO\TopUserDTO;
use Ghunti\HighchartsPHP\Highchart;
use Symfony\Contracts\Translation\TranslatorInterface;
class ChartGenerator
{
public function __construct(
private readonly TranslatorInterface $translator,
) {
}
/**
* @param DailyEventsDTO[] $events
*/
public function eventsDynamicChart(array $events): Highchart
{
$data = [];
foreach ($events as $event) {
$data[$event->date->format('d.m')] = $event->eventsCount;
}
return $this->createChart('line', $data, 'Events by day', 'amount');
}
/**
* @param TopUserDTO[] $topUsers
*/
public function topUsersChart(array $topUsers): Highchart
{
$data = [];
foreach ($topUsers as $topUser) {
$data[$topUser->login] = $topUser->subscribersCount;
}
return $this->createChart('bar', $data, 'Top users', 'amount');
}
/** @see https://github.com/ghunti/HighchartsPHP/blob/master/demos/highcharts/line/basic_line.php */
private function createChart(
string $type,
array $data,
string $bottomLabel,
string $amountLabel,
): Highchart {
$c = new Highchart();
$c->chart->type = $type;
$c->title->text = $this->translator->trans($bottomLabel);
// Preparing chart data
foreach ($data as $key => $value) {
$chartData['keys'][] = $key;
$chartData['values'][] = $value;
}
$c->xAxis->title = ['text' => null];
$c->xAxis->categories = $chartData['keys'] ?? [];
$c->yAxis->title->text = $this->translator->trans($amountLabel);
$c->yAxis->plotOptions->bar->dataLabels->enabled = true;
$c->series[] = [
'name' => $this->translator->trans($amountLabel),
'data' => $chartData['values'] ?? [],
];
return $c;
}
}

View file

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Chart\ChartGenerator;
use App\Repository\{SubscriptionEventRepository, UserRepository};
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
class StatsController extends AbstractController
{
public function show(
UserRepository $userRepository,
SubscriptionEventRepository $subscriptionEventRepository,
ChartGenerator $chartGenerator,
): Response {
$topUsers = $userRepository->getTopUsersBySubscribersCount();
$eventsByDay = $subscriptionEventRepository->getLastEventsByDay();
return $this->render('Web/User/top.html.twig', [
'events_dynamic_chat' => $chartGenerator->eventsDynamicChart($eventsByDay),
'top_chart' => $chartGenerator->topUsersChart($topUsers),
]);
}
}

View file

@ -3,29 +3,21 @@ declare(strict_types=1);
namespace App\Controller; namespace App\Controller;
use App\DTO\{TopUserDTO, DailyEventsDTO};
use Knp\Component\Pager\PaginatorInterface;
use Ob\HighchartsBundle\Highcharts\Highchart;
use App\Entity\User; use App\Entity\User;
use App\Repository\{SubscriptionEventRepository, UserRenameEventRepository, UserRepository}; use App\Repository\{SubscriptionEventRepository, UserRenameEventRepository, UserRepository};
use Knp\Component\Pager\PaginatorInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\{Request, Response}; use Symfony\Component\HttpFoundation\{Request, Response};
use Symfony\Contracts\Translation\TranslatorInterface;
class UserController extends AbstractController class UserController extends AbstractController
{ {
public function __construct(
private readonly TranslatorInterface $translator,
) {
}
public function show( public function show(
Request $request, Request $request,
string $login, string $login,
SubscriptionEventRepository $subscriptionEventRepository, SubscriptionEventRepository $subscriptionEventRepository,
UserRepository $userRepository, UserRepository $userRepository,
UserRenameEventRepository $renameEventRepository, UserRenameEventRepository $renameEventRepository,
PaginatorInterface $paginator PaginatorInterface $paginator,
): Response { ): Response {
/** @var User $user */ /** @var User $user */
$user = $userRepository->findUserByLogin($login); $user = $userRepository->findUserByLogin($login);
@ -37,7 +29,7 @@ class UserController extends AbstractController
$subscriberEventsPagination = $paginator->paginate( $subscriberEventsPagination = $paginator->paginate(
$subscriptionEventRepository->createUserLastSubscribersEventsQuery($user), $subscriptionEventRepository->createUserLastSubscribersEventsQuery($user),
$request->query->getInt('page', 1), $request->query->getInt('page', 1),
10 10,
); );
return $this->render('Web/User/show.html.twig', [ return $this->render('Web/User/show.html.twig', [
@ -47,84 +39,4 @@ class UserController extends AbstractController
'rename_log' => $renameEventRepository->findBy(['user' => $user], ['date' => 'DESC'], 10), 'rename_log' => $renameEventRepository->findBy(['user' => $user], ['date' => 'DESC'], 10),
]); ]);
} }
public function top(UserRepository $userRepository, SubscriptionEventRepository $subscriptionEventRepository): Response
{
$topUsers = $userRepository->getTopUsers();
$eventsByDay = $subscriptionEventRepository->getLastEventsByDay();
return $this->render('Web/User/top.html.twig', [
'events_dynamic_chat' => $this->createEventsDynamicChart($eventsByDay),
'top_chart' => $this->createTopUsersGraph($topUsers),
]);
}
/**
* @param DailyEventsDTO[] $eventsByDay
*@todo move to the service
*
*/
private function createEventsDynamicChart(array $eventsByDay = []): Highchart
{
$data = [];
foreach ($eventsByDay as $dailyEvents) {
$data[$dailyEvents->date->format('d.m')] = $dailyEvents->eventsCount;
}
return $this->createChart('eventschart', 'line', $data, 'Events by day', 'amount');
}
/**
* @todo move to the service
*
* @param TopUserDTO[] $topUsers
*/
private function createTopUsersGraph(array $topUsers = []): Highchart
{
$data = [];
foreach ($topUsers as $topUser) {
$data[$topUser->login] = $topUser->subscribersCount;
}
return $this->createChart('topchart', 'bar', $data, 'Top users', 'amount');
}
private function createChart(string $blockId, string $type, array $data, string $bottomLabel, string $amountLabel): Highchart
{
$chartData = [
'keys' => [],
'values' => [],
];
// Preparing chart data
foreach ($data as $key => $value) {
$chartData['keys'][] = $key;
$chartData['values'][] = $value;
}
// Chart
$series = [[
'name' => $this->translator->trans($amountLabel),
'data' => $chartData['values'],
]];
// Initializing chart
$ob = new Highchart();
$ob->chart->renderTo($blockId);
$ob->chart->type($type);
$ob->title->text($this->translator->trans($bottomLabel));
$ob->xAxis->title(['text' => null]);
$ob->xAxis->categories($chartData['keys']);
$ob->yAxis->title(['text' => $this->translator->trans($amountLabel)]);
$ob->plotOptions->bar([
'dataLabels' => [
'enabled' => true
]
]);
$ob->series($series);
return $ob;
}
} }

View file

@ -5,8 +5,7 @@ declare(strict_types=1);
namespace App\Repository; namespace App\Repository;
use App\DTO\DailyEventsDTO; use App\DTO\DailyEventsDTO;
use App\Entity\SubscriptionEvent; use App\Entity\{SubscriptionEvent, User};
use App\Entity\User;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ManagerRegistry; use Doctrine\Persistence\ManagerRegistry;
@ -63,11 +62,7 @@ class SubscriptionEventRepository extends ServiceEntityRepository
; ;
} }
/** /** @return list<SubscriptionEvent> */
* Get last user subscriber events
*
* @return SubscriptionEvent[]
*/
public function getUserLastSubscribersEvents(User $user, int $limit = 20): array public function getUserLastSubscribersEvents(User $user, int $limit = 20): array
{ {
$qb = $this->createUserLastSubscribersEventsQuery($user); $qb = $this->createUserLastSubscribersEventsQuery($user);
@ -89,11 +84,7 @@ class SubscriptionEventRepository extends ServiceEntityRepository
; ;
} }
/** /** @return SubscriptionEvent[] */
* Get last global subscription events
*
* @return SubscriptionEvent[]
*/
public function getLastSubscriptionEvents(int $limit = 20): array public function getLastSubscriptionEvents(int $limit = 20): array
{ {
$qb = $this->createLastSubscriptionEventsQuery(); $qb = $this->createLastSubscriptionEventsQuery();

View file

@ -4,8 +4,7 @@ declare(strict_types=1);
namespace App\Repository; namespace App\Repository;
use App\DTO\TopUserDTO; use App\DTO\TopUserDTO;
use App\Entity\Subscription; use App\Entity\{Subscription, User};
use App\Entity\User;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry; use Doctrine\Persistence\ManagerRegistry;
@ -108,12 +107,8 @@ class UserRepository extends ServiceEntityRepository
; ;
} }
/** /** @return list<TopUserDTO> */
* Returns top users by subscribers count public function getTopUsersBySubscribersCount(int $limit = 30): array
*
* @return TopUserDTO[]
*/
public function getTopUsers(int $limit = 30): array
{ {
$qb = $this->getEntityManager()->getRepository(Subscription::class)->createQueryBuilder('s'); $qb = $this->getEntityManager()->getRepository(Subscription::class)->createQueryBuilder('s');

View file

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace App\Twig;
use Ghunti\HighchartsPHP\Highchart;
use Twig\Extension\AbstractExtension;
use Twig\{TwigFilter, TwigFunction};
class HighchartExtension extends AbstractExtension
{
public function getFilters(): array
{
return [
new TwigFilter('hc_scripts', [$this, 'printScripts'], ['is_safe' => ['html']]),
new TwigFilter('hc_render', [$this, 'render'], ['is_safe' => ['html']]),
new TwigFilter('hc_options', [$this, 'renderOptions'], ['is_safe' => ['html']]),
];
}
public function getFunctions(): array
{
return [
new TwigFunction('hc_scripts', [$this, 'printScripts'], ['is_safe' => ['html']]),
new TwigFunction('hc_render', [$this, 'render'], ['is_safe' => ['html']]),
new TwigFunction('hc_options', [$this, 'renderOptions'], ['is_safe' => ['html']]),
];
}
public function printScripts(Highchart $c): string
{
return $c->printScripts(true);
}
public function render(Highchart $c, string $blockId, ?string $varName = null, bool $withScriptTag = false): string
{
$c->chart->renderTo = $blockId;
return $c->render($varName, withScriptTag: $withScriptTag);
}
public function renderOptions(Highchart $c): string
{
return $c->renderOptions();
}
}

View file

@ -9,14 +9,13 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<script type="text/javascript">
// Top chart
{{ chart(top_chart) }}
// Events by day chart
{{ chart(events_dynamic_chat) }}
</script>
<div id="topchart" style="min-width: 400px; height: 600px; margin: 0 auto;"></div> <div id="topchart" style="min-width: 400px; height: 600px; margin: 0 auto;"></div>
<div id="eventschart" style="min-width: 400px; height: 600px; margin: 0 auto;"></div> <div id="eventschart" style="min-width: 400px; height: 600px; margin: 0 auto;"></div>
<script type="text/javascript">
$(function() {
{{ top_chart | hc_render('topchart') }}
{{ events_dynamic_chat | hc_render('eventschart') }}
});
</script>
{% endblock %} {% endblock %}