Release 2.7.0 (#4506)

* added api URL for simpler integration
* allow to request password change upon next login
* make ModifiedAt timesheet independent
* improved plugin api
* replace kernel calls with AutoconfigureTag and TaggedIterator attributes
* new translation keys (e.g. for days)
* support setting min and max date on date and daterange pickers
This commit is contained in:
Kevin Papst
2023-12-26 14:49:11 +01:00
committed by GitHub
parent f86eca05ae
commit ad645f5b58
72 changed files with 378 additions and 735 deletions

View File

@@ -57,6 +57,16 @@ export default class KimaiDatePicker extends KimaiFormPlugin {
if (element.dataset.format === undefined) {
console.log('Trying to bind litepicker to an element without data-format attribute');
}
if (element.hasAttribute('min') !== undefined) {
options = {...options, ...{
'minDate': element.getAttribute('min'),
}};
}
if (element.hasAttribute('max') !== undefined) {
options = {...options, ...{
'maxDate': element.getAttribute('max'),
}};
}
options = {...options, ...{
format: element.dataset.format,
showTooltip: false,
@@ -66,7 +76,7 @@ export default class KimaiDatePicker extends KimaiFormPlugin {
firstDay: FIRST_DOW, // Litepicker: 0 = Sunday, 1 = Monday
setup: (picker) => {
// nasty hack, because litepicker does not trigger change event on the input and the available
// event "selected" is triggered why to often, even when moving the cursor inside the input
// event "selected" is triggered way to often, even when moving the cursor inside the input
// element (not even typing is necessary) and so we have to make sure that the manual "click" event
// (works for touch as well) happened before we actually dispatch the change event manually ...
// what? report forms would be submitted upon cursor move without the "preselect” check

View File

@@ -60,18 +60,6 @@ services:
arguments:
$cacheDirectory: '%kernel.cache_dir%'
App\Plugin\PluginManager:
arguments: [!tagged kimai.plugin]
App\Validator\Constraints\TimesheetValidator:
arguments: [!tagged timesheet.validator]
App\Validator\Constraints\ProjectValidator:
arguments: [!tagged project.validator]
App\Validator\Constraints\QuickEntryTimesheetValidator:
arguments: [!tagged timesheet.validator]
App\Utils\FileHelper:
arguments:
$dataDir: '%kimai.data_dir%'
@@ -83,35 +71,19 @@ services:
arguments:
$mailer: '@App\Mail\KimaiMailer'
# ================================================================================
# DATABASE
# ================================================================================
# updates timesheet records and apply configured rate & rounding rules
App\Doctrine\TimesheetSubscriber:
class: App\Doctrine\TimesheetSubscriber
arguments: [!tagged timesheet.calculator]
# updates timestampable columns (higher priority, so the TimesheetSubscriber will be executed later)
App\Doctrine\ModifiedSubscriber:
class: App\Doctrine\ModifiedSubscriber
# ================================================================================
# TIMESHEET RECORD CALCULATOR
# ================================================================================
App\Timesheet\RoundingService:
arguments:
$roundingModes: !tagged timesheet.rounding_mode
# this is currently required, as local.yaml allows to configure several rules,
# while the database system only allows one rounding rule
$rules: '%kimai.timesheet.rounding%'
App\Timesheet\RateService:
arguments: ['%kimai.timesheet.rates%']
App\Timesheet\TrackingModeService:
arguments:
$modes: !tagged timesheet.tracking_mode
# ================================================================================
# SECURITY & VOTER
# ================================================================================

View File

@@ -81,11 +81,6 @@ parameters:
count: 1
path: src/API/Model/PageAction.php
-
message: "#^Cannot call method getVersion\\(\\) on App\\\\Plugin\\\\PluginMetadata\\|null\\.$#"
count: 1
path: src/API/Model/Plugin.php
-
message: "#^Property App\\\\API\\\\Model\\\\Plugin\\:\\:\\$name is never read, only written\\.$#"
count: 1
@@ -526,16 +521,6 @@ parameters:
count: 1
path: src/Command/InvoiceCreateCommand.php
-
message: "#^Cannot call method getKimaiVersion\\(\\) on App\\\\Plugin\\\\PluginMetadata\\|null\\.$#"
count: 1
path: src/Command/PluginCommand.php
-
message: "#^Cannot call method getVersion\\(\\) on App\\\\Plugin\\\\PluginMetadata\\|null\\.$#"
count: 1
path: src/Command/PluginCommand.php
-
message: "#^Method App\\\\Command\\\\PromoteUserCommand\\:\\:executeRoleCommand\\(\\) has parameter \\$role with no type specified\\.$#"
count: 1
@@ -1936,11 +1921,6 @@ parameters:
count: 2
path: src/EventSubscriber/Actions/ActivitySubscriber.php
-
message: "#^Cannot call method getHomepage\\(\\) on App\\\\Plugin\\\\PluginMetadata\\|null\\.$#"
count: 1
path: src/EventSubscriber/Actions/PluginSubscriber.php
-
message: "#^Cannot call method getId\\(\\) on App\\\\Entity\\\\User\\|null\\.$#"
count: 1
@@ -5231,21 +5211,6 @@ parameters:
count: 1
path: src/Timesheet/Rounding/FloorRounding.php
-
message: "#^Method App\\\\Timesheet\\\\RoundingService\\:\\:__construct\\(\\) has parameter \\$rules with no value type specified in iterable type array\\.$#"
count: 1
path: src/Timesheet/RoundingService.php
-
message: "#^Method App\\\\Timesheet\\\\RoundingService\\:\\:getRoundingRules\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1
path: src/Timesheet/RoundingService.php
-
message: "#^Property App\\\\Timesheet\\\\RoundingService\\:\\:\\$rulesCache type has no value type specified in iterable type array\\.$#"
count: 1
path: src/Timesheet/RoundingService.php
-
message: "#^Cannot call method getTimezone\\(\\) on App\\\\Entity\\\\User\\|null\\.$#"
count: 2

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -3,7 +3,7 @@
"app": {
"js": [
"/build/runtime.f0079159.js",
"/build/app.63b27bee.js"
"/build/app.adf0b03a.js"
],
"css": [
"/build/app.0ecb28c1.css"
@@ -63,7 +63,7 @@
},
"integrity": {
"/build/runtime.f0079159.js": "sha384-H22sAW1aTvyIPqvHOvGXWSWTxf0y6mptp+MsVmyXCfjx/WJjBbhX9gbUZ+qIuihV",
"/build/app.63b27bee.js": "sha384-1CV+he7m+KfetJpq/DmRwKfUhKH4L7MImv7rRUz/mr/KNR3VkT0fQJHF7ViPTVYE",
"/build/app.adf0b03a.js": "sha384-z0Evh/1m2s8eBABcPDKISlFOKIzUpZE5CJt7HQIVgciHgD8GxpK50XZyNCNq+gyd",
"/build/app.0ecb28c1.css": "sha384-iCao48T4VAR0rv39D1+kCk8GCRtve2QDTH5gGq1I1TMXmy1xA/OPxjeBFgGKkRk+",
"/build/export-pdf.d367a32e.js": "sha384-Z5baqnzjI636nYFs4g63ViIKBZKRW4Jhv/7PQmTEQlqhfA7eK0vUMUtiyy0R5A9u",
"/build/export-pdf.d8a6c23b.css": "sha384-ztepocHE4rnGE9eKZ4kL6jTKaePUyiwiB9TjJjstjpf/ckcKg1HedrEOOk/8ElJg",

View File

@@ -1,6 +1,6 @@
{
"build/app.css": "/build/app.0ecb28c1.css",
"build/app.js": "/build/app.63b27bee.js",
"build/app.js": "/build/app.adf0b03a.js",
"build/export-pdf.css": "/build/export-pdf.d8a6c23b.css",
"build/export-pdf.js": "/build/export-pdf.d367a32e.js",
"build/invoice.css": "/build/invoice.5bea118e.css",

View File

@@ -66,7 +66,6 @@ final class StatusController extends BaseApiController
{
$plugins = [];
foreach ($pluginManager->getPlugins() as $plugin) {
$pluginManager->loadMetadata($plugin);
$plugins[] = new Plugin($plugin);
}

View File

@@ -16,6 +16,7 @@ use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
@@ -43,6 +44,7 @@ final class CreateUserCommand extends AbstractUserCommand
User::DEFAULT_ROLE
)
->addArgument('password', InputArgument::OPTIONAL, 'Password for the new user (requested if not provided)')
->addOption('request-password', null, InputOption::VALUE_NONE, 'The user needs to set a new password during next login')
;
}
@@ -69,6 +71,10 @@ final class CreateUserCommand extends AbstractUserCommand
$user->setEnabled(true);
$user->setRoles(explode(',', $role));
if ($input->getOption('request-password') === true) {
$user->setRequiresPasswordReset(true);
}
try {
$this->userService->saveNewUser($user);
$io->success(sprintf('Success! Created user: %s', $username));

View File

@@ -48,16 +48,16 @@ final class PluginCommand extends Command
$rows = [];
foreach ($plugins as $plugin) {
$this->plugins->loadMetadata($plugin);
$meta = $plugin->getMetadata();
$rows[] = [
$plugin->getId(),
$plugin->getName(),
$meta->getVersion(),
$meta->getKimaiVersion(),
$plugin->getPath(),
];
}
$io->table(['Name', 'Version', 'Requires', 'Directory'], $rows);
$io->table(['Id', 'Name', 'Version', 'Requires', 'Directory'], $rows);
return Command::SUCCESS;
}

View File

@@ -17,11 +17,11 @@ class Constants
/**
* The current release version
*/
public const VERSION = '2.6.0';
public const VERSION = '2.7.0';
/**
* The current release: major * 10000 + minor * 100 + patch
*/
public const VERSION_ID = 20600;
public const VERSION_ID = 20700;
/**
* The software name
*/

View File

@@ -28,7 +28,6 @@ final class PluginController extends AbstractController
$installed = [];
$plugins = $manager->getPlugins();
foreach ($plugins as $plugin) {
$manager->loadMetadata($plugin);
$installed[] = $plugin->getId();
}

View File

@@ -9,8 +9,10 @@
namespace App\DependencyInjection\Compiler;
use App\Export\ExportRepositoryInterface;
use App\Export\RendererInterface;
use App\Export\ServiceExport;
use App\Kernel;
use App\Export\TimesheetExportInterface;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
@@ -24,17 +26,17 @@ final class ExportServiceCompilerPass implements CompilerPassInterface
{
$definition = $container->findDefinition(ServiceExport::class);
$taggedRenderer = $container->findTaggedServiceIds(Kernel::TAG_EXPORT_RENDERER);
$taggedRenderer = $container->findTaggedServiceIds(RendererInterface::class);
foreach ($taggedRenderer as $id => $tags) {
$definition->addMethodCall('addRenderer', [new Reference($id)]);
}
$taggedExporter = $container->findTaggedServiceIds(Kernel::TAG_TIMESHEET_EXPORTER);
$taggedExporter = $container->findTaggedServiceIds(TimesheetExportInterface::class);
foreach ($taggedExporter as $id => $tags) {
$definition->addMethodCall('addTimesheetExporter', [new Reference($id)]);
}
$taggedRepository = $container->findTaggedServiceIds(Kernel::TAG_EXPORT_REPOSITORY);
$taggedRepository = $container->findTaggedServiceIds(ExportRepositoryInterface::class);
foreach ($taggedRepository as $id => $tags) {
$definition->addMethodCall('addExportRepository', [new Reference($id)]);
}

View File

@@ -9,8 +9,11 @@
namespace App\DependencyInjection\Compiler;
use App\Invoice\CalculatorInterface;
use App\Invoice\InvoiceItemRepositoryInterface;
use App\Invoice\NumberGeneratorInterface;
use App\Invoice\RendererInterface;
use App\Invoice\ServiceInvoice;
use App\Kernel;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
@@ -24,22 +27,22 @@ final class InvoiceServiceCompilerPass implements CompilerPassInterface
{
$definition = $container->findDefinition(ServiceInvoice::class);
$taggedRenderer = $container->findTaggedServiceIds(Kernel::TAG_INVOICE_RENDERER);
$taggedRenderer = $container->findTaggedServiceIds(RendererInterface::class);
foreach ($taggedRenderer as $id => $tags) {
$definition->addMethodCall('addRenderer', [new Reference($id)]);
}
$taggedGenerator = $container->findTaggedServiceIds(Kernel::TAG_INVOICE_NUMBER_GENERATOR);
$taggedGenerator = $container->findTaggedServiceIds(NumberGeneratorInterface::class);
foreach ($taggedGenerator as $id => $tags) {
$definition->addMethodCall('addNumberGenerator', [new Reference($id)]);
}
$taggedCalculator = $container->findTaggedServiceIds(Kernel::TAG_INVOICE_CALCULATOR);
$taggedCalculator = $container->findTaggedServiceIds(CalculatorInterface::class);
foreach ($taggedCalculator as $id => $tags) {
$definition->addMethodCall('addCalculator', [new Reference($id)]);
}
$taggedRepository = $container->findTaggedServiceIds(Kernel::TAG_INVOICE_REPOSITORY);
$taggedRepository = $container->findTaggedServiceIds(InvoiceItemRepositoryInterface::class);
foreach ($taggedRepository as $id => $tags) {
$definition->addMethodCall('addInvoiceItemRepository', [new Reference($id)]);
}

View File

@@ -9,7 +9,7 @@
namespace App\DependencyInjection\Compiler;
use App\Kernel;
use App\Widget\WidgetInterface;
use App\Widget\WidgetService;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
@@ -24,7 +24,7 @@ final class WidgetCompilerPass implements CompilerPassInterface
{
$definition = $container->findDefinition(WidgetService::class);
$taggedRenderer = $container->findTaggedServiceIds(Kernel::TAG_WIDGET);
$taggedRenderer = $container->findTaggedServiceIds(WidgetInterface::class);
foreach ($taggedRenderer as $id => $tags) {
$definition->addMethodCall('registerWidget', [new Reference($id)]);
}

View File

@@ -0,0 +1,15 @@
<?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\Doctrine;
interface ModifiedAt
{
public function setModifiedAt(\DateTimeImmutable $dateTime): void;
}

View File

@@ -9,14 +9,13 @@
namespace App\Doctrine;
use App\Entity\Timesheet;
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\OnFlushEventArgs;
use Doctrine\ORM\Events;
/**
* Automatically set the modifiedAt field for all Timesheet entries.
* Automatically set the modifiedAt field for all ModifiedAt instances.
*/
#[AsDoctrineListener(event: Events::onFlush, priority: 60)]
final class ModifiedSubscriber implements EventSubscriber, DataSubscriberInterface
@@ -34,17 +33,15 @@ final class ModifiedSubscriber implements EventSubscriber, DataSubscriberInterfa
$now = new \DateTimeImmutable('now', new \DateTimeZone('UTC'));
foreach ($uow->getScheduledEntityUpdates() as $entity) {
if (!($entity instanceof Timesheet)) {
continue;
}
if ($entity instanceof ModifiedAt) {
$entity->setModifiedAt($now);
}
}
foreach ($uow->getScheduledEntityInsertions() as $entity) {
if (!($entity instanceof Timesheet)) {
continue;
}
if ($entity instanceof ModifiedAt) {
$entity->setModifiedAt($now);
}
}
}
}

View File

@@ -15,9 +15,12 @@ use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\OnFlushEventArgs;
use Doctrine\ORM\Events;
use Symfony\Component\DependencyInjection\Attribute\TaggedIterator;
/**
* A listener to make sure all Timesheet entries will be calculated properly (e.g. duration and rates).
* A listener to make sure all Timesheet entries will be calculated properly.
*
* E.g. updates timesheet records and applies configured rate & rounding rules.
*/
#[AsDoctrineListener(event: Events::onFlush, priority: 50)]
final class TimesheetSubscriber implements EventSubscriber, DataSubscriberInterface
@@ -30,7 +33,10 @@ final class TimesheetSubscriber implements EventSubscriber, DataSubscriberInterf
/**
* @param CalculatorInterface[] $calculators
*/
public function __construct(private iterable $calculators)
public function __construct(
#[TaggedIterator(CalculatorInterface::class)]
private readonly iterable $calculators
)
{
}

View File

@@ -9,6 +9,7 @@
namespace App\Entity;
use App\Doctrine\ModifiedAt;
use App\Validator\Constraints as Constraints;
use DateTime;
use DateTimeZone;
@@ -45,7 +46,7 @@ use Symfony\Component\Validator\Constraints as Assert;
#[Serializer\VirtualProperty('TagsAsArray', exp: 'object.getTagsAsArray()', options: [new Serializer\SerializedName('tags'), new Serializer\Type(name: 'array<string>'), new Serializer\Groups(['Default'])])]
#[Constraints\Timesheet]
#[Constraints\TimesheetDeactivated]
class Timesheet implements EntityWithMetaFields, ExportableItem
class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt
{
/**
* Category: Normal work-time (default category)

View File

@@ -11,7 +11,9 @@ namespace App\Export;
use App\Entity\ExportableItem;
use App\Repository\Query\ExportQuery;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
#[AutoconfigureTag]
interface ExportRepositoryInterface
{
/**

View File

@@ -9,6 +9,9 @@
namespace App\Export;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
#[AutoconfigureTag]
interface RendererInterface extends ExportRendererInterface
{
}

View File

@@ -11,8 +11,10 @@ namespace App\Export;
use App\Entity\Timesheet;
use App\Repository\Query\TimesheetQuery;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
use Symfony\Component\HttpFoundation\Response;
#[AutoconfigureTag]
interface TimesheetExportInterface
{
/**

View File

@@ -15,6 +15,8 @@ use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
@@ -50,6 +52,20 @@ class DatePickerType extends AbstractType
));
}
public function buildView(FormView $view, FormInterface $form, array $options): void
{
if ($options['min_day'] !== null) {
$view->vars['attr'] = array_merge($view->vars['attr'], [
'min' => $options['min_day'],
]);
}
if ($options['max_day'] !== null) {
$view->vars['attr'] = array_merge($view->vars['attr'], [
'max' => $options['max_day'],
]);
}
}
public function configureOptions(OptionsResolver $resolver): void
{
$format = $this->localeService->getDateFormat(\Locale::getDefault());
@@ -64,6 +80,8 @@ class DatePickerType extends AbstractType
'model_timezone' => date_default_timezone_get(),
'view_timezone' => date_default_timezone_get(),
'force_time' => null,
'min_day' => null,
'max_day' => null,
]);
}

View File

@@ -50,6 +50,8 @@ final class DateRangeType extends AbstractType
'separator' => self::DATE_SPACER,
'allow_empty' => true,
'with_presets' => true,
'min_day' => null,
'max_day' => null,
'attr' => [
'pattern' => $pattern . self::DATE_SPACER . $pattern
],
@@ -87,6 +89,18 @@ final class DateRangeType extends AbstractType
$view->vars['attr'] = array_merge($view->vars['attr'], [
'data-separator' => $options['separator'],
]);
if ($options['min_day'] !== null) {
$view->vars['attr'] = array_merge($view->vars['attr'], [
'min' => $options['min_day'],
]);
}
if ($options['max_day'] !== null) {
$view->vars['attr'] = array_merge($view->vars['attr'], [
'max' => $options['max_day'],
]);
}
}
public function buildForm(FormBuilderInterface $builder, array $options): void

View File

@@ -9,9 +9,12 @@
namespace App\Invoice;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
/**
* CalculatorInterface defines all methods for any invoice price calculator.
*/
#[AutoconfigureTag]
interface CalculatorInterface
{
/**

View File

@@ -11,7 +11,9 @@ namespace App\Invoice;
use App\Entity\ExportableItem;
use App\Repository\Query\InvoiceQuery;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
#[AutoconfigureTag]
interface InvoiceItemRepositoryInterface
{
/**

View File

@@ -9,9 +9,12 @@
namespace App\Invoice;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
/**
* Class NumberGeneratorInterface defines all methods that invoice number generator have to implement.
*/
#[AutoconfigureTag]
interface NumberGeneratorInterface
{
public function setModel(InvoiceModel $model): void;

View File

@@ -10,8 +10,10 @@
namespace App\Invoice;
use App\Model\InvoiceDocument;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
use Symfony\Component\HttpFoundation\Response;
#[AutoconfigureTag]
interface RendererInterface
{
/**

View File

@@ -14,22 +14,9 @@ use App\DependencyInjection\Compiler\ExportServiceCompilerPass;
use App\DependencyInjection\Compiler\InvoiceServiceCompilerPass;
use App\DependencyInjection\Compiler\TwigContextCompilerPass;
use App\DependencyInjection\Compiler\WidgetCompilerPass;
use App\Export\ExportRepositoryInterface;
use App\Export\RendererInterface as ExportRendererInterface;
use App\Export\TimesheetExportInterface;
use App\Invoice\CalculatorInterface as InvoiceCalculator;
use App\Invoice\InvoiceItemRepositoryInterface;
use App\Invoice\NumberGeneratorInterface;
use App\Invoice\RendererInterface as InvoiceRendererInterface;
use App\Ldap\FormLoginLdapFactory;
use App\Plugin\PluginInterface;
use App\Plugin\PluginMetadata;
use App\Timesheet\CalculatorInterface as TimesheetCalculator;
use App\Timesheet\Rounding\RoundingInterface;
use App\Timesheet\TrackingMode\TrackingModeInterface;
use App\Validator\Constraints\ProjectConstraint;
use App\Validator\Constraints\TimesheetConstraint;
use App\Widget\WidgetInterface;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Bundle\SecurityBundle\DependencyInjection\SecurityExtension;
use Symfony\Component\Config\Loader\LoaderInterface;
@@ -47,21 +34,6 @@ class Kernel extends BaseKernel
public const PLUGIN_DIRECTORY = '/var/plugins';
public const CONFIG_EXTS = '.{php,yaml}';
public const TAG_PLUGIN = 'kimai.plugin';
public const TAG_WIDGET = 'widget';
public const TAG_EXPORT_RENDERER = 'export.renderer';
public const TAG_EXPORT_REPOSITORY = 'export.repository';
public const TAG_INVOICE_RENDERER = 'invoice.renderer';
public const TAG_INVOICE_NUMBER_GENERATOR = 'invoice.number_generator';
public const TAG_INVOICE_CALCULATOR = 'invoice.calculator';
public const TAG_INVOICE_REPOSITORY = 'invoice.repository';
public const TAG_TIMESHEET_CALCULATOR = 'timesheet.calculator';
public const TAG_TIMESHEET_VALIDATOR = 'timesheet.validator';
public const TAG_TIMESHEET_EXPORTER = 'timesheet.exporter';
public const TAG_TIMESHEET_TRACKING_MODE = 'timesheet.tracking_mode';
public const TAG_TIMESHEET_ROUNDING_MODE = 'timesheet.rounding_mode';
public const TAG_PROJECT_VALIDATOR = 'project.validator';
public function getCacheDir(): string
{
return $this->getProjectDir() . '/var/cache/' . $this->environment;
@@ -74,21 +46,6 @@ class Kernel extends BaseKernel
protected function build(ContainerBuilder $container): void
{
$container->registerForAutoconfiguration(TimesheetCalculator::class)->addTag(self::TAG_TIMESHEET_CALCULATOR);
$container->registerForAutoconfiguration(ExportRendererInterface::class)->addTag(self::TAG_EXPORT_RENDERER);
$container->registerForAutoconfiguration(ExportRepositoryInterface::class)->addTag(self::TAG_EXPORT_REPOSITORY);
$container->registerForAutoconfiguration(InvoiceRendererInterface::class)->addTag(self::TAG_INVOICE_RENDERER);
$container->registerForAutoconfiguration(NumberGeneratorInterface::class)->addTag(self::TAG_INVOICE_NUMBER_GENERATOR);
$container->registerForAutoconfiguration(InvoiceCalculator::class)->addTag(self::TAG_INVOICE_CALCULATOR);
$container->registerForAutoconfiguration(InvoiceItemRepositoryInterface::class)->addTag(self::TAG_INVOICE_REPOSITORY);
$container->registerForAutoconfiguration(PluginInterface::class)->addTag(self::TAG_PLUGIN);
$container->registerForAutoconfiguration(WidgetInterface::class)->addTag(self::TAG_WIDGET);
$container->registerForAutoconfiguration(TimesheetExportInterface::class)->addTag(self::TAG_TIMESHEET_EXPORTER);
$container->registerForAutoconfiguration(TrackingModeInterface::class)->addTag(self::TAG_TIMESHEET_TRACKING_MODE);
$container->registerForAutoconfiguration(RoundingInterface::class)->addTag(self::TAG_TIMESHEET_ROUNDING_MODE);
$container->registerForAutoconfiguration(TimesheetConstraint::class)->addTag(self::TAG_TIMESHEET_VALIDATOR);
$container->registerForAutoconfiguration(ProjectConstraint::class)->addTag(self::TAG_PROJECT_VALIDATOR);
/** @var SecurityExtension $extension */
$extension = $container->getExtension('security');
$extension->addAuthenticatorFactory(new FormLoginLdapFactory());
@@ -152,7 +109,7 @@ class Kernel extends BaseKernel
throw new \Exception(sprintf('Bundle "%s" does not implement %s, which is not supported since 2.0.', $bundleName, PluginInterface::class));
}
$meta = PluginMetadata::loadFromComposer($fullPath);
$meta = new PluginMetadata($fullPath);
if ($meta->getKimaiVersion() > Constants::VERSION_ID) {
throw new \Exception(sprintf('Bundle "%s" requires minimum Kimai version %s, but yours is lower: %s (%s). Please update Kimai or use a lower Plugin version.', $bundleName, $meta->getKimaiVersion(), Constants::VERSION, Constants::VERSION_ID));
@@ -173,7 +130,7 @@ class Kernel extends BaseKernel
$confDir = $this->getProjectDir() . '/config';
// using this one instead of $loader->load($confDir . '/packages/*' . self::CONFIG_EXTS, 'glob');
// to get rid of the local.yaml from the list, we load it afterwards explicit
// to get rid of the local.yaml from the list, we load it afterward explicit
$finder = (new Finder())
->files()
->in([$confDir . '/packages/'])
@@ -199,8 +156,7 @@ class Kernel extends BaseKernel
$container->addCompilerPass(new WidgetCompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -1000);
}
/** @phpstan-ignore-next-line */
private function configureRoutes(RoutingConfigurator $routes): void
private function configureRoutes(RoutingConfigurator $routes): void // @phpstan-ignore-line
{
$configDir = $this->getConfigDir();

View File

@@ -11,42 +11,38 @@ namespace App\Plugin;
final class Plugin
{
private string $id;
private string $path;
private ?PluginMetadata $metadata = null;
public function __construct(PluginInterface $bundle)
public function __construct(private readonly PluginInterface $bundle)
{
$this->id = $bundle->getName();
$this->path = $bundle->getPath();
}
public function getMetadata(): ?PluginMetadata
public function getMetadata(): PluginMetadata
{
if ($this->metadata === null) {
$this->metadata = new PluginMetadata($this->getPath());
}
return $this->metadata;
}
public function setMetadata(PluginMetadata $metadata): void
{
$this->metadata = $metadata;
}
public function getPath(): string
{
return $this->path;
return $this->bundle->getPath();
}
public function getName(): string
{
if ($this->metadata !== null && $this->metadata->getName() !== null) {
return $this->metadata->getName();
$meta = $this->getMetadata();
if ($meta->getName() !== null) {
return $meta->getName();
}
return $this->id;
return $this->getId();
}
public function getId(): string
{
return $this->id;
return $this->bundle->getName();
}
}

View File

@@ -9,6 +9,9 @@
namespace App\Plugin;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
#[AutoconfigureTag]
interface PluginInterface
{
public function getName(): string;

View File

@@ -9,23 +9,23 @@
namespace App\Plugin;
use Symfony\Component\DependencyInjection\Attribute\TaggedIterator;
final class PluginManager
{
/**
* @var array<Plugin>|null
*/
private ?array $plugins = null;
/**
* @var iterable<PluginInterface>
*/
private iterable $bundles;
/**
* @param iterable<PluginInterface> $plugins
* @param iterable<PluginInterface> $bundles
*/
public function __construct(iterable $plugins)
public function __construct(
#[TaggedIterator(PluginInterface::class)]
private readonly iterable $bundles
)
{
$this->bundles = $plugins;
}
/**
@@ -46,29 +46,27 @@ final class PluginManager
return $this->plugins;
}
public function hasPlugin(string $name): bool
{
foreach ($this->bundles as $plugin) {
if ($plugin->getName() === $name) {
return true;
}
}
return false;
}
public function getPlugin(string $name): ?Plugin
{
$plugins = $this->getPlugins();
foreach ($plugins as $plugin) {
if ($plugin->getName() === $name) {
if ($plugin->getId() === $name) {
return $plugin;
}
}
return null;
}
/**
* Call this method and pass a plugin, to set its metadata.
* This is not pre-filled by default, as it would mean to parse several composer.json on each request.
*
* @param Plugin $plugin
* @throws \Exception
*/
public function loadMetadata(Plugin $plugin): void
{
$meta = PluginMetadata::loadFromComposer($plugin->getPath());
$plugin->setMetadata($meta);
}
}

View File

@@ -20,11 +20,9 @@ class PluginMetadata
private ?string $name = null;
/**
* @param string $path
* @return PluginMetadata
* @throws \Exception
*/
public static function loadFromComposer(string $path): PluginMetadata
public function __construct(string $path)
{
if (!is_dir($path) || !is_readable($path)) {
throw new \Exception(sprintf('Bundle directory "%s" cannot be accessed.', $path));
@@ -59,16 +57,13 @@ class PluginMetadata
throw new \Exception(sprintf('Bundle "%s" defines an invalid Kimai minimum version in extra.kimai.require. Please provide an integer as in Constants::VERSION_ID.', $pluginName));
}
$meta = new self();
$meta->description = $json['description'] ?? '';
$meta->homepage = $json['homepage'] ?? Constants::HOMEPAGE . '/store/';
$meta->name = $json['extra']['kimai']['name'];
$meta->kimaiVersion = $json['extra']['kimai']['require'];
$this->description = $json['description'] ?? '';
$this->homepage = $json['homepage'] ?? Constants::HOMEPAGE . '/store/';
$this->name = $json['extra']['kimai']['name'];
$this->kimaiVersion = $json['extra']['kimai']['require'];
// the version field is required if we use composer to install a plugin via var/packages/
$meta->version = $json['extra']['kimai']['version'] ?? ($json['version'] ?? 'unknown');
return $meta;
$this->version = $json['extra']['kimai']['version'] ?? ($json['version'] ?? 'unknown');
}
public function getDescription(): ?string

View File

@@ -10,11 +10,13 @@
namespace App\Timesheet;
use App\Entity\Timesheet;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
/**
* A calculator is called before a Timesheet entity will be updated.
* These classes will normally be used when calculating duration or rates.
*/
#[AutoconfigureTag]
interface CalculatorInterface
{
/**

View File

@@ -10,10 +10,12 @@
namespace App\Timesheet\Rounding;
use App\Entity\Timesheet;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
/**
* Apply rounding rules to the given timesheet.
*/
#[AutoconfigureTag]
interface RoundingInterface
{
public function roundBegin(Timesheet $record, int $minutes): void;

View File

@@ -12,45 +12,59 @@ namespace App\Timesheet;
use App\Configuration\SystemConfiguration;
use App\Entity\Timesheet;
use App\Timesheet\Rounding\RoundingInterface;
use Symfony\Component\DependencyInjection\Attribute\TaggedIterator;
final class RoundingService
{
/**
* @var array
* @var array<string, array{'days': array<string>, 'begin': int, 'end': int, 'duration': int, 'mode': string}>
*/
private $rulesCache;
private ?array $rulesCache = null;
/**
* @param SystemConfiguration $configuration
* @param RoundingInterface[] $roundingModes
* @param array $rules
* @param array<string, array{'days': array<string>, 'begin': int, 'end': int, 'duration': int, 'mode': string}> $rules
*/
public function __construct(private SystemConfiguration $configuration, private iterable $roundingModes, private array $rules)
public function __construct(
private readonly SystemConfiguration $configuration,
#[TaggedIterator(RoundingInterface::class)]
private readonly iterable $roundingModes,
private readonly array $rules
)
{
}
/**
* @return array<string, array{'days': array<string>, 'begin': int, 'end': int, 'duration': int, 'mode': string}>
*/
private function getRoundingRules(): array
{
if (empty($this->rulesCache)) {
$this->rulesCache = $this->rules;
if (empty($this->rulesCache) || \array_key_exists('default', $this->rulesCache)) {
$this->rulesCache['default']['days'] = $this->configuration->getTimesheetDefaultRoundingDays();
$this->rulesCache['default']['begin'] = $this->configuration->getTimesheetDefaultRoundingBegin();
$this->rulesCache['default']['end'] = $this->configuration->getTimesheetDefaultRoundingEnd();
$this->rulesCache['default']['duration'] = $this->configuration->getTimesheetDefaultRoundingDuration();
$this->rulesCache['default']['mode'] = $this->configuration->getTimesheetDefaultRoundingMode();
}
if ($this->rulesCache === null) {
$rules = $this->rules;
$rules['default']['days'] = $this->configuration->getTimesheetDefaultRoundingDays();
$rules['default']['begin'] = $this->configuration->getTimesheetDefaultRoundingBegin();
$rules['default']['end'] = $this->configuration->getTimesheetDefaultRoundingEnd();
$rules['default']['duration'] = $this->configuration->getTimesheetDefaultRoundingDuration();
$rules['default']['mode'] = $this->configuration->getTimesheetDefaultRoundingMode();
// see AppExtension, conversion from string to array due to system configuration ont allowing to store arrays
foreach ($this->rulesCache as $key => $settings) {
// see AppExtension, conversion from string to array due to system configuration not allowing to store arrays
foreach ($rules as $key => $settings) {
if (\is_array($settings['days'])) {
continue;
}
if ($settings['days'] === '') {
$rules[$key]['days'] = [];
continue;
}
$days = explode(',', $settings['days']);
$days = array_map('trim', $days);
$days = array_map('strtolower', $days);
$this->rulesCache[$key]['days'] = $days;
$rules[$key]['days'] = $days;
}
$this->rulesCache = $rules; // @phpstan-ignore-line
}
return $this->rulesCache;
return $this->rulesCache; // @phpstan-ignore-line
}
public function roundBegin(Timesheet $record): void
@@ -61,7 +75,7 @@ final class RoundingService
}
$weekday = $record->getBegin()->format('l');
if (\in_array(strtolower($weekday), $rounding['days'])) {
if (\in_array(strtolower($weekday), $rounding['days'], true)) {
$rounder = $this->getRoundingMode($rounding['mode']);
$rounder->roundBegin($record, $rounding['begin']);
}
@@ -76,7 +90,7 @@ final class RoundingService
}
$weekday = $record->getEnd()->format('l');
if (\in_array(strtolower($weekday), $rounding['days'])) {
if (\in_array(strtolower($weekday), $rounding['days'], true)) {
$rounder = $this->getRoundingMode($rounding['mode']);
$rounder->roundEnd($record, $rounding['end']);
}
@@ -91,7 +105,7 @@ final class RoundingService
}
$weekday = $record->getEnd()->format('l');
if (\in_array(strtolower($weekday), $rounding['days'])) {
if (\in_array(strtolower($weekday), $rounding['days'], true)) {
$rounder = $this->getRoundingMode($rounding['mode']);
$rounder->roundDuration($record, $rounding['duration']);
}
@@ -110,7 +124,7 @@ final class RoundingService
}
$weekday = $record->getEnd()->format('l');
if (\in_array(strtolower($weekday), $rounding['days'])) {
if (\in_array(strtolower($weekday), $rounding['days'], true)) {
$rounder = $this->getRoundingMode($rounding['mode']);
$rounder->roundBegin($record, $rounding['begin']);
$rounder->roundEnd($record, $rounding['end']);

View File

@@ -10,6 +10,7 @@
namespace App\Timesheet\TrackingMode;
use App\Entity\Timesheet;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
use Symfony\Component\HttpFoundation\Request;
/**
@@ -18,6 +19,7 @@ use Symfony\Component\HttpFoundation\Request;
*
* @internal do not implement this interface in your bundle, but rather drop a PR to add it to Kimai core
*/
#[AutoconfigureTag]
interface TrackingModeInterface
{
/**

View File

@@ -11,6 +11,7 @@ namespace App\Timesheet;
use App\Configuration\SystemConfiguration;
use App\Timesheet\TrackingMode\TrackingModeInterface;
use Symfony\Component\DependencyInjection\Attribute\TaggedIterator;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
final class TrackingModeService
@@ -19,7 +20,11 @@ final class TrackingModeService
* @param SystemConfiguration $configuration
* @param TrackingModeInterface[] $modes
*/
public function __construct(private SystemConfiguration $configuration, private iterable $modes)
public function __construct(
private readonly SystemConfiguration $configuration,
#[TaggedIterator(TrackingModeInterface::class)]
private readonly iterable $modes
)
{
}

View File

@@ -9,11 +9,13 @@
namespace App\Validator\Constraints;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
use Symfony\Component\Validator\Constraint;
/**
* Extend this class if you want to add dynamic project validation (eg. via a bundle).
*/
#[AutoconfigureTag]
abstract class ProjectConstraint extends Constraint
{
}

View File

@@ -10,7 +10,8 @@
namespace App\Validator\Constraints;
use App\Entity\Project;
use App\Validator\Constraints\Project as ProjectConstraint;
use App\Validator\Constraints\Project as ProjectEntityConstraint;
use Symfony\Component\DependencyInjection\Attribute\TaggedIterator;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
@@ -19,9 +20,12 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
final class ProjectValidator extends ConstraintValidator
{
/**
* @param Constraint[] $constraints
* @param ProjectConstraint[] $constraints
*/
public function __construct(private iterable $constraints = [])
public function __construct(
#[TaggedIterator(ProjectConstraint::class)]
private iterable $constraints = []
)
{
}
@@ -31,8 +35,8 @@ final class ProjectValidator extends ConstraintValidator
*/
public function validate(mixed $value, Constraint $constraint): void
{
if (!($constraint instanceof ProjectConstraint)) {
throw new UnexpectedTypeException($constraint, ProjectConstraint::class);
if (!($constraint instanceof ProjectEntityConstraint)) {
throw new UnexpectedTypeException($constraint, ProjectEntityConstraint::class);
}
if (!\is_object($value) || !($value instanceof Project)) {
@@ -52,10 +56,10 @@ final class ProjectValidator extends ConstraintValidator
protected function validateProject(Project $project, ExecutionContextInterface $context): void
{
if (null !== $project->getStart() && null !== $project->getEnd() && $project->getStart()->getTimestamp() > $project->getEnd()->getTimestamp()) {
$context->buildViolation(ProjectConstraint::getErrorName(ProjectConstraint::END_BEFORE_BEGIN_ERROR))
$context->buildViolation(ProjectEntityConstraint::getErrorName(ProjectEntityConstraint::END_BEFORE_BEGIN_ERROR))
->atPath('end')
->setTranslationDomain('validators')
->setCode(ProjectConstraint::END_BEFORE_BEGIN_ERROR)
->setCode(ProjectEntityConstraint::END_BEFORE_BEGIN_ERROR)
->addViolation();
}
}

View File

@@ -11,6 +11,7 @@ namespace App\Validator\Constraints;
use App\Entity\Timesheet as TimesheetEntity;
use App\Validator\Constraints\QuickEntryTimesheet as QuickEntryTimesheetConstraint;
use Symfony\Component\DependencyInjection\Attribute\TaggedIterator;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
@@ -18,9 +19,12 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
final class QuickEntryTimesheetValidator extends ConstraintValidator
{
/**
* @param Constraint[] $constraints
* @param TimesheetConstraint[] $constraints
*/
public function __construct(private iterable $constraints)
public function __construct(
#[TaggedIterator(TimesheetConstraint::class)]
private iterable $constraints
)
{
}

View File

@@ -9,11 +9,13 @@
namespace App\Validator\Constraints;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
use Symfony\Component\Validator\Constraint;
/**
* Extend this class if you want to add dynamic timesheet validation (eg. via a bundle).
*/
#[AutoconfigureTag]
abstract class TimesheetConstraint extends Constraint
{
}

View File

@@ -10,7 +10,8 @@
namespace App\Validator\Constraints;
use App\Entity\Timesheet as TimesheetEntity;
use App\Validator\Constraints\Timesheet as TimesheetConstraint;
use App\Validator\Constraints\Timesheet as TimesheetEntityConstraint;
use Symfony\Component\DependencyInjection\Attribute\TaggedIterator;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
@@ -18,9 +19,12 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
final class TimesheetValidator extends ConstraintValidator
{
/**
* @param Constraint[] $constraints
* @param TimesheetConstraint[] $constraints
*/
public function __construct(private iterable $constraints)
public function __construct(
#[TaggedIterator(TimesheetConstraint::class)]
private iterable $constraints
)
{
}
@@ -30,8 +34,8 @@ final class TimesheetValidator extends ConstraintValidator
*/
public function validate(mixed $timesheet, Constraint $constraint): void
{
if (!($constraint instanceof TimesheetConstraint)) {
throw new UnexpectedTypeException($constraint, Timesheet::class);
if (!($constraint instanceof TimesheetEntityConstraint)) {
throw new UnexpectedTypeException($constraint, TimesheetEntityConstraint::class);
}
if (!\is_object($timesheet) || !($timesheet instanceof TimesheetEntity)) {

View File

@@ -10,12 +10,14 @@
namespace App\Widget;
use App\Entity\User;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
use Symfony\Component\Form\Form;
/**
* No BC promise!
* Use AbstractWidget to get a BC safe base class.
*/
#[AutoconfigureTag]
interface WidgetInterface
{
public const COLOR_TODAY = 'green';

View File

@@ -15,8 +15,10 @@
<div class="d-flex">
<div class="me-auto">
<p>{{ 'api_password.intro'|trans }}</p>
<p>{{ 'username'|trans }}: {{ user.userIdentifier }}</p>
<ul>
<li>{{ 'username'|trans }}: {{ user.userIdentifier }}</li>
<li>URL: {{ url('api.swagger_ui', {}, false)|replace({'/doc': ''}) }}</li>
</ul>
</div>
<div class="ms-auto">

View File

@@ -17,22 +17,22 @@ use App\Entity\User;
*/
class StatusControllerTest extends APIControllerBaseTest
{
public function testIsSecurePing()
public function testIsSecurePing(): void
{
$this->assertUrlIsSecured('/api/ping');
}
public function testIsSecureVersion()
public function testIsSecureVersion(): void
{
$this->assertUrlIsSecured('/api/version');
}
public function testIsSecurePlugins()
public function testIsSecurePlugins(): void
{
$this->assertUrlIsSecured('/api/plugins');
}
public function testPing()
public function testPing(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->assertAccessIsGranted($client, '/api/ping');
@@ -42,7 +42,7 @@ class StatusControllerTest extends APIControllerBaseTest
$this->assertEquals(['message' => 'pong'], $result);
}
public function testVersion()
public function testVersion(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->assertAccessIsGranted($client, '/api/version');
@@ -62,12 +62,13 @@ class StatusControllerTest extends APIControllerBaseTest
);
}
public function testPlugins()
public function testPlugins(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->assertAccessIsGranted($client, '/api/plugins');
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
// no asserts, as plugins are disabled in tests
}
}

View File

@@ -24,10 +24,7 @@ use Symfony\Component\Console\Tester\CommandTester;
*/
class ActivateUserCommandTest extends KernelTestCase
{
/**
* @var Application
*/
private $application;
private Application $application;
protected function setUp(): void
{
@@ -41,7 +38,7 @@ class ActivateUserCommandTest extends KernelTestCase
$this->application->add(new ActivateUserCommand($userService));
}
public function testCommandName()
public function testCommandName(): void
{
$application = $this->application;
@@ -49,7 +46,7 @@ class ActivateUserCommandTest extends KernelTestCase
self::assertInstanceOf(ActivateUserCommand::class, $command);
}
protected function callCommand(?string $username)
private function callCommand(?string $username): CommandTester
{
$command = $this->application->find('kimai:user:activate');
$input = [
@@ -66,7 +63,7 @@ class ActivateUserCommandTest extends KernelTestCase
return $commandTester;
}
public function testActivate()
public function testActivate(): void
{
$commandTester = $this->callCommand('chris_user');
@@ -81,7 +78,7 @@ class ActivateUserCommandTest extends KernelTestCase
self::assertTrue($user->isEnabled());
}
public function testActivateOnActiveUser()
public function testActivateOnActiveUser(): void
{
$commandTester = $this->callCommand('susan_super');
@@ -89,7 +86,7 @@ class ActivateUserCommandTest extends KernelTestCase
$this->assertStringContainsString('[WARNING] User "susan_super" is already active.', $output);
}
public function testWithMissingUsername()
public function testWithMissingUsername(): void
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Not enough arguments (missing: "username").');

View File

@@ -30,7 +30,7 @@ class BundleInstallerCommandTest extends KernelTestCase
/**
* @param class-string $className
*/
protected function getCommand(string $className): Command
private function getCommand(string $className): Command
{
$kernel = self::bootKernel();
$this->application = new Application($kernel);

View File

@@ -48,7 +48,7 @@ class ChangePasswordCommandTest extends KernelTestCase
self::assertInstanceOf(ChangePasswordCommand::class, $command);
}
protected function callCommand(?string $username, ?string $password): CommandTester
private function callCommand(?string $username, ?string $password): CommandTester
{
$command = $this->application->find('kimai:user:password');
$input = [

View File

@@ -24,10 +24,7 @@ use Symfony\Component\Console\Tester\CommandTester;
*/
class DeactivateUserCommandTest extends KernelTestCase
{
/**
* @var Application
*/
private $application;
private Application $application;
protected function setUp(): void
{
@@ -41,7 +38,7 @@ class DeactivateUserCommandTest extends KernelTestCase
$this->application->add(new DeactivateUserCommand($userService));
}
public function testCommandName()
public function testCommandName(): void
{
$application = $this->application;
@@ -49,7 +46,7 @@ class DeactivateUserCommandTest extends KernelTestCase
self::assertInstanceOf(DeactivateUserCommand::class, $command);
}
protected function callCommand(?string $username)
protected function callCommand(?string $username): CommandTester
{
$command = $this->application->find('kimai:user:deactivate');
$input = [
@@ -66,7 +63,7 @@ class DeactivateUserCommandTest extends KernelTestCase
return $commandTester;
}
public function testDeactivate()
public function testDeactivate(): void
{
$commandTester = $this->callCommand('john_user');
@@ -81,7 +78,7 @@ class DeactivateUserCommandTest extends KernelTestCase
self::assertFalse($user->isEnabled());
}
public function testDeactivateOnDeactivatedUser()
public function testDeactivateOnDeactivatedUser(): void
{
$commandTester = $this->callCommand('chris_user');
@@ -89,7 +86,7 @@ class DeactivateUserCommandTest extends KernelTestCase
$this->assertStringContainsString('[WARNING] User "chris_user" is already deactivated.', $output);
}
public function testWithMissingUsername()
public function testWithMissingUsername(): void
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Not enough arguments (missing: "username").');

View File

@@ -25,10 +25,7 @@ use Symfony\Component\Console\Tester\CommandTester;
*/
class DemoteUserCommandTest extends KernelTestCase
{
/**
* @var Application
*/
private $application;
private Application $application;
protected function setUp(): void
{
@@ -42,7 +39,7 @@ class DemoteUserCommandTest extends KernelTestCase
$this->application->add(new DemoteUserCommand($userService));
}
public function testCommandName()
public function testCommandName(): void
{
$application = $this->application;
@@ -50,7 +47,7 @@ class DemoteUserCommandTest extends KernelTestCase
self::assertInstanceOf(DemoteUserCommand::class, $command);
}
protected function callCommand(?string $username, ?string $role, bool $super = false)
private function callCommand(?string $username, ?string $role, bool $super = false): CommandTester
{
$command = $this->application->find('kimai:user:demote');
$input = [
@@ -75,7 +72,7 @@ class DemoteUserCommandTest extends KernelTestCase
return $commandTester;
}
public function testDemoteRole()
public function testDemoteRole(): void
{
$commandTester = $this->callCommand('tony_teamlead', 'ROLE_TEAMLEAD');
@@ -90,7 +87,7 @@ class DemoteUserCommandTest extends KernelTestCase
self::assertFalse($user->hasTeamleadRole());
}
public function testDemoteSuper()
public function testDemoteSuper(): void
{
$commandTester = $this->callCommand('susan_super', null, true);
@@ -105,7 +102,7 @@ class DemoteUserCommandTest extends KernelTestCase
self::assertFalse($user->isSuperAdmin());
}
public function testDemoteSuperFailsOnTeamlead()
public function testDemoteSuperFailsOnTeamlead(): void
{
$commandTester = $this->callCommand('tony_teamlead', null, true);
@@ -113,7 +110,7 @@ class DemoteUserCommandTest extends KernelTestCase
$this->assertStringContainsString('[WARNING] User "tony_teamlead" doesn\'t have the super administrator role.', $output);
}
public function testDemoteAdminFailsOnTeamlead()
public function testDemoteAdminFailsOnTeamlead(): void
{
$commandTester = $this->callCommand('tony_teamlead', 'ROLE_ADMIN', false);
@@ -121,7 +118,7 @@ class DemoteUserCommandTest extends KernelTestCase
$this->assertStringContainsString('[WARNING] User "tony_teamlead" didn\'t have "ROLE_ADMIN" role.', $output);
}
public function testDemoteRoleAndSuperFails()
public function testDemoteRoleAndSuperFails(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('You can pass either the role or the --super option (but not both simultaneously).');
@@ -129,7 +126,7 @@ class DemoteUserCommandTest extends KernelTestCase
$this->callCommand('john_user', 'ROLE_TEAMLEAD', true);
}
public function testWithMissingUsername()
public function testWithMissingUsername(): void
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Not enough arguments (missing: "username").');

View File

@@ -37,9 +37,9 @@ class ExportCreateCommandTest extends KernelTestCase
{
use KernelTestTrait;
protected Application $application;
private Application $application;
private function clearExportFiles()
private function clearExportFiles(): void
{
$path = __DIR__ . '/../_data/export/';
@@ -103,7 +103,7 @@ class ExportCreateCommandTest extends KernelTestCase
* @param array $options
* @return CommandTester
*/
protected function createExport(array $options = [])
protected function createExport(array $options = []): CommandTester
{
$command = $this->application->find('kimai:export:create');
$commandTester = new CommandTester($command);
@@ -114,7 +114,7 @@ class ExportCreateCommandTest extends KernelTestCase
return $commandTester;
}
protected function assertCommandErrors(array $options = [], string $errorMessage = '')
protected function assertCommandErrors(array $options = [], string $errorMessage = ''): void
{
$commandTester = $this->createExport($options);
@@ -122,7 +122,7 @@ class ExportCreateCommandTest extends KernelTestCase
$this->assertStringContainsString('[ERROR] ' . $errorMessage, $output);
}
protected function assertCommandResult(array $options = [], string $message = '')
protected function assertCommandResult(array $options = [], string $message = ''): void
{
$commandTester = $this->createExport($options);
@@ -130,47 +130,47 @@ class ExportCreateCommandTest extends KernelTestCase
$this->assertStringContainsString('[OK] ' . $message, $output);
}
public function testCreateWithUnknownExportFilter()
public function testCreateWithUnknownExportFilter(): void
{
$this->assertCommandErrors(['--exported' => 'foo'], 'Unknown "exported" filter given');
}
public function testCreateWithUnknownTemplate()
public function testCreateWithUnknownTemplate(): void
{
$this->assertCommandErrors(['--template' => 'foo'], 'Unknown export "template", available are:');
}
public function testCreateWithMissingTemplate()
public function testCreateWithMissingTemplate(): void
{
$this->assertCommandErrors([], 'You must pass the "template" option');
}
public function testCreateWithInvalidStart()
public function testCreateWithInvalidStart(): void
{
$this->assertCommandErrors(['--template' => 'csv', '--start' => '202ß-ä1-01'], 'Invalid start date given');
}
public function testCreateWithInvalidEnd()
public function testCreateWithInvalidEnd(): void
{
$this->assertCommandErrors(['--template' => 'csv', '--end' => '202ß-ä1-01'], 'Invalid end date given');
}
public function testCreateWithInvalidDirectory()
public function testCreateWithInvalidDirectory(): void
{
$this->assertCommandErrors(['--template' => 'csv', '--directory' => '/tzuikmnbgtz/'], 'Invalid "directory" given: /tzuikmnbgtz/');
}
public function testCreateWithInvalidEmail()
public function testCreateWithInvalidEmail(): void
{
$this->assertCommandErrors(['--template' => 'csv', '--email' => ['tzuikmnbgtz']], 'Invalid "email" given: tzuikmnbgtz');
}
public function testCreateWithInvalidEmails()
public function testCreateWithInvalidEmails(): void
{
$this->assertCommandErrors(['--template' => 'csv', '--email' => ['foo@example.com', 'foo@1']], 'Invalid "email" given: foo@1');
}
public function testCreateWithMissingEntries()
public function testCreateWithMissingEntries(): void
{
$options = ['--set-exported' => null, '--customer' => [1], '--template' => 'csv', '--start' => '2020-01-01', '--end' => '2020-03-01'];
$commandTester = $this->createExport($options);
@@ -179,7 +179,11 @@ class ExportCreateCommandTest extends KernelTestCase
$this->assertStringContainsString('[OK] No entries found, skipping', $output);
}
protected function prepareFixtures(\DateTime $start)
/**
* @param \DateTime $start
* @return array{0: Customer, 1: array<Project>}
*/
private function prepareFixtures(\DateTime $start): array
{
$fixture = new CustomerFixtures();
$fixture->setAmount(1);
@@ -200,7 +204,7 @@ class ExportCreateCommandTest extends KernelTestCase
return [$customer, $project];
}
public function testCreateExportByCustomer()
public function testCreateExportByCustomer(): void
{
$start = new \DateTime('-2 months');
$end = new \DateTime();
@@ -213,7 +217,7 @@ class ExportCreateCommandTest extends KernelTestCase
$this->assertStringContainsString('Saved export to: ', $output);
}
public function testCreateExportByProject()
public function testCreateExportByProject(): void
{
$start = new \DateTime('-2 months');
$end = new \DateTime();
@@ -226,7 +230,7 @@ class ExportCreateCommandTest extends KernelTestCase
$this->assertStringContainsString('Saved export to: ', $output);
}
public function testCreateExportWithEmail()
public function testCreateExportWithEmail(): void
{
$start = new \DateTime('-2 months');
$end = new \DateTime();

View File

@@ -19,10 +19,7 @@ use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
*/
class InstallCommandTest extends KernelTestCase
{
/**
* @var Application
*/
protected $application;
private Application $application;
protected function setUp(): void
{
@@ -37,7 +34,7 @@ class InstallCommandTest extends KernelTestCase
));
}
public function testCommandName()
public function testCommandName(): void
{
$command = $this->application->find('kimai:install');
self::assertInstanceOf(InstallCommand::class, $command);

View File

@@ -35,7 +35,7 @@ class InvoiceCreateCommandTest extends KernelTestCase
{
use KernelTestTrait;
protected Application $application;
private Application $application;
private function clearInvoiceFiles(): void
{

View File

@@ -22,16 +22,13 @@ use Symfony\Component\Console\Tester\CommandTester;
*/
class PluginCommandTest extends KernelTestCase
{
/**
* @var Application
*/
protected $application;
private Application $application;
public function testWithPlugins()
public function testWithPlugins(): void
{
$plugin1 = $this->getMockBuilder(PluginInterface::class)->onlyMethods(['getName', 'getPath'])->getMock();
$plugin1->expects($this->any())->method('getName')->willReturn('TestBundle');
$plugin1->expects($this->once())->method('getPath')->willReturn(__DIR__ . '/../Plugin/Fixtures/TestPlugin');
$plugin1->expects($this->exactly(2))->method('getPath')->willReturn(__DIR__ . '/../Plugin/Fixtures/TestPlugin');
$commandTester = $this->getCommandTester([$plugin1], []);
$output = $commandTester->getDisplay();
@@ -40,7 +37,7 @@ class PluginCommandTest extends KernelTestCase
$this->assertStringContainsString('TestPlugin from composer.json', $output);
}
protected function getCommandTester(array $plugins, array $options = [])
private function getCommandTester(array $plugins, array $options = []): CommandTester
{
$kernel = self::bootKernel();
$this->application = new Application($kernel);

View File

@@ -25,10 +25,7 @@ use Symfony\Component\Console\Tester\CommandTester;
*/
class PromoteUserCommandTest extends KernelTestCase
{
/**
* @var Application
*/
private $application;
private Application $application;
protected function setUp(): void
{
@@ -42,7 +39,7 @@ class PromoteUserCommandTest extends KernelTestCase
$this->application->add(new PromoteUserCommand($userService));
}
public function testCommandName()
public function testCommandName(): void
{
$application = $this->application;
@@ -50,7 +47,7 @@ class PromoteUserCommandTest extends KernelTestCase
self::assertInstanceOf(PromoteUserCommand::class, $command);
}
protected function callCommand(?string $username, ?string $role, bool $super = false)
protected function callCommand(?string $username, ?string $role, bool $super = false): CommandTester
{
$command = $this->application->find('kimai:user:promote');
$input = [
@@ -75,7 +72,7 @@ class PromoteUserCommandTest extends KernelTestCase
return $commandTester;
}
public function testPromoteRole()
public function testPromoteRole(): void
{
$commandTester = $this->callCommand('john_user', 'ROLE_TEAMLEAD');
@@ -90,7 +87,7 @@ class PromoteUserCommandTest extends KernelTestCase
self::assertTrue($user->hasTeamleadRole());
}
public function testPromoteSuper()
public function testPromoteSuper(): void
{
$commandTester = $this->callCommand('john_user', null, true);
@@ -105,7 +102,7 @@ class PromoteUserCommandTest extends KernelTestCase
self::assertTrue($user->isSuperAdmin());
}
public function testPromoteSuperFailsOnSuperAdmin()
public function testPromoteSuperFailsOnSuperAdmin(): void
{
$commandTester = $this->callCommand('susan_super', null, true);
@@ -113,7 +110,7 @@ class PromoteUserCommandTest extends KernelTestCase
$this->assertStringContainsString('[WARNING] User "susan_super" does already have the super administrator role.', $output);
}
public function testPromoteTeamleadFailsOnTeamlead()
public function testPromoteTeamleadFailsOnTeamlead(): void
{
$commandTester = $this->callCommand('tony_teamlead', 'ROLE_TEAMLEAD', false);
@@ -121,7 +118,7 @@ class PromoteUserCommandTest extends KernelTestCase
$this->assertStringContainsString('[WARNING] User "tony_teamlead" did already have "ROLE_TEAMLEAD" role.', $output);
}
public function testPromoteRoleAndSuperFails()
public function testPromoteRoleAndSuperFails(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('You can pass either the role or the --super option (but not both simultaneously).');
@@ -129,7 +126,7 @@ class PromoteUserCommandTest extends KernelTestCase
$this->callCommand('john_user', 'ROLE_TEAMLEAD', true);
}
public function testWithMissingUsername()
public function testWithMissingUsername(): void
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Not enough arguments (missing: "username").');

View File

@@ -19,10 +19,7 @@ use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
*/
class ReloadCommandTest extends KernelTestCase
{
/**
* @var Application
*/
protected $application;
protected Application $application;
protected function setUp(): void
{

View File

@@ -21,10 +21,7 @@ use Symfony\Component\Console\Tester\CommandTester;
*/
class TimesheetStopAllCommandTest extends KernelTestCase
{
/**
* @var Application
*/
protected $application;
private Application $application;
protected function setUp(): void
{

View File

@@ -22,10 +22,7 @@ use Symfony\Component\Console\Tester\CommandTester;
*/
class UpdateCommandTest extends KernelTestCase
{
/**
* @var Application
*/
protected $application;
private Application $application;
protected function getCommand(): Command
{
@@ -41,7 +38,7 @@ class UpdateCommandTest extends KernelTestCase
return $this->application->find('kimai:update');
}
public function testFullRun()
public function testFullRun(): void
{
$command = $this->getCommand();
$commandTester = new CommandTester($command);

View File

@@ -21,10 +21,7 @@ use Symfony\Component\Console\Tester\CommandTester;
*/
class VersionCommandTest extends KernelTestCase
{
/**
* @var Application
*/
protected $application;
private Application $application;
protected function setUp(): void
{
@@ -38,14 +35,14 @@ class VersionCommandTest extends KernelTestCase
/**
* @dataProvider getTestData
*/
public function testVersion(array $options, $result)
public function testVersion(array $options, $result): void
{
$commandTester = $this->getCommandTester($options);
$output = $commandTester->getDisplay();
$this->assertEquals($result . PHP_EOL, $output);
}
public function getTestData()
public function getTestData(): array // @phpstan-ignore-line
{
return [
[[], 'Kimai ' . Constants::VERSION . ' by Kevin Papst.'],
@@ -54,7 +51,7 @@ class VersionCommandTest extends KernelTestCase
];
}
protected function getCommandTester(array $options = [])
protected function getCommandTester(array $options = []): CommandTester
{
$command = $this->application->find('kimai:version');
$commandTester = new CommandTester($command);

View File

@@ -10,13 +10,15 @@
namespace App\Tests\DependencyInjection\Compiler;
use App\DependencyInjection\Compiler\ExportServiceCompilerPass;
use App\Export\ExportRepositoryInterface;
use App\Export\Renderer\CsvRenderer;
use App\Export\Renderer\HtmlRenderer;
use App\Export\RendererInterface;
use App\Export\ServiceExport;
use App\Export\Timesheet\PDFRenderer;
use App\Export\Timesheet\XlsxRenderer;
use App\Export\TimesheetExportInterface;
use App\Export\TimesheetExportRepository;
use App\Kernel;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
@@ -38,17 +40,17 @@ class ExportServiceCompilerPassTest extends TestCase
$renderers = [CsvRenderer::class, HtmlRenderer::class];
foreach ($renderers as $renderer) {
$container->register($renderer)->addTag(Kernel::TAG_EXPORT_RENDERER);
$container->register($renderer)->addTag(RendererInterface::class);
}
$exporters = [PDFRenderer::class, XlsxRenderer::class];
foreach ($exporters as $exporter) {
$container->register($exporter)->addTag(Kernel::TAG_TIMESHEET_EXPORTER);
$container->register($exporter)->addTag(TimesheetExportInterface::class);
}
$repositories = [TimesheetExportRepository::class];
foreach ($repositories as $repository) {
$container->register($repository)->addTag(Kernel::TAG_EXPORT_REPOSITORY);
$container->register($repository)->addTag(ExportRepositoryInterface::class);
}
return $container;

View File

@@ -13,11 +13,14 @@ use App\DependencyInjection\Compiler\InvoiceServiceCompilerPass;
use App\Invoice\Calculator\DefaultCalculator;
use App\Invoice\Calculator\ShortInvoiceCalculator;
use App\Invoice\Calculator\UserInvoiceCalculator;
use App\Invoice\CalculatorInterface;
use App\Invoice\InvoiceItemRepositoryInterface;
use App\Invoice\NumberGenerator\ConfigurableNumberGenerator;
use App\Invoice\NumberGenerator\DateNumberGenerator;
use App\Invoice\NumberGeneratorInterface;
use App\Invoice\Renderer\DocxRenderer;
use App\Invoice\RendererInterface;
use App\Invoice\ServiceInvoice;
use App\Kernel;
use App\Repository\TimesheetInvoiceItemRepository;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\ContainerBuilder;
@@ -37,22 +40,22 @@ class InvoiceServiceCompilerPassTest extends TestCase
$renderers = [DocxRenderer::class];
foreach ($renderers as $renderer) {
$container->register($renderer)->addTag(Kernel::TAG_INVOICE_RENDERER);
$container->register($renderer)->addTag(RendererInterface::class);
}
$numberGenerators = [DateNumberGenerator::class, ConfigurableNumberGenerator::class];
foreach ($numberGenerators as $numberGenerator) {
$container->register($numberGenerator)->addTag(Kernel::TAG_INVOICE_NUMBER_GENERATOR);
$container->register($numberGenerator)->addTag(NumberGeneratorInterface::class);
}
$calculators = [DefaultCalculator::class, UserInvoiceCalculator::class, ShortInvoiceCalculator::class];
foreach ($calculators as $calculator) {
$container->register($calculator)->addTag(Kernel::TAG_INVOICE_CALCULATOR);
$container->register($calculator)->addTag(CalculatorInterface::class);
}
$repositories = [TimesheetInvoiceItemRepository::class];
foreach ($repositories as $repository) {
$container->register($repository)->addTag(Kernel::TAG_INVOICE_REPOSITORY);
$container->register($repository)->addTag(InvoiceItemRepositoryInterface::class);
}
return $container;

View File

@@ -10,9 +10,9 @@
namespace App\Tests\DependencyInjection\Compiler;
use App\DependencyInjection\Compiler\WidgetCompilerPass;
use App\Kernel;
use App\Widget\Type\ActiveTimesheets;
use App\Widget\Type\ActiveUsersMonth;
use App\Widget\WidgetInterface;
use App\Widget\WidgetService;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\ContainerBuilder;
@@ -32,7 +32,7 @@ class WidgetCompilerPassTest extends TestCase
$widgets = [ActiveTimesheets::class, ActiveUsersMonth::class];
foreach ($widgets as $widget) {
$container->register($widget)->addTag(Kernel::TAG_WIDGET);
$container->register($widget)->addTag(WidgetInterface::class);
}
return $container;

View File

@@ -12,29 +12,27 @@ namespace App\Tests\Plugin;
use App\Plugin\Plugin;
use App\Plugin\PluginInterface;
use App\Plugin\PluginManager;
use App\Plugin\PluginMetadata;
use App\Tests\Plugin\Fixtures\TestPlugin\TestPlugin;
use App\Tests\Plugin\Fixtures\TestPlugin2\TestPlugin2;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\Plugin\PluginManager
* @covers \App\Plugin\Plugin
* @covers \App\Plugin\PluginMetadata
*/
class PluginManagerTest extends TestCase
{
public function testEmptyObject()
public function testEmptyObject(): void
{
$sut = new PluginManager([]);
$this->assertEmpty($sut->getPlugins());
$this->assertNull($sut->getPlugin('foo'));
$this->assertFalse($sut->hasPlugin('foo'));
}
public function testUnknownRendererReturnsNull()
{
$sut = new PluginManager([]);
$this->assertNull($sut->getPlugin('foo'));
}
public function testAdd()
public function testAdd(): void
{
$plugin = $this->createMock(PluginInterface::class);
$plugin->expects($this->any())->method('getName')->willReturn('foo');
@@ -52,27 +50,23 @@ class PluginManagerTest extends TestCase
// make sure a plugin with the same name is not added twice, the first one wins!
$this->assertEquals(2, \count($sut->getPlugins()));
$this->assertFalse($sut->hasPlugin('bar'));
$this->assertTrue($sut->hasPlugin('foo'));
$foo = $sut->getPlugin('foo');
$this->assertInstanceOf(Plugin::class, $foo);
$this->assertEquals('foo', $foo->getName());
$this->assertEquals('foo', $foo->getId());
$this->assertEquals('bar', $foo->getPath());
$test = $sut->getPlugin('TestPlugin');
$this->assertInstanceOf(Plugin::class, $test);
$this->assertEquals('TestPlugin', $test->getName());
$this->assertNull($test->getMetadata());
}
$this->assertEquals('TestPlugin', $test->getId());
$this->assertEquals('TestPlugin from composer.json', $test->getName());
$this->assertInstanceOf(PluginMetadata::class, $test->getMetadata());
public function testLoadMetadata()
{
$sut = new PluginManager([new TestPlugin()]);
$plugin = $sut->getPlugin('TestPlugin');
$sut->loadMetadata($plugin);
$meta = $plugin->getMetadata();
$meta = $test->getMetadata();
$this->assertEquals(10000, $meta->getKimaiVersion());
$this->assertEquals('1.0', $meta->getVersion());
$this->assertEquals('TestPlugin', $plugin->getId());
$this->assertEquals('TestPlugin', $test->getId());
$this->assertEquals('TestPlugin from composer.json', $meta->getName());
$this->assertEquals('Just a test fixture for the PluginManager', $meta->getDescription());
$this->assertEquals('https://github.com/kimai/kimai', $meta->getHomepage());

View File

@@ -17,12 +17,11 @@ use PHPUnit\Framework\TestCase;
*/
class PluginMetadataTest extends TestCase
{
public function testEmptyObject()
public function testNonExistingDirectoryThrowsException(): void
{
$sut = new PluginMetadata();
$this->assertNull($sut->getDescription());
$this->assertNull($sut->getHomepage());
$this->assertNull($sut->getVersion());
$this->assertNull($sut->getKimaiVersion());
$this->expectException(\Exception::class);
$this->expectExceptionMessage('Bundle "Plugin" does not ship composer.json, which is required since 2.0.');
new PluginMetadata(__DIR__);
}
}

View File

@@ -11,7 +11,6 @@ namespace App\Tests\Plugin;
use App\Plugin\Plugin;
use App\Plugin\PluginInterface;
use App\Plugin\PluginMetadata;
use PHPUnit\Framework\TestCase;
/**
@@ -19,22 +18,10 @@ use PHPUnit\Framework\TestCase;
*/
class PluginTest extends TestCase
{
public function testEmptyObject()
public function testEmptyObject(): void
{
$plugin = new Plugin($this->createMock(PluginInterface::class));
$this->assertEquals('', $plugin->getId());
$this->assertEquals('', $plugin->getName());
$this->assertEquals('', $plugin->getPath());
$this->assertNull($plugin->getMetadata());
}
public function testGetterAndSetter()
{
$metadata = new PluginMetadata();
$plugin = new Plugin($this->createMock(PluginInterface::class));
$plugin->setMetadata($metadata);
$this->assertEquals($metadata, $plugin->getMetadata());
}
}

View File

@@ -1067,36 +1067,6 @@ parameters:
count: 1
path: API/Serializer/ValidationFailedExceptionErrorHandlerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\StatusControllerTest\\:\\:testIsSecurePing\\(\\) has no return type specified\\.$#"
count: 1
path: API/StatusControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\StatusControllerTest\\:\\:testIsSecurePlugins\\(\\) has no return type specified\\.$#"
count: 1
path: API/StatusControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\StatusControllerTest\\:\\:testIsSecureVersion\\(\\) has no return type specified\\.$#"
count: 1
path: API/StatusControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\StatusControllerTest\\:\\:testPing\\(\\) has no return type specified\\.$#"
count: 1
path: API/StatusControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\StatusControllerTest\\:\\:testPlugins\\(\\) has no return type specified\\.$#"
count: 1
path: API/StatusControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\StatusControllerTest\\:\\:testVersion\\(\\) has no return type specified\\.$#"
count: 1
path: API/StatusControllerTest.php
-
message: "#^Parameter \\#1 \\$json of function json_decode expects string, string\\|false given\\.$#"
count: 3
@@ -1307,31 +1277,6 @@ parameters:
count: 1
path: Command/ActivateUserCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\ActivateUserCommandTest\\:\\:callCommand\\(\\) has no return type specified\\.$#"
count: 1
path: Command/ActivateUserCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\ActivateUserCommandTest\\:\\:testActivate\\(\\) has no return type specified\\.$#"
count: 1
path: Command/ActivateUserCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\ActivateUserCommandTest\\:\\:testActivateOnActiveUser\\(\\) has no return type specified\\.$#"
count: 1
path: Command/ActivateUserCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\ActivateUserCommandTest\\:\\:testCommandName\\(\\) has no return type specified\\.$#"
count: 1
path: Command/ActivateUserCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\ActivateUserCommandTest\\:\\:testWithMissingUsername\\(\\) has no return type specified\\.$#"
count: 1
path: Command/ActivateUserCommandTest.php
-
message: "#^Parameter \\#1 \\$userService of class App\\\\Command\\\\ActivateUserCommand constructor expects App\\\\User\\\\UserService, object\\|null given\\.$#"
count: 1
@@ -1392,31 +1337,6 @@ parameters:
count: 1
path: Command/DeactivateUserCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\DeactivateUserCommandTest\\:\\:callCommand\\(\\) has no return type specified\\.$#"
count: 1
path: Command/DeactivateUserCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\DeactivateUserCommandTest\\:\\:testCommandName\\(\\) has no return type specified\\.$#"
count: 1
path: Command/DeactivateUserCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\DeactivateUserCommandTest\\:\\:testDeactivate\\(\\) has no return type specified\\.$#"
count: 1
path: Command/DeactivateUserCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\DeactivateUserCommandTest\\:\\:testDeactivateOnDeactivatedUser\\(\\) has no return type specified\\.$#"
count: 1
path: Command/DeactivateUserCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\DeactivateUserCommandTest\\:\\:testWithMissingUsername\\(\\) has no return type specified\\.$#"
count: 1
path: Command/DeactivateUserCommandTest.php
-
message: "#^Parameter \\#1 \\$userService of class App\\\\Command\\\\DeactivateUserCommand constructor expects App\\\\User\\\\UserService, object\\|null given\\.$#"
count: 1
@@ -1427,46 +1347,6 @@ parameters:
count: 2
path: Command/DemoteUserCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\DemoteUserCommandTest\\:\\:callCommand\\(\\) has no return type specified\\.$#"
count: 1
path: Command/DemoteUserCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\DemoteUserCommandTest\\:\\:testCommandName\\(\\) has no return type specified\\.$#"
count: 1
path: Command/DemoteUserCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\DemoteUserCommandTest\\:\\:testDemoteAdminFailsOnTeamlead\\(\\) has no return type specified\\.$#"
count: 1
path: Command/DemoteUserCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\DemoteUserCommandTest\\:\\:testDemoteRole\\(\\) has no return type specified\\.$#"
count: 1
path: Command/DemoteUserCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\DemoteUserCommandTest\\:\\:testDemoteRoleAndSuperFails\\(\\) has no return type specified\\.$#"
count: 1
path: Command/DemoteUserCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\DemoteUserCommandTest\\:\\:testDemoteSuper\\(\\) has no return type specified\\.$#"
count: 1
path: Command/DemoteUserCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\DemoteUserCommandTest\\:\\:testDemoteSuperFailsOnTeamlead\\(\\) has no return type specified\\.$#"
count: 1
path: Command/DemoteUserCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\DemoteUserCommandTest\\:\\:testWithMissingUsername\\(\\) has no return type specified\\.$#"
count: 1
path: Command/DemoteUserCommandTest.php
-
message: "#^Parameter \\#1 \\$userService of class App\\\\Command\\\\DemoteUserCommand constructor expects App\\\\User\\\\UserService, object\\|null given\\.$#"
count: 1
@@ -1477,31 +1357,16 @@ parameters:
count: 1
path: Command/ExportCreateCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\ExportCreateCommandTest\\:\\:assertCommandErrors\\(\\) has no return type specified\\.$#"
count: 1
path: Command/ExportCreateCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\ExportCreateCommandTest\\:\\:assertCommandErrors\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: Command/ExportCreateCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\ExportCreateCommandTest\\:\\:assertCommandResult\\(\\) has no return type specified\\.$#"
count: 1
path: Command/ExportCreateCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\ExportCreateCommandTest\\:\\:assertCommandResult\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: Command/ExportCreateCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\ExportCreateCommandTest\\:\\:clearExportFiles\\(\\) has no return type specified\\.$#"
count: 1
path: Command/ExportCreateCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\ExportCreateCommandTest\\:\\:createApplication\\(\\) has parameter \\$mailer with no type specified\\.$#"
count: 1
@@ -1517,71 +1382,6 @@ parameters:
count: 1
path: Command/ExportCreateCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\ExportCreateCommandTest\\:\\:prepareFixtures\\(\\) has no return type specified\\.$#"
count: 1
path: Command/ExportCreateCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\ExportCreateCommandTest\\:\\:testCreateExportByCustomer\\(\\) has no return type specified\\.$#"
count: 1
path: Command/ExportCreateCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\ExportCreateCommandTest\\:\\:testCreateExportByProject\\(\\) has no return type specified\\.$#"
count: 1
path: Command/ExportCreateCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\ExportCreateCommandTest\\:\\:testCreateExportWithEmail\\(\\) has no return type specified\\.$#"
count: 1
path: Command/ExportCreateCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\ExportCreateCommandTest\\:\\:testCreateWithInvalidDirectory\\(\\) has no return type specified\\.$#"
count: 1
path: Command/ExportCreateCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\ExportCreateCommandTest\\:\\:testCreateWithInvalidEmail\\(\\) has no return type specified\\.$#"
count: 1
path: Command/ExportCreateCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\ExportCreateCommandTest\\:\\:testCreateWithInvalidEmails\\(\\) has no return type specified\\.$#"
count: 1
path: Command/ExportCreateCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\ExportCreateCommandTest\\:\\:testCreateWithInvalidEnd\\(\\) has no return type specified\\.$#"
count: 1
path: Command/ExportCreateCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\ExportCreateCommandTest\\:\\:testCreateWithInvalidStart\\(\\) has no return type specified\\.$#"
count: 1
path: Command/ExportCreateCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\ExportCreateCommandTest\\:\\:testCreateWithMissingEntries\\(\\) has no return type specified\\.$#"
count: 1
path: Command/ExportCreateCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\ExportCreateCommandTest\\:\\:testCreateWithMissingTemplate\\(\\) has no return type specified\\.$#"
count: 1
path: Command/ExportCreateCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\ExportCreateCommandTest\\:\\:testCreateWithUnknownExportFilter\\(\\) has no return type specified\\.$#"
count: 1
path: Command/ExportCreateCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\ExportCreateCommandTest\\:\\:testCreateWithUnknownTemplate\\(\\) has no return type specified\\.$#"
count: 1
path: Command/ExportCreateCommandTest.php
-
message: "#^Parameter \\#1 \\$serviceExport of class App\\\\Command\\\\ExportCreateCommand constructor expects App\\\\Export\\\\ServiceExport, object\\|null given\\.$#"
count: 1
@@ -1617,11 +1417,6 @@ parameters:
count: 1
path: Command/InstallCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\InstallCommandTest\\:\\:testCommandName\\(\\) has no return type specified\\.$#"
count: 1
path: Command/InstallCommandTest.php
-
message: "#^Argument of an invalid type array\\<int, string\\>\\|false supplied for foreach, only iterables are supported\\.$#"
count: 1
@@ -1672,11 +1467,6 @@ parameters:
count: 1
path: Command/InvoiceCreateCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\PluginCommandTest\\:\\:getCommandTester\\(\\) has no return type specified\\.$#"
count: 1
path: Command/PluginCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\PluginCommandTest\\:\\:getCommandTester\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
@@ -1687,56 +1477,11 @@ parameters:
count: 1
path: Command/PluginCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\PluginCommandTest\\:\\:testWithPlugins\\(\\) has no return type specified\\.$#"
count: 1
path: Command/PluginCommandTest.php
-
message: "#^Cannot call method getRepository\\(\\) on object\\|null\\.$#"
count: 2
path: Command/PromoteUserCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\PromoteUserCommandTest\\:\\:callCommand\\(\\) has no return type specified\\.$#"
count: 1
path: Command/PromoteUserCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\PromoteUserCommandTest\\:\\:testCommandName\\(\\) has no return type specified\\.$#"
count: 1
path: Command/PromoteUserCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\PromoteUserCommandTest\\:\\:testPromoteRole\\(\\) has no return type specified\\.$#"
count: 1
path: Command/PromoteUserCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\PromoteUserCommandTest\\:\\:testPromoteRoleAndSuperFails\\(\\) has no return type specified\\.$#"
count: 1
path: Command/PromoteUserCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\PromoteUserCommandTest\\:\\:testPromoteSuper\\(\\) has no return type specified\\.$#"
count: 1
path: Command/PromoteUserCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\PromoteUserCommandTest\\:\\:testPromoteSuperFailsOnSuperAdmin\\(\\) has no return type specified\\.$#"
count: 1
path: Command/PromoteUserCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\PromoteUserCommandTest\\:\\:testPromoteTeamleadFailsOnTeamlead\\(\\) has no return type specified\\.$#"
count: 1
path: Command/PromoteUserCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\PromoteUserCommandTest\\:\\:testWithMissingUsername\\(\\) has no return type specified\\.$#"
count: 1
path: Command/PromoteUserCommandTest.php
-
message: "#^Parameter \\#1 \\$userService of class App\\\\Command\\\\PromoteUserCommand constructor expects App\\\\User\\\\UserService, object\\|null given\\.$#"
count: 1
@@ -1782,31 +1527,11 @@ parameters:
count: 1
path: Command/UpdateCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\UpdateCommandTest\\:\\:testFullRun\\(\\) has no return type specified\\.$#"
count: 1
path: Command/UpdateCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\VersionCommandTest\\:\\:getCommandTester\\(\\) has no return type specified\\.$#"
count: 1
path: Command/VersionCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\VersionCommandTest\\:\\:getCommandTester\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: Command/VersionCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\VersionCommandTest\\:\\:getTestData\\(\\) has no return type specified\\.$#"
count: 1
path: Command/VersionCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\VersionCommandTest\\:\\:testVersion\\(\\) has no return type specified\\.$#"
count: 1
path: Command/VersionCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\VersionCommandTest\\:\\:testVersion\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
@@ -6937,81 +6662,6 @@ parameters:
count: 1
path: Pdf/PdfContextTest.php
-
message: "#^Cannot call method getDescription\\(\\) on App\\\\Plugin\\\\PluginMetadata\\|null\\.$#"
count: 1
path: Plugin/PluginManagerTest.php
-
message: "#^Cannot call method getHomepage\\(\\) on App\\\\Plugin\\\\PluginMetadata\\|null\\.$#"
count: 1
path: Plugin/PluginManagerTest.php
-
message: "#^Cannot call method getId\\(\\) on App\\\\Plugin\\\\Plugin\\|null\\.$#"
count: 1
path: Plugin/PluginManagerTest.php
-
message: "#^Cannot call method getKimaiVersion\\(\\) on App\\\\Plugin\\\\PluginMetadata\\|null\\.$#"
count: 1
path: Plugin/PluginManagerTest.php
-
message: "#^Cannot call method getMetadata\\(\\) on App\\\\Plugin\\\\Plugin\\|null\\.$#"
count: 1
path: Plugin/PluginManagerTest.php
-
message: "#^Cannot call method getName\\(\\) on App\\\\Plugin\\\\PluginMetadata\\|null\\.$#"
count: 1
path: Plugin/PluginManagerTest.php
-
message: "#^Cannot call method getVersion\\(\\) on App\\\\Plugin\\\\PluginMetadata\\|null\\.$#"
count: 1
path: Plugin/PluginManagerTest.php
-
message: "#^Method App\\\\Tests\\\\Plugin\\\\PluginManagerTest\\:\\:testAdd\\(\\) has no return type specified\\.$#"
count: 1
path: Plugin/PluginManagerTest.php
-
message: "#^Method App\\\\Tests\\\\Plugin\\\\PluginManagerTest\\:\\:testEmptyObject\\(\\) has no return type specified\\.$#"
count: 1
path: Plugin/PluginManagerTest.php
-
message: "#^Method App\\\\Tests\\\\Plugin\\\\PluginManagerTest\\:\\:testLoadMetadata\\(\\) has no return type specified\\.$#"
count: 1
path: Plugin/PluginManagerTest.php
-
message: "#^Method App\\\\Tests\\\\Plugin\\\\PluginManagerTest\\:\\:testUnknownRendererReturnsNull\\(\\) has no return type specified\\.$#"
count: 1
path: Plugin/PluginManagerTest.php
-
message: "#^Parameter \\#1 \\$plugin of method App\\\\Plugin\\\\PluginManager\\:\\:loadMetadata\\(\\) expects App\\\\Plugin\\\\Plugin, App\\\\Plugin\\\\Plugin\\|null given\\.$#"
count: 1
path: Plugin/PluginManagerTest.php
-
message: "#^Method App\\\\Tests\\\\Plugin\\\\PluginMetadataTest\\:\\:testEmptyObject\\(\\) has no return type specified\\.$#"
count: 1
path: Plugin/PluginMetadataTest.php
-
message: "#^Method App\\\\Tests\\\\Plugin\\\\PluginTest\\:\\:testEmptyObject\\(\\) has no return type specified\\.$#"
count: 1
path: Plugin/PluginTest.php
-
message: "#^Method App\\\\Tests\\\\Plugin\\\\PluginTest\\:\\:testGetterAndSetter\\(\\) has no return type specified\\.$#"
count: 1
path: Plugin/PluginTest.php
-
message: "#^Method App\\\\Tests\\\\Project\\\\ProjectServiceTest\\:\\:testCannotSavePersistedProjectAsNew\\(\\) has no return type specified\\.$#"
count: 1

View File

@@ -1685,6 +1685,10 @@
<source>Custom content</source>
<target>Eigene Inhalte</target>
</trans-unit>
<trans-unit id="q1EATp1" resname="days">
<source>days</source>
<target>Tage</target>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -1685,6 +1685,10 @@
<source>Custom content</source>
<target>Custom content</target>
</trans-unit>
<trans-unit id="q1EATp1" resname="days">
<source>days</source>
<target>Days</target>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -150,6 +150,10 @@
<source>You can book a maximum of {{ value }} days at once.</source>
<target>Sie können maximal {{ value }} Tage auf einmal buchen.</target>
</trans-unit>
<trans-unit id="BztgSSP" resname="The period of absence must not extend beyond the turn of the year.">
<source>The period of absence must not extend beyond the turn of the year.</source>
<target>Der Zeitraum der Abwesenheit darf nicht über einen Jahreswechsel hinweg gehen.</target>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -150,6 +150,10 @@
<source>You can book a maximum of {{ value }} days at once.</source>
<target>You can book a maximum of {{ value }} days at once.</target>
</trans-unit>
<trans-unit id="BztgSSP" resname="The period of absence must not extend beyond the turn of the year.">
<source>The period of absence must not extend beyond the turn of the year.</source>
<target>The period of absence must not extend beyond the turn of the year.</target>
</trans-unit>
</body>
</file>
</xliff>