Next major version 2 with PHP 8.1, Symfony 6, Tabler UI, 2FA ... (#2902)
This commit is contained in:
@@ -18,12 +18,14 @@ use App\Entity\Timesheet;
|
||||
|
||||
final class Color
|
||||
{
|
||||
// @see https://clrs.cc
|
||||
public const PALETTE_1 = ['#0074D9', '#7FDBFF', '#39CCCC', '#B10DC9', '#F012BE', '#85144b', '#FF4136', '#FF851B', '#FFDC00', '#3D9970', '#2ECC40', '#01FF70', '#AAAAAA', '#DDDDDD'];
|
||||
// old avatar color set
|
||||
public const PALETTE_2 = ['#a972c9', '#9C27B0', '#673AB7', '#5319e7', '#041fd1', '#3F51B5', '#2196F3', '#03A9F4', '#00BCD4', '#006b75', '#009688', '#00bb32', '#4CAF50', '#8BC34A', '#CDDC39', '#FFC107', '#FF9800', '#FF5722', '#f41a00', '#E91E63', '#b60205', '#cc317c', '#d82d80', '#e135f4', '#2d3748', '#4a5568', '#718096'];
|
||||
// all mixed together
|
||||
public const PALETTE_3 = ['#AAAAAA', '#DDDDDD', '#a972c9', '#9C27B0', '#673AB7', '#041fd1', '#5319e7', '#3F51B5', '#0074D9', '#2196F3', '#03A9F4', '#7FDBFF', '#39CCCC', '#00BCD4', '#006b75', '#009688', '#00bb32', '#4CAF50', '#3D9970', '#2ECC40', '#01FF70', '#8BC34A', '#CDDC39', '#FFDC00', '#FFC107', '#FF851B', '#FF9800', '#FF5722', '#f41a00', '#E91E63', '#85144b', '#b60205', '#FF4136', '#cc317c', '#F012BE', '#d82d80', '#B10DC9', '#e135f4', '#2d3748', '#4a5568', '#718096'];
|
||||
private const PALETTE = [
|
||||
'#AAAAAA', '#DDDDDD', '#a972c9', '#9C27B0', '#673AB7', '#041fd1', '#5319e7',
|
||||
'#3F51B5', '#0074D9', '#2196F3', '#03A9F4', '#7FDBFF', '#39CCCC', '#00BCD4',
|
||||
'#006b75', '#009688', '#00bb32', '#4CAF50', '#3D9970', '#2ECC40', '#01FF70',
|
||||
'#8BC34A', '#CDDC39', '#FFDC00', '#FFC107', '#FF851B', '#FF9800', '#FF5722',
|
||||
'#f41a00', '#E91E63', '#85144b', '#b60205', '#FF4136', '#cc317c', '#F012BE',
|
||||
'#d82d80', '#B10DC9', '#e135f4', '#2d3748', '#4a5568', '#718096'
|
||||
];
|
||||
|
||||
public function getTimesheetColor(Timesheet $timesheet): string
|
||||
{
|
||||
@@ -104,10 +106,9 @@ final class Color
|
||||
$id += mb_ord($input[$pos], 'UTF-8');
|
||||
}
|
||||
|
||||
$colors = self::PALETTE_3;
|
||||
$key = $id % \count($colors);
|
||||
$key = $id % \count(self::PALETTE);
|
||||
|
||||
return $colors[$key];
|
||||
return self::PALETTE[$key];
|
||||
}
|
||||
|
||||
public function getFontContrastColor(string $color): string
|
||||
|
||||
@@ -1,70 +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\Utils;
|
||||
|
||||
use App\Validator\ValidationFailedException;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
final class CommandStyle
|
||||
{
|
||||
private $input;
|
||||
private $output;
|
||||
private $style;
|
||||
|
||||
public function __construct(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$this->input = $input;
|
||||
$this->output = $output;
|
||||
}
|
||||
|
||||
private function getStyle(): SymfonyStyle
|
||||
{
|
||||
if ($this->style === null) {
|
||||
$this->style = new SymfonyStyle($this->input, $this->output);
|
||||
}
|
||||
|
||||
return $this->style;
|
||||
}
|
||||
|
||||
public function success($message): void
|
||||
{
|
||||
$this->getStyle()->success($message);
|
||||
}
|
||||
|
||||
public function error($message): void
|
||||
{
|
||||
$this->getStyle()->error($message);
|
||||
}
|
||||
|
||||
public function warning($message): void
|
||||
{
|
||||
$this->getStyle()->warning($message);
|
||||
}
|
||||
|
||||
public function validationError(ValidationFailedException $exception): void
|
||||
{
|
||||
$errors = $exception->getViolations();
|
||||
if ($errors->count() > 0) {
|
||||
$style = $this->getStyle();
|
||||
/** @var \Symfony\Component\Validator\ConstraintViolation $error */
|
||||
foreach ($errors as $error) {
|
||||
$value = $error->getInvalidValue();
|
||||
$style->error(
|
||||
$error->getPropertyPath()
|
||||
. ' (' . (\is_array($value) ? implode(',', $value) : $value) . ')'
|
||||
. "\n "
|
||||
. $error->getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,11 +13,8 @@ use App\Entity\User;
|
||||
|
||||
final class Context
|
||||
{
|
||||
private $user;
|
||||
|
||||
public function __construct(User $user)
|
||||
public function __construct(private User $user)
|
||||
{
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
public function getUser(): User
|
||||
|
||||
191
src/Utils/DataTable.php
Normal file
191
src/Utils/DataTable.php
Normal file
@@ -0,0 +1,191 @@
|
||||
<?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\Utils;
|
||||
|
||||
use App\Repository\Query\BaseQuery;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
use Traversable;
|
||||
|
||||
final class DataTable implements \Countable, \IteratorAggregate
|
||||
{
|
||||
private ?Pagination $pagination = null;
|
||||
private ?FormInterface $searchForm = null;
|
||||
private ?FormInterface $batchForm = null;
|
||||
private array $columns = [];
|
||||
private array $reloadEvents = [];
|
||||
private bool $configuration = true;
|
||||
private bool $sticky = true;
|
||||
private ?string $paginationRoute = null;
|
||||
|
||||
public function __construct(private string $tableName, private BaseQuery $query)
|
||||
{
|
||||
}
|
||||
|
||||
public function hasResults(): bool
|
||||
{
|
||||
return $this->pagination !== null && $this->pagination->count() > 0;
|
||||
}
|
||||
|
||||
public function getResults(): ?iterable
|
||||
{
|
||||
return $this->pagination;
|
||||
}
|
||||
|
||||
public function getPagination(): ?Pagination
|
||||
{
|
||||
return $this->pagination;
|
||||
}
|
||||
|
||||
public function setPagination(?Pagination $pagination): void
|
||||
{
|
||||
$this->pagination = $pagination;
|
||||
}
|
||||
|
||||
public function getTableName(): string
|
||||
{
|
||||
return $this->tableName;
|
||||
}
|
||||
|
||||
public function getQuery(): BaseQuery
|
||||
{
|
||||
return $this->query;
|
||||
}
|
||||
|
||||
public function getSearchForm(): ?FormView
|
||||
{
|
||||
return $this->searchForm?->createView();
|
||||
}
|
||||
|
||||
public function setSearchForm(?FormInterface $searchForm): void
|
||||
{
|
||||
$this->searchForm = $searchForm;
|
||||
}
|
||||
|
||||
public function hasBatchForm(): bool
|
||||
{
|
||||
return $this->batchForm !== null;
|
||||
}
|
||||
|
||||
public function getBatchForm(): ?FormView
|
||||
{
|
||||
return $this->batchForm?->createView();
|
||||
}
|
||||
|
||||
public function setBatchForm(?FormInterface $batchForm): void
|
||||
{
|
||||
$this->batchForm = $batchForm;
|
||||
|
||||
if (!\array_key_exists('id', $this->columns)) {
|
||||
$this->addColumn('id', [
|
||||
'class' => 'alwaysVisible multiCheckbox',
|
||||
'orderBy' => false,
|
||||
'title' => false,
|
||||
'batchUpdate' => true
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function getSortedColumnNames(): array
|
||||
{
|
||||
$columns = [];
|
||||
foreach ($this->columns as $key => $options) {
|
||||
$columns[$key] = \array_key_exists('data', $options) ? $options['data'] : [];
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
public function getColumns(): array
|
||||
{
|
||||
return $this->columns;
|
||||
}
|
||||
|
||||
public function setColumns(array $columns): void
|
||||
{
|
||||
$this->columns = $columns;
|
||||
}
|
||||
|
||||
public function addColumn(string $name, array $column): void
|
||||
{
|
||||
$this->columns[$name] = $column;
|
||||
}
|
||||
|
||||
public function deactivateConfiguration(): void
|
||||
{
|
||||
$this->configuration = false;
|
||||
}
|
||||
|
||||
public function hasConfiguration(): bool
|
||||
{
|
||||
return $this->configuration;
|
||||
}
|
||||
|
||||
public function getPaginationRoute(): ?string
|
||||
{
|
||||
return $this->paginationRoute;
|
||||
}
|
||||
|
||||
public function setPaginationRoute(?string $paginationRoute): void
|
||||
{
|
||||
$this->paginationRoute = $paginationRoute;
|
||||
}
|
||||
|
||||
public function getOptions(): array
|
||||
{
|
||||
$options = [
|
||||
'columnConfig' => false,
|
||||
'sticky' => $this->sticky,
|
||||
];
|
||||
|
||||
if (\count($this->reloadEvents) > 0) {
|
||||
$options['reload'] = $this->getReloadEvents();
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
public function getReloadEvents(): string
|
||||
{
|
||||
return implode(' ', $this->reloadEvents);
|
||||
}
|
||||
|
||||
public function setReloadEvents(string|array $reloadEvents): void
|
||||
{
|
||||
if (\is_string($reloadEvents)) {
|
||||
$reloadEvents = explode(' ', $reloadEvents);
|
||||
}
|
||||
$this->reloadEvents = $reloadEvents;
|
||||
}
|
||||
|
||||
public function addReloadEvent(string $reloadEvent): void
|
||||
{
|
||||
$this->reloadEvents[] = $reloadEvent;
|
||||
}
|
||||
|
||||
public function setSticky(bool $sticky = true): void
|
||||
{
|
||||
$this->sticky = $sticky;
|
||||
}
|
||||
|
||||
public function getIterator(): Traversable
|
||||
{
|
||||
return $this->pagination?->getIterator();
|
||||
}
|
||||
|
||||
public function count(): int
|
||||
{
|
||||
if ($this->pagination === null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return $this->pagination->count();
|
||||
}
|
||||
}
|
||||
@@ -1,33 +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\Utils;
|
||||
|
||||
class DateFormatConverter
|
||||
{
|
||||
/**
|
||||
* This defines the mapping between PHP date format (key) and ICU date format (value).
|
||||
* https://unicode-org.github.io/icu/userguide/format_parse/datetime/#datetime-format-syntax
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $formatConvertRules = [
|
||||
// hours
|
||||
'h' => 'hh', 'H' => 'HH',
|
||||
// minutes
|
||||
'i' => 'mm',
|
||||
// am/pm to AM/PM
|
||||
'A' => 'a'
|
||||
];
|
||||
|
||||
public function convert(string $format): string
|
||||
{
|
||||
return strtr($format, self::$formatConvertRules);
|
||||
}
|
||||
}
|
||||
@@ -12,21 +12,12 @@ namespace App\Utils;
|
||||
/**
|
||||
* Convert duration strings into seconds.
|
||||
*/
|
||||
class Duration
|
||||
final class Duration
|
||||
{
|
||||
public const FORMAT_COLON = 'colon';
|
||||
public const FORMAT_NATURAL = 'natural';
|
||||
public const FORMAT_DECIMAL = 'decimal';
|
||||
|
||||
/**
|
||||
* @deprecated since 1.13
|
||||
*/
|
||||
public const FORMAT_SECONDS = 'seconds';
|
||||
/**
|
||||
* @deprecated since 1.21
|
||||
*/
|
||||
public const FORMAT_WITH_SECONDS = '%h:%m';
|
||||
public const FORMAT_NO_SECONDS = '%h:%m';
|
||||
public const FORMAT_DEFAULT = '%h:%m';
|
||||
|
||||
/**
|
||||
* Transforms seconds into a duration string.
|
||||
@@ -35,7 +26,7 @@ class Duration
|
||||
* @param string $format
|
||||
* @return string|null
|
||||
*/
|
||||
public function format(?int $seconds, string $format = self::FORMAT_NO_SECONDS)
|
||||
public function format(?int $seconds, string $format = self::FORMAT_DEFAULT): ?string
|
||||
{
|
||||
if (null === $seconds) {
|
||||
return null;
|
||||
@@ -50,12 +41,11 @@ class Duration
|
||||
|
||||
$hour = (int) floor($seconds / 3600);
|
||||
$minute = (int) floor((int) ($seconds / 60) % 60);
|
||||
|
||||
$hour = $hour > 9 ? $hour : '0' . $hour;
|
||||
$minute = $minute > 9 ? $minute : '0' . $minute;
|
||||
$formatted = str_replace('%h', $hour, $format);
|
||||
|
||||
return str_replace('%m', $minute, $formatted);
|
||||
$formatted = str_replace('%h', (string) $hour, $format);
|
||||
|
||||
return str_replace('%m', (string) $minute, $formatted);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,11 +60,7 @@ class Duration
|
||||
return $this->parseDuration($duration, self::FORMAT_COLON);
|
||||
}
|
||||
|
||||
if (strpos($duration, '.') !== false || strpos($duration, ',') !== false) {
|
||||
return $this->parseDuration($duration, self::FORMAT_DECIMAL);
|
||||
}
|
||||
|
||||
if (is_numeric($duration) && $duration == (int) $duration) {
|
||||
if (str_contains($duration, '.') || str_contains($duration, ',') || is_numeric($duration)) {
|
||||
return $this->parseDuration($duration, self::FORMAT_DECIMAL);
|
||||
}
|
||||
|
||||
@@ -95,32 +81,15 @@ class Duration
|
||||
return 0;
|
||||
}
|
||||
|
||||
switch ($mode) {
|
||||
case self::FORMAT_COLON:
|
||||
$seconds = $this->parseColonFormat($duration);
|
||||
break;
|
||||
|
||||
case self::FORMAT_NATURAL:
|
||||
$seconds = $this->parseNaturalFormat($duration);
|
||||
break;
|
||||
|
||||
case self::FORMAT_DECIMAL:
|
||||
$seconds = $this->parseDecimalFormat($duration);
|
||||
break;
|
||||
|
||||
case self::FORMAT_SECONDS:
|
||||
@trigger_error('Duration format FORMAT_SECONDS is deprecated and will be removed with 2.0', E_USER_DEPRECATED);
|
||||
$seconds = (int) $duration;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new \InvalidArgumentException(sprintf('Unsupported duration format "%s"', $mode));
|
||||
}
|
||||
|
||||
return $seconds;
|
||||
return match ($mode) {
|
||||
self::FORMAT_COLON => $this->parseColonFormat($duration),
|
||||
self::FORMAT_NATURAL => $this->parseNaturalFormat($duration),
|
||||
self::FORMAT_DECIMAL => $this->parseDecimalFormat($duration),
|
||||
default => throw new \InvalidArgumentException(sprintf('Unsupported duration format "%s"', $mode)),
|
||||
};
|
||||
}
|
||||
|
||||
protected function parseNaturalFormat(string $duration): int
|
||||
private function parseNaturalFormat(string $duration): int
|
||||
{
|
||||
try {
|
||||
$interval = new \DateInterval('PT' . strtoupper($duration));
|
||||
@@ -133,7 +102,7 @@ class Duration
|
||||
}
|
||||
}
|
||||
|
||||
protected function parseDecimalFormat(string $duration): int
|
||||
private function parseDecimalFormat(string $duration): int
|
||||
{
|
||||
$duration = str_replace(',', '.', $duration);
|
||||
$duration = (float) $duration;
|
||||
@@ -142,7 +111,7 @@ class Duration
|
||||
return (int) $duration;
|
||||
}
|
||||
|
||||
protected function parseColonFormat(string $duration): int
|
||||
private function parseColonFormat(string $duration): int
|
||||
{
|
||||
$parts = explode(':', $duration);
|
||||
if (\count($parts) < 2 || \count($parts) > 3) {
|
||||
@@ -168,7 +137,7 @@ class Duration
|
||||
|
||||
$seconds = 0;
|
||||
|
||||
if (3 == \count($parts)) {
|
||||
if (3 === \count($parts)) {
|
||||
$seconds += (int) array_pop($parts);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,25 +14,17 @@ use Symfony\Component\String\UnicodeString;
|
||||
|
||||
final class FileHelper
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $dataDir;
|
||||
/**
|
||||
* @var Filesystem
|
||||
*/
|
||||
private $filesystem;
|
||||
private Filesystem $filesystem;
|
||||
|
||||
public function __construct(string $dataDir)
|
||||
public function __construct(private string $dataDir)
|
||||
{
|
||||
$this->dataDir = $dataDir;
|
||||
$this->filesystem = new Filesystem();
|
||||
}
|
||||
|
||||
/**
|
||||
* @CloudRequired
|
||||
*/
|
||||
public function setDataDirectory(string $directory)
|
||||
public function setDataDirectory(string $directory): void
|
||||
{
|
||||
$this->dataDir = $directory;
|
||||
}
|
||||
|
||||
96
src/Utils/FormFormatConverter.php
Normal file
96
src/Utils/FormFormatConverter.php
Normal file
@@ -0,0 +1,96 @@
|
||||
<?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\Utils;
|
||||
|
||||
final class FormFormatConverter
|
||||
{
|
||||
public const PATTERN_DAY_SINGLE = '(([1-9])|([1-2][0-9])|(3[01]))';
|
||||
public const PATTERN_DAY_DOUBLE = '((0[1-9])|([1-2][0-9])|(3[01]))';
|
||||
public const PATTERN_MONTH_SINGLE = '(([1-9])|([1][0-2]))';
|
||||
public const PATTERN_MONTH_DOUBLE = '((0[1-9])|([1][0-2]))';
|
||||
public const PATTERN_YEAR = '(19|20)\d{2}';
|
||||
public const PATTERN_HOUR_SINGLE = '([0-9]|[1][0-9]|2[0-3])';
|
||||
public const PATTERN_HOUR_DOUBLE = '([0-9]|[01][0-9]|2[0-3])';
|
||||
public const PATTERN_MINUTES = '([0-5][0-9])';
|
||||
|
||||
/**
|
||||
* This defines the mapping between ICU date format and PHP Date format.
|
||||
*
|
||||
* @see https://www.php.net/manual/en/datetime.format.php
|
||||
* @var array
|
||||
*/
|
||||
private static array $formatConvertRules = [
|
||||
// Litepicker interprets a year like 22 as 1922 instead of 2022
|
||||
// so we have to make sure that it is always a4-digit year
|
||||
"'h'" => "\h", // special format for fr_CA which includes 'h' as character
|
||||
'yy' => 'yyyy',
|
||||
'y' => 'yyyy',
|
||||
'mm' => 'i', // ICU 2 letter minutes
|
||||
'a' => 'A', // uppercase AM/PM, Luxon only supports uppercase
|
||||
'HH' => 'H', // H = 24-hour format of an hour with leading zeros
|
||||
'h' => 'g', // g = 12-hour format of an hour without leading zeros 1 through 12
|
||||
'H' => 'G', // G = 24-hour format of an hour without leading zeros 0 through 23
|
||||
// h = 12-hour format of an hour with leading zeros 01 through 12
|
||||
];
|
||||
|
||||
public function convert(string $format): string
|
||||
{
|
||||
return strtr($format, self::$formatConvertRules);
|
||||
}
|
||||
|
||||
/**
|
||||
* This works with ICU and DateTime format.
|
||||
*
|
||||
* @param string $format
|
||||
* @param bool $html
|
||||
* @return string
|
||||
*/
|
||||
public function convertToPattern(string $format, bool $html = true): string
|
||||
{
|
||||
if (!$html) {
|
||||
$format = preg_quote($format, '/');
|
||||
}
|
||||
|
||||
$pattern = $format;
|
||||
|
||||
// special case fr_CA
|
||||
$pattern = str_replace('\\\\h', '*****', $pattern);
|
||||
$pattern = str_replace('\\h', '*****', $pattern);
|
||||
$pattern = str_replace("'h'", '*****', $pattern);
|
||||
|
||||
// days
|
||||
$pattern = str_replace('dd', self::PATTERN_DAY_DOUBLE, $pattern);
|
||||
$pattern = str_replace('d', self::PATTERN_DAY_SINGLE, $pattern);
|
||||
// months
|
||||
$pattern = str_replace('MM', self::PATTERN_MONTH_DOUBLE, $pattern);
|
||||
$pattern = str_replace('M', self::PATTERN_MONTH_SINGLE, $pattern);
|
||||
// years
|
||||
$pattern = str_replace('yyyy', self::PATTERN_YEAR, $pattern);
|
||||
$pattern = str_replace('yy', self::PATTERN_YEAR, $pattern);
|
||||
$pattern = str_replace('y', self::PATTERN_YEAR, $pattern);
|
||||
// time
|
||||
$pattern = str_replace('HH', self::PATTERN_HOUR_DOUBLE, $pattern);
|
||||
$pattern = str_replace('H', self::PATTERN_HOUR_DOUBLE, $pattern);
|
||||
$pattern = str_replace('G', self::PATTERN_HOUR_SINGLE, $pattern);
|
||||
$pattern = str_replace('h', self::PATTERN_HOUR_SINGLE, $pattern);
|
||||
$pattern = str_replace('g', self::PATTERN_HOUR_SINGLE, $pattern);
|
||||
$pattern = str_replace('i', self::PATTERN_MINUTES, $pattern);
|
||||
$pattern = str_replace('mm', self::PATTERN_MINUTES, $pattern);
|
||||
$pattern = str_replace('A', '(AM|PM){1}', $pattern);
|
||||
$pattern = str_replace('a', '(AM|PM){1}', $pattern);
|
||||
$pattern = str_replace('*****', 'h', $pattern);
|
||||
|
||||
if (!$html) {
|
||||
$pattern = '/^' . $pattern . '$/';
|
||||
}
|
||||
|
||||
return $pattern;
|
||||
}
|
||||
}
|
||||
@@ -1,24 +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\Utils;
|
||||
|
||||
interface HtmlToPdfConverter
|
||||
{
|
||||
/**
|
||||
* Returns the binary content of the PDF, which can be saved as file.
|
||||
* Throws an exception if conversion fails.
|
||||
*
|
||||
* @param string $html
|
||||
* @param array $options
|
||||
* @return string
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function convertToPdf(string $html, array $options = []): string;
|
||||
}
|
||||
44
src/Utils/JavascriptFormatConverter.php
Normal file
44
src/Utils/JavascriptFormatConverter.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?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\Utils;
|
||||
|
||||
final class JavascriptFormatConverter
|
||||
{
|
||||
/**
|
||||
* Convert PHP date format to Luxon compatible format.
|
||||
*
|
||||
* @see https://moment.github.io/luxon/#/formatting?id=table-of-tokens
|
||||
* @see https://www.php.net/manual/en/datetime.format.php
|
||||
* @var array
|
||||
*/
|
||||
private static array $formatConvertRules = [
|
||||
// year: Litepicker interprets 2-digit year as 1900, so we have to convert 20 to 2022.
|
||||
'yyyy' => 'YYYY', 'yy' => 'YYYY', 'y' => 'YYYY',
|
||||
// day
|
||||
'dd' => 'DD', 'd' => 'D',
|
||||
// day of week
|
||||
'EE' => 'ddd', 'EEEEEE' => 'dd',
|
||||
// timezone
|
||||
'ZZZZZ' => 'Z', 'ZZZ' => 'ZZ',
|
||||
// letter 'T'
|
||||
'\'T\'' => 'T',
|
||||
// am/pm (a) to AM/PM (A) - Luxon always produces uppercase AM/PM
|
||||
'a' => 'A',
|
||||
];
|
||||
|
||||
/**
|
||||
* The output of this format is used only to convert the Litepicker date object
|
||||
* to the input field (expected by Symfony form).
|
||||
*/
|
||||
public function convert(string $format): string
|
||||
{
|
||||
return strtr($format, self::$formatConvertRules);
|
||||
}
|
||||
}
|
||||
@@ -1,49 +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\Utils;
|
||||
|
||||
use App\Constants;
|
||||
|
||||
final class LanguageService
|
||||
{
|
||||
/**
|
||||
* @var string[]|string
|
||||
*/
|
||||
private $locales;
|
||||
|
||||
public function __construct(string $locales)
|
||||
{
|
||||
$this->locales = $locales;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getAllLanguages(): array
|
||||
{
|
||||
if (!\is_array($this->locales)) {
|
||||
// no further checks, because the list of languages is hard coded and we can be sure that
|
||||
// it is well formatted and contains the default langauge english
|
||||
$this->locales = array_unique(explode('|', trim($this->locales)));
|
||||
}
|
||||
|
||||
return $this->locales;
|
||||
}
|
||||
|
||||
public function isKnownLanguage(string $language): bool
|
||||
{
|
||||
return \in_array($language, $this->getAllLanguages());
|
||||
}
|
||||
|
||||
public function getDefaultLanguage(): string
|
||||
{
|
||||
return Constants::DEFAULT_LOCALE;
|
||||
}
|
||||
}
|
||||
@@ -1,125 +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\Utils;
|
||||
|
||||
use App\Configuration\LanguageFormattings;
|
||||
use App\Constants;
|
||||
|
||||
/**
|
||||
* Use this class, when you want information about formats for a locale.
|
||||
*/
|
||||
class LocaleFormats
|
||||
{
|
||||
/**
|
||||
* @var LanguageFormattings
|
||||
*/
|
||||
private $formats;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $locale = Constants::DEFAULT_LOCALE;
|
||||
|
||||
public function __construct(LanguageFormattings $formats, string $locale)
|
||||
{
|
||||
$this->formats = $formats;
|
||||
$this->locale = $locale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array with all available locale/language codes.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getAvailableLanguages(): array
|
||||
{
|
||||
return $this->formats->getAvailableLanguages();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current locale used by the user in this request.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLocale(): string
|
||||
{
|
||||
return $this->locale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the format which is used by the form component to handle date values.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDateTypeFormat(): string
|
||||
{
|
||||
return $this->formats->getDateTypeFormat($this->getLocale());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the format which is used by the Javascript component to handle date values.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDatePickerFormat(): string
|
||||
{
|
||||
return $this->formats->getDatePickerFormat($this->getLocale());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the format which is used by the form component to handle datetime values.
|
||||
*
|
||||
* @deprecated since 1.16
|
||||
* @return string
|
||||
*/
|
||||
public function getDateTimeTypeFormat(): string
|
||||
{
|
||||
return $this->formats->getDateTypeFormat($this->getLocale()) . ' HH:mm';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the locale specific date format, which should be used in combination with the twig filter "|date".
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDateFormat(): string
|
||||
{
|
||||
return $this->formats->getDateFormat($this->getLocale());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the locale specific time format, which should be used in combination with the twig filter "|time".
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTimeFormat(): string
|
||||
{
|
||||
return $this->formats->getTimeFormat($this->getLocale());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the locale specific datetime format, which should be used in combination with the twig filter "|date".
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDateTimeFormat(): string
|
||||
{
|
||||
return $this->formats->getDateTimeFormat($this->getLocale());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the format used in the "|duration" twig filter to display a Timesheet duration.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDurationFormat(): string
|
||||
{
|
||||
return $this->formats->getDurationFormat($this->getLocale());
|
||||
}
|
||||
}
|
||||
@@ -9,103 +9,67 @@
|
||||
|
||||
namespace App\Utils;
|
||||
|
||||
use App\Configuration\LanguageFormattings;
|
||||
use App\Configuration\LocaleService;
|
||||
use App\Entity\Timesheet;
|
||||
use DateTime;
|
||||
use Exception;
|
||||
use IntlDateFormatter;
|
||||
use Symfony\Component\Intl\Locales;
|
||||
use NumberFormatter;
|
||||
use Symfony\Component\Intl\Currencies;
|
||||
|
||||
/**
|
||||
* Use this class to format values into locale specific representations.
|
||||
*/
|
||||
final class LocaleFormatter
|
||||
{
|
||||
/**
|
||||
* @var LocaleFormats
|
||||
*/
|
||||
private $localeFormats;
|
||||
/**
|
||||
* @var Duration
|
||||
*/
|
||||
private $durationFormatter;
|
||||
/**
|
||||
* @var LocaleHelper
|
||||
*/
|
||||
private $helper;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $locale;
|
||||
// ---------------- private cache below ----------------
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $dateFormat = null;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $dateTimeFormat = null;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $dateTypeFormat = null;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $dateTimeTypeFormat = null;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $timeFormat = null;
|
||||
private ?Duration $durationFormatter = null;
|
||||
private ?IntlDateFormatter $dateFormatter = null;
|
||||
private ?IntlDateFormatter $dateTimeFormatter = null;
|
||||
private ?IntlDateFormatter $timeFormatter = null;
|
||||
private ?NumberFormatter $numberFormatter = null;
|
||||
private ?NumberFormatter $decimalFormatter = null;
|
||||
private ?NumberFormatter $moneyFormatter = null;
|
||||
private ?NumberFormatter $moneyFormatterNoCurrency = null;
|
||||
|
||||
public function __construct(LanguageFormattings $formats, string $locale)
|
||||
public function __construct(private LocaleService $localeService, private string $locale)
|
||||
{
|
||||
$this->locale = $locale;
|
||||
$this->durationFormatter = new Duration();
|
||||
$this->helper = new LocaleHelper($locale);
|
||||
$this->localeFormats = new LocaleFormats($formats, $locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms seconds into a duration string.
|
||||
*
|
||||
* @param int|Timesheet|null $duration
|
||||
* @param bool $decimal
|
||||
* @return string
|
||||
*/
|
||||
public function duration($duration, $decimal = false)
|
||||
public function duration(int|Timesheet|string|null $duration, bool $decimal = false): string
|
||||
{
|
||||
if ($decimal) {
|
||||
return $this->durationDecimal($duration);
|
||||
}
|
||||
|
||||
$seconds = $this->getSecondsForDuration($duration);
|
||||
$format = $this->localeFormats->getDurationFormat();
|
||||
|
||||
return $this->formatDuration($seconds, $format);
|
||||
return $this->formatDuration(
|
||||
$this->getSecondsForDuration($duration),
|
||||
$this->localeService->getDurationFormat($this->locale)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms seconds into a decimal formatted duration string.
|
||||
*
|
||||
* @param int|Timesheet|null $duration
|
||||
* @return string
|
||||
*/
|
||||
public function durationDecimal($duration)
|
||||
public function durationDecimal(Timesheet|int|string|null $duration): string
|
||||
{
|
||||
if (null === $this->numberFormatter) {
|
||||
$this->decimalFormatter = new NumberFormatter($this->locale, NumberFormatter::DECIMAL);
|
||||
$this->decimalFormatter->setAttribute(NumberFormatter::FRACTION_DIGITS, 2);
|
||||
}
|
||||
|
||||
$seconds = $this->getSecondsForDuration($duration);
|
||||
|
||||
return $this->helper->durationDecimal($seconds);
|
||||
$value = round($seconds / 3600, 2);
|
||||
|
||||
return $this->decimalFormatter->format($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|Timesheet|null $duration
|
||||
* @return int
|
||||
*/
|
||||
private function getSecondsForDuration($duration): int
|
||||
private function getSecondsForDuration(string|int|Timesheet|null $duration): int
|
||||
{
|
||||
if (null === $duration) {
|
||||
if ($duration === null || $duration === '') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -113,7 +77,7 @@ final class LocaleFormatter
|
||||
if (null === $duration->getEnd()) {
|
||||
$duration = time() - $duration->getBegin()->getTimestamp();
|
||||
} else {
|
||||
$duration = $duration->getDuration();
|
||||
$duration = $duration->getDuration() ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,209 +86,202 @@ final class LocaleFormatter
|
||||
|
||||
private function formatDuration(int $seconds, string $format): string
|
||||
{
|
||||
if ($this->durationFormatter === null) {
|
||||
$this->durationFormatter = new Duration();
|
||||
}
|
||||
|
||||
return $this->durationFormatter->format($seconds, $format);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|float $amount
|
||||
* @return bool|false|string
|
||||
* Used in twig filter |amount and invoice templates.
|
||||
*/
|
||||
public function amount($amount)
|
||||
public function amount(null|int|float|string $amount): string
|
||||
{
|
||||
return $this->helper->amount($amount);
|
||||
if ($amount === null || $amount === '') {
|
||||
return '0';
|
||||
}
|
||||
|
||||
if (null === $this->numberFormatter) {
|
||||
$this->numberFormatter = new NumberFormatter($this->locale, NumberFormatter::DECIMAL);
|
||||
}
|
||||
|
||||
$formatted = $this->numberFormatter->format($amount);
|
||||
|
||||
if (!\is_string($formatted)) {
|
||||
throw new \Exception('Could not convert into monetary string: ' . $amount);
|
||||
}
|
||||
|
||||
return $formatted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currency symbol.
|
||||
*
|
||||
* @param string $currency
|
||||
* @return string
|
||||
*/
|
||||
public function currency($currency)
|
||||
public function currency(?string $currency): string
|
||||
{
|
||||
return $this->helper->currency($currency);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $language
|
||||
* @return string
|
||||
*/
|
||||
public function language($language)
|
||||
{
|
||||
return $this->helper->language($language);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $country
|
||||
* @return string
|
||||
*/
|
||||
public function country($country)
|
||||
{
|
||||
return $this->helper->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->helper->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()
|
||||
{
|
||||
$locales = [];
|
||||
foreach ($this->localeFormats->getAvailableLanguages() as $locale) {
|
||||
$locales[] = ['code' => $locale, 'name' => Locales::getName($locale, $locale)];
|
||||
if ($currency === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $locales;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DateTime|string $date
|
||||
* @return string
|
||||
*/
|
||||
public function dateShort($date)
|
||||
{
|
||||
if (null === $this->dateFormat) {
|
||||
$this->dateFormat = $this->localeFormats->getDateFormat();
|
||||
try {
|
||||
return Currencies::getSymbol(strtoupper($currency), $this->locale);
|
||||
} catch (\Exception $ex) {
|
||||
}
|
||||
|
||||
if (!$date instanceof DateTime) {
|
||||
return $currency;
|
||||
}
|
||||
|
||||
public function money(null|int|float $amount, ?string $currency = null, bool $withCurrency = true): string
|
||||
{
|
||||
if ($currency === null) {
|
||||
$withCurrency = false;
|
||||
}
|
||||
|
||||
if ($amount === null) {
|
||||
$amount = 0;
|
||||
}
|
||||
|
||||
if (false === $withCurrency) {
|
||||
if (null === $this->moneyFormatterNoCurrency) {
|
||||
$this->moneyFormatterNoCurrency = new NumberFormatter($this->locale, NumberFormatter::CURRENCY);
|
||||
$this->moneyFormatterNoCurrency->setTextAttribute(NumberFormatter::POSITIVE_PREFIX, '');
|
||||
$this->moneyFormatterNoCurrency->setTextAttribute(NumberFormatter::POSITIVE_SUFFIX, '');
|
||||
$this->moneyFormatterNoCurrency->setTextAttribute(NumberFormatter::NEGATIVE_PREFIX, '-');
|
||||
$this->moneyFormatterNoCurrency->setTextAttribute(NumberFormatter::NEGATIVE_SUFFIX, '');
|
||||
}
|
||||
|
||||
return $this->moneyFormatterNoCurrency->format($amount, NumberFormatter::TYPE_DEFAULT);
|
||||
}
|
||||
|
||||
if (null === $this->moneyFormatter) {
|
||||
$this->moneyFormatter = new NumberFormatter($this->locale, NumberFormatter::CURRENCY);
|
||||
}
|
||||
|
||||
return $this->moneyFormatter->formatCurrency($amount, $currency);
|
||||
}
|
||||
|
||||
public function dateShort(\DateTimeInterface|string|null $date): ?string
|
||||
{
|
||||
if ($date === null || $date === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (null === $this->dateFormatter) {
|
||||
$this->dateFormatter = new IntlDateFormatter(
|
||||
$this->locale,
|
||||
IntlDateFormatter::MEDIUM,
|
||||
IntlDateFormatter::MEDIUM,
|
||||
date_default_timezone_get(),
|
||||
IntlDateFormatter::GREGORIAN,
|
||||
$this->localeService->getDateFormat($this->locale)
|
||||
);
|
||||
}
|
||||
|
||||
if (!$date instanceof \DateTimeInterface) {
|
||||
try {
|
||||
$date = new DateTime($date);
|
||||
$date = new \DateTimeImmutable($date);
|
||||
} catch (Exception $ex) {
|
||||
return $date;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return $date->format($this->dateFormat);
|
||||
}
|
||||
$formatted = $this->dateFormatter->format($date);
|
||||
|
||||
private function getDateTypeFormat(): string
|
||||
{
|
||||
if (null === $this->dateTypeFormat) {
|
||||
$this->dateTypeFormat = $this->localeFormats->getDateTypeFormat();
|
||||
if ($formatted === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->dateTypeFormat;
|
||||
return (string) $formatted;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DateTime|string $date
|
||||
* @return string
|
||||
*/
|
||||
public function dateTime($date)
|
||||
public function dateTime(DateTime|string|null $date): ?string
|
||||
{
|
||||
if (null === $this->dateTimeFormat) {
|
||||
$this->dateTimeFormat = $this->localeFormats->getDateTimeFormat();
|
||||
if ($date === null || $date === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$date instanceof DateTime) {
|
||||
if (null === $this->dateTimeFormatter) {
|
||||
$this->dateTimeFormatter = new IntlDateFormatter(
|
||||
$this->locale,
|
||||
IntlDateFormatter::MEDIUM,
|
||||
IntlDateFormatter::MEDIUM,
|
||||
date_default_timezone_get(),
|
||||
IntlDateFormatter::GREGORIAN,
|
||||
$this->localeService->getDateTimeFormat($this->locale)
|
||||
);
|
||||
}
|
||||
|
||||
if (!$date instanceof \DateTimeInterface) {
|
||||
try {
|
||||
$date = new DateTime($date);
|
||||
$date = new \DateTimeImmutable($date);
|
||||
} catch (Exception $ex) {
|
||||
return $date;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return $date->format($this->dateTimeFormat);
|
||||
$formatted = $this->dateTimeFormatter->format($date);
|
||||
|
||||
if ($formatted === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (string) $formatted;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DateTime|string $date
|
||||
* @param string $timeFormat
|
||||
* @param bool $stripMidnight
|
||||
* @return bool|false|string
|
||||
*/
|
||||
public function dateTimeFull($date, string $timeFormat, bool $stripMidnight = false)
|
||||
public function dateFormat(\DateTimeInterface|string|null $date, string $format): ?string
|
||||
{
|
||||
if (null === $this->dateTimeTypeFormat) {
|
||||
$converter = new DateFormatConverter();
|
||||
$this->dateTimeTypeFormat = $this->getDateTypeFormat() . ' ' . $converter->convert($timeFormat);
|
||||
if ($date === null || $date === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$date instanceof DateTime) {
|
||||
if (!$date instanceof \DateTimeInterface) {
|
||||
try {
|
||||
$date = new DateTime($date);
|
||||
$date = new \DateTimeImmutable($date);
|
||||
} catch (Exception $ex) {
|
||||
return $date;
|
||||
}
|
||||
}
|
||||
|
||||
$format = $this->dateTimeTypeFormat;
|
||||
|
||||
if ($stripMidnight && $date->format('H') == '00' && $date->format('i') == '00') {
|
||||
$format = $this->localeFormats->getDateTypeFormat();
|
||||
}
|
||||
|
||||
$formatter = new IntlDateFormatter(
|
||||
$this->locale,
|
||||
IntlDateFormatter::MEDIUM,
|
||||
IntlDateFormatter::MEDIUM,
|
||||
date_default_timezone_get(),
|
||||
IntlDateFormatter::GREGORIAN,
|
||||
$format
|
||||
);
|
||||
|
||||
return $formatter->format($date);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DateTime|string $date
|
||||
* @param string $format
|
||||
* @return false|string
|
||||
* @throws Exception
|
||||
*/
|
||||
public function dateFormat($date, string $format)
|
||||
{
|
||||
if (!$date instanceof DateTime) {
|
||||
try {
|
||||
$date = new DateTime($date);
|
||||
} catch (Exception $ex) {
|
||||
return $date;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return $date->format($format);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DateTime|string $date
|
||||
* @return string
|
||||
* @throws Exception
|
||||
*/
|
||||
public function time($date, string $format = null)
|
||||
public function time(\DateTimeInterface|string|null $date): ?string
|
||||
{
|
||||
if (null === $this->timeFormat) {
|
||||
$this->timeFormat = $this->localeFormats->getTimeFormat();
|
||||
if ($date === null || $date === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$date instanceof DateTime) {
|
||||
$date = new DateTime($date);
|
||||
if (null === $this->timeFormatter) {
|
||||
$this->timeFormatter = new IntlDateFormatter(
|
||||
$this->locale,
|
||||
IntlDateFormatter::MEDIUM,
|
||||
IntlDateFormatter::MEDIUM,
|
||||
date_default_timezone_get(),
|
||||
IntlDateFormatter::GREGORIAN,
|
||||
$this->localeService->getTimeFormat($this->locale)
|
||||
);
|
||||
}
|
||||
|
||||
return $date->format($format ?? $this->timeFormat);
|
||||
if (!$date instanceof \DateTimeInterface) {
|
||||
try {
|
||||
$date = new \DateTimeImmutable($date);
|
||||
} catch (Exception $ex) {
|
||||
return $date;
|
||||
}
|
||||
}
|
||||
|
||||
$formatted = $this->timeFormatter->format($date);
|
||||
|
||||
if ($formatted === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (string) $formatted;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://framework.zend.com/manual/1.12/en/zend.date.constants.html#zend.date.constants.selfdefinedformats
|
||||
* @see http://userguide.icu-project.org/formatparse/datetime
|
||||
*
|
||||
* @param DateTime $dateTime
|
||||
* @param string $format
|
||||
* @return string
|
||||
* @see https://unicode-org.github.io/icu/userguide/format_parse/datetime/
|
||||
*/
|
||||
private function formatIntl(\DateTime $dateTime, string $format): string
|
||||
{
|
||||
@@ -337,7 +294,13 @@ final class LocaleFormatter
|
||||
$format
|
||||
);
|
||||
|
||||
return $formatter->format($dateTime);
|
||||
$formatted = $formatter->format($dateTime);
|
||||
|
||||
if ($formatted === false) {
|
||||
throw new \Exception('Invalid dateformat given for formatIntl()');
|
||||
}
|
||||
|
||||
return (string) $formatted;
|
||||
}
|
||||
|
||||
public function monthName(\DateTime $dateTime, bool $withYear = false): string
|
||||
|
||||
@@ -1,185 +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\Utils;
|
||||
|
||||
use NumberFormatter;
|
||||
use Symfony\Component\Intl\Countries;
|
||||
use Symfony\Component\Intl\Currencies;
|
||||
use Symfony\Component\Intl\Languages;
|
||||
|
||||
final class LocaleHelper
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $locale;
|
||||
/**
|
||||
* @var NumberFormatter
|
||||
*/
|
||||
private $numberFormatter;
|
||||
/**
|
||||
* @var NumberFormatter
|
||||
*/
|
||||
private $durationFormatter;
|
||||
/**
|
||||
* @var NumberFormatter
|
||||
*/
|
||||
private $moneyFormatter;
|
||||
/**
|
||||
* @var NumberFormatter
|
||||
*/
|
||||
private $moneyFormatterNoCurrency;
|
||||
|
||||
public function __construct(string $locale)
|
||||
{
|
||||
$this->locale = $locale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms seconds into a decimal formatted duration string.
|
||||
*
|
||||
* @param int|null $seconds
|
||||
* @return string
|
||||
*/
|
||||
public function durationDecimal(?int $seconds): string
|
||||
{
|
||||
if ($seconds === null) {
|
||||
$seconds = 0;
|
||||
}
|
||||
|
||||
$value = round($seconds / 3600, 2);
|
||||
|
||||
return $this->getDurationFormatter()->format($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Only used in twig filter |amount and invoice templates
|
||||
*
|
||||
* @param string|float|null $amount
|
||||
* @return bool|false|string
|
||||
*/
|
||||
public function amount($amount)
|
||||
{
|
||||
if ($amount === null) {
|
||||
$amount = 0.00;
|
||||
}
|
||||
|
||||
return $this->getNumberFormatter()->format($amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $currency
|
||||
* @return string
|
||||
*/
|
||||
public function currency(?string $currency)
|
||||
{
|
||||
if ($currency === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
return Currencies::getSymbol(strtoupper($currency), $this->locale);
|
||||
} catch (\Exception $ex) {
|
||||
}
|
||||
|
||||
return $currency;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $language
|
||||
* @return string
|
||||
*/
|
||||
public function language(string $language)
|
||||
{
|
||||
try {
|
||||
return Languages::getName(strtolower($language), $this->locale);
|
||||
} catch (\Exception $ex) {
|
||||
}
|
||||
|
||||
return $language;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $country
|
||||
* @return string
|
||||
*/
|
||||
public function country(string $country)
|
||||
{
|
||||
try {
|
||||
return Countries::getName(strtoupper($country), $this->locale);
|
||||
} catch (\Exception $ex) {
|
||||
}
|
||||
|
||||
return $country;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|float|null $amount
|
||||
* @param string|null $currency
|
||||
* @param bool $withCurrency
|
||||
* @return string
|
||||
*/
|
||||
public function money($amount, ?string $currency = null, bool $withCurrency = true)
|
||||
{
|
||||
if (null === $currency) {
|
||||
$withCurrency = false;
|
||||
}
|
||||
|
||||
if ($amount === null) {
|
||||
$amount = 0;
|
||||
}
|
||||
|
||||
if (false === $withCurrency) {
|
||||
return $this->getMoneyFormatter($withCurrency)->format($amount, NumberFormatter::TYPE_DEFAULT);
|
||||
}
|
||||
|
||||
return $this->getMoneyFormatter($withCurrency)->formatCurrency($amount, $currency);
|
||||
}
|
||||
|
||||
private function getNumberFormatter(): NumberFormatter
|
||||
{
|
||||
if (null === $this->numberFormatter) {
|
||||
$this->numberFormatter = new NumberFormatter($this->locale, NumberFormatter::DECIMAL);
|
||||
}
|
||||
|
||||
return $this->numberFormatter;
|
||||
}
|
||||
|
||||
private function getDurationFormatter(): NumberFormatter
|
||||
{
|
||||
if (null === $this->numberFormatter) {
|
||||
$this->durationFormatter = new NumberFormatter($this->locale, NumberFormatter::DECIMAL);
|
||||
$this->durationFormatter->setAttribute(NumberFormatter::FRACTION_DIGITS, 2);
|
||||
}
|
||||
|
||||
return $this->durationFormatter;
|
||||
}
|
||||
|
||||
private function getMoneyFormatter(bool $withCurrency = true): NumberFormatter
|
||||
{
|
||||
if ($withCurrency) {
|
||||
if (null === $this->moneyFormatter) {
|
||||
$this->moneyFormatter = new NumberFormatter($this->locale, NumberFormatter::CURRENCY);
|
||||
}
|
||||
|
||||
return $this->moneyFormatter;
|
||||
}
|
||||
|
||||
if (null === $this->moneyFormatterNoCurrency) {
|
||||
$this->moneyFormatterNoCurrency = new NumberFormatter($this->locale, NumberFormatter::CURRENCY);
|
||||
$this->moneyFormatterNoCurrency->setTextAttribute(NumberFormatter::POSITIVE_PREFIX, '');
|
||||
$this->moneyFormatterNoCurrency->setTextAttribute(NumberFormatter::POSITIVE_SUFFIX, '');
|
||||
$this->moneyFormatterNoCurrency->setTextAttribute(NumberFormatter::NEGATIVE_PREFIX, '-');
|
||||
$this->moneyFormatterNoCurrency->setTextAttribute(NumberFormatter::NEGATIVE_SUFFIX, '');
|
||||
}
|
||||
|
||||
return $this->moneyFormatterNoCurrency;
|
||||
}
|
||||
}
|
||||
@@ -1,44 +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\Utils;
|
||||
|
||||
use App\Configuration\LanguageFormattings;
|
||||
use App\Constants;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
|
||||
/**
|
||||
* Use this class, when you want information about formats for the "current request locale".
|
||||
*/
|
||||
final class LocaleSettings extends LocaleFormats
|
||||
{
|
||||
private $requestStack;
|
||||
private $locale;
|
||||
|
||||
public function __construct(RequestStack $requestStack, LanguageFormattings $formats)
|
||||
{
|
||||
parent::__construct($formats, Constants::DEFAULT_LOCALE);
|
||||
$this->requestStack = $requestStack;
|
||||
}
|
||||
|
||||
public function getLocale(): string
|
||||
{
|
||||
if ($this->locale === null) {
|
||||
$locale = \Locale::getDefault();
|
||||
|
||||
// request is null in a console command
|
||||
if (null !== $this->requestStack->getMasterRequest()) {
|
||||
$locale = $this->requestStack->getMasterRequest()->getLocale();
|
||||
}
|
||||
$this->locale = $locale;
|
||||
}
|
||||
|
||||
return $this->locale;
|
||||
}
|
||||
}
|
||||
@@ -1,151 +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\Utils;
|
||||
|
||||
use App\Constants;
|
||||
use Mpdf\Config\ConfigVariables;
|
||||
use Mpdf\Config\FontVariables;
|
||||
use Mpdf\Mpdf;
|
||||
use Mpdf\Output\Destination;
|
||||
|
||||
class MPdfConverter implements HtmlToPdfConverter
|
||||
{
|
||||
/**
|
||||
* @var FileHelper
|
||||
*/
|
||||
private $fileHelper;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $cacheDirectory;
|
||||
|
||||
public function __construct(FileHelper $fileHelper, string $cacheDirectory)
|
||||
{
|
||||
$this->fileHelper = $fileHelper;
|
||||
$this->cacheDirectory = $cacheDirectory;
|
||||
}
|
||||
|
||||
protected function sanitizeOptions(array $options): array
|
||||
{
|
||||
$configs = new ConfigVariables();
|
||||
$fonts = new FontVariables();
|
||||
$allowed = [
|
||||
'mode', 'format', 'default_font_size', 'default_font', 'margin_left', 'margin_right', 'margin_top',
|
||||
'margin_bottom', 'margin_header', 'margin_footer', 'orientation', 'fonts',
|
||||
];
|
||||
|
||||
$filtered = array_filter($options, function ($key) use ($allowed, $configs, $fonts) {
|
||||
if (!\in_array($key, $allowed)) {
|
||||
if (!\array_key_exists($key, $configs->getDefaults())) {
|
||||
return \array_key_exists($key, $fonts->getDefaults());
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}, ARRAY_FILTER_USE_KEY);
|
||||
|
||||
if (\array_key_exists('tempDir', $filtered)) {
|
||||
unset($filtered['tempDir']);
|
||||
}
|
||||
|
||||
return $filtered;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $html
|
||||
* @param array $options
|
||||
* @return string
|
||||
* @throws \Mpdf\MpdfException
|
||||
*/
|
||||
public function convertToPdf(string $html, array $options = []): string
|
||||
{
|
||||
$options = array_merge(
|
||||
$this->sanitizeOptions($options),
|
||||
['tempDir' => $this->cacheDirectory, 'exposeVersion' => false]
|
||||
);
|
||||
|
||||
$mpdf = $this->initMpdf($options);
|
||||
|
||||
// some OS do not follow the PHP default settings
|
||||
if ((int) ini_get('pcre.backtrack_limit') < 1000000) {
|
||||
@ini_set('pcre.backtrack_limit', '1000000');
|
||||
}
|
||||
|
||||
// large amount of data take time
|
||||
@ini_set('max_execution_time', '120');
|
||||
|
||||
// reduce the size of content parts that are passed to MPDF, to prevent
|
||||
// https://mpdf.github.io/troubleshooting/known-issues.html#blank-pages-or-some-sections-missing
|
||||
$parts = explode('<pagebreak>', $html);
|
||||
for ($i = 0; $i < \count($parts); $i++) {
|
||||
if (stripos($parts[$i], '<!-- CONTENT_PART -->') !== false) {
|
||||
$subParts = explode('<!-- CONTENT_PART -->', $parts[$i]);
|
||||
foreach ($subParts as $subPart) {
|
||||
$mpdf->WriteHTML($subPart);
|
||||
}
|
||||
} else {
|
||||
$mpdf->WriteHTML($parts[$i]);
|
||||
}
|
||||
|
||||
if ($i < \count($parts) - 1) {
|
||||
$mpdf->WriteHTML('<pagebreak>');
|
||||
}
|
||||
}
|
||||
|
||||
return $mpdf->Output('', Destination::STRING_RETURN);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $options
|
||||
* @return Mpdf
|
||||
* @throws \Mpdf\MpdfException
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function initMpdf(array $options): Mpdf
|
||||
{
|
||||
$options['fontDir'] = $this->getFontDirectories();
|
||||
$options['fontdata'] = $this->mergeFontData($options);
|
||||
|
||||
$mpdf = new Mpdf($options);
|
||||
$mpdf->creator = Constants::SOFTWARE;
|
||||
|
||||
return $mpdf;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function getFontDirectories(): array
|
||||
{
|
||||
$defaultConfig = (new ConfigVariables())->getDefaults();
|
||||
$fontDirectories = $defaultConfig['fontDir'];
|
||||
$fontDirectories[] = $this->fileHelper->getDataDirectory('fonts');
|
||||
|
||||
return $fontDirectories;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $options
|
||||
* @return array
|
||||
*/
|
||||
private function mergeFontData(array $options): array
|
||||
{
|
||||
$defaultFontConfig = (new FontVariables())->getDefaults();
|
||||
$fontData = $defaultFontConfig['fontdata'];
|
||||
|
||||
if (\array_key_exists('fonts', $options)) {
|
||||
$fontData = array_merge($fontData, $options['fonts']);
|
||||
}
|
||||
|
||||
return $fontData;
|
||||
}
|
||||
}
|
||||
@@ -10,41 +10,36 @@
|
||||
namespace App\Utils;
|
||||
|
||||
/**
|
||||
* Parse markdown syntax and return HTML.
|
||||
* Parse Markdown syntax and return HTML.
|
||||
*/
|
||||
final class Markdown
|
||||
{
|
||||
/**
|
||||
* @var ParsedownExtension
|
||||
*/
|
||||
private $parser;
|
||||
private ?ParsedownExtension $parser = null;
|
||||
private ?\Parsedown $parserFull = null;
|
||||
|
||||
public function toHtml(string $text, bool $safe = true): string
|
||||
public function toHtml(string $text): string
|
||||
{
|
||||
if ($this->parser === null) {
|
||||
$this->parser = new ParsedownExtension();
|
||||
$this->parser->setUrlsLinked(true);
|
||||
$this->parser->setBreaksEnabled(true);
|
||||
$this->parser->setSafeMode(true);
|
||||
$this->parser->setMarkupEscaped(true);
|
||||
}
|
||||
|
||||
if ($safe !== true) {
|
||||
@trigger_error('Only safe mode is supported in Markdown since 1.16.3 to prevent XSS attacks. Parameter $safe will be removed with 2.0', E_USER_DEPRECATED);
|
||||
}
|
||||
|
||||
$this->parser->setSafeMode(true);
|
||||
$this->parser->setMarkupEscaped(true);
|
||||
|
||||
return $this->parser->text($text);
|
||||
}
|
||||
|
||||
public function withFullMarkdownSupport(string $text): string
|
||||
{
|
||||
$parser = new \Parsedown();
|
||||
$parser->setUrlsLinked(true);
|
||||
$parser->setBreaksEnabled(true);
|
||||
$parser->setSafeMode(true);
|
||||
$parser->setMarkupEscaped(true);
|
||||
if ($this->parserFull === null) {
|
||||
$this->parserFull = new \Parsedown();
|
||||
$this->parserFull->setUrlsLinked(true);
|
||||
$this->parserFull->setBreaksEnabled(true);
|
||||
$this->parserFull->setSafeMode(true);
|
||||
$this->parserFull->setMarkupEscaped(true);
|
||||
}
|
||||
|
||||
return $parser->text($text);
|
||||
return $this->parserFull->text($text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,11 +9,247 @@
|
||||
|
||||
namespace App\Utils;
|
||||
|
||||
use KevinPapst\AdminLTEBundle\Model\MenuItemModel as BaseMenuItemModel;
|
||||
use KevinPapst\TablerBundle\Model\MenuItemInterface;
|
||||
|
||||
class MenuItemModel extends BaseMenuItemModel
|
||||
final class MenuItemModel implements MenuItemInterface
|
||||
{
|
||||
private $childRoutes = [];
|
||||
private string $identifier;
|
||||
private string $label;
|
||||
private ?string $route;
|
||||
private array $routeArgs;
|
||||
private bool $isActive = false;
|
||||
/** @var array<MenuItemModel> */
|
||||
private array $children = [];
|
||||
private ?string $icon;
|
||||
private ?MenuItemModel $parent = null;
|
||||
private ?string $badge = null;
|
||||
private ?string $badgeColor = null;
|
||||
private static int $dividerId = 0;
|
||||
private bool $divider = false;
|
||||
private bool $lastWasDivider = false;
|
||||
private bool $expanded = false;
|
||||
|
||||
public function __construct(
|
||||
string $id,
|
||||
string $label,
|
||||
?string $route = null,
|
||||
array $routeArgs = [],
|
||||
?string $icon = null
|
||||
) {
|
||||
$this->identifier = $id;
|
||||
$this->label = $label;
|
||||
$this->route = $route;
|
||||
$this->routeArgs = $routeArgs;
|
||||
$this->icon = $icon;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MenuItemModel[]
|
||||
*/
|
||||
public function getChildren(): array
|
||||
{
|
||||
return $this->children;
|
||||
}
|
||||
|
||||
public function getChild(string $id): ?MenuItemModel
|
||||
{
|
||||
foreach ($this->children as $child) {
|
||||
if ($child->getIdentifier() === $id) {
|
||||
return $child;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<MenuItemModel> $children
|
||||
*/
|
||||
public function setChildren(array $children): void
|
||||
{
|
||||
$this->children = $children;
|
||||
}
|
||||
|
||||
public function getIcon(): ?string
|
||||
{
|
||||
return $this->icon;
|
||||
}
|
||||
|
||||
public function setIcon(string $icon): void
|
||||
{
|
||||
$this->icon = $icon;
|
||||
}
|
||||
|
||||
public function getIdentifier(): string
|
||||
{
|
||||
return $this->identifier;
|
||||
}
|
||||
|
||||
public function getIsActive(): bool
|
||||
{
|
||||
return $this->isActive;
|
||||
}
|
||||
|
||||
public function setIsActive(bool $isActive): void
|
||||
{
|
||||
$this->getParent()?->setIsActive($isActive);
|
||||
|
||||
$this->isActive = $isActive;
|
||||
}
|
||||
|
||||
public function hasParent(): bool
|
||||
{
|
||||
return $this->parent !== null;
|
||||
}
|
||||
|
||||
public function getParent(): ?MenuItemModel
|
||||
{
|
||||
return $this->parent;
|
||||
}
|
||||
|
||||
public function setParent(MenuItemInterface $parent): void
|
||||
{
|
||||
if (!($parent instanceof MenuItemModel)) {
|
||||
throw new \Exception('MenuItemModel::setParent() expects a MenuItemModel');
|
||||
}
|
||||
$this->parent = $parent;
|
||||
}
|
||||
|
||||
public function getLabel(): string
|
||||
{
|
||||
return $this->label;
|
||||
}
|
||||
|
||||
public function setLabel(string $label): void
|
||||
{
|
||||
$this->label = $label;
|
||||
}
|
||||
|
||||
public function getRoute(): ?string
|
||||
{
|
||||
return $this->route;
|
||||
}
|
||||
|
||||
public function setRoute(?string $route): void
|
||||
{
|
||||
$this->route = $route;
|
||||
}
|
||||
|
||||
public function getRouteArgs(): array
|
||||
{
|
||||
return $this->routeArgs;
|
||||
}
|
||||
|
||||
public function setRouteArgs(array $routeArgs): void
|
||||
{
|
||||
$this->routeArgs = $routeArgs;
|
||||
}
|
||||
|
||||
public function hasChildren(): bool
|
||||
{
|
||||
if (\count($this->children) < 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($this->children as $child) {
|
||||
if (!$child->isDivider()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function addChild(MenuItemInterface $child): void
|
||||
{
|
||||
if (!($child instanceof MenuItemModel)) {
|
||||
throw new \Exception('MenuItemModel::addChild() expects a MenuItemModel');
|
||||
}
|
||||
|
||||
// first item should not be a divider
|
||||
if (!$this->hasChildren() && $child->isDivider()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// two divider should not be added as direct siblings
|
||||
if ($this->lastWasDivider && $child->isDivider()) {
|
||||
return;
|
||||
}
|
||||
$this->lastWasDivider = $child->isDivider();
|
||||
|
||||
$child->setParent($this);
|
||||
$this->children[] = $child;
|
||||
}
|
||||
|
||||
public function removeChild(MenuItemInterface $child): void
|
||||
{
|
||||
if (false !== ($key = array_search($child, $this->children))) {
|
||||
unset($this->children[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
public function findChild(string $identifier): ?MenuItemModel
|
||||
{
|
||||
return $this->find($identifier, $this);
|
||||
}
|
||||
|
||||
private function find(string $identifier, MenuItemModel $menu): ?MenuItemModel
|
||||
{
|
||||
if ($menu->getIdentifier() === $identifier) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
foreach ($menu->getChildren() as $child) {
|
||||
if ($child->getIdentifier() === $identifier) {
|
||||
return $child;
|
||||
}
|
||||
if ($child->hasChildren()) {
|
||||
if (($tmp = $this->find($identifier, $child)) !== null) {
|
||||
return $tmp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getActiveChild(): ?MenuItemModel
|
||||
{
|
||||
foreach ($this->children as $child) {
|
||||
if ($child->isActive()) {
|
||||
return $child;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function isActive(): bool
|
||||
{
|
||||
return $this->isActive;
|
||||
}
|
||||
|
||||
public function setBadge(?string $badge): void
|
||||
{
|
||||
$this->badge = $badge;
|
||||
}
|
||||
|
||||
public function setBadgeColor(?string $badgeColor): void
|
||||
{
|
||||
$this->badgeColor = $badgeColor;
|
||||
}
|
||||
|
||||
public function getBadge(): ?string
|
||||
{
|
||||
return $this->badge;
|
||||
}
|
||||
|
||||
public function getBadgeColor(): ?string
|
||||
{
|
||||
return $this->badgeColor;
|
||||
}
|
||||
|
||||
private array $childRoutes = [];
|
||||
|
||||
public function setChildRoutes(array $routes): MenuItemModel
|
||||
{
|
||||
@@ -33,4 +269,32 @@ class MenuItemModel extends BaseMenuItemModel
|
||||
{
|
||||
return \in_array($route, $this->childRoutes);
|
||||
}
|
||||
|
||||
public static function createDivider(): MenuItemModel
|
||||
{
|
||||
$model = new MenuItemModel('divider_' . self::$dividerId++, '');
|
||||
$model->setDivider(true);
|
||||
|
||||
return $model;
|
||||
}
|
||||
|
||||
public function isDivider(): bool
|
||||
{
|
||||
return $this->divider;
|
||||
}
|
||||
|
||||
public function setDivider(bool $divider): void
|
||||
{
|
||||
$this->divider = $divider;
|
||||
}
|
||||
|
||||
public function isExpanded(): bool
|
||||
{
|
||||
return $this->expanded;
|
||||
}
|
||||
|
||||
public function setExpanded(bool $expanded): void
|
||||
{
|
||||
$this->expanded = $expanded;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,51 +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\Utils;
|
||||
|
||||
/**
|
||||
* This class is used to convert PHP date format to moment.js format.
|
||||
*
|
||||
* @author Yonel Ceruto <yonelceruto@gmail.com>
|
||||
*/
|
||||
class MomentFormatConverter
|
||||
{
|
||||
/**
|
||||
* This defines the mapping between PHP ICU date format (key) and moment.js date format (value)
|
||||
* For ICU formats see https://unicode-org.github.io/icu/userguide/format_parse/datetime/#datetime-format-syntax
|
||||
* For Moment formats see http://momentjs.com/docs/#/displaying/format/
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $formatConvertRules = [
|
||||
// year
|
||||
'yyyy' => 'YYYY', 'yy' => 'YY', 'y' => 'YYYY',
|
||||
// day
|
||||
'dd' => 'DD', 'd' => 'D',
|
||||
// day of week
|
||||
'EE' => 'ddd', 'EEEEEE' => 'dd',
|
||||
// timezone
|
||||
'ZZZZZ' => 'Z', 'ZZZ' => 'ZZ',
|
||||
// letter 'T'
|
||||
'\'T\'' => 'T',
|
||||
// am/pm to AM/PM
|
||||
'a' => 'A',
|
||||
];
|
||||
|
||||
/**
|
||||
* Returns associated moment.js format.
|
||||
*
|
||||
* @param string $format
|
||||
* @return string
|
||||
*/
|
||||
public function convert(string $format): string
|
||||
{
|
||||
return strtr($format, self::$formatConvertRules);
|
||||
}
|
||||
}
|
||||
103
src/Utils/NumberGenerator.php
Normal file
103
src/Utils/NumberGenerator.php
Normal file
@@ -0,0 +1,103 @@
|
||||
<?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\Utils;
|
||||
|
||||
final class NumberGenerator
|
||||
{
|
||||
/**
|
||||
* @param string $format
|
||||
* @param callable $patternReplacer (receives the parameters: string $originalFormat, string $format, int $increaseBy)
|
||||
*/
|
||||
public function __construct(private string $format, private $patternReplacer)
|
||||
{
|
||||
}
|
||||
|
||||
public function getNumber(int $startWith = 0): string
|
||||
{
|
||||
$result = $this->format;
|
||||
|
||||
preg_match_all('/{[^}]*?}/', $result, $matches);
|
||||
|
||||
foreach ($matches[0] as $part) {
|
||||
$partialResult = $this->parseReplacer($part, $startWith);
|
||||
$result = str_replace($part, $partialResult, $result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function parseReplacer(string $originalFormat, int $increaseBy): string
|
||||
{
|
||||
$formatterLength = null;
|
||||
$formatPattern = str_replace(['{', '}'], '', $originalFormat);
|
||||
|
||||
$parts = preg_split('/([+\-,])+/', $formatPattern, -1, PREG_SPLIT_DELIM_CAPTURE);
|
||||
if ($parts === false) {
|
||||
throw new \InvalidArgumentException('Invalid number format received');
|
||||
}
|
||||
$format = array_shift($parts);
|
||||
|
||||
if (\count($parts) % 2 !== 0) {
|
||||
throw new \InvalidArgumentException('Invalid number format configuration found');
|
||||
}
|
||||
|
||||
while (null !== ($tmp = array_shift($parts))) {
|
||||
switch ($tmp) {
|
||||
case '+':
|
||||
$local = array_shift($parts);
|
||||
if (!is_numeric($local)) {
|
||||
throw new \InvalidArgumentException('Unknown increment found');
|
||||
}
|
||||
$increaseBy = $increaseBy + \intval($local);
|
||||
break;
|
||||
|
||||
case '-':
|
||||
$local = array_shift($parts);
|
||||
if (!is_numeric($local)) {
|
||||
throw new \InvalidArgumentException('Unknown decrement found');
|
||||
}
|
||||
$increaseBy = $increaseBy - \intval($local);
|
||||
break;
|
||||
|
||||
case ',':
|
||||
$local = array_shift($parts);
|
||||
if (!is_numeric($local)) {
|
||||
throw new \InvalidArgumentException('Unknown format length found');
|
||||
}
|
||||
$formatterLength = \intval($local);
|
||||
if ((string) $formatterLength !== $local) {
|
||||
throw new \InvalidArgumentException('Unknown format length found');
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new \InvalidArgumentException('Unknown pattern found');
|
||||
}
|
||||
}
|
||||
|
||||
if ($increaseBy === 0) {
|
||||
$increaseBy = 1;
|
||||
}
|
||||
|
||||
$partialResult = \call_user_func($this->patternReplacer, $originalFormat, $format, $increaseBy);
|
||||
|
||||
if (!\is_string($partialResult) && !\is_int($partialResult) && !\is_float($partialResult)) {
|
||||
throw new \Exception('Number generator callback must return string or integer');
|
||||
}
|
||||
|
||||
$partialResult = (string) $partialResult;
|
||||
|
||||
if (null !== $formatterLength) {
|
||||
$partialResult = str_pad($partialResult, $formatterLength, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
return $partialResult;
|
||||
}
|
||||
}
|
||||
93
src/Utils/PageSetup.php
Normal file
93
src/Utils/PageSetup.php
Normal file
@@ -0,0 +1,93 @@
|
||||
<?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\Utils;
|
||||
|
||||
final class PageSetup
|
||||
{
|
||||
private ?string $help = null;
|
||||
private ?string $actionName = null;
|
||||
private string $actionView = 'index';
|
||||
private array $actionPayload = [];
|
||||
private ?DataTable $dataTable = null;
|
||||
|
||||
public function __construct(private string $title)
|
||||
{
|
||||
}
|
||||
|
||||
public function hasDataTable(): bool
|
||||
{
|
||||
return $this->dataTable !== null;
|
||||
}
|
||||
|
||||
public function hasSearchForm(): bool
|
||||
{
|
||||
return $this->dataTable !== null && $this->dataTable->getSearchForm() !== null;
|
||||
}
|
||||
|
||||
public function getDataTable(): ?DataTable
|
||||
{
|
||||
return $this->dataTable;
|
||||
}
|
||||
|
||||
public function setDataTable(?DataTable $dataTable): void
|
||||
{
|
||||
$this->dataTable = $dataTable;
|
||||
}
|
||||
|
||||
public function getHelp(): ?string
|
||||
{
|
||||
return $this->help;
|
||||
}
|
||||
|
||||
public function setHelp(?string $help): void
|
||||
{
|
||||
$this->help = $help;
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function getActionName(): ?string
|
||||
{
|
||||
return $this->actionName;
|
||||
}
|
||||
|
||||
public function setActionName(?string $actionName): void
|
||||
{
|
||||
$this->actionName = $actionName;
|
||||
}
|
||||
|
||||
public function getActionView(): string
|
||||
{
|
||||
return $this->actionView;
|
||||
}
|
||||
|
||||
public function setActionView(string $actionView): void
|
||||
{
|
||||
$this->actionView = $actionView;
|
||||
}
|
||||
|
||||
public function getActionPayload(): array
|
||||
{
|
||||
return $this->actionPayload;
|
||||
}
|
||||
|
||||
public function setActionPayload(array $actionPayload): void
|
||||
{
|
||||
$this->actionPayload = $actionPayload;
|
||||
}
|
||||
|
||||
public function isTableAction(): bool
|
||||
{
|
||||
return \in_array($this->actionView, ['detail', 'custom', 'table']);
|
||||
}
|
||||
}
|
||||
21
src/Utils/Pagination.php
Normal file
21
src/Utils/Pagination.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?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\Utils;
|
||||
|
||||
use Pagerfanta\Adapter\AdapterInterface;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
|
||||
final class Pagination extends Pagerfanta
|
||||
{
|
||||
public function __construct(AdapterInterface $adapter)
|
||||
{
|
||||
parent::__construct($adapter);
|
||||
}
|
||||
}
|
||||
60
src/Utils/PaginationTemplate.php
Normal file
60
src/Utils/PaginationTemplate.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?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\Utils;
|
||||
|
||||
use Pagerfanta\View\Template\TwitterBootstrap5Template;
|
||||
|
||||
final class PaginationTemplate extends TwitterBootstrap5Template
|
||||
{
|
||||
protected function getDefaultOptions(): array
|
||||
{
|
||||
return array_merge(
|
||||
parent::getDefaultOptions(),
|
||||
[
|
||||
//'prev_message' = '←',
|
||||
//'next_message' = '→',
|
||||
'prev_message' => '<i class="fas fa-chevron-left"></i>',
|
||||
'next_message' => '<i class="fas fa-chevron-right"></i>',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $class
|
||||
* @param string $href
|
||||
* @param int|string $text
|
||||
* @param string|null $rel
|
||||
* @return string
|
||||
*/
|
||||
protected function linkLi(string $class, string $href, $text, ?string $rel = null): string
|
||||
{
|
||||
$liClass = implode(' ', array_filter(['page-item', $class]));
|
||||
$rel = $rel ? sprintf(' rel="%s"', $rel) : '';
|
||||
|
||||
return sprintf('<li class="%s"><a class="page-link pagination-link" href="%s"%s>%s</a></li>', $liClass, $href, $rel, $text);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $class
|
||||
* @param string $text
|
||||
* @return string
|
||||
*/
|
||||
protected function spanLi(string $class, $text): string
|
||||
{
|
||||
$liClass = implode(' ', array_filter(['page-item', $class]));
|
||||
|
||||
return sprintf('<li class="%s"><span class="page-link pagination-link">%s</span></li>', $liClass, $text);
|
||||
}
|
||||
|
||||
public function current(int $page): string
|
||||
{
|
||||
return $this->linkLi($this->option('css_active_class'), $this->generateRoute($page), $page);
|
||||
}
|
||||
}
|
||||
26
src/Utils/PaginationView.php
Normal file
26
src/Utils/PaginationView.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?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\Utils;
|
||||
|
||||
use Pagerfanta\View\Template\TemplateInterface;
|
||||
use Pagerfanta\View\TwitterBootstrap5View;
|
||||
|
||||
final class PaginationView extends TwitterBootstrap5View
|
||||
{
|
||||
protected function getDefaultProximity(): int
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
|
||||
protected function createDefaultTemplate(): TemplateInterface
|
||||
{
|
||||
return new PaginationTemplate();
|
||||
}
|
||||
}
|
||||
@@ -12,9 +12,10 @@ namespace App\Utils;
|
||||
/**
|
||||
* This Class extends the default Parsedown Class for custom methods.
|
||||
*/
|
||||
class ParsedownExtension extends \Parsedown
|
||||
final class ParsedownExtension extends \Parsedown
|
||||
{
|
||||
private $ids = [];
|
||||
/** @var array<string> */
|
||||
private array $ids = [];
|
||||
|
||||
/**
|
||||
* Overwritten to prevent # to show up as headings for two reasons:
|
||||
@@ -71,16 +72,16 @@ class ParsedownExtension extends \Parsedown
|
||||
* - added support for file:///
|
||||
* - open links in new windows
|
||||
*/
|
||||
protected function inlineUrl($Excerpt)
|
||||
protected function inlineUrl($Excerpt): ?array
|
||||
{
|
||||
if ($this->urlsLinked !== true or !isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/') {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (preg_match('/\b(https?:[\/]{2}|file:[\/]{3})[^\s<]+\b\/*/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE)) {
|
||||
$url = $matches[0][0];
|
||||
|
||||
$Inline = [
|
||||
return [
|
||||
'extent' => \strlen($matches[0][0]),
|
||||
'position' => $matches[0][1],
|
||||
'element' => [
|
||||
@@ -92,14 +93,14 @@ class ParsedownExtension extends \Parsedown
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
return $Inline;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function blockHeader($line)
|
||||
protected function blockHeader($Line)
|
||||
{
|
||||
$block = parent::blockHeader($line);
|
||||
$block = parent::blockHeader($Line);
|
||||
|
||||
$text = $block['element']['text'];
|
||||
$id = $this->getIDfromText($text);
|
||||
@@ -151,8 +152,8 @@ class ParsedownExtension extends \Parsedown
|
||||
{
|
||||
$Block = parent::blockTable($Line, $Block);
|
||||
|
||||
if (\is_null($Block)) {
|
||||
return;
|
||||
if ($Block === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$Block['element']['attributes']['class'] = 'table';
|
||||
|
||||
89
src/Utils/ProfileManager.php
Normal file
89
src/Utils/ProfileManager.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?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\Utils;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Session\Session;
|
||||
|
||||
final class ProfileManager
|
||||
{
|
||||
public const SESSION_PROFILE = 'datatable_profile';
|
||||
public const PROFILE_DESKTOP = 'desktop';
|
||||
public const PROFILE_MOBILE = 'mobile';
|
||||
public const COOKIE_PROFILE = 'K2P';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function isValidProfile(string $profile): bool
|
||||
{
|
||||
return \in_array($profile, [self::PROFILE_DESKTOP, self::PROFILE_MOBILE]);
|
||||
}
|
||||
|
||||
public function getDatatableName(string $dataTable, ?string $profile = null): string
|
||||
{
|
||||
if (empty($profile) || $profile === self::PROFILE_DESKTOP) {
|
||||
return $dataTable;
|
||||
}
|
||||
|
||||
return trim($dataTable . '_' . $profile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Always returns a valid profile name (default: desktop).
|
||||
*
|
||||
* @param string $profile
|
||||
* @return string
|
||||
*/
|
||||
public function getProfile(string $profile): string
|
||||
{
|
||||
if (!\in_array($profile, [self::PROFILE_DESKTOP, self::PROFILE_MOBILE])) {
|
||||
return self::PROFILE_DESKTOP;
|
||||
}
|
||||
|
||||
return $profile;
|
||||
}
|
||||
|
||||
public function setProfile(Session $session, string $profile): void
|
||||
{
|
||||
if ($profile === self::PROFILE_MOBILE) {
|
||||
$session->set(self::SESSION_PROFILE, $profile);
|
||||
} else {
|
||||
$session->remove(self::SESSION_PROFILE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Always returns a valid profile name (default: desktop).
|
||||
*
|
||||
* @param Request $request
|
||||
* @return string
|
||||
*/
|
||||
public function getProfileFromCookie(Request $request): string
|
||||
{
|
||||
$profile = $request->cookies->get(self::COOKIE_PROFILE, self::PROFILE_DESKTOP);
|
||||
|
||||
return $this->getProfile($profile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Always returns a valid profile name (default: desktop).
|
||||
*
|
||||
* @param Session $session
|
||||
* @return string
|
||||
*/
|
||||
public function getProfileFromSession(Session $session): string
|
||||
{
|
||||
$profile = $session->get(self::SESSION_PROFILE, self::PROFILE_DESKTOP);
|
||||
|
||||
return $this->getProfile($profile);
|
||||
}
|
||||
}
|
||||
121
src/Utils/ReleaseVersion.php
Normal file
121
src/Utils/ReleaseVersion.php
Normal file
@@ -0,0 +1,121 @@
|
||||
<?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\Utils;
|
||||
|
||||
use App\Constants;
|
||||
use Composer\Semver\Semver;
|
||||
use Composer\Semver\VersionParser;
|
||||
|
||||
/**
|
||||
* Code inspired by https://github.com/consolidation/self-update (MIT license - 03 Sept. 2022)
|
||||
*/
|
||||
final class ReleaseVersion
|
||||
{
|
||||
/**
|
||||
* Get all releases from GitHub.
|
||||
*
|
||||
* @throws \Exception
|
||||
* @return array
|
||||
*/
|
||||
private function getReleasesFromGithub(): array
|
||||
{
|
||||
$versionParser = new VersionParser();
|
||||
$opts = [
|
||||
'http' => [
|
||||
'method' => 'GET',
|
||||
'header' => [
|
||||
'User-Agent: ' . Constants::SOFTWARE . ' ' . Constants::VERSION . ' Update-Check (PHP)',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$context = stream_context_create($opts);
|
||||
|
||||
$releases = file_get_contents('https://api.github.com/repos/' . Constants::GITHUB_REPO . '/releases', false, $context);
|
||||
$releases = json_decode($releases);
|
||||
|
||||
if (!isset($releases[0])) {
|
||||
throw new \Exception('API error - no release found at GitHub repository: ' . Constants::GITHUB_REPO);
|
||||
}
|
||||
$parsed = [];
|
||||
foreach ($releases as $release) {
|
||||
if ($release->draft || $release->prerelease) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$normalized = $versionParser->normalize($release->tag_name);
|
||||
} catch (\UnexpectedValueException $e) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (VersionParser::parseStability($normalized) !== 'stable') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$date = $release->published_at;
|
||||
try {
|
||||
$date = new \DateTimeImmutable($date);
|
||||
} catch (\Exception $ex) {
|
||||
// can be ignored, we return a string
|
||||
}
|
||||
|
||||
$parsed[$normalized] = [
|
||||
'version' => $release->tag_name,
|
||||
'date' => $date,
|
||||
'url' => $release->html_url,
|
||||
'download' => $release->zipball_url,
|
||||
'content' => $release->body,
|
||||
];
|
||||
}
|
||||
$versions = Semver::rsort(array_keys($parsed));
|
||||
$releases = [];
|
||||
foreach ($versions as $version) {
|
||||
$releases[$version] = $parsed[$version];
|
||||
}
|
||||
|
||||
return $releases;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array with the keys:
|
||||
* - version (string, tag name)
|
||||
* - date (string, release date)
|
||||
* - url (string, web address)
|
||||
* - download (string, ZIP URL)
|
||||
* - content (string, release notes)
|
||||
*
|
||||
* @param bool $compatible
|
||||
* @return array|null
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getLatestReleaseFromGithub(bool $compatible): ?array
|
||||
{
|
||||
foreach ($this->getReleasesFromGithub() as $release) {
|
||||
$releaseVersion = $release['version'];
|
||||
if ($compatible && !$this->satisfiesMajorVersionConstraint($releaseVersion)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return $release;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function satisfiesMajorVersionConstraint(string $releaseVersion): bool
|
||||
{
|
||||
if (preg_match('/^v?(\d+)/', Constants::VERSION, $matches)) {
|
||||
return Semver::satisfies($releaseVersion, '^' . $matches[1]);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -11,27 +11,16 @@ namespace App\Utils;
|
||||
|
||||
final class SearchTerm
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $originalTerm;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $term;
|
||||
private string $originalTerm;
|
||||
private string $term;
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private $fields = [];
|
||||
private array $fields;
|
||||
|
||||
public function __construct(string $searchTerm)
|
||||
{
|
||||
$this->originalTerm = $searchTerm;
|
||||
$this->parse($searchTerm);
|
||||
}
|
||||
|
||||
private function parse(string $searchTerm)
|
||||
{
|
||||
$terms = explode(' ', $searchTerm);
|
||||
$fields = [];
|
||||
$finalTerm = [];
|
||||
|
||||
@@ -1,119 +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\Utils;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Translation\Translator as BaseTranslator;
|
||||
use Symfony\Component\Translation\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Translation\MessageCatalogueInterface;
|
||||
use Symfony\Component\Translation\TranslatorBagInterface;
|
||||
use Symfony\Contracts\Translation\LocaleAwareInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class Translator implements TranslatorInterface, TranslatorBagInterface, LocaleAwareInterface
|
||||
{
|
||||
/**
|
||||
* @var BaseTranslator
|
||||
*/
|
||||
private $translator;
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $localDomains;
|
||||
|
||||
public function __construct(BaseTranslator $translator, array $localDomains = [])
|
||||
{
|
||||
$this->translator = $translator;
|
||||
$this->localDomains = $localDomains;
|
||||
}
|
||||
|
||||
public function trans($id, array $parameters = [], $domain = 'messages', $locale = null)
|
||||
{
|
||||
if (null === $domain) {
|
||||
$domain = 'messages';
|
||||
}
|
||||
|
||||
foreach ($this->localDomains as $localDomain) {
|
||||
if (false !== $this->hasLocalOverwrite($id, $localDomain, $locale)) {
|
||||
$domain = $localDomain;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->translator->trans($id, $parameters, $domain, $locale);
|
||||
}
|
||||
|
||||
protected function hasLocalOverwrite($id, $domain, $locale = null): bool
|
||||
{
|
||||
$catalogue = $this->getCatalogue($locale);
|
||||
while (false === ($found = $catalogue->defines($id, $domain))) {
|
||||
if ($cat = $catalogue->getFallbackCatalogue()) {
|
||||
$catalogue = $cat;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $found;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the catalogue by locale.
|
||||
*
|
||||
* @param string|null $locale The locale or null to use the default
|
||||
*
|
||||
* @return MessageCatalogueInterface
|
||||
*
|
||||
* @throws InvalidArgumentException If the locale contains invalid characters
|
||||
*/
|
||||
public function getCatalogue($locale = null)
|
||||
{
|
||||
return $this->translator->getCatalogue($locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current locale.
|
||||
*
|
||||
* @param string $locale The locale
|
||||
*
|
||||
* @throws \InvalidArgumentException If the locale contains invalid characters
|
||||
*/
|
||||
public function setLocale($locale)
|
||||
{
|
||||
$this->translator->setLocale($locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current locale.
|
||||
*
|
||||
* @return string The locale
|
||||
*/
|
||||
public function getLocale()
|
||||
{
|
||||
return $this->translator->getLocale();
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates the given choice message by choosing a translation according to a number.
|
||||
*
|
||||
* @param string $id The message id (may also be an object that can be cast to string)
|
||||
* @param int $number The number to use to find the index of the message
|
||||
* @param array $parameters An array of parameters for the message
|
||||
* @param string|null $domain The domain for the message or null to use the default
|
||||
* @param string|null $locale The locale or null to use the default
|
||||
*
|
||||
* @return string The translated string
|
||||
*
|
||||
* @throws InvalidArgumentException If the locale contains invalid characters
|
||||
*/
|
||||
public function transChoice($id, $number, array $parameters = [], $domain = null, $locale = null)
|
||||
{
|
||||
return $this->translator->transChoice($id, $number, $parameters, $domain, $locale);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user