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:
@@ -66,7 +66,6 @@ final class StatusController extends BaseApiController
|
||||
{
|
||||
$plugins = [];
|
||||
foreach ($pluginManager->getPlugins() as $plugin) {
|
||||
$pluginManager->loadMetadata($plugin);
|
||||
$plugins[] = new Plugin($plugin);
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -28,7 +28,6 @@ final class PluginController extends AbstractController
|
||||
$installed = [];
|
||||
$plugins = $manager->getPlugins();
|
||||
foreach ($plugins as $plugin) {
|
||||
$manager->loadMetadata($plugin);
|
||||
$installed[] = $plugin->getId();
|
||||
}
|
||||
|
||||
|
||||
@@ -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)]);
|
||||
}
|
||||
|
||||
@@ -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)]);
|
||||
}
|
||||
|
||||
@@ -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)]);
|
||||
}
|
||||
|
||||
15
src/Doctrine/ModifiedAt.php
Normal file
15
src/Doctrine/ModifiedAt.php
Normal 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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
$entity->setModifiedAt($now);
|
||||
}
|
||||
|
||||
foreach ($uow->getScheduledEntityInsertions() as $entity) {
|
||||
if (!($entity instanceof Timesheet)) {
|
||||
continue;
|
||||
if ($entity instanceof ModifiedAt) {
|
||||
$entity->setModifiedAt($now);
|
||||
}
|
||||
$entity->setModifiedAt($now);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/**
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
|
||||
namespace App\Export;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
|
||||
|
||||
#[AutoconfigureTag]
|
||||
interface RendererInterface extends ExportRendererInterface
|
||||
{
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/**
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
|
||||
namespace App\Plugin;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
|
||||
|
||||
#[AutoconfigureTag]
|
||||
interface PluginInterface
|
||||
{
|
||||
public function getName(): string;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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']);
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user