From 4c33ce9d84a6d47acbb2815049002360e2985471 Mon Sep 17 00:00:00 2001 From: Alexey Skobkin Date: Fri, 18 Aug 2023 20:20:26 +0300 Subject: [PATCH] Fixing stats page, replacing ob/highcharts-bundle with own implementation based on ghunti/highcharts-php. Some refactoring. --- composer.json | 1 + composer.lock | 56 ++++++++++- config/routes.yaml | 2 +- .../Repository/UserRepositoryTest.php | 2 +- src/Chart/ChartGenerator.php | 78 +++++++++++++++ src/Controller/StatsController.php | 26 +++++ src/Controller/UserController.php | 94 +------------------ .../SubscriptionEventRepository.php | 17 +--- src/Repository/UserRepository.php | 11 +-- src/Twig/HighchartExtension.php | 46 +++++++++ templates/Web/User/top.html.twig | 15 ++- 11 files changed, 225 insertions(+), 123 deletions(-) create mode 100644 src/Chart/ChartGenerator.php create mode 100644 src/Controller/StatsController.php create mode 100644 src/Twig/HighchartExtension.php diff --git a/composer.json b/composer.json index 45649a3..8488fc7 100644 --- a/composer.json +++ b/composer.json @@ -13,6 +13,7 @@ "doctrine/doctrine-fixtures-bundle": "^3.4", "doctrine/doctrine-migrations-bundle": "^3.2", "doctrine/orm": "^2.14", + "ghunti/highcharts-php": "^5.0", "jms/serializer-bundle": "^5.2", "knplabs/knp-paginator-bundle": "^6.2", "league/commonmark": "^2.4", diff --git a/composer.lock b/composer.lock index 950d51c..d9ff62b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "6088d0629768085da296aa5598445c4f", + "content-hash": "274812d219a3d19c925c0e67b5f86926", "packages": [ { "name": "dflydev/dot-access-data", @@ -1635,6 +1635,60 @@ }, "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", "version": "2.8.0", diff --git a/config/routes.yaml b/config/routes.yaml index ba85270..67b4fbe 100644 --- a/config/routes.yaml +++ b/config/routes.yaml @@ -26,7 +26,7 @@ user_show: statistics: path: /statistics - defaults: { _controller: App\Controller\UserController::top } + defaults: { _controller: App\Controller\StatsController::show } methods: [GET] events_last: diff --git a/old/tests/Skobkin/PointToolsBundle/Repository/UserRepositoryTest.php b/old/tests/Skobkin/PointToolsBundle/Repository/UserRepositoryTest.php index bb3327d..08d32ed 100644 --- a/old/tests/Skobkin/PointToolsBundle/Repository/UserRepositoryTest.php +++ b/old/tests/Skobkin/PointToolsBundle/Repository/UserRepositoryTest.php @@ -89,7 +89,7 @@ class UserRepositoryTest extends KernelTestCase public function testGetTopUsers() { - $topUsers = $this->userRepo->getTopUsers(); + $topUsers = $this->userRepo->getTopUsersBySubscribersCount(); $this->assertCount(3, $topUsers, 'Found not exactly 3 top users'); diff --git a/src/Chart/ChartGenerator.php b/src/Chart/ChartGenerator.php new file mode 100644 index 0000000..1858f95 --- /dev/null +++ b/src/Chart/ChartGenerator.php @@ -0,0 +1,78 @@ +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; + } +} \ No newline at end of file diff --git a/src/Controller/StatsController.php b/src/Controller/StatsController.php new file mode 100644 index 0000000..19a43d0 --- /dev/null +++ b/src/Controller/StatsController.php @@ -0,0 +1,26 @@ +getTopUsersBySubscribersCount(); + $eventsByDay = $subscriptionEventRepository->getLastEventsByDay(); + + return $this->render('Web/User/top.html.twig', [ + 'events_dynamic_chat' => $chartGenerator->eventsDynamicChart($eventsByDay), + 'top_chart' => $chartGenerator->topUsersChart($topUsers), + ]); + } +} diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index 824ded7..4aebf47 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -3,29 +3,21 @@ declare(strict_types=1); namespace App\Controller; -use App\DTO\{TopUserDTO, DailyEventsDTO}; -use Knp\Component\Pager\PaginatorInterface; -use Ob\HighchartsBundle\Highcharts\Highchart; use App\Entity\User; use App\Repository\{SubscriptionEventRepository, UserRenameEventRepository, UserRepository}; +use Knp\Component\Pager\PaginatorInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\{Request, Response}; -use Symfony\Contracts\Translation\TranslatorInterface; class UserController extends AbstractController { - public function __construct( - private readonly TranslatorInterface $translator, - ) { - } - public function show( Request $request, string $login, SubscriptionEventRepository $subscriptionEventRepository, UserRepository $userRepository, UserRenameEventRepository $renameEventRepository, - PaginatorInterface $paginator + PaginatorInterface $paginator, ): Response { /** @var User $user */ $user = $userRepository->findUserByLogin($login); @@ -37,7 +29,7 @@ class UserController extends AbstractController $subscriberEventsPagination = $paginator->paginate( $subscriptionEventRepository->createUserLastSubscribersEventsQuery($user), $request->query->getInt('page', 1), - 10 + 10, ); 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), ]); } - - 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; - } } diff --git a/src/Repository/SubscriptionEventRepository.php b/src/Repository/SubscriptionEventRepository.php index 84fc711..dff1545 100644 --- a/src/Repository/SubscriptionEventRepository.php +++ b/src/Repository/SubscriptionEventRepository.php @@ -5,8 +5,7 @@ declare(strict_types=1); namespace App\Repository; use App\DTO\DailyEventsDTO; -use App\Entity\SubscriptionEvent; -use App\Entity\User; +use App\Entity\{SubscriptionEvent, User}; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ManagerRegistry; @@ -63,11 +62,7 @@ class SubscriptionEventRepository extends ServiceEntityRepository ; } - /** - * Get last user subscriber events - * - * @return SubscriptionEvent[] - */ + /** @return list */ public function getUserLastSubscribersEvents(User $user, int $limit = 20): array { $qb = $this->createUserLastSubscribersEventsQuery($user); @@ -89,11 +84,7 @@ class SubscriptionEventRepository extends ServiceEntityRepository ; } - /** - * Get last global subscription events - * - * @return SubscriptionEvent[] - */ + /** @return SubscriptionEvent[] */ public function getLastSubscriptionEvents(int $limit = 20): array { $qb = $this->createLastSubscriptionEventsQuery(); @@ -107,7 +98,7 @@ class SubscriptionEventRepository extends ServiceEntityRepository { $qb = $this->createQueryBuilder('se'); - $rows = $qb + $rows = $qb ->select([ 'NEW App\DTO\DailyEventsDTO(DAY(se.date), COUNT(se))', 'DAY(se.date) as day', diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php index a0d5007..214578b 100644 --- a/src/Repository/UserRepository.php +++ b/src/Repository/UserRepository.php @@ -4,8 +4,7 @@ declare(strict_types=1); namespace App\Repository; use App\DTO\TopUserDTO; -use App\Entity\Subscription; -use App\Entity\User; +use App\Entity\{Subscription, User}; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Persistence\ManagerRegistry; @@ -108,12 +107,8 @@ class UserRepository extends ServiceEntityRepository ; } - /** - * Returns top users by subscribers count - * - * @return TopUserDTO[] - */ - public function getTopUsers(int $limit = 30): array + /** @return list */ + public function getTopUsersBySubscribersCount(int $limit = 30): array { $qb = $this->getEntityManager()->getRepository(Subscription::class)->createQueryBuilder('s'); diff --git a/src/Twig/HighchartExtension.php b/src/Twig/HighchartExtension.php new file mode 100644 index 0000000..2ec6504 --- /dev/null +++ b/src/Twig/HighchartExtension.php @@ -0,0 +1,46 @@ + ['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(); + } +} \ No newline at end of file diff --git a/templates/Web/User/top.html.twig b/templates/Web/User/top.html.twig index c422c13..2095e0c 100644 --- a/templates/Web/User/top.html.twig +++ b/templates/Web/User/top.html.twig @@ -9,14 +9,13 @@ {% endblock %} {% block content %} - -
-
+ + {% endblock %}