performance improvements (#2329)

* remove work from constructor
* refactor twig extensions
* upgrade theme
* replace sub-request with direct template rendering
This commit is contained in:
Kevin Papst
2021-02-20 23:52:01 +01:00
committed by GitHub
parent 9eb25c412e
commit 607d09aefb
40 changed files with 992 additions and 1092 deletions

View File

@@ -30,7 +30,7 @@
"hslavich/oneloginsaml-bundle": "^1.4",
"jms/metadata": "^2.0",
"jms/serializer-bundle": "^3.2",
"kevinpapst/adminlte-bundle": "^3.0",
"kevinpapst/adminlte-bundle": "^3.3",
"kimai/user-bundle": "^1.1",
"laravolt/avatar": "^3.0",
"league/csv": "^9.4",

38
composer.lock generated
View File

@@ -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": "7931f435fc1cfba7a09fe9d65369984e",
"content-hash": "25136f17833c529f24e50535c155dda5",
"packages": [
{
"name": "beberlei/doctrineextensions",
@@ -2795,40 +2795,42 @@
},
{
"name": "kevinpapst/adminlte-bundle",
"version": "3.2.4",
"version": "3.3",
"source": {
"type": "git",
"url": "https://github.com/kevinpapst/AdminLTEBundle.git",
"reference": "c7cd7c5f7f3f49fae9df24c70e175be77c2cab61"
"reference": "67183f1684c71620cd42901539b7196d97f90e2c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/kevinpapst/AdminLTEBundle/zipball/c7cd7c5f7f3f49fae9df24c70e175be77c2cab61",
"reference": "c7cd7c5f7f3f49fae9df24c70e175be77c2cab61",
"url": "https://api.github.com/repos/kevinpapst/AdminLTEBundle/zipball/67183f1684c71620cd42901539b7196d97f90e2c",
"reference": "67183f1684c71620cd42901539b7196d97f90e2c",
"shasum": ""
},
"require": {
"php": "^7.1.3",
"symfony/config": ">4.1",
"symfony/dependency-injection": ">4.0",
"php": ">=7.1.3",
"symfony/asset": ">4.3",
"symfony/config": ">4.3",
"symfony/dependency-injection": ">4.3",
"symfony/event-dispatcher": ">4.3",
"symfony/http-foundation": ">4.0",
"symfony/http-kernel": ">4.0",
"symfony/options-resolver": ">4.0",
"symfony/security-core": ">4.0",
"symfony/http-foundation": ">4.3",
"symfony/http-kernel": ">4.3",
"symfony/options-resolver": ">4.3",
"symfony/security-core": ">4.3",
"symfony/translation": ">4.3",
"symfony/twig-bridge": ">4.3",
"twig/twig": ">2.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.10",
"knplabs/knp-menu-bundle": "^2.2",
"phpspec/prophecy": "^1.6",
"phpstan/phpstan": "^0.11.8",
"phpstan/phpstan-phpunit": "^0.11",
"phpstan/phpstan": "^0.12",
"phpstan/phpstan-phpunit": "^0.12",
"phpunit/phpunit": "^7.3",
"symfony/framework-bundle": ">4.0"
"symfony/framework-bundle": ">4.3"
},
"suggest": {
"friendsofsymfony/user-bundle": "Allows easy user management and security support",
"knplabs/knp-menu-bundle": "Allows easy menu integration"
},
"type": "symfony-bundle",
@@ -2858,7 +2860,7 @@
"description": "Admin theme bundle for Symfony 4+ based on AdminLTE 2.4 with FOSUserBundle support",
"support": {
"issues": "https://github.com/kevinpapst/AdminLTEBundle/issues",
"source": "https://github.com/kevinpapst/AdminLTEBundle/tree/3.2.4"
"source": "https://github.com/kevinpapst/AdminLTEBundle/tree/3.3"
},
"funding": [
{
@@ -2870,7 +2872,7 @@
"type": "github"
}
],
"time": "2020-07-07T11:40:52+00:00"
"time": "2021-02-20T16:05:21+00:00"
},
{
"name": "kimai/user-bundle",

View File

@@ -1,39 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Controller;
use App\Configuration\SystemConfiguration;
use App\Event\RecentActivityEvent;
use App\Repository\TimesheetRepository;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
* Used for the (initial) page rendering.
*/
class LayoutController extends AbstractController
{
public function activeEntries(TimesheetRepository $repository, SystemConfiguration $configuration, EventDispatcherInterface $dispatcher): Response
{
$user = $this->getUser();
$activeEntries = $repository->getActiveEntries($user);
$recentActivity = new RecentActivityEvent($this->getUser(), $activeEntries);
$dispatcher->dispatch($recentActivity);
return $this->render(
'navbar/active-entries.html.twig',
[
'entries' => $recentActivity->getRecentActivities(),
'soft_limit' => $configuration->getTimesheetActiveEntriesSoftLimit(),
]
);
}
}

View File

@@ -20,22 +20,17 @@ use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInt
/**
* Allows dynamic injection of theme related options.
*/
class ThemeOptionsSubscriber implements EventSubscriberInterface
final class ThemeOptionsSubscriber implements EventSubscriberInterface
{
/**
* @var TokenStorageInterface
*/
protected $storage;
private $storage;
/**
* @var ContextHelper
*/
protected $helper;
private $helper;
/**
* @param TokenStorageInterface $storage
* @param ContextHelper $helper
*/
public function __construct(TokenStorageInterface $storage, ContextHelper $helper)
{
$this->storage = $storage;
@@ -52,18 +47,24 @@ class ThemeOptionsSubscriber implements EventSubscriberInterface
];
}
/**
* @param KernelEvent $event
*/
public function setThemeOptions(KernelEvent $event)
public function setThemeOptions(KernelEvent $event): void
{
if (!$this->canHandleEvent($event)) {
// Ignore sub-requests
if (!$event->isMasterRequest()) {
return;
}
// ignore events like the toolbar where we do not have a token
if (null === $this->storage->getToken()) {
return;
}
/** @var User $user */
$user = $this->storage->getToken()->getUser();
if (!($user instanceof User)) {
return;
}
/** @var UserPreference $ref */
foreach ($user->getPreferences() as $ref) {
$name = $ref->getName();
@@ -90,26 +91,4 @@ class ThemeOptionsSubscriber implements EventSubscriberInterface
}
}
}
/**
* @param KernelEvent $event
* @return bool
*/
protected function canHandleEvent(KernelEvent $event): bool
{
// Ignore sub-requests
if (!$event->isMasterRequest()) {
return false;
}
// ignore events like the toolbar where we do not have a token
if (null === $this->storage->getToken()) {
return false;
}
/** @var User $user */
$user = $this->storage->getToken()->getUser();
return ($user instanceof User);
}
}

View File

@@ -16,7 +16,7 @@ use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
class UserEnvironmentSubscriber implements EventSubscriberInterface
final class UserEnvironmentSubscriber implements EventSubscriberInterface
{
/**
* @var TokenStorageInterface
@@ -40,20 +40,22 @@ class UserEnvironmentSubscriber implements EventSubscriberInterface
];
}
public function prepareEnvironment(RequestEvent $event)
public function prepareEnvironment(RequestEvent $event): void
{
// ignore sub-requests
if (!$event->isMasterRequest()) {
return;
}
if (null === $this->storage->getToken()) {
// the locale depends on the request, not on the user configuration
\Locale::setDefault($event->getRequest()->getLocale());
// ignore events like the toolbar where we do not have a token
if (null === ($token = $this->storage->getToken())) {
return;
}
$user = $this->storage->getToken()->getUser();
// the locale depends on the request, not on the user configuration
\Locale::setDefault($event->getRequest()->getLocale());
$user = $token->getUser();
if ($user instanceof User) {
date_default_timezone_set($user->getTimezone());

View File

@@ -28,20 +28,20 @@ use Symfony\Component\Form\Extension\Core\Type\TimezoneType;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Validator\Constraints\Range;
class UserPreferenceSubscriber implements EventSubscriberInterface
final class UserPreferenceSubscriber implements EventSubscriberInterface
{
/**
* @var EventDispatcherInterface
*/
protected $eventDispatcher;
private $eventDispatcher;
/**
* @var AuthorizationCheckerInterface
*/
protected $voter;
private $voter;
/**
* @var SystemConfiguration
*/
protected $configuration;
private $configuration;
public function __construct(EventDispatcherInterface $dispatcher, AuthorizationCheckerInterface $voter, SystemConfiguration $formConfig)
{
@@ -57,43 +57,23 @@ class UserPreferenceSubscriber implements EventSubscriberInterface
];
}
private function getDefaultTheme(): ?string
{
return $this->configuration->getUserDefaultTheme();
}
private function getDefaultCurrency(): string
{
return $this->configuration->getUserDefaultCurrency();
}
private function getDefaultLanguage(): string
{
return $this->configuration->getUserDefaultLanguage();
}
private function getDefaultTimezone(): string
{
$timezone = $this->configuration->getUserDefaultTimezone();
if (null === $timezone) {
$timezone = date_default_timezone_get();
}
return $timezone;
}
/**
* @param User $user
* @return UserPreference[]
*/
public function getDefaultPreferences(User $user)
{
$timezone = $this->configuration->getUserDefaultTimezone();
if (null === $timezone) {
$timezone = date_default_timezone_get();
}
$enableHourlyRate = false;
$hourlyRateOptions = [];
if ($this->voter->isGranted('hourly-rate', $user)) {
$enableHourlyRate = true;
$hourlyRateOptions = ['currency' => $this->getDefaultCurrency()];
$hourlyRateOptions = ['currency' => $this->configuration->getUserDefaultCurrency()];
}
return [
@@ -119,14 +99,14 @@ class UserPreferenceSubscriber implements EventSubscriberInterface
(new UserPreference())
->setName(UserPreference::TIMEZONE)
->setValue($this->getDefaultTimezone())
->setValue($timezone)
->setOrder(200)
->setSection('locale')
->setType(TimezoneType::class),
(new UserPreference())
->setName(UserPreference::LOCALE)
->setValue($this->getDefaultLanguage())
->setValue($this->configuration->getUserDefaultLanguage())
->setOrder(250)
->setSection('locale')
->setType(LanguageType::class),
@@ -140,7 +120,7 @@ class UserPreferenceSubscriber implements EventSubscriberInterface
(new UserPreference())
->setName(UserPreference::SKIN)
->setValue($this->getDefaultTheme())
->setValue($this->configuration->getUserDefaultTheme())
->setOrder(400)
->setSection('theme')
->setType(SkinType::class),

View File

@@ -43,32 +43,21 @@ final class UserProfileSubscriber implements EventSubscriberInterface
public function prepareUserProfile(KernelEvent $event): void
{
if (!$this->canHandleEvent($event)) {
// ignore sub-requests
if (!$event->isMasterRequest()) {
return;
}
/** @var User $user */
$user = $this->storage->getToken()->getUser();
$event = new PrepareUserEvent($user);
$this->eventDispatcher->dispatch($event);
}
private function canHandleEvent(KernelEvent $event): bool
{
// Ignore sub-requests
if (!$event->isMasterRequest()) {
return false;
}
// ignore events like the toolbar where we do not have a token
if (null === $this->storage->getToken()) {
return false;
if (null === ($token = $this->storage->getToken())) {
return;
}
/** @var User $user */
$user = $this->storage->getToken()->getUser();
$user = $token->getUser();
return ($user instanceof User);
if ($user instanceof User) {
$event = new PrepareUserEvent($user);
$this->eventDispatcher->dispatch($event);
}
}
}

View File

@@ -19,7 +19,7 @@ use App\Event\UserPreferenceDisplayEvent;
use App\Export\ExportItemInterface;
use App\Repository\Query\CustomerQuery;
use App\Repository\Query\TimesheetQuery;
use App\Twig\DateExtensions;
use App\Twig\LocaleFormatExtensions;
use DateTime;
use PhpOffice\PhpSpreadsheet\Cell\DataType;
use PhpOffice\PhpSpreadsheet\Shared\Date;
@@ -47,7 +47,7 @@ abstract class AbstractSpreadsheetRenderer
public const RATE_FORMAT = self::RATE_FORMAT_LEFT;
/**
* @var DateExtensions
* @var LocaleFormatExtensions
*/
protected $dateExtension;
/**
@@ -92,7 +92,7 @@ abstract class AbstractSpreadsheetRenderer
'user-meta' => [],
];
public function __construct(TranslatorInterface $translator, DateExtensions $dateExtension, EventDispatcherInterface $dispatcher, AuthorizationCheckerInterface $voter)
public function __construct(TranslatorInterface $translator, LocaleFormatExtensions $dateExtension, EventDispatcherInterface $dispatcher, AuthorizationCheckerInterface $voter)
{
$this->translator = $translator;
$this->dateExtension = $dateExtension;

View File

@@ -12,8 +12,7 @@ namespace App\Invoice\Renderer;
use App\Entity\InvoiceDocument;
use App\Invoice\InvoiceModel;
use App\Invoice\RendererInterface;
use App\Twig\DateExtensions;
use App\Twig\LocaleExtensions;
use App\Twig\LocaleFormatExtensions;
use Symfony\Bridge\Twig\Extension\TranslationExtension;
use Symfony\Contracts\Translation\LocaleAwareInterface;
use Twig\Environment;
@@ -61,12 +60,8 @@ abstract class AbstractTwigRenderer implements RendererInterface
$translator->setLocale($locale);
/** @var LocaleExtensions $extension */
$extension = $twig->getExtension(LocaleExtensions::class);
$extension->setLocale($locale);
/** @var DateExtensions $extension */
$extension = $twig->getExtension(DateExtensions::class);
/** @var LocaleFormatExtensions $extension */
$extension = $twig->getExtension(LocaleFormatExtensions::class);
$extension->setLocale($locale);
return $previousLocale;

View File

@@ -31,17 +31,30 @@ class WidgetRepository
/**
* @var array
*/
private $definitions = [];
private $definitions;
/**
* @var array
*/
private $customDefinition;
public function __construct(TimesheetRepository $repository, array $widgets)
{
$this->repository = $repository;
$this->definitions = array_merge($this->getDefaultWidgets(), $widgets);
$this->customDefinition = $widgets;
}
private function getDefinedWidgets(): array
{
if (null === $this->definitions) {
$this->definitions = array_merge($this->getDefaultWidgets(), $this->customDefinition);
}
return $this->definitions;
}
public function has(string $id): bool
{
return isset($this->definitions[$id]) || isset($this->widgets[$id]);
return isset($this->getDefinedWidgets()[$id]) || isset($this->widgets[$id]);
}
public function registerWidget(WidgetInterface $widget): WidgetRepository
@@ -64,7 +77,7 @@ class WidgetRepository
}
// this code should ONLY be reached for internal (pre-registered) widgets
$this->registerWidget($this->create($id, $this->definitions[$id]));
$this->registerWidget($this->create($id, $this->getDefinedWidgets()[$id]));
return $this->widgets[$id];
}

View File

@@ -9,18 +9,18 @@
namespace App\Twig;
use App\Configuration\ThemeConfiguration;
use App\Configuration\SystemConfiguration;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
class ConfigExtension extends AbstractExtension
final class ConfigExtension extends AbstractExtension
{
/**
* @var ThemeConfiguration
* @var SystemConfiguration
*/
protected $configuration;
private $configuration;
public function __construct(ThemeConfiguration $configuration)
public function __construct(SystemConfiguration $configuration)
{
$this->configuration = $configuration;
}
@@ -41,6 +41,15 @@ class ConfigExtension extends AbstractExtension
*/
public function getThemeConfig(string $name)
{
switch ($name) {
case 'soft_limit':
return $this->configuration->getTimesheetActiveEntriesSoftLimit();
default:
$name = 'theme.' . $name;
break;
}
return $this->configuration->find($name);
}
}

View File

@@ -1,189 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Twig;
use App\Configuration\LanguageFormattings;
use App\Constants;
use App\Entity\User;
use App\Utils\LocaleFormats;
use App\Utils\LocaleFormatter;
use DateTime;
use Symfony\Component\HttpFoundation\RequestStack;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
use Twig\TwigFunction;
use Twig\TwigTest;
/**
* Date specific twig extensions
*/
class DateExtensions extends AbstractExtension
{
/**
* @var LocaleFormats|null
*/
protected $localeFormats = null;
/**
* @var LocaleFormatter
*/
private $formatter;
/**
* @var LanguageFormattings
*/
private $formats;
public function __construct(RequestStack $requestStack, LanguageFormattings $formats)
{
$locale = Constants::DEFAULT_LOCALE;
// request is null in a console command
if (null !== $requestStack->getMasterRequest()) {
$locale = $requestStack->getMasterRequest()->getLocale();
}
$this->formats = $formats;
$this->setLocale($locale);
}
/**
* {@inheritdoc}
*/
public function getFilters()
{
return [
new TwigFilter('month_name', [$this, 'monthName']),
new TwigFilter('day_name', [$this, 'dayName']),
new TwigFilter('date_short', [$this, 'dateShort']),
new TwigFilter('date_time', [$this, 'dateTime']),
new TwigFilter('date_full', [$this, 'dateTimeFull']),
new TwigFilter('date_format', [$this, 'dateFormat']),
new TwigFilter('time', [$this, 'time']),
new TwigFilter('hour24', [$this, 'hour24']),
];
}
public function getTests()
{
return [
new TwigTest('weekend', function ($dateTime) {
if (!$dateTime instanceof \DateTime) {
return false;
}
$day = (int) $dateTime->format('w');
return ($day === 0 || $day === 6);
}),
];
}
/**
* {@inheritdoc}
*/
public function getFunctions()
{
return [
new TwigFunction('get_format_duration', [$this, 'getDurationFormat']),
new TwigFunction('create_date', [$this, 'createDate']),
];
}
/**
* Allows to switch the locale used for all twig filter and functions.
*
* @param string $locale
*/
public function setLocale(string $locale)
{
$this->formatter = new LocaleFormatter($this->formats, $locale);
$this->localeFormats = new LocaleFormats($this->formats, $locale);
}
/**
* @param DateTime|string $date
* @return string
*/
public function dateShort($date)
{
return $this->formatter->dateShort($date);
}
/**
* @param DateTime|string $date
* @return string
*/
public function dateTime($date)
{
return $this->formatter->dateTime($date);
}
/**
* @param DateTime|string $date
* @return bool|false|string
*/
public function dateTimeFull($date)
{
return $this->formatter->dateTimeFull($date);
}
public function createDate(string $date, ?User $user = null): \DateTime
{
$timezone = $user !== null ? $user->getTimezone() : date_default_timezone_get();
return new DateTime($date, new \DateTimeZone($timezone));
}
/**
* @param DateTime|string $date
* @param string $format
* @return false|string
* @throws \Exception
*/
public function dateFormat($date, string $format)
{
return $this->formatter->dateFormat($date, $format);
}
/**
* @param DateTime|string $date
* @return string
*/
public function time($date)
{
return $this->formatter->time($date);
}
public function monthName(\DateTime $dateTime, bool $withYear = false): string
{
return $this->formatter->monthName($dateTime, $withYear);
}
public function dayName(\DateTime $dateTime, bool $short = false): string
{
return $this->formatter->dayName($dateTime, $short);
}
/**
* @param mixed $twentyFour
* @param mixed $twelveHour
* @return mixed
*/
public function hour24($twentyFour, $twelveHour)
{
return $this->formatter->hour24($twentyFour, $twelveHour);
}
/**
* @return string
*/
public function getDurationFormat()
{
return $this->localeFormats->getDurationFormat();
}
}

View File

@@ -1,28 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Twig;
use App\Twig\Runtime\ThemeEventExtension;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
class EventExtensions extends AbstractExtension
{
/**
* {@inheritdoc}
*/
public function getFunctions()
{
return [
new TwigFunction('trigger', [ThemeEventExtension::class, 'trigger']),
new TwigFunction('javascript_translations', [ThemeEventExtension::class, 'getJavascriptTranslations']),
];
}
}

View File

@@ -1,169 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Twig;
use App\Configuration\LanguageFormattings;
use App\Constants;
use App\Entity\Timesheet;
use App\Utils\Duration;
use App\Utils\LocaleFormatter;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Intl\Locales;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
use Twig\TwigFunction;
/**
* Locale specific Twig extensions
*/
final class LocaleExtensions extends AbstractExtension
{
/**
* @var LocaleFormatter
*/
private $formatter;
/**
* @var LanguageFormattings
*/
private $formats;
public function __construct(RequestStack $requestStack, LanguageFormattings $formats)
{
$locale = Constants::DEFAULT_LOCALE;
// request is null in a console command
if (null !== $requestStack->getMasterRequest()) {
$locale = $requestStack->getMasterRequest()->getLocale();
}
$this->formats = $formats;
$this->setLocale($locale);
}
/**
* {@inheritdoc}
*/
public function getFilters()
{
return [
new TwigFilter('duration', [$this, 'duration']),
new TwigFilter('duration_decimal', [$this, 'durationDecimal']),
new TwigFilter('money', [$this, 'money']),
new TwigFilter('currency', [$this, 'currency']),
new TwigFilter('country', [$this, 'country']),
new TwigFilter('language', [$this, 'language']),
new TwigFilter('amount', [$this, 'amount']),
];
}
/**
* {@inheritdoc}
*/
public function getFunctions()
{
return [
new TwigFunction('locales', [$this, 'getLocales']),
];
}
/**
* Allows to switch the locale used for all twig filter and functions.
*
* @param string $locale
*/
public function setLocale(string $locale)
{
$this->formatter = new LocaleFormatter($this->formats, $locale);
}
/**
* Transforms seconds into a duration string.
*
* @param int|Timesheet|null $duration
* @param bool $decimal
* @return string
*/
public function duration($duration, $decimal = false)
{
return $this->formatter->duration($duration, $decimal);
}
/**
* Transforms seconds into a decimal formatted duration string.
*
* @param int|Timesheet|null $duration
* @return string
*/
public function durationDecimal($duration)
{
return $this->formatter->durationDecimal($duration);
}
/**
* @param string|float $amount
* @return bool|false|string
*/
public function amount($amount)
{
return $this->formatter->amount($amount);
}
/**
* Returns the currency symbol.
*
* @param string $currency
* @return string
*/
public function currency($currency)
{
return $this->formatter->currency($currency);
}
/**
* @param string $language
* @return string
*/
public function language($language)
{
return $this->formatter->language($language);
}
/**
* @param string $country
* @return string
*/
public function country($country)
{
return $this->formatter->country($country);
}
/**
* @param float $amount
* @param string|null $currency
* @param bool $withCurrency
* @return string
*/
public function money($amount, ?string $currency = null, bool $withCurrency = true)
{
return $this->formatter->money($amount, $currency, $withCurrency);
}
/**
* Takes the list of codes of the locales (languages) enabled in the
* application and returns an array with the name of each locale written
* in its own language (e.g. English, Français, Español, etc.)
*
* @return array
*/
public function getLocales()
{
return $this->formatter->getLocales();
}
}

View File

@@ -0,0 +1,298 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Twig;
use App\Configuration\LanguageFormattings;
use App\Entity\Timesheet;
use App\Entity\User;
use App\Utils\LocaleFormats;
use App\Utils\LocaleFormatter;
use DateTime;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
use Twig\TwigFunction;
use Twig\TwigTest;
final class LocaleFormatExtensions extends AbstractExtension
{
/**
* @var LanguageFormattings|null
*/
private $formats;
/**
* @var LocaleFormats|null
*/
private $localeFormats;
/**
* @var LocaleFormatter|null
*/
private $formatter;
/**
* @var string
*/
private $locale;
public function __construct(LanguageFormattings $formats)
{
$this->formats = $formats;
}
/**
* {@inheritdoc}
*/
public function getFilters()
{
return [
new TwigFilter('month_name', [$this, 'monthName']),
new TwigFilter('day_name', [$this, 'dayName']),
new TwigFilter('date_short', [$this, 'dateShort']),
new TwigFilter('date_time', [$this, 'dateTime']),
new TwigFilter('date_full', [$this, 'dateTimeFull']),
new TwigFilter('date_format', [$this, 'dateFormat']),
new TwigFilter('time', [$this, 'time']),
new TwigFilter('hour24', [$this, 'hour24']),
new TwigFilter('duration', [$this, 'duration']),
new TwigFilter('duration_decimal', [$this, 'durationDecimal']),
new TwigFilter('money', [$this, 'money']),
new TwigFilter('currency', [$this, 'currency']),
new TwigFilter('country', [$this, 'country']),
new TwigFilter('language', [$this, 'language']),
new TwigFilter('amount', [$this, 'amount']),
];
}
public function getTests()
{
return [
new TwigTest('weekend', function ($dateTime) {
if (!$dateTime instanceof \DateTime) {
return false;
}
$day = (int) $dateTime->format('w');
return ($day === 0 || $day === 6);
}),
];
}
/**
* {@inheritdoc}
*/
public function getFunctions()
{
return [
new TwigFunction('get_format_duration', [$this, 'getDurationFormat']),
new TwigFunction('create_date', [$this, 'createDate']),
new TwigFunction('locales', [$this, 'getLocales']),
];
}
/**
* Allows to switch the locale used for all twig filter and functions.
*
* @param string $locale
*/
public function setLocale(string $locale)
{
$this->locale = $locale;
$this->formatter = null;
$this->localeFormats = null;
}
private function getLocaleFormats(): LocaleFormats
{
if (null === $this->localeFormats) {
$this->localeFormats = new LocaleFormats($this->formats, $this->getLocale());
}
return $this->localeFormats;
}
private function getFormatter(): LocaleFormatter
{
if (null === $this->formatter) {
$this->formatter = new LocaleFormatter($this->formats, $this->getLocale());
}
return $this->formatter;
}
private function getLocale()
{
if (null === $this->locale) {
$this->locale = \Locale::getDefault();
}
return $this->locale;
}
/**
* @param DateTime|string $date
* @return string
*/
public function dateShort($date)
{
return $this->getFormatter()->dateShort($date);
}
/**
* @param DateTime|string $date
* @return string
*/
public function dateTime($date)
{
return $this->getFormatter()->dateTime($date);
}
/**
* @param DateTime|string $date
* @return bool|false|string
*/
public function dateTimeFull($date)
{
return $this->getFormatter()->dateTimeFull($date);
}
public function createDate(string $date, ?User $user = null): \DateTime
{
$timezone = $user !== null ? $user->getTimezone() : date_default_timezone_get();
return new DateTime($date, new \DateTimeZone($timezone));
}
/**
* @param DateTime|string $date
* @param string $format
* @return false|string
* @throws \Exception
*/
public function dateFormat($date, string $format)
{
return $this->getFormatter()->dateFormat($date, $format);
}
/**
* @param DateTime|string $date
* @return string
*/
public function time($date)
{
return $this->getFormatter()->time($date);
}
public function monthName(\DateTime $dateTime, bool $withYear = false): string
{
return $this->getFormatter()->monthName($dateTime, $withYear);
}
public function dayName(\DateTime $dateTime, bool $short = false): string
{
return $this->getFormatter()->dayName($dateTime, $short);
}
/**
* @param mixed $twentyFour
* @param mixed $twelveHour
* @return mixed
*/
public function hour24($twentyFour, $twelveHour)
{
return $this->getFormatter()->hour24($twentyFour, $twelveHour);
}
public function getDurationFormat(): string
{
return $this->getLocaleFormats()->getDurationFormat();
}
/**
* Transforms seconds into a duration string.
*
* @param int|Timesheet|null $duration
* @param bool $decimal
* @return string
*/
public function duration($duration, $decimal = false)
{
return $this->getFormatter()->duration($duration, $decimal);
}
/**
* Transforms seconds into a decimal formatted duration string.
*
* @param int|Timesheet|null $duration
* @return string
*/
public function durationDecimal($duration)
{
return $this->getFormatter()->durationDecimal($duration);
}
/**
* @param string|float $amount
* @return bool|false|string
*/
public function amount($amount)
{
return $this->getFormatter()->amount($amount);
}
/**
* Returns the currency symbol.
*
* @param string $currency
* @return string
*/
public function currency($currency)
{
return $this->getFormatter()->currency($currency);
}
/**
* @param string $language
* @return string
*/
public function language($language)
{
return $this->getFormatter()->language($language);
}
/**
* @param string $country
* @return string
*/
public function country($country)
{
return $this->getFormatter()->country($country);
}
/**
* @param float $amount
* @param string|null $currency
* @param bool $withCurrency
* @return string
*/
public function money($amount, ?string $currency = null, bool $withCurrency = true)
{
return $this->getFormatter()->money($amount, $currency, $withCurrency);
}
/**
* Takes the list of codes of the locales (languages) enabled in the
* application and returns an array with the name of each locale written
* in its own language (e.g. English, Français, Español, etc.)
*
* @return array
*/
public function getLocales()
{
return $this->getFormatter()->getLocales();
}
}

View File

@@ -31,7 +31,6 @@ class PaginationExtension extends AbstractExtension
public function __construct(UrlGeneratorInterface $router)
{
$this->view = new TwitterBootstrap3View();
$this->router = $router;
}
@@ -46,6 +45,15 @@ class PaginationExtension extends AbstractExtension
];
}
private function getView(): ViewInterface
{
if (null === $this->view) {
$this->view = new TwitterBootstrap3View();
}
return $this->view;
}
/**
* @deprecated since 1.8
*/
@@ -70,7 +78,7 @@ class PaginationExtension extends AbstractExtension
$options['prev_message'] = '<i class="fas fa-chevron-left"></i>';
$options['next_message'] = '<i class="fas fa-chevron-right"></i>';
return $this->view->render($pagerfanta, $routeGenerator, $options);
return $this->getView()->render($pagerfanta, $routeGenerator, $options);
}
private function createRouteGenerator(array $options = [])

View File

@@ -7,15 +7,14 @@
* file that was distributed with this source code.
*/
namespace App\Twig;
namespace App\Twig\Runtime;
use Psr\Container\ContainerInterface;
use Symfony\Contracts\Service\ServiceSubscriberInterface;
use Symfony\WebpackEncoreBundle\Asset\EntrypointLookupInterface;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
use Twig\Extension\RuntimeExtensionInterface;
class EncoreExtension extends AbstractExtension implements ServiceSubscriberInterface
final class EncoreExtension implements RuntimeExtensionInterface, ServiceSubscriberInterface
{
/**
* @var string
@@ -39,16 +38,6 @@ class EncoreExtension extends AbstractExtension implements ServiceSubscriberInte
];
}
/**
* {@inheritdoc}
*/
public function getFunctions()
{
return [
new TwigFunction('encore_entry_css_source', [$this, 'getEncoreEntryCssSource']),
];
}
public function getEncoreEntryCssSource(string $packageName): string
{
$lookup = $this->container->get(EntrypointLookupInterface::class);

View File

@@ -7,13 +7,12 @@
* file that was distributed with this source code.
*/
namespace App\Twig;
namespace App\Twig\Runtime;
use App\Export\ServiceExport;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
use Twig\Extension\RuntimeExtensionInterface;
class TimesheetExtension extends AbstractExtension
final class ExporterExtension implements RuntimeExtensionInterface
{
/**
* @var ServiceExport
@@ -25,16 +24,6 @@ class TimesheetExtension extends AbstractExtension
$this->service = $service;
}
/**
* {@inheritdoc}
*/
public function getFunctions()
{
return [
new TwigFunction('timesheet_exporter', [$this, 'getTimesheetExporter'], []),
];
}
public function getTimesheetExporter(): array
{
$ids = [];

View File

@@ -7,17 +7,13 @@
* file that was distributed with this source code.
*/
namespace App\Twig;
namespace App\Twig\Runtime;
use App\Configuration\SystemConfiguration;
use App\Utils\Markdown;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
use Twig\Extension\RuntimeExtensionInterface;
/**
* A twig extension to handle markdown content.
*/
final class MarkdownExtension extends AbstractExtension
final class MarkdownExtension implements RuntimeExtensionInterface
{
/**
* @var Markdown
@@ -27,26 +23,24 @@ final class MarkdownExtension extends AbstractExtension
* @var SystemConfiguration
*/
private $configuration;
/**
* @param Markdown $parser
* @var bool|null
*/
private $markdownEnabled;
public function __construct(Markdown $parser, SystemConfiguration $configuration)
{
$this->markdown = $parser;
$this->configuration = $configuration;
}
/**
* @return TwigFilter[]
*/
public function getFilters()
private function isMarkdownEnabled(): bool
{
return [
new TwigFilter('md2html', [$this, 'markdownToHtml'], ['pre_escape' => 'html', 'is_safe' => ['html']]),
new TwigFilter('desc2html', [$this, 'timesheetContent'], ['pre_escape' => 'html', 'is_safe' => ['html']]),
new TwigFilter('comment2html', [$this, 'commentContent'], ['pre_escape' => 'html', 'is_safe' => ['html']]),
];
if (null === $this->markdownEnabled) {
$this->markdownEnabled = $this->configuration->isTimesheetMarkdownEnabled();
}
return $this->markdownEnabled;
}
/**
@@ -66,7 +60,7 @@ final class MarkdownExtension extends AbstractExtension
$content = trim(substr($content, 0, 100)) . ' &hellip;';
}
if ($this->configuration->isTimesheetMarkdownEnabled()) {
if ($this->isMarkdownEnabled()) {
$content = $this->markdown->toHtml($content, false);
} elseif ($fullLength) {
$content = '<p>' . nl2br($content) . '</p>';
@@ -87,7 +81,7 @@ final class MarkdownExtension extends AbstractExtension
return '';
}
if ($this->configuration->isTimesheetMarkdownEnabled()) {
if ($this->isMarkdownEnabled()) {
return $this->markdown->toHtml($content, false);
}

View File

@@ -9,37 +9,40 @@
namespace App\Twig\Runtime;
use App\Entity\User;
use App\Event\ThemeEvent;
use App\Event\ThemeJavascriptTranslationsEvent;
use App\Security\CurrentUser;
use Symfony\Bridge\Twig\AppVariable;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Twig\Environment;
use Twig\Extension\RuntimeExtensionInterface;
final class ThemeEventExtension implements RuntimeExtensionInterface
final class ThemeExtension implements RuntimeExtensionInterface
{
/**
* @var EventDispatcherInterface
*/
private $eventDispatcher;
/**
* @var CurrentUser
*/
private $user;
public function __construct(EventDispatcherInterface $dispatcher, CurrentUser $user)
public function __construct(EventDispatcherInterface $dispatcher)
{
$this->eventDispatcher = $dispatcher;
$this->user = $user;
}
/**
* @param Environment $environment
* @param string $eventName
* @param mixed|null $payload
* @return ThemeEvent
*/
public function trigger(string $eventName, $payload = null): ThemeEvent
public function trigger(Environment $environment, string $eventName, $payload = null): ThemeEvent
{
$themeEvent = new ThemeEvent($this->user->getUser(), $payload);
/** @var AppVariable $app */
$app = $environment->getGlobals()['app'];
/** @var User $user */
$user = $app->getUser();
$themeEvent = new ThemeEvent($user, $payload);
if ($this->eventDispatcher->hasListeners($eventName)) {
$this->eventDispatcher->dispatch($themeEvent, $eventName);

View File

@@ -0,0 +1,32 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Twig\Runtime;
use App\Entity\User;
use App\Repository\TimesheetRepository;
use Twig\Extension\RuntimeExtensionInterface;
final class TimesheetExtension implements RuntimeExtensionInterface
{
/**
* @var TimesheetRepository
*/
private $repository;
public function __construct(TimesheetRepository $repository)
{
$this->repository = $repository;
}
public function activeEntries(User $user): array
{
return $this->repository->getActiveEntries($user);
}
}

View File

@@ -7,37 +7,25 @@
* file that was distributed with this source code.
*/
namespace App\Twig;
namespace App\Twig\Runtime;
use App\Widget\WidgetException;
use App\Widget\WidgetInterface;
use App\Widget\WidgetService;
use InvalidArgumentException;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
use Twig\Extension\RuntimeExtensionInterface;
class WidgetExtension extends AbstractExtension
final class WidgetExtension implements RuntimeExtensionInterface
{
/**
* @var WidgetService
*/
protected $service;
private $service;
public function __construct(WidgetService $service)
{
$this->service = $service;
}
/**
* {@inheritdoc}
*/
public function getFunctions()
{
return [
new TwigFunction('render_widget', [$this, 'renderWidget'], ['is_safe' => ['html']]),
];
}
/**
* @param WidgetInterface|string $widget
* @param array $options
@@ -47,12 +35,12 @@ class WidgetExtension extends AbstractExtension
public function renderWidget($widget, array $options = [])
{
if (!($widget instanceof WidgetInterface) && !\is_string($widget)) {
throw new InvalidArgumentException('Widget must either implement WidgetInterface or be a string');
throw new \InvalidArgumentException('Widget must either implement WidgetInterface or be a string');
}
if (\is_string($widget)) {
if (!$this->service->hasWidget($widget)) {
throw new InvalidArgumentException(sprintf('Unknown widget "%s" requested', $widget));
throw new \InvalidArgumentException(sprintf('Unknown widget "%s" requested', $widget));
}
$widget = $this->service->getWidget($widget);

View File

@@ -0,0 +1,50 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Twig;
use App\Twig\Runtime\EncoreExtension;
use App\Twig\Runtime\ExporterExtension;
use App\Twig\Runtime\MarkdownExtension;
use App\Twig\Runtime\ThemeExtension;
use App\Twig\Runtime\TimesheetExtension;
use App\Twig\Runtime\WidgetExtension;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
use Twig\TwigFunction;
class RuntimeExtensions extends AbstractExtension
{
/**
* {@inheritdoc}
*/
public function getFunctions()
{
return [
new TwigFunction('trigger', [ThemeExtension::class, 'trigger'], ['needs_environment' => true]),
new TwigFunction('javascript_translations', [ThemeExtension::class, 'getJavascriptTranslations']),
new TwigFunction('timesheet_exporter', [ExporterExtension::class, 'getTimesheetExporter']),
new TwigFunction('active_timesheets', [TimesheetExtension::class, 'activeEntries']),
new TwigFunction('encore_entry_css_source', [EncoreExtension::class, 'getEncoreEntryCssSource']),
new TwigFunction('render_widget', [WidgetExtension::class, 'renderWidget'], ['is_safe' => ['html']]),
];
}
/**
* {@inheritdoc}
*/
public function getFilters()
{
return [
new TwigFilter('md2html', [MarkdownExtension::class, 'markdownToHtml'], ['pre_escape' => 'html', 'is_safe' => ['html']]),
new TwigFilter('desc2html', [MarkdownExtension::class, 'timesheetContent'], ['pre_escape' => 'html', 'is_safe' => ['html']]),
new TwigFilter('comment2html', [MarkdownExtension::class, 'commentContent'], ['pre_escape' => 'html', 'is_safe' => ['html']]),
];
}
}

View File

@@ -11,12 +11,15 @@ namespace App\Widget;
use App\Repository\WidgetRepository;
/**
* @final
*/
class WidgetService
{
/**
* @var WidgetRendererInterface[]
*/
private $renderer = [];
private $renderer;
/**
* @var WidgetRepository
*/
@@ -28,9 +31,7 @@ class WidgetService
*/
public function __construct(WidgetRepository $repository, iterable $renderer)
{
foreach ($renderer as $render) {
$this->addRenderer($render);
}
$this->renderer = $renderer;
$this->repository = $repository;
}
@@ -70,7 +71,7 @@ class WidgetService
/**
* @return WidgetRendererInterface[]
*/
public function getRenderer(): array
public function getRenderer(): iterable
{
return $this->renderer;
}

View File

@@ -122,7 +122,53 @@
</li>
{% endif %}
{% block navbar_extensions %}{% endblock %}
{{ render(controller('App\\Controller\\LayoutController::activeEntries')) }}
{% set active_timesheets = active_timesheets(app.user) %}
{% set soft_limit = theme_config('soft_limit') %}
<li class="dropdown messages-menu" style="{% if active_timesheets is empty %}display:none{% endif %}">
<a href="#" class="dropdown-toggle ddt-large ticktac" data-toggle="dropdown">
<i class="{{ 'start'|icon }} fa-2x"></i>
<span data-warning="{{ soft_limit }}" class="label label-{% if active_timesheets|length > soft_limit %}danger{% else %}warning{% endif %}">{% if active_timesheets|length > 0 %}{{ active_timesheets|length }}{% endif %}</span>
</a>
<ul class="dropdown-menu"
data-api="{{ path('active_timesheet') }}"
data-href="{{ path('stop_timesheet', {'id' : '000'}) }}"
data-icon="{{ 'stop-small'|icon }}"
data-format="{{ get_format_duration() }}">
<li class="header">
{{ 'active.entries'|trans }}
</li>
<li>
<ul class="menu">
{% for entry in active_timesheets %}
<li>
<a href="{{ path('stop_timesheet', {'id' : entry.id}) }}" class="api-link" data-event="kimai.timesheetStop kimai.timesheetUpdate" data-method="PATCH" data-msg-error="timesheet.stop.error" data-msg-success="timesheet.stop.success">
<div class="pull-left">
<i class="{{ 'stop-small'|icon }} fa-2x"></i>
</div>
<h4>
<span>{{ entry.activity.name }}</span>
<small>
<span data-title="true" data-since="{{ entry.begin.format(constant('DATE_ISO8601')) }}" data-format="{{ get_format_duration() }}">{{ entry|duration }}</span>
</small>
</h4>
<p>{{ entry.project.name }} ({{ entry.project.customer.name }})</p>
</a>
</li>
{% endfor %}
</ul>
</li>
{% if is_granted('create_own_timesheet') %}
<li class="footer"><a href="{{ path('timesheet_create') }}" class="modal-ajax-form">{{ 'timesheet.start'|trans }}</a></li>
{% endif %}
</ul>
</li>
{% if is_granted('create_own_timesheet') %}
<li class="messages-menu-empty" style="{% if active_timesheets is not empty %}display:none{% endif %}">
<a href="{{ path('timesheet_create') }}" class="ddt-large modal-ajax-form">
<i class="{{ 'start'|icon }} fa-2x"></i>
</a>
</li>
{% endif %}
{% endif %}
{% endblock %}

View File

@@ -77,10 +77,7 @@ class LayoutControllerTest extends ControllerBaseTest
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$em = $this->getEntityManager();
$user = $this->getUserByRole(User::ROLE_USER);
$this->request($client, '/layou/active_entries');
$this->request($client, '/dashboard/');
$this->assertTrue($client->getResponse()->isSuccessful());
$content = $client->getResponse()->getContent();

View File

@@ -28,13 +28,11 @@ use App\Event\TimesheetMetaDisplayEvent;
use App\Export\ExportRendererInterface;
use App\Export\RendererInterface;
use App\Repository\Query\TimesheetQuery;
use App\Twig\DateExtensions;
use App\Twig\LocaleFormatExtensions;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -46,7 +44,6 @@ abstract class AbstractRendererTest extends KernelTestCase
*/
protected function getAbstractRenderer(string $classname)
{
$requestStack = new RequestStack();
$languages = [
'en' => [
'date' => 'Y.m.d',
@@ -55,12 +52,8 @@ abstract class AbstractRendererTest extends KernelTestCase
]
];
$request = new Request();
$request->setLocale('en');
$requestStack->push($request);
$translator = $this->createMock(TranslatorInterface::class);
$dateExtension = new DateExtensions($requestStack, new LanguageFormattings($languages));
$dateExtension = new LocaleFormatExtensions(new LanguageFormattings($languages));
$dispatcher = new EventDispatcher();
$dispatcher->addSubscriber(new MetaFieldColumnSubscriber());

View File

@@ -27,13 +27,11 @@ use App\Event\ProjectMetaDisplayEvent;
use App\Event\TimesheetMetaDisplayEvent;
use App\Export\TimesheetExportInterface;
use App\Repository\Query\TimesheetQuery;
use App\Twig\DateExtensions;
use App\Twig\LocaleFormatExtensions;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -45,7 +43,6 @@ abstract class AbstractRendererTest extends KernelTestCase
*/
protected function getAbstractRenderer(string $classname)
{
$requestStack = new RequestStack();
$languages = [
'en' => [
'date' => 'Y.m.d',
@@ -54,12 +51,8 @@ abstract class AbstractRendererTest extends KernelTestCase
]
];
$request = new Request();
$request->setLocale('en');
$requestStack->push($request);
$translator = $this->getMockBuilder(TranslatorInterface::class)->getMock();
$dateExtension = new DateExtensions($requestStack, new LanguageFormattings($languages));
$dateExtension = new LocaleFormatExtensions(new LanguageFormattings($languages));
$dispatcher = new EventDispatcher();
$dispatcher->addSubscriber(new MetaFieldColumnSubscriber());

View File

@@ -9,7 +9,7 @@
namespace App\Tests\Twig;
use App\Configuration\ThemeConfiguration;
use App\Configuration\SystemConfiguration;
use App\Tests\Configuration\TestConfigLoader;
use App\Twig\ConfigExtension;
use PHPUnit\Framework\TestCase;
@@ -23,7 +23,7 @@ class ConfigExtensionTest extends TestCase
protected function getSut(array $settings, array $loaderSettings = []): ConfigExtension
{
$loader = new TestConfigLoader($loaderSettings);
$config = new ThemeConfiguration($loader, $settings);
$config = new SystemConfiguration($loader, ['theme' => $settings]);
return new ConfigExtension($config);
}

View File

@@ -1,238 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Twig;
use App\Configuration\LanguageFormattings;
use App\Entity\User;
use App\Twig\DateExtensions;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Twig\TwigFilter;
use Twig\TwigFunction;
/**
* @covers \App\Twig\DateExtensions
* @covers \App\Utils\LocaleFormats
* @covers \App\Utils\LocaleFormatter
*/
class DateExtensionsTest extends TestCase
{
/**
* @param string $locale
* @param array $dateSettings
* @return DateExtensions
*/
protected function getSut($locale, array $dateSettings)
{
$request = new Request();
$request->setLocale($locale);
$requestStack = new RequestStack();
$requestStack->push($request);
return new DateExtensions($requestStack, new LanguageFormattings($dateSettings));
}
public function testGetFilters()
{
$filters = ['month_name', 'day_name', 'date_short', 'date_time', 'date_full', 'date_format', 'time', 'hour24'];
$sut = $this->getSut('de', []);
$twigFilters = $sut->getFilters();
$this->assertCount(\count($filters), $twigFilters);
$i = 0;
foreach ($twigFilters as $filter) {
$this->assertInstanceOf(TwigFilter::class, $filter);
$this->assertEquals($filters[$i++], $filter->getName());
}
}
public function testGetFunctions()
{
$functions = ['get_format_duration', 'create_date'];
$sut = $this->getSut('de', []);
$twigFunctions = $sut->getFunctions();
$this->assertCount(\count($functions), $twigFunctions);
$i = 0;
/** @var TwigFunction $filter */
foreach ($twigFunctions as $filter) {
$this->assertInstanceOf(TwigFunction::class, $filter);
$this->assertEquals($functions[$i++], $filter->getName());
}
}
/**
* @param string $locale
* @param \DateTime|string $date
* @param string $result
* @dataProvider getDateShortData
*/
public function testDateShort($locale, $date, $result)
{
$sut = $this->getSut($locale, [
'de' => ['date' => 'd.m.Y'],
'en' => ['date' => 'Y-m-d'],
'ru' => ['date' => 'd.m.Y'],
]);
$this->assertEquals($result, $sut->dateShort($date));
}
public function getDateShortData()
{
return [
['en', new \DateTime('7 January 2010'), '2010-01-07'],
['en', new \DateTime('2016-06-23'), '2016-06-23'],
['de', new \DateTime('1980-12-14'), '14.12.1980'],
['ru', new \DateTime('1980-12-14'), '14.12.1980'],
['ru', '1980-12-14', '14.12.1980'],
['ru', 1.2345, 1.2345],
];
}
/**
* @param string $locale
* @param \DateTime|string $date
* @param string $result
* @dataProvider getDateTimeData
*/
public function testDateTime($locale, $date, $result)
{
$sut = $this->getSut($locale, [
'de' => ['date_time' => 'd.m.Y H:i:s'],
'en' => ['date_time' => 'Y-m-d h:m A'],
]);
$this->assertEquals($result, $sut->dateTime($date));
}
public function getDateTimeData()
{
return [
['en', new \DateTime('7 January 2010'), '2010-01-07 12:01 AM'],
['de', (new \DateTime('1980-12-14'))->setTime(13, 27, 55), '14.12.1980 13:27:55'],
['de', '1980-12-14 13:27:55', '14.12.1980 13:27:55'],
['de', 1.2345, 1.2345],
];
}
/**
* @dataProvider getDayNameTestData
*/
public function testDayName(string $locale, string $date, string $expectedName, bool $short)
{
$sut = $this->getSut($locale, []);
self::assertEquals($expectedName, $sut->dayName(new \DateTime($date), $short));
}
public function getDayNameTestData()
{
return [
['de', '2020-07-09 12:00:00', 'Donnerstag', false],
['en', '2020-07-09 12:00:00', 'Thursday', false],
['de', '2020-07-09 12:00:00', 'Do.', true],
['en', '2020-07-09 12:00:00', 'Thu', true],
];
}
/**
* @dataProvider getMonthNameTestData
*/
public function testMonthName(string $locale, string $date, string $expectedName, bool $withYear = false)
{
$sut = $this->getSut($locale, []);
self::assertEquals($expectedName, $sut->monthName(new \DateTime($date), $withYear));
}
public function getMonthNameTestData()
{
return [
['de', '2020-07-09 23:59:59', 'Juli', false],
['en', '2020-07-09 23:59:59', 'July', false],
['de', 'January 2016', 'Januar', false],
['en', 'January 2016', 'January', false],
['en', '2016-12-23', 'December', false],
['ru', '2016-12-23', 'декабрь', false],
['de', '2020-07-09 23:59:59', 'Juli 2020', true],
['en', '2020-07-09 23:59:59', 'July 2020', true],
['de', 'January 2016', 'Januar 2016', true],
['en', 'January 2016', 'January 2016', true],
['en', '2015-12-23', 'December 2015', true],
['ru', '2015-12-23', 'декабрь 2015', true],
];
}
public function testDateFormat()
{
$date = new \DateTime('7 January 2010 17:43:21', new \DateTimeZone('Europe/Berlin'));
$sut = $this->getSut('en', []);
$this->assertEquals('2010-01-07T17:43:21+01:00', $sut->dateFormat($date, 'c'));
$this->assertStringStartsWith('2010-01-07T17:43:21', $sut->dateFormat('7 January 2010 17:43:21', 'c'));
// next test checks the fallback for errors while converting the date
/* @phpstan-ignore-next-line */
$this->assertEquals(2010.0107, $sut->dateFormat(2010.0107, 'c'));
}
public function testTime()
{
$time = new \DateTime('2016-06-23');
$time->setTime(17, 53, 23);
$sut = $this->getSut('en', ['en' => ['time' => 'H:i']]);
$this->assertEquals('17:53', $sut->time($time));
$this->assertEquals('17:53', $sut->time('2016-06-23 17:53'));
}
public function testHour24()
{
$sut = $this->getSut('en', [
'en' => ['24_hours' => false],
]);
$this->assertEquals('bar', $sut->hour24('foo', 'bar'));
$sut = $this->getSut('de', [
'de' => ['24_hours' => true],
]);
$this->assertEquals('foo', $sut->hour24('foo', 'bar'));
}
public function testDateTimeFull()
{
$sut = $this->getSut('en', [
'en' => ['date_time_type' => 'yyyy-MM-dd HH:mm:ss'],
]);
$dateTime = new \DateTime('2019-08-17 12:29:47', new \DateTimeZone(date_default_timezone_get()));
$dateTime->setDate(2019, 8, 17);
$dateTime->setTime(12, 29, 47);
$this->assertEquals('2019-08-17 12:29:47', $sut->dateTimeFull($dateTime));
$this->assertEquals('2019-08-17 12:29:47', $sut->dateTimeFull('2019-08-17 12:29:47'));
// next test checks the fallback for errors while converting the date
/* @phpstan-ignore-next-line */
$this->assertEquals(189.45, $sut->dateTimeFull(189.45));
}
public function testCreateDate()
{
$user = new User();
$user->setTimezone('Europe/Berlin');
$sut = $this->getSut('en', []);
$date = $sut->createDate('now', $user);
$this->assertEquals('Europe/Berlin', $date->getTimezone()->getName());
$user->setTimezone('Asia/Dubai');
$date = $sut->createDate('2019-08-27 16:30:45', $user);
$this->assertEquals('2019-08-27T16:30:45+0400', $date->format(DATE_ISO8601));
$this->assertEquals('Asia/Dubai', $date->getTimezone()->getName());
$date = $sut->createDate('2019-08-27 16:30:45', null);
$this->assertEquals(date_default_timezone_get(), $date->getTimezone()->getName());
}
}

View File

@@ -1,39 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Twig;
use App\Twig\EventExtensions;
use PHPUnit\Framework\TestCase;
use Twig\TwigFunction;
/**
* @covers \App\Twig\EventExtensions
*/
class EventExtensionsTest extends TestCase
{
protected function getSut(): EventExtensions
{
return new EventExtensions();
}
public function testGetFunctions()
{
$functions = ['trigger', 'javascript_translations'];
$sut = $this->getSut();
$twigFunctions = $sut->getFunctions();
self::assertCount(\count($functions), $twigFunctions);
$i = 0;
/** @var TwigFunction $filter */
foreach ($twigFunctions as $filter) {
self::assertInstanceOf(TwigFunction::class, $filter);
self::assertEquals($functions[$i++], $filter->getName());
}
}
}

View File

@@ -11,19 +11,19 @@ namespace App\Tests\Twig;
use App\Configuration\LanguageFormattings;
use App\Entity\Timesheet;
use App\Twig\LocaleExtensions;
use App\Entity\User;
use App\Twig\LocaleFormatExtensions;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Intl\Util\IntlTestHelper;
use Twig\TwigFilter;
use Twig\TwigFunction;
/**
* @covers \App\Twig\LocaleExtensions
* @covers \App\Twig\LocaleFormatExtensions
* @covers \App\Utils\LocaleFormatter
* @covers \App\Utils\LocaleFormats
*/
class LocaleExtensionsTest extends TestCase
class LocaleFormatExtensionsTest extends TestCase
{
private $localeEn = ['en' => ['date' => 'Y-m-d', 'duration' => '%h:%m h']];
private $localeDe = ['de' => ['date' => 'd.m.Y', 'duration' => '%h:%m h']];
@@ -31,28 +31,35 @@ class LocaleExtensionsTest extends TestCase
private $localeFake = ['XX' => ['date' => 'd.m.Y', 'duration' => '%h - %m - %s Zeit']];
/**
* @param array $locales
* @param string $locale
* @return LocaleExtensions
* @param string|array $locale
* @param array|string $dateSettings
* @return LocaleFormatExtensions
*/
protected function getSut($locales, $locale = 'en')
protected function getSut($locale, $dateSettings)
{
$request = new Request();
$request->setLocale($locale);
$requestStack = new RequestStack();
$requestStack->push($request);
$language = $locale;
if (\is_array($locale)) {
$language = $dateSettings;
$dateSettings = $locale;
}
$sut = new LocaleFormatExtensions(new LanguageFormattings($dateSettings));
$sut->setLocale($language);
return new LocaleExtensions($requestStack, new LanguageFormattings($locales));
return $sut;
}
public function testGetFilters()
{
$filters = ['duration', 'duration_decimal', 'money', 'currency', 'country', 'language', 'amount'];
$sut = $this->getSut($this->localeDe);
$filters = [
'month_name', 'day_name', 'date_short', 'date_time', 'date_full', 'date_format', 'time', 'hour24',
'duration', 'duration_decimal', 'money', 'currency', 'country', 'language', 'amount'
];
$i = 0;
$sut = $this->getSut('de', []);
$twigFilters = $sut->getFilters();
$this->assertCount(\count($filters), $twigFilters);
$i = 0;
/** @var TwigFilter $filter */
foreach ($twigFilters as $filter) {
$this->assertInstanceOf(TwigFilter::class, $filter);
$this->assertEquals($filters[$i++], $filter->getName());
@@ -61,11 +68,13 @@ class LocaleExtensionsTest extends TestCase
public function testGetFunctions()
{
$functions = ['locales'];
$sut = $this->getSut($this->localeDe);
$functions = ['get_format_duration', 'create_date', 'locales'];
$i = 0;
$sut = $this->getSut('de', []);
$twigFunctions = $sut->getFunctions();
$this->assertCount(\count($functions), $twigFunctions);
$i = 0;
/** @var TwigFunction $filter */
foreach ($twigFunctions as $filter) {
$this->assertInstanceOf(TwigFunction::class, $filter);
@@ -73,6 +82,175 @@ class LocaleExtensionsTest extends TestCase
}
}
/**
* @param string $locale
* @param \DateTime|string $date
* @param string $result
* @dataProvider getDateShortData
*/
public function testDateShort($locale, $date, $result)
{
$sut = $this->getSut($locale, [
'de' => ['date' => 'd.m.Y'],
'en' => ['date' => 'Y-m-d'],
'ru' => ['date' => 'd.m.Y'],
]);
$this->assertEquals($result, $sut->dateShort($date));
}
public function getDateShortData()
{
return [
['en', new \DateTime('7 January 2010'), '2010-01-07'],
['en', new \DateTime('2016-06-23'), '2016-06-23'],
['de', new \DateTime('1980-12-14'), '14.12.1980'],
['ru', new \DateTime('1980-12-14'), '14.12.1980'],
['ru', '1980-12-14', '14.12.1980'],
['ru', 1.2345, 1.2345],
];
}
/**
* @param string $locale
* @param \DateTime|string $date
* @param string $result
* @dataProvider getDateTimeData
*/
public function testDateTime($locale, $date, $result)
{
$sut = $this->getSut($locale, [
'de' => ['date_time' => 'd.m.Y H:i:s'],
'en' => ['date_time' => 'Y-m-d h:m A'],
]);
$this->assertEquals($result, $sut->dateTime($date));
}
public function getDateTimeData()
{
return [
['en', new \DateTime('7 January 2010'), '2010-01-07 12:01 AM'],
['de', (new \DateTime('1980-12-14'))->setTime(13, 27, 55), '14.12.1980 13:27:55'],
['de', '1980-12-14 13:27:55', '14.12.1980 13:27:55'],
['de', 1.2345, 1.2345],
];
}
/**
* @dataProvider getDayNameTestData
*/
public function testDayName(string $locale, string $date, string $expectedName, bool $short)
{
$sut = $this->getSut($locale, []);
self::assertEquals($expectedName, $sut->dayName(new \DateTime($date), $short));
}
public function getDayNameTestData()
{
return [
['de', '2020-07-09 12:00:00', 'Donnerstag', false],
['en', '2020-07-09 12:00:00', 'Thursday', false],
['de', '2020-07-09 12:00:00', 'Do.', true],
['en', '2020-07-09 12:00:00', 'Thu', true],
];
}
/**
* @dataProvider getMonthNameTestData
*/
public function testMonthName(string $locale, string $date, string $expectedName, bool $withYear = false)
{
$sut = $this->getSut($locale, []);
self::assertEquals($expectedName, $sut->monthName(new \DateTime($date), $withYear));
}
public function getMonthNameTestData()
{
return [
['de', '2020-07-09 23:59:59', 'Juli', false],
['en', '2020-07-09 23:59:59', 'July', false],
['de', 'January 2016', 'Januar', false],
['en', 'January 2016', 'January', false],
['en', '2016-12-23', 'December', false],
['ru', '2016-12-23', 'декабрь', false],
['de', '2020-07-09 23:59:59', 'Juli 2020', true],
['en', '2020-07-09 23:59:59', 'July 2020', true],
['de', 'January 2016', 'Januar 2016', true],
['en', 'January 2016', 'January 2016', true],
['en', '2015-12-23', 'December 2015', true],
['ru', '2015-12-23', 'декабрь 2015', true],
];
}
public function testDateFormat()
{
$date = new \DateTime('7 January 2010 17:43:21', new \DateTimeZone('Europe/Berlin'));
$sut = $this->getSut('en', []);
$this->assertEquals('2010-01-07T17:43:21+01:00', $sut->dateFormat($date, 'c'));
$this->assertStringStartsWith('2010-01-07T17:43:21', $sut->dateFormat('7 January 2010 17:43:21', 'c'));
// next test checks the fallback for errors while converting the date
/* @phpstan-ignore-next-line */
$this->assertEquals(2010.0107, $sut->dateFormat(2010.0107, 'c'));
}
public function testTime()
{
$time = new \DateTime('2016-06-23');
$time->setTime(17, 53, 23);
$sut = $this->getSut('en', ['en' => ['time' => 'H:i']]);
$this->assertEquals('17:53', $sut->time($time));
$this->assertEquals('17:53', $sut->time('2016-06-23 17:53'));
}
public function testHour24()
{
$sut = $this->getSut('en', [
'en' => ['24_hours' => false],
]);
$this->assertEquals('bar', $sut->hour24('foo', 'bar'));
$sut = $this->getSut('de', [
'de' => ['24_hours' => true],
]);
$this->assertEquals('foo', $sut->hour24('foo', 'bar'));
}
public function testDateTimeFull()
{
$sut = $this->getSut('en', [
'en' => ['date_time_type' => 'yyyy-MM-dd HH:mm:ss'],
]);
$dateTime = new \DateTime('2019-08-17 12:29:47', new \DateTimeZone(date_default_timezone_get()));
$dateTime->setDate(2019, 8, 17);
$dateTime->setTime(12, 29, 47);
$this->assertEquals('2019-08-17 12:29:47', $sut->dateTimeFull($dateTime));
$this->assertEquals('2019-08-17 12:29:47', $sut->dateTimeFull('2019-08-17 12:29:47'));
// next test checks the fallback for errors while converting the date
/* @phpstan-ignore-next-line */
$this->assertEquals(189.45, $sut->dateTimeFull(189.45));
}
public function testCreateDate()
{
$user = new User();
$user->setTimezone('Europe/Berlin');
$sut = $this->getSut('en', []);
$date = $sut->createDate('now', $user);
$this->assertEquals('Europe/Berlin', $date->getTimezone()->getName());
$user->setTimezone('Asia/Dubai');
$date = $sut->createDate('2019-08-27 16:30:45', $user);
$this->assertEquals('2019-08-27T16:30:45+0400', $date->format(DATE_ISO8601));
$this->assertEquals('Asia/Dubai', $date->getTimezone()->getName());
$date = $sut->createDate('2019-08-27 16:30:45', null);
$this->assertEquals(date_default_timezone_get(), $date->getTimezone()->getName());
}
public function testLocales()
{
$locales = [
@@ -82,7 +260,7 @@ class LocaleExtensionsTest extends TestCase
];
$appLocales = array_merge($this->localeEn, $this->localeDe, $this->localeRu);
$sut = $this->getSut($appLocales);
$sut = $this->getSut('en', $appLocales);
$this->assertEquals($locales, $sut->getLocales());
}
@@ -96,7 +274,7 @@ class LocaleExtensionsTest extends TestCase
123 => 123,
];
$sut = $this->getSut($this->localeEn);
$sut = $this->getSut('en', $this->localeEn);
foreach ($symbols as $name => $symbol) {
$this->assertEquals($symbol, $sut->currency($name));
}
@@ -112,7 +290,7 @@ class LocaleExtensionsTest extends TestCase
'12' => '12',
];
$sut = $this->getSut($this->localeEn);
$sut = $this->getSut('en', $this->localeEn);
foreach ($countries as $locale => $name) {
$this->assertEquals($name, $sut->country($locale));
}
@@ -128,7 +306,7 @@ class LocaleExtensionsTest extends TestCase
'12' => '12',
];
$sut = $this->getSut($this->localeEn);
$sut = $this->getSut('en', $this->localeEn);
foreach ($languages as $locale => $name) {
$this->assertEquals($name, $sut->language($locale));
}
@@ -254,7 +432,7 @@ class LocaleExtensionsTest extends TestCase
{
$record = $this->getTimesheet(9437);
$sut = $this->getSut($this->localeEn);
$sut = $this->getSut('en', $this->localeEn);
$this->assertEquals('02:37 h', $sut->duration($record->getDuration()));
$this->assertEquals('2.62', $sut->duration($record->getDuration(), true));
@@ -284,7 +462,7 @@ class LocaleExtensionsTest extends TestCase
{
$record = $this->getTimesheet(9437);
$sut = $this->getSut($this->localeEn);
$sut = $this->getSut('en', $this->localeEn);
$this->assertEquals('2.62', $sut->durationDecimal($record->getDuration()));
// test Timesheet object

View File

@@ -7,17 +7,16 @@
* file that was distributed with this source code.
*/
namespace App\Tests\Twig;
namespace App\Tests\Twig\Runtime;
use App\Twig\EncoreExtension;
use App\Twig\Runtime\EncoreExtension;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
use Symfony\WebpackEncoreBundle\Asset\EntrypointLookupInterface;
use Twig\TwigFunction;
/**
* @covers \App\Twig\EncoreExtension
* @covers \App\Twig\Runtime\EncoreExtension
*/
class EncoreExtensionTest extends TestCase
{
@@ -29,7 +28,7 @@ class EncoreExtensionTest extends TestCase
$container = new Container(new ParameterBag([]));
$container->set(EntrypointLookupInterface::class, $entryLookup);
return new EncoreExtension($container, __DIR__);
return new EncoreExtension($container, __DIR__ . '/../');
}
public function testGetSubscribedServices()
@@ -37,20 +36,6 @@ class EncoreExtensionTest extends TestCase
self::assertEquals([EntrypointLookupInterface::class], EncoreExtension::getSubscribedServices());
}
public function testGetFunctions()
{
$functions = ['encore_entry_css_source'];
$sut = $this->getSut();
$twigFunctions = $sut->getFunctions();
self::assertCount(\count($functions), $twigFunctions);
$i = 0;
/** @var TwigFunction $filter */
foreach ($twigFunctions as $filter) {
self::assertInstanceOf(TwigFunction::class, $filter);
self::assertEquals($functions[$i++], $filter->getName());
}
}
public function testGetEncoreEntryCssSource()
{
$sut = $this->getSut(['test.css', 'test1.css']);

View File

@@ -0,0 +1,33 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Twig\Runtime;
use App\Export\ServiceExport;
use App\Twig\Runtime\ExporterExtension;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\Twig\Runtime\ExporterExtension
*/
class ExporterExtensionTest extends TestCase
{
protected function getSut(): ExporterExtension
{
$service = new ServiceExport();
return new ExporterExtension($service);
}
public function testGetExporter()
{
$sut = $this->getSut();
self::assertEquals([], $sut->getTimesheetExporter());
}
}

View File

@@ -7,44 +7,19 @@
* file that was distributed with this source code.
*/
namespace App\Tests\Twig;
namespace App\Tests\Twig\Runtime;
use App\Configuration\ConfigLoaderInterface;
use App\Configuration\SystemConfiguration;
use App\Twig\MarkdownExtension;
use App\Twig\Runtime\MarkdownExtension;
use App\Utils\Markdown;
use PHPUnit\Framework\TestCase;
use Twig\Node\Node;
/**
* @covers \App\Twig\MarkdownExtension
* @covers \App\Twig\Runtime\MarkdownExtension
*/
class MarkdownExtensionTest extends TestCase
{
public function testGetFilters()
{
$loader = $this->createMock(ConfigLoaderInterface::class);
$config = new SystemConfiguration($loader, ['timesheet' => ['markdown_content' => true]]);
$sut = new MarkdownExtension(new Markdown(), $config);
$filters = $sut->getFilters();
$this->assertCount(3, $filters);
// make sure that the md2html filter does proper escaping
$this->assertEquals('md2html', $filters[0]->getName());
self::assertEquals('html', $filters[0]->getPreEscape());
self::assertEquals(['html'], $filters[0]->getSafe(new Node()));
// make sure that the desc2html filter does proper escaping
$this->assertEquals('desc2html', $filters[1]->getName());
self::assertEquals('html', $filters[1]->getPreEscape());
self::assertEquals(['html'], $filters[1]->getSafe(new Node()));
// make sure that the comment2html filter does proper escaping
$this->assertEquals('comment2html', $filters[2]->getName());
self::assertEquals('html', $filters[2]->getPreEscape());
self::assertEquals(['html'], $filters[2]->getSafe(new Node()));
}
public function testMarkdownToHtml()
{
$loader = $this->createMock(ConfigLoaderInterface::class);

View File

@@ -12,16 +12,21 @@ namespace App\Tests\Twig\Runtime;
use App\Entity\User;
use App\Event\ThemeEvent;
use App\Tests\Mocks\Security\CurrentUserFactory;
use App\Twig\Runtime\ThemeEventExtension;
use App\Twig\Runtime\ThemeExtension;
use PHPUnit\Framework\TestCase;
use Symfony\Bridge\Twig\AppVariable;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Twig\Environment;
use Twig\Loader\FilesystemLoader;
/**
* @covers \App\Twig\Runtime\ThemeEventExtension
* @covers \App\Twig\Runtime\ThemeExtension
*/
class ThemeEventExtensionTest extends TestCase
{
protected function getSut(bool $hasListener = true): ThemeEventExtension
protected function getSut(bool $hasListener = true): ThemeExtension
{
$dispatcher = $this->createMock(EventDispatcherInterface::class);
$dispatcher->method('hasListeners')->willReturn($hasListener);
@@ -29,20 +34,39 @@ class ThemeEventExtensionTest extends TestCase
$user = (new CurrentUserFactory($this))->create(new User());
return new ThemeEventExtension($dispatcher, $user);
return new ThemeExtension($dispatcher);
}
protected function getEnvironment(): Environment
{
$mock = $this->getMockBuilder(UsernamePasswordToken::class)->onlyMethods(['getUser'])->disableOriginalConstructor()->getMock();
$mock->method('getUser')->willReturn(new User());
/** @var UsernamePasswordToken $token */
$token = $mock;
$tokenStorage = new TokenStorage();
$tokenStorage->setToken($token);
$app = new AppVariable();
$app->setTokenStorage($tokenStorage);
$environment = new Environment(new FilesystemLoader());
$environment->addGlobal('app', $app);
return $environment;
}
public function testTrigger()
{
$sut = $this->getSut();
$event = $sut->trigger('foo', []);
$event = $sut->trigger($this->getEnvironment(), 'foo', []);
self::assertInstanceOf(ThemeEvent::class, $event);
}
public function testTriggerWithoutListener()
{
$sut = $this->getSut(false);
$event = $sut->trigger('foo', []);
$event = $sut->trigger($this->getEnvironment(), 'foo', []);
self::assertInstanceOf(ThemeEvent::class, $event);
}

View File

@@ -0,0 +1,33 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Twig\Runtime;
use App\Entity\Timesheet;
use App\Entity\User;
use App\Repository\TimesheetRepository;
use App\Twig\Runtime\TimesheetExtension;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\Twig\Runtime\TimesheetExtension
*/
class TimesheetExtensionTest extends TestCase
{
public function testGetExporter()
{
$entries = [new Timesheet(), new Timesheet()];
$repository = $this->createMock(TimesheetRepository::class);
$repository->method('getActiveEntries')->willReturn($entries);
$sut = new TimesheetExtension($repository);
self::assertEquals($entries, $sut->activeEntries(new User()));
}
}

View File

@@ -7,24 +7,23 @@
* file that was distributed with this source code.
*/
namespace App\Tests\Twig;
namespace App\Tests\Twig\Runtime;
use App\Twig\WidgetExtension;
use App\Twig\Runtime\WidgetExtension;
use App\Widget\Type\More;
use App\Widget\WidgetInterface;
use App\Widget\WidgetRendererInterface;
use App\Widget\WidgetService;
use PHPUnit\Framework\TestCase;
use Twig\TwigFunction;
/**
* @covers \App\Twig\WidgetExtension
* @covers \App\Twig\Runtime\WidgetExtension
*/
class WidgetExtensionTest extends TestCase
{
protected function getSut($hasWidget = null, $getWidget = null, $renderer = null): WidgetExtension
{
$service = $this->getMockBuilder(WidgetService::class)->disableOriginalConstructor()->onlyMethods(['hasWidget', 'getWidget', 'findRenderer'])->getMock();
$service = $this->createMock(WidgetService::class);
if (null !== $hasWidget) {
$service->expects($this->once())->method('hasWidget')->willReturn($hasWidget);
}
@@ -38,20 +37,6 @@ class WidgetExtensionTest extends TestCase
return new WidgetExtension($service);
}
public function testGetFunctions()
{
$functions = ['render_widget'];
$sut = $this->getSut();
$twigFunctions = $sut->getFunctions();
$this->assertCount(\count($functions), $twigFunctions);
$i = 0;
/** @var TwigFunction $function */
foreach ($twigFunctions as $function) {
$this->assertInstanceOf(TwigFunction::class, $function);
$this->assertEquals($functions[$i++], $function->getName());
}
}
public function testRenderWidgetForInvalidValue()
{
$this->expectException(\InvalidArgumentException::class);

View File

@@ -0,0 +1,87 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Twig;
use App\Twig\RuntimeExtensions;
use PHPUnit\Framework\TestCase;
use Twig\Node\Node;
use Twig\TwigFilter;
use Twig\TwigFunction;
/**
* @covers \App\Twig\RuntimeExtensions
*/
class RuntimeExtensionsTest extends TestCase
{
public function testGetFilters()
{
$expected = ['md2html', 'desc2html', 'comment2html'];
$i = 0;
$sut = new RuntimeExtensions();
$twigFilters = $sut->getFilters();
$this->assertCount(\count($expected), $twigFilters);
foreach ($twigFilters as $filter) {
$this->assertInstanceOf(TwigFilter::class, $filter);
$this->assertEquals($expected[$i++], $filter->getName());
}
}
public function testGetFunctions()
{
$expected = ['trigger', 'javascript_translations', 'timesheet_exporter', 'active_timesheets', 'encore_entry_css_source', 'render_widget'];
$i = 0;
$sut = new RuntimeExtensions();
$twigFunctions = $sut->getFunctions();
$this->assertCount(\count($expected), $twigFunctions);
/** @var TwigFunction $filter */
foreach ($twigFunctions as $filter) {
$this->assertInstanceOf(TwigFunction::class, $filter);
$this->assertEquals($expected[$i++], $filter->getName());
}
}
public function testGetFilterDefinition()
{
$sut = new RuntimeExtensions();
$filters = $sut->getFilters();
$found_md2html = false;
$found_desc2html = false;
$found_comment2html = false;
foreach ($filters as $filter) {
switch ($filter->getName()) {
case 'md2html':
self::assertEquals('html', $filters[0]->getPreEscape());
self::assertEquals(['html'], $filters[0]->getSafe(new Node()));
$found_md2html = true;
break;
case 'desc2html':
self::assertEquals('html', $filters[1]->getPreEscape());
self::assertEquals(['html'], $filters[1]->getSafe(new Node()));
$found_desc2html = true;
break;
case 'comment2html':
self::assertEquals('html', $filters[2]->getPreEscape());
self::assertEquals(['html'], $filters[2]->getSafe(new Node()));
$found_comment2html = true;
break;
}
}
self::assertTrue($found_md2html, 'Missing filter: md2html');
self::assertTrue($found_desc2html, 'Missing filter: desc2html');
self::assertTrue($found_comment2html, 'Missing filter: comment2html');
}
}

View File

@@ -1,48 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Twig;
use App\Export\ServiceExport;
use App\Twig\TimesheetExtension;
use PHPUnit\Framework\TestCase;
use Twig\TwigFunction;
/**
* @covers \App\Twig\TimesheetExtension
*/
class TimesheetExtensionTest extends TestCase
{
protected function getSut(): TimesheetExtension
{
$service = new ServiceExport();
return new TimesheetExtension($service);
}
public function testGetFunctions()
{
$sut = $this->getSut();
$functions = ['timesheet_exporter'];
$twigFunctions = $sut->getFunctions();
$this->assertCount(\count($functions), $twigFunctions);
$i = 0;
/** @var TwigFunction $function */
foreach ($twigFunctions as $function) {
$this->assertInstanceOf(TwigFunction::class, $function);
$this->assertEquals($functions[$i++], $function->getName());
}
}
public function testGetExporter()
{
$sut = $this->getSut();
self::assertEquals([], $sut->getTimesheetExporter());
}
}