Next major version 2 with PHP 8.1, Symfony 6, Tabler UI, 2FA ... (#2902)

This commit is contained in:
Kevin Papst
2022-12-31 21:19:55 +01:00
committed by GitHub
parent 95e06746bd
commit 90a0fd8a22
2164 changed files with 83700 additions and 86426 deletions

View File

@@ -1,45 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
/**
* @Annotation
* @Target({"PROPERTY", "METHOD", "ANNOTATION"})
*/
class AllowedHtmlTags extends Constraint
{
public const DISALLOWED_TAGS_FOUND = 'kimai-allowed-html-tags-00';
public $tags;
protected static $errorNames = [
self::DISALLOWED_TAGS_FOUND => 'The given value contains disallowed HTML tags.',
];
public $message = 'This string contains invalid HTML tags.';
/**
* {@inheritdoc}
*/
public function getDefaultOption()
{
return 'tags';
}
/**
* {@inheritdoc}
*/
public function getRequiredOptions()
{
return ['tags'];
}
}

View File

@@ -1,47 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Exception\UnexpectedValueException;
class AllowedHtmlTagsValidator extends ConstraintValidator
{
/**
* @param string|mixed $value
* @param Constraint $constraint
*/
public function validate($value, Constraint $constraint)
{
if (!($constraint instanceof AllowedHtmlTags)) {
throw new UnexpectedTypeException($constraint, __NAMESPACE__ . '\AllowedHtmlTags');
}
if (null === $value || '' === $value) {
return;
}
if (!is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) {
throw new UnexpectedValueException($value, 'string');
}
$value = (string) $value;
if (strip_tags($value, $constraint->tags) !== $value) {
$this->context->buildViolation('This string contains invalid HTML tags.')
->setTranslationDomain('validators')
->setParameter('{{ value }}', $this->formatValue($value))
->setCode(AllowedHtmlTags::DISALLOWED_TAGS_FOUND)
->addViolation();
}
}
}

View File

@@ -11,7 +11,7 @@ namespace App\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
class ColorChoices extends Constraint
final class ColorChoices extends Constraint
{
public const COLOR_CHOICES_ERROR = 'ui5hffg-dsfef3-1234-5678-2g8jkfr56d84';
public const COLOR_CHOICES_NAME_ERROR = 'ui5hffg-dsfef3-1234-5679-2g8jkfr56d84';
@@ -21,7 +21,7 @@ class ColorChoices extends Constraint
self::COLOR_CHOICES_NAME_ERROR => 'COLOR_CHOICES_NAME_ERROR',
];
public $message = 'The given value {{ value }} is not a valid hexadecimal color.';
public $invalidNameMessage = 'The given value {{ name }} is not a valid color name for {{ color }}. Allowed are {{ max }} alpha-numerical characters, including minus and space.';
public $maxLength = 20;
public string $message = 'The given value {{ value }} is not a valid hexadecimal color.';
public string $invalidNameMessage = 'The given value {{ name }} is not a valid color name for {{ color }}. Allowed are {{ max }} alpha-numerical characters, including minus and space.';
public int $maxLength = 20;
}

View File

@@ -13,24 +13,19 @@ use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
class ColorChoicesValidator extends ConstraintValidator
final class ColorChoicesValidator extends ConstraintValidator
{
/**
* {@inheritdoc}
*/
public function validate($value, Constraint $constraint)
public function validate(mixed $value, Constraint $constraint): void
{
if (!$constraint instanceof ColorChoices) {
throw new UnexpectedTypeException($constraint, ColorChoices::class);
}
$color = $value;
if ($color === null || (\is_string($color) && empty(trim($color)))) {
if (!\is_string($value) || trim($value) === '') {
return;
}
$colors = explode(',', $color);
$colors = explode(',', $value);
foreach ($colors as $color) {
$color = explode('|', $color);
@@ -44,7 +39,7 @@ class ColorChoicesValidator extends ConstraintValidator
$name = $code;
}
if (!\is_string($code) || 1 !== preg_match('/^#[0-9a-fA-F]{6}$/i', $code)) {
if (1 !== preg_match('/^#[0-9a-fA-F]{6}$/i', $code)) {
$this->context->buildViolation($constraint->message)
->setParameter('{{ value }}', $this->formatValue($code))
->setCode(ColorChoices::COLOR_CHOICES_ERROR)
@@ -60,7 +55,7 @@ class ColorChoicesValidator extends ConstraintValidator
$name = str_replace(['-', ' '], '', $name);
$length = mb_strlen($name);
if (!\is_string($name) || $length > $constraint->maxLength || !ctype_alnum($name)) {
if ($length > $constraint->maxLength || !preg_match('/^[a-zA-Z0-9]+$/', $name)) {
$this->context->buildViolation($constraint->invalidNameMessage)
->setParameter('{{ name }}', $this->formatValue($name))
->setParameter('{{ color }}', $this->formatValue($code))

View File

@@ -11,7 +11,7 @@ namespace App\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
class DateTimeFormat extends Constraint
final class DateTimeFormat extends Constraint
{
public const INVALID_FORMAT = 'kimai-datetime-00';
@@ -19,9 +19,10 @@ class DateTimeFormat extends Constraint
self::INVALID_FORMAT => 'The given value is not a valid datetime format.',
];
public $message = 'This datetime format is invalid.';
public ?string $separator = null;
public ?string $message = 'This datetime format is invalid.';
public function getTargets()
public function getTargets(): string|array
{
return self::PROPERTY_CONSTRAINT;
}

View File

@@ -13,22 +13,42 @@ use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
class DateTimeFormatValidator extends ConstraintValidator
final class DateTimeFormatValidator extends ConstraintValidator
{
/**
* @param string|mixed $value
* @param string|mixed|null $value
* @param Constraint $constraint
*/
public function validate($value, Constraint $constraint)
public function validate(mixed $value, Constraint $constraint): void
{
if (!($constraint instanceof DateTimeFormat)) {
throw new UnexpectedTypeException($constraint, __NAMESPACE__ . '\DateTimeFormat');
}
if ($value === null) {
if (!\is_string($value)) {
return;
}
if ($constraint->separator === null || $constraint->separator === '') {
if (str_contains($value, ',')) {
$this->context->buildViolation('The given value should not contain a comma.')
->setTranslationDomain('validators')
->setCode(DateTimeFormat::INVALID_FORMAT)
->addViolation();
}
$this->validateDateTime($value);
return;
}
foreach (explode($constraint->separator, $value) as $v) {
$this->validateDateTime($v);
}
}
private function validateDateTime(mixed $value): void
{
$valid = true;
if (!\is_string($value)) {

View File

@@ -11,11 +11,7 @@ namespace App\Validator\Constraints;
use Symfony\Component\Validator\Constraints\Regex;
/**
* @Annotation
* @Target({"PROPERTY", "METHOD", "ANNOTATION"})
*/
class Duration extends Regex
final class Duration extends Regex
{
public function __construct($options = null)
{
@@ -24,7 +20,7 @@ class Duration extends Regex
// negative times -? are allowed, because plugins could allow negative times
'-?[0-9]{1,}',
'-?[0-9]{1,}[,.]{1}[0-9]{1,}',
// ASP.NET style time spans - https://momentjs.com/docs/#/durations/
// ISO style time spans like 01:37
'-?[0-9]{1,}:[0-9]{1,}:[0-9]{1,}',
'-?[0-9]{1,}:[0-9]{1,}',
// https://en.wikipedia.org/wiki/ISO_8601#Time_intervals

View File

@@ -11,6 +11,6 @@ namespace App\Validator\Constraints;
use Symfony\Component\Validator\Constraints\RegexValidator;
class DurationValidator extends RegexValidator
final class DurationValidator extends RegexValidator
{
}

View File

@@ -11,11 +11,8 @@ namespace App\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
/**
* @Annotation
* @Target({"PROPERTY", "METHOD", "ANNOTATION"})
*/
class HexColor extends Constraint
#[\Attribute(\Attribute::TARGET_PROPERTY)]
final class HexColor extends Constraint
{
public const HEX_COLOR_ERROR = 'xd5hffg-dsfef3-426a-83d7-2g8jkfr56d84';
@@ -23,5 +20,5 @@ class HexColor extends Constraint
self::HEX_COLOR_ERROR => 'HEX_COLOR_ERROR',
];
public $message = 'The given value is not a valid hexadecimal color.';
public string $message = 'The given value is not a valid hexadecimal color.';
}

View File

@@ -13,12 +13,9 @@ use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
class HexColorValidator extends ConstraintValidator
final class HexColorValidator extends ConstraintValidator
{
/**
* {@inheritdoc}
*/
public function validate($value, Constraint $constraint)
public function validate(mixed $value, Constraint $constraint): void
{
if (!$constraint instanceof HexColor) {
throw new UnexpectedTypeException($constraint, __NAMESPACE__ . '\HexColor');

View File

@@ -9,14 +9,10 @@
namespace App\Validator\Constraints;
use Doctrine\Common\Annotations\Annotation\Target;
use Symfony\Component\Validator\Constraint;
/**
* @Annotation
* @Target({"CLASS", "PROPERTY", "METHOD", "ANNOTATION"})
*/
class Project extends Constraint
#[\Attribute(\Attribute::TARGET_CLASS)]
final class Project extends Constraint
{
public const END_BEFORE_BEGIN_ERROR = 'kimai-project-00';
@@ -24,9 +20,9 @@ class Project extends Constraint
self::END_BEFORE_BEGIN_ERROR => 'End date must not be earlier then start date.',
];
public $message = 'This project has invalid settings.';
public string $message = 'This project has invalid settings.';
public function getTargets()
public function getTargets(): string|array
{
return self::CLASS_CONSTRAINT;
}

View File

@@ -16,26 +16,20 @@ use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
class ProjectValidator extends ConstraintValidator
final class ProjectValidator extends ConstraintValidator
{
/**
* @var Constraint[]
*/
private $constraints;
/**
* @param Constraint[] $constraints
*/
public function __construct(iterable $constraints = [])
public function __construct(private iterable $constraints = [])
{
$this->constraints = $constraints;
}
/**
* @param Project|mixed $value
* @param Constraint $constraint
*/
public function validate($value, Constraint $constraint)
public function validate(mixed $value, Constraint $constraint): void
{
if (!($constraint instanceof ProjectConstraint)) {
throw new UnexpectedTypeException($constraint, ProjectConstraint::class);
@@ -47,18 +41,18 @@ class ProjectValidator extends ConstraintValidator
$this->validateProject($value, $this->context);
foreach ($this->constraints as $constraint) {
foreach ($this->constraints as $innerConstraint) {
$this->context
->getValidator()
->inContext($this->context)
->validate($value, $constraint, [Constraint::DEFAULT_GROUP]);
->validate($value, $innerConstraint, [Constraint::DEFAULT_GROUP]);
}
}
protected function validateProject(Project $project, ExecutionContextInterface $context)
protected function validateProject(Project $project, ExecutionContextInterface $context): void
{
if (null !== $project->getStart() && null !== $project->getEnd() && $project->getStart()->getTimestamp() > $project->getEnd()->getTimestamp()) {
$context->buildViolation('End date must not be earlier then start date.')
$context->buildViolation(ProjectConstraint::getErrorName(ProjectConstraint::END_BEFORE_BEGIN_ERROR))
->atPath('end')
->setTranslationDomain('validators')
->setCode(ProjectConstraint::END_BEFORE_BEGIN_ERROR)

View File

@@ -11,15 +11,11 @@ namespace App\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
/**
* @Annotation
* @Target({"CLASS"})
*/
class QuickEntryModel extends Constraint
final class QuickEntryModel extends Constraint
{
public const ACTIVITY_REQUIRED = 'quick-entry-model-01';
public const PROJECT_REQUIRED = 'quick-entry-model-02';
public $messageActivityRequired = 'An activity needs to be selected.';
public $messageProjectRequired = 'A project needs to be selected.';
public string $messageActivityRequired = 'An activity needs to be selected.';
public string $messageProjectRequired = 'A project needs to be selected.';
}

View File

@@ -15,12 +15,9 @@ use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
class QuickEntryModelValidator extends ConstraintValidator
final class QuickEntryModelValidator extends ConstraintValidator
{
/**
* {@inheritdoc}
*/
public function validate($value, Constraint $constraint)
public function validate(mixed $value, Constraint $constraint): void
{
if (!$constraint instanceof QuickEntryModelConstraint) {
throw new UnexpectedTypeException($constraint, QuickEntryModelConstraint::class);
@@ -30,7 +27,6 @@ class QuickEntryModelValidator extends ConstraintValidator
throw new UnexpectedTypeException($value, QuickEntryModel::class);
}
/** @var QuickEntryModel $model */
$model = $value;
if ($model->isPrototype()) {

View File

@@ -11,10 +11,6 @@ namespace App\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
/**
* @Annotation
* @Target({"CLASS"})
*/
class QuickEntryTimesheet extends Constraint
final class QuickEntryTimesheet extends Constraint
{
}

View File

@@ -15,25 +15,16 @@ use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
class QuickEntryTimesheetValidator extends ConstraintValidator
final class QuickEntryTimesheetValidator extends ConstraintValidator
{
/**
* @var Constraint[]
*/
private $constraints;
/**
* @param Constraint[] $constraints
*/
public function __construct(iterable $constraints)
public function __construct(private iterable $constraints)
{
$this->constraints = $constraints;
}
/**
* {@inheritdoc}
*/
public function validate($value, Constraint $constraint)
public function validate(mixed $value, Constraint $constraint): void
{
if (!$constraint instanceof QuickEntryTimesheetConstraint) {
throw new UnexpectedTypeException($constraint, QuickEntryTimesheetConstraint::class);
@@ -43,19 +34,18 @@ class QuickEntryTimesheetValidator extends ConstraintValidator
throw new UnexpectedTypeException($value, TimesheetEntity::class);
}
/** @var TimesheetEntity $timesheet */
$timesheet = $value;
if ($timesheet->getId() === null && $timesheet->getDuration(false) === null) {
return;
}
foreach ($this->constraints as $constraint) {
foreach ($this->constraints as $innerConstraint) {
$this->context
->getValidator()
->inContext($this->context)
->atPath('duration')
->validate($timesheet, $constraint, [Constraint::DEFAULT_GROUP]);
->validate($timesheet, $innerConstraint, [Constraint::DEFAULT_GROUP]);
}
}
}

View File

@@ -11,11 +11,8 @@ namespace App\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
/**
* @Annotation
* @Target({"PROPERTY", "METHOD", "ANNOTATION"})
*/
class Role extends Constraint
#[\Attribute(\Attribute::TARGET_PROPERTY)]
final class Role extends Constraint
{
public const ROLE_ERROR = 'xd5hffg-dsfef3-426a-83d7-1f2d33hs5d84';
@@ -23,5 +20,5 @@ class Role extends Constraint
self::ROLE_ERROR => 'ROLE_ERROR',
];
public $message = 'This value is not a valid role.';
public string $message = 'This value is not a valid role.';
}

View File

@@ -0,0 +1,24 @@
<?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\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
#[\Attribute(\Attribute::TARGET_PROPERTY)]
final class RoleName extends Constraint
{
public const ROLE_NAME_ERROR = 'xd5hffg-dsfef3-426a-83d7-1f2d33hs5d85';
protected static $errorNames = [
self::ROLE_NAME_ERROR => 'ROLE_NAME_ERROR',
];
public string $message = 'This value is not a valid role name.';
}

View File

@@ -0,0 +1,39 @@
<?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\Validator\Constraints;
use App\Security\RoleService;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
final class RoleNameValidator extends ConstraintValidator
{
public function __construct(private RoleService $service)
{
}
public function validate(mixed $value, Constraint $constraint): void
{
if (!$constraint instanceof RoleName) {
throw new UnexpectedTypeException($constraint, RoleName::class);
}
// user entity uses uppercase for the roles
$roles = $this->service->getAvailableNames();
if (!\is_string($value) || \in_array($value, $roles, true) || preg_match('/^[A-Z_]{5,}$/', $value) !== 1 || str_contains($value, '__') || str_starts_with($value, '_') || str_ends_with($value, '_')) {
$this->context->buildViolation($constraint->message)
->setParameter('{{ value }}', $this->formatValue($value))
->setCode(RoleName::ROLE_NAME_ERROR)
->addViolation();
}
}
}

View File

@@ -14,22 +14,13 @@ use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
class RoleValidator extends ConstraintValidator
final class RoleValidator extends ConstraintValidator
{
/**
* @var RoleService
*/
private $service;
public function __construct(RoleService $service)
public function __construct(private RoleService $service)
{
$this->service = $service;
}
/**
* {@inheritdoc}
*/
public function validate($value, Constraint $constraint)
public function validate(mixed $value, Constraint $constraint): void
{
if (!$constraint instanceof Role) {
throw new UnexpectedTypeException($constraint, __NAMESPACE__ . '\Role');
@@ -41,11 +32,11 @@ class RoleValidator extends ConstraintValidator
$roles = [$roles];
}
// the fos user entity uppercases the roles by default
$allowedRoles = array_map('strtoupper', $this->service->getAvailableNames());
// user entity uses uppercase for the roles
$allowedRoles = $this->service->getAvailableNames();
foreach ($roles as $role) {
if (!\is_string($role) || !\in_array($role, $allowedRoles)) {
if (!\is_string($role) || !\in_array($role, $allowedRoles, true)) {
$this->context->buildViolation($constraint->message)
->setParameter('{{ value }}', $this->formatValue($role))
->setCode(Role::ROLE_ERROR)

View File

@@ -9,14 +9,10 @@
namespace App\Validator\Constraints;
use Doctrine\Common\Annotations\Annotation\Target;
use Symfony\Component\Validator\Constraint;
/**
* @Annotation
* @Target({"CLASS", "PROPERTY", "METHOD", "ANNOTATION"})
*/
class Team extends Constraint
#[\Attribute(\Attribute::TARGET_CLASS)]
final class Team extends Constraint
{
public const MISSING_TEAMLEAD = 'kimai-team-001';
@@ -24,9 +20,9 @@ class Team extends Constraint
self::MISSING_TEAMLEAD => 'At least one team leader must be assigned to the team.',
];
public $message = 'The team has invalid settings.';
public string $message = 'The team has invalid settings.';
public function getTargets()
public function getTargets(): string|array
{
return self::CLASS_CONSTRAINT;
}

View File

@@ -14,13 +14,13 @@ use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
class TeamValidator extends ConstraintValidator
final class TeamValidator extends ConstraintValidator
{
/**
* @param TeamEntity $value
* @param Constraint $constraint
*/
public function validate($value, Constraint $constraint)
public function validate(mixed $value, Constraint $constraint): void
{
if (!($constraint instanceof Team)) {
throw new UnexpectedTypeException($constraint, Team::class);
@@ -32,7 +32,7 @@ class TeamValidator extends ConstraintValidator
if (!$value->hasTeamleads()) {
$this->context->buildViolation(Team::getErrorName(Team::MISSING_TEAMLEAD))
->atPath('teamleads')
->atPath('members')
->setTranslationDomain('validators')
->setCode(Team::MISSING_TEAMLEAD)
->addViolation();

View File

@@ -11,7 +11,7 @@ namespace App\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
class TimeFormat extends Constraint
final class TimeFormat extends Constraint
{
public const INVALID_FORMAT = 'kimai-time-00';
@@ -19,9 +19,9 @@ class TimeFormat extends Constraint
self::INVALID_FORMAT => 'The given value is not a valid time.',
];
public $message = 'This time format is invalid.';
public string $message = 'This time format is invalid.';
public function getTargets()
public function getTargets(): string|array
{
return self::PROPERTY_CONSTRAINT;
}

View File

@@ -14,13 +14,13 @@ use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Exception\UnexpectedValueException;
class TimeFormatValidator extends ConstraintValidator
final class TimeFormatValidator extends ConstraintValidator
{
/**
* @param string|mixed $value
* @param Constraint $constraint
*/
public function validate($value, Constraint $constraint)
public function validate(mixed $value, Constraint $constraint): void
{
if (!($constraint instanceof TimeFormat)) {
throw new UnexpectedTypeException($constraint, __NAMESPACE__ . '\TimeFormat');
@@ -30,7 +30,7 @@ class TimeFormatValidator extends ConstraintValidator
return;
}
if (!is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) {
if (!\is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) {
throw new UnexpectedValueException($value, 'string');
}

View File

@@ -9,52 +9,14 @@
namespace App\Validator\Constraints;
use Doctrine\Common\Annotations\Annotation\Target;
use Symfony\Component\Validator\Constraint;
/**
* @Annotation
* @Target({"CLASS", "PROPERTY", "METHOD", "ANNOTATION"})
*/
class Timesheet extends Constraint
#[\Attribute(\Attribute::TARGET_CLASS)]
final class Timesheet extends Constraint
{
/** @deprecated since 1.15.3 - use TimesheetBasic::MISSING_BEGIN_ERROR instead */
public const MISSING_BEGIN_ERROR = TimesheetBasic::MISSING_BEGIN_ERROR;
/** @deprecated since 1.15.3 - use TimesheetBasic::END_BEFORE_BEGIN_ERROR instead */
public const END_BEFORE_BEGIN_ERROR = TimesheetBasic::END_BEFORE_BEGIN_ERROR;
/** @deprecated since 1.15.3 - use TimesheetBasic::MISSING_ACTIVITY_ERROR instead */
public const MISSING_ACTIVITY_ERROR = TimesheetBasic::MISSING_ACTIVITY_ERROR;
/** @deprecated since 1.15.3 - use TimesheetBasic::MISSING_PROJECT_ERROR instead */
public const MISSING_PROJECT_ERROR = TimesheetBasic::MISSING_PROJECT_ERROR;
/** @deprecated since 1.15.3 - use TimesheetBasic::ACTIVITY_PROJECT_MISMATCH_ERROR instead */
public const ACTIVITY_PROJECT_MISMATCH_ERROR = TimesheetBasic::ACTIVITY_PROJECT_MISMATCH_ERROR;
/** @deprecated since 1.15.3 - use TimesheetBasic::DISABLED_ACTIVITY_ERROR instead */
public const DISABLED_ACTIVITY_ERROR = TimesheetBasic::DISABLED_ACTIVITY_ERROR;
/** @deprecated since 1.15.3 - use TimesheetBasic::DISABLED_PROJECT_ERROR instead */
public const DISABLED_PROJECT_ERROR = TimesheetBasic::DISABLED_PROJECT_ERROR;
/** @deprecated since 1.15.3 - use TimesheetBasic::DISABLED_CUSTOMER_ERROR instead */
public const DISABLED_CUSTOMER_ERROR = TimesheetBasic::DISABLED_CUSTOMER_ERROR;
/** @deprecated since 1.15.3 - use TimesheetBasic::PROJECT_NOT_STARTED instead */
public const PROJECT_NOT_STARTED = TimesheetBasic::PROJECT_NOT_STARTED;
/** @deprecated since 1.15.3 - use TimesheetBasic::PROJECT_ALREADY_ENDED instead */
public const PROJECT_ALREADY_ENDED = TimesheetBasic::PROJECT_ALREADY_ENDED;
public string $message = 'This timesheet has invalid settings.';
protected static $errorNames = [
self::MISSING_BEGIN_ERROR => 'You must submit a begin date.',
self::END_BEFORE_BEGIN_ERROR => 'End date must not be earlier then start date.',
self::MISSING_ACTIVITY_ERROR => 'An activity needs to be selected.',
self::MISSING_PROJECT_ERROR => 'A project needs to be selected.',
self::ACTIVITY_PROJECT_MISMATCH_ERROR => 'Project mismatch, project specific activity and timesheet project are different.',
self::DISABLED_ACTIVITY_ERROR => 'Cannot start a disabled activity.',
self::DISABLED_PROJECT_ERROR => 'Cannot start a disabled project.',
self::DISABLED_CUSTOMER_ERROR => 'Cannot start a disabled customer.',
self::PROJECT_NOT_STARTED => 'The project has not started at that time.',
self::PROJECT_ALREADY_ENDED => 'The project is finished at that time.',
];
public $message = 'This timesheet has invalid settings.';
public function getTargets()
public function getTargets(): string|array
{
return self::CLASS_CONSTRAINT;
}

View File

@@ -9,13 +9,7 @@
namespace App\Validator\Constraints;
use Doctrine\Common\Annotations\Annotation\Target;
/**
* @Annotation
* @Target({"CLASS", "PROPERTY", "METHOD", "ANNOTATION"})
*/
class TimesheetBasic extends TimesheetConstraint
final class TimesheetBasic extends TimesheetConstraint
{
public const MISSING_BEGIN_ERROR = 'kimai-timesheet-81';
public const END_BEFORE_BEGIN_ERROR = 'kimai-timesheet-82';
@@ -43,9 +37,9 @@ class TimesheetBasic extends TimesheetConstraint
self::PROJECT_DISALLOWS_GLOBAL_ACTIVITY => 'Global activities are forbidden for the selected project.',
];
public $message = 'This timesheet has invalid settings.';
public string $message = 'This timesheet has invalid settings.';
public function getTargets()
public function getTargets(): string|array
{
return self::CLASS_CONSTRAINT;
}

View File

@@ -9,6 +9,7 @@
namespace App\Validator\Constraints;
use App\Configuration\SystemConfiguration;
use App\Entity\Timesheet as TimesheetEntity;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
@@ -17,36 +18,36 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
final class TimesheetBasicValidator extends ConstraintValidator
{
/**
* @param TimesheetEntity $timesheet
* @param Constraint $constraint
*/
public function validate($timesheet, Constraint $constraint)
public function __construct(private SystemConfiguration $systemConfiguration)
{
}
public function validate(mixed $value, Constraint $constraint): void
{
if (!($constraint instanceof TimesheetBasic)) {
throw new UnexpectedTypeException($constraint, TimesheetBasic::class);
}
if (!\is_object($timesheet) || !($timesheet instanceof TimesheetEntity)) {
throw new UnexpectedTypeException($timesheet, TimesheetEntity::class);
if (!\is_object($value) || !($value instanceof TimesheetEntity)) {
throw new UnexpectedTypeException($value, TimesheetEntity::class);
}
$this->validateBeginAndEnd($timesheet, $this->context);
$this->validateActivityAndProject($timesheet, $this->context);
$this->validateBeginAndEnd($value, $this->context);
$this->validateActivityAndProject($value, $this->context);
}
/**
* @param TimesheetEntity $timesheet
* @param ExecutionContextInterface $context
*/
protected function validateBeginAndEnd(TimesheetEntity $timesheet, ExecutionContextInterface $context)
protected function validateBeginAndEnd(TimesheetEntity $timesheet, ExecutionContextInterface $context): void
{
$begin = $timesheet->getBegin();
$end = $timesheet->getEnd();
if (null === $begin) {
$context->buildViolation('You must submit a begin date.')
->atPath('begin')
$context->buildViolation(TimesheetBasic::getErrorName(TimesheetBasic::MISSING_BEGIN_ERROR))
->atPath('begin_date')
->setTranslationDomain('validators')
->setCode(TimesheetBasic::MISSING_BEGIN_ERROR)
->addViolation();
@@ -55,8 +56,8 @@ final class TimesheetBasicValidator extends ConstraintValidator
}
if (null !== $end && $begin > $end) {
$context->buildViolation('End date must not be earlier then start date.')
->atPath('end')
$context->buildViolation(TimesheetBasic::getErrorName(TimesheetBasic::END_BEFORE_BEGIN_ERROR))
->atPath('end_date')
->setTranslationDomain('validators')
->setCode(TimesheetBasic::END_BEFORE_BEGIN_ERROR)
->addViolation();
@@ -67,10 +68,12 @@ final class TimesheetBasicValidator extends ConstraintValidator
* @param TimesheetEntity $timesheet
* @param ExecutionContextInterface $context
*/
protected function validateActivityAndProject(TimesheetEntity $timesheet, ExecutionContextInterface $context)
protected function validateActivityAndProject(TimesheetEntity $timesheet, ExecutionContextInterface $context): void
{
if (null === ($activity = $timesheet->getActivity())) {
$context->buildViolation('An activity needs to be selected.')
$activity = $timesheet->getActivity();
if ($this->systemConfiguration->isTimesheetRequiresActivity() && null === $activity) {
$context->buildViolation(TimesheetBasic::getErrorName(TimesheetBasic::MISSING_ACTIVITY_ERROR))
->atPath('activity')
->setTranslationDomain('validators')
->setCode(TimesheetBasic::MISSING_ACTIVITY_ERROR)
@@ -78,19 +81,21 @@ final class TimesheetBasicValidator extends ConstraintValidator
}
if (null === ($project = $timesheet->getProject())) {
$context->buildViolation('A project needs to be selected.')
$context->buildViolation(TimesheetBasic::getErrorName(TimesheetBasic::MISSING_PROJECT_ERROR))
->atPath('project')
->setTranslationDomain('validators')
->setCode(TimesheetBasic::MISSING_PROJECT_ERROR)
->addViolation();
}
if (null === $activity || null === $project) {
$hasActivity = null !== $activity;
if (null === $project) {
return;
}
if (null !== $activity->getProject() && $activity->getProject() !== $project) {
$context->buildViolation('Project mismatch, project specific activity and timesheet project are different.')
if ($hasActivity && null !== $activity->getProject() && $activity->getProject() !== $project) {
$context->buildViolation(TimesheetBasic::getErrorName(TimesheetBasic::ACTIVITY_PROJECT_MISMATCH_ERROR))
->atPath('project')
->setTranslationDomain('validators')
->setCode(TimesheetBasic::ACTIVITY_PROJECT_MISMATCH_ERROR)
@@ -100,8 +105,8 @@ final class TimesheetBasicValidator extends ConstraintValidator
$timesheetEnd = $timesheet->getEnd();
$newOrStarted = null === $timesheetEnd || $timesheet->getId() === null;
if ($newOrStarted && !$activity->isVisible()) {
$context->buildViolation('Cannot start a disabled activity.')
if ($newOrStarted && $hasActivity && !$activity->isVisible()) {
$context->buildViolation(TimesheetBasic::getErrorName(TimesheetBasic::DISABLED_ACTIVITY_ERROR))
->atPath('activity')
->setTranslationDomain('validators')
->setCode(TimesheetBasic::DISABLED_ACTIVITY_ERROR)
@@ -109,7 +114,7 @@ final class TimesheetBasicValidator extends ConstraintValidator
}
if ($newOrStarted && !$project->isVisible()) {
$context->buildViolation('Cannot start a disabled project.')
$context->buildViolation(TimesheetBasic::getErrorName(TimesheetBasic::DISABLED_PROJECT_ERROR))
->atPath('project')
->setTranslationDomain('validators')
->setCode(TimesheetBasic::DISABLED_PROJECT_ERROR)
@@ -117,21 +122,24 @@ final class TimesheetBasicValidator extends ConstraintValidator
}
if ($newOrStarted && !$project->getCustomer()->isVisible()) {
$context->buildViolation('Cannot start a disabled customer.')
$context->buildViolation(TimesheetBasic::getErrorName(TimesheetBasic::DISABLED_CUSTOMER_ERROR))
->atPath('customer')
->setTranslationDomain('validators')
->setCode(TimesheetBasic::DISABLED_CUSTOMER_ERROR)
->addViolation();
}
if (!$project->isGlobalActivities() && $activity->isGlobal()) {
$context->buildViolation('Global activities are forbidden for the selected project.')
if ($hasActivity && !$project->isGlobalActivities() && $activity->isGlobal()) {
$context->buildViolation(TimesheetBasic::getErrorName(TimesheetBasic::PROJECT_DISALLOWS_GLOBAL_ACTIVITY))
->atPath('activity')
->setTranslationDomain('validators')
->setCode(TimesheetBasic::PROJECT_DISALLOWS_GLOBAL_ACTIVITY)
->addViolation();
}
$pathStart = 'begin_date';
$pathEnd = 'end_date';
$projectBegin = $project->getStart();
$projectEnd = $project->getEnd();
@@ -139,21 +147,18 @@ final class TimesheetBasicValidator extends ConstraintValidator
return;
}
$pathStart = 'begin';
$pathEnd = 'end';
$timesheetStart = $timesheet->getBegin();
$timesheetEnd = $timesheet->getEnd();
if (null !== $timesheetStart) {
if (null !== $projectBegin && $timesheetStart->getTimestamp() < $projectBegin->getTimestamp()) {
$context->buildViolation('The project has not started at that time.')
$context->buildViolation(TimesheetBasic::getErrorName(TimesheetBasic::PROJECT_NOT_STARTED))
->atPath($pathStart)
->setTranslationDomain('validators')
->setCode(TimesheetBasic::PROJECT_NOT_STARTED)
->addViolation();
} elseif (null !== $projectEnd && $timesheetStart->getTimestamp() > $projectEnd->getTimestamp()) {
$context->buildViolation('The project is finished at that time.')
$context->buildViolation(TimesheetBasic::getErrorName(TimesheetBasic::PROJECT_ALREADY_ENDED))
->atPath($pathStart)
->setTranslationDomain('validators')
->setCode(TimesheetBasic::PROJECT_ALREADY_ENDED)
@@ -163,13 +168,13 @@ final class TimesheetBasicValidator extends ConstraintValidator
if (null !== $timesheetEnd) {
if (null !== $projectEnd && $timesheetEnd->getTimestamp() > $projectEnd->getTimestamp()) {
$context->buildViolation('The project is finished at that time.')
$context->buildViolation(TimesheetBasic::getErrorName(TimesheetBasic::PROJECT_ALREADY_ENDED))
->atPath($pathEnd)
->setTranslationDomain('validators')
->setCode(TimesheetBasic::PROJECT_ALREADY_ENDED)
->addViolation();
} elseif (null !== $projectBegin && $timesheetEnd->getTimestamp() < $projectBegin->getTimestamp()) {
$context->buildViolation('The project has not started at that time.')
$context->buildViolation(TimesheetBasic::getErrorName(TimesheetBasic::PROJECT_NOT_STARTED))
->atPath($pathEnd)
->setTranslationDomain('validators')
->setCode(TimesheetBasic::PROJECT_NOT_STARTED)

View File

@@ -12,7 +12,7 @@ namespace App\Validator\Constraints;
final class TimesheetBudgetUsed extends TimesheetConstraint
{
// same messages, so we can re-use the validation translation!
public $messageRate = 'The budget is completely used.';
public $messageTime = 'The budget is completely used.';
public $messagePermission = 'Sorry, the budget is used up.';
public string $messageRate = 'The budget is completely used.';
public string $messageTime = 'The budget is completely used.';
public string $messagePermission = 'Sorry, the budget is used up.';
}

View File

@@ -10,6 +10,7 @@
namespace App\Validator\Constraints;
use App\Activity\ActivityStatisticService;
use App\Configuration\LocaleService;
use App\Configuration\SystemConfiguration;
use App\Customer\CustomerStatisticService;
use App\Entity\Timesheet;
@@ -18,7 +19,7 @@ use App\Project\ProjectStatisticService;
use App\Repository\TimesheetRepository;
use App\Timesheet\RateServiceInterface;
use App\Utils\Duration;
use App\Utils\LocaleHelper;
use App\Utils\LocaleFormatter;
use DateTime;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Validator\Constraint;
@@ -27,30 +28,23 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
final class TimesheetBudgetUsedValidator extends ConstraintValidator
{
private $customerStatisticService;
private $projectStatisticService;
private $activityStatisticService;
private $timesheetRepository;
private $rateService;
private $configuration;
private $security;
public function __construct(SystemConfiguration $configuration, CustomerStatisticService $customerStatisticService, ProjectStatisticService $projectStatisticService, ActivityStatisticService $activityStatisticService, TimesheetRepository $timesheetRepository, RateServiceInterface $rateService, AuthorizationCheckerInterface $security)
{
$this->configuration = $configuration;
$this->customerStatisticService = $customerStatisticService;
$this->projectStatisticService = $projectStatisticService;
$this->activityStatisticService = $activityStatisticService;
$this->timesheetRepository = $timesheetRepository;
$this->rateService = $rateService;
$this->security = $security;
public function __construct(
private SystemConfiguration $configuration,
private CustomerStatisticService $customerStatisticService,
private ProjectStatisticService $projectStatisticService,
private ActivityStatisticService $activityStatisticService,
private TimesheetRepository $timesheetRepository,
private RateServiceInterface $rateService,
private AuthorizationCheckerInterface $security,
private LocaleService $localeService
) {
}
/**
* @param Timesheet $timesheet
* @param Constraint $constraint
*/
public function validate($timesheet, Constraint $constraint)
public function validate(mixed $timesheet, Constraint $constraint): void
{
if (!($constraint instanceof TimesheetBudgetUsed)) {
throw new UnexpectedTypeException($constraint, TimesheetBudgetUsed::class);
@@ -193,10 +187,10 @@ final class TimesheetBudgetUsedValidator extends ConstraintValidator
return false;
}
private function addBudgetViolation(TimesheetBudgetUsed $constraint, Timesheet $timesheet, string $field, float $budget, float $rate)
private function addBudgetViolation(TimesheetBudgetUsed $constraint, Timesheet $timesheet, string $field, float $budget, float $rate): void
{
// using the locale of the assigned user is not the best solution, but allows to be independent of the request stack
$helper = new LocaleHelper($timesheet->getUser()->getLanguage());
$helper = new LocaleFormatter($this->localeService, $timesheet->getUser()->getLanguage());
$currency = $timesheet->getProject()->getCustomer()->getCurrency();
$free = $budget - $rate;
@@ -219,7 +213,7 @@ final class TimesheetBudgetUsedValidator extends ConstraintValidator
;
}
private function addTimeBudgetViolation(TimesheetBudgetUsed $constraint, string $field, int $budget, int $duration)
private function addTimeBudgetViolation(TimesheetBudgetUsed $constraint, string $field, int $budget, int $duration): void
{
$durationFormat = new Duration();

View File

@@ -17,13 +17,14 @@ final class TimesheetExported extends TimesheetConstraint
self::TIMESHEET_EXPORTED => 'This timesheet is already exported.',
];
public $message = 'This timesheet is already exported.';
public string $message = 'This timesheet is already exported.';
/**
* @var \DateTime|string|null
*/
public $now;
public null|\DateTime|string $now;
public function getTargets()
public function getTargets(): string|array
{
return self::CLASS_CONSTRAINT;
}

View File

@@ -17,18 +17,15 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
final class TimesheetExportedValidator extends ConstraintValidator
{
private $security;
public function __construct(Security $security)
public function __construct(private Security $security)
{
$this->security = $security;
}
/**
* @param TimesheetEntity $timesheet
* @param Constraint $constraint
*/
public function validate($timesheet, Constraint $constraint)
public function validate(mixed $timesheet, Constraint $constraint): void
{
if (!($constraint instanceof TimesheetExported)) {
throw new UnexpectedTypeException($constraint, TimesheetExported::class);
@@ -47,7 +44,7 @@ final class TimesheetExportedValidator extends ConstraintValidator
}
// this was "edit_exported_timesheet" before, but that was wrong, because the first time this
// can trigger is the moment when the "export" flag ist set from the "edit form".
// can trigger is right when the "export" flag ist set from the "edit form".
// most teamleads should not have "edit_exported_timesheet" but only "edit_export_other_timesheet"
if (null !== $this->security->getUser() && $this->security->isGranted('edit_export', $timesheet)) {

View File

@@ -17,9 +17,9 @@ final class TimesheetFutureTimes extends TimesheetConstraint
self::BEGIN_IN_FUTURE_ERROR => 'The begin date cannot be in the future.',
];
public $message = 'The begin date cannot be in the future.';
public string $message = 'The begin date cannot be in the future.';
public function getTargets()
public function getTargets(): string|array
{
return self::CLASS_CONSTRAINT;
}

View File

@@ -17,21 +17,15 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
final class TimesheetFutureTimesValidator extends ConstraintValidator
{
/**
* @var SystemConfiguration
*/
private $configuration;
public function __construct(SystemConfiguration $configuration)
public function __construct(private SystemConfiguration $configuration)
{
$this->configuration = $configuration;
}
/**
* @param TimesheetEntity $timesheet
* @param Constraint $constraint
*/
public function validate($timesheet, Constraint $constraint)
public function validate(mixed $timesheet, Constraint $constraint): void
{
if (!($constraint instanceof TimesheetFutureTimes)) {
throw new UnexpectedTypeException($constraint, TimesheetFutureTimes::class);
@@ -51,7 +45,7 @@ final class TimesheetFutureTimesValidator extends ConstraintValidator
$allowedDiff = ($this->configuration->getTimesheetDefaultRoundingBegin() * 60) + 60;
if (($now->getTimestamp() + $allowedDiff) < $timesheet->getBegin()->getTimestamp()) {
$this->context->buildViolation('The begin date cannot be in the future.')
->atPath('begin')
->atPath('begin_date')
->setTranslationDomain('validators')
->setCode(TimesheetFutureTimes::BEGIN_IN_FUTURE_ERROR)
->addViolation();

View File

@@ -17,13 +17,13 @@ final class TimesheetLockdown extends TimesheetConstraint
self::PERIOD_LOCKED => 'This period is locked, please choose a later date.',
];
public $message = 'This period is locked, please choose a later date.';
public string $message = 'This period is locked, please choose a later date.';
/**
* @var \DateTime|string|null
*/
public $now;
public \DateTime|string|null $now;
public function getTargets()
public function getTargets(): string|array
{
return self::CLASS_CONSTRAINT;
}

View File

@@ -18,20 +18,15 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
final class TimesheetLockdownValidator extends ConstraintValidator
{
private $lockdownService;
private $security;
public function __construct(Security $security, LockdownService $lockdownService)
public function __construct(private Security $security, private LockdownService $lockdownService)
{
$this->security = $security;
$this->lockdownService = $lockdownService;
}
/**
* @param TimesheetEntity $timesheet
* @param Constraint $constraint
*/
public function validate($timesheet, Constraint $constraint)
public function validate(mixed $timesheet, Constraint $constraint): void
{
if (!($constraint instanceof TimesheetLockdown)) {
throw new UnexpectedTypeException($constraint, TimesheetLockdown::class);
@@ -78,7 +73,7 @@ final class TimesheetLockdownValidator extends ConstraintValidator
// raise a violation for all entries before the start of lockdown period
$this->context->buildViolation('This period is locked, please choose a later date.')
->atPath('begin')
->atPath('begin_date')
->setTranslationDomain('validators')
->setCode(TimesheetLockdown::PERIOD_LOCKED)
->addViolation();

View File

@@ -19,10 +19,10 @@ final class TimesheetLongRunning extends TimesheetConstraint
self::MAXIMUM => 'MAXIMUM',
];
public $message = 'Maximum duration of {{ value }} hours exceeded.';
public $maximumMessage = 'Maximum duration exceeded.';
public string $message = 'Maximum duration of {{ value }} hours exceeded.';
public string $maximumMessage = 'Maximum duration exceeded.';
public function getTargets()
public function getTargets(): string|array
{
return self::CLASS_CONSTRAINT;
}

View File

@@ -17,18 +17,15 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
final class TimesheetLongRunningValidator extends ConstraintValidator
{
private $systemConfiguration;
public function __construct(SystemConfiguration $systemConfiguration)
public function __construct(private SystemConfiguration $systemConfiguration)
{
$this->systemConfiguration = $systemConfiguration;
}
/**
* @param TimesheetEntity $timesheet
* @param Constraint $constraint
*/
public function validate($timesheet, Constraint $constraint)
public function validate(mixed $timesheet, Constraint $constraint): void
{
if (!($constraint instanceof TimesheetLongRunning)) {
throw new UnexpectedTypeException($constraint, TimesheetLongRunning::class);
@@ -64,14 +61,14 @@ final class TimesheetLongRunningValidator extends ConstraintValidator
// float on purpose, because one second more than the configured minutes is already too long
$minutes = $duration / 60;
if ($minutes < $maxMinutes) {
// allow maximum of the exact configured minutes
if ($minutes <= $maxMinutes) {
return;
}
$format = new \App\Utils\Duration();
$hours = $format->format($maxMinutes * 60);
// raise a violation for all entries before the start of lockdown period
$this->context->buildViolation($constraint->message)
->setParameter('{{ value }}', $hours)
->setTranslationDomain('validators')

View File

@@ -9,14 +9,10 @@
namespace App\Validator\Constraints;
use Doctrine\Common\Annotations\Annotation\Target;
use Symfony\Component\Validator\Constraint;
/**
* @Annotation
* @Target({"CLASS", "PROPERTY", "METHOD", "ANNOTATION"})
*/
class TimesheetMultiUpdate extends Constraint
#[\Attribute(\Attribute::TARGET_CLASS)]
final class TimesheetMultiUpdate extends Constraint
{
public const MISSING_ACTIVITY_ERROR = 'ts-multi-update-84';
public const MISSING_PROJECT_ERROR = 'ts-multi-update-85';
@@ -36,9 +32,9 @@ class TimesheetMultiUpdate extends Constraint
self::HOURLY_RATE_FIXED_RATE => 'Cannot set hourly rate and fixed rate at the same time.',
];
public $message = 'This form has invalid settings.';
public string $message = 'This form has invalid settings.';
public function getTargets()
public function getTargets(): string|array
{
return self::CLASS_CONSTRAINT;
}

View File

@@ -22,7 +22,7 @@ final class TimesheetMultiUpdateValidator extends ConstraintValidator
* @param TimesheetMultiUpdateDTO|mixed $value
* @param Constraint $constraint
*/
public function validate($value, Constraint $constraint)
public function validate(mixed $value, Constraint $constraint): void
{
if (!($constraint instanceof TimesheetMultiUpdateConstraint)) {
throw new UnexpectedTypeException($constraint, __NAMESPACE__ . '\TimesheetMultiUpdate');
@@ -53,7 +53,7 @@ final class TimesheetMultiUpdateValidator extends ConstraintValidator
* @param TimesheetMultiUpdateDTO $dto
* @param ExecutionContextInterface $context
*/
protected function validateActivityAndProject(TimesheetMultiUpdateDTO $dto, ExecutionContextInterface $context)
protected function validateActivityAndProject(TimesheetMultiUpdateDTO $dto, ExecutionContextInterface $context): void
{
$activity = $dto->getActivity();
$project = $dto->getProject();

View File

@@ -9,14 +9,10 @@
namespace App\Validator\Constraints;
use Doctrine\Common\Annotations\Annotation\Target;
use Symfony\Component\Validator\Constraint;
/**
* @Annotation
* @Target({"CLASS", "PROPERTY", "METHOD", "ANNOTATION"})
*/
class TimesheetMultiUser extends Constraint
#[\Attribute(\Attribute::TARGET_CLASS)]
final class TimesheetMultiUser extends Constraint
{
public const MISSING_USER_OR_TEAM = 'ts-multi-user-01';
@@ -24,9 +20,9 @@ class TimesheetMultiUser extends Constraint
self::MISSING_USER_OR_TEAM => 'You must select at least one user or team.',
];
public $message = 'This form has invalid settings.';
public string $message = 'This form has invalid settings.';
public function getTargets()
public function getTargets(): string|array
{
return self::CLASS_CONSTRAINT;
}

View File

@@ -20,7 +20,7 @@ final class TimesheetMultiUserValidator extends ConstraintValidator
* @param Timesheet|mixed $value
* @param Constraint $constraint
*/
public function validate($value, Constraint $constraint)
public function validate(mixed $value, Constraint $constraint): void
{
if (!($constraint instanceof TimesheetMultiUser)) {
throw new UnexpectedTypeException($constraint, TimesheetMultiUser::class);

View File

@@ -17,9 +17,9 @@ final class TimesheetOverlapping extends TimesheetConstraint
self::RECORD_OVERLAPPING => 'You already have an entry for this time.',
];
public $message = 'You already have an entry for this time.';
public string $message = 'You already have an entry for this time.';
public function getTargets()
public function getTargets(): string|array
{
return self::CLASS_CONSTRAINT;
}

View File

@@ -18,26 +18,15 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
final class TimesheetOverlappingValidator extends ConstraintValidator
{
/**
* @var SystemConfiguration
*/
private $configuration;
/**
* @var TimesheetRepository
*/
private $repository;
public function __construct(SystemConfiguration $configuration, TimesheetRepository $repository)
public function __construct(private SystemConfiguration $configuration, private TimesheetRepository $repository)
{
$this->configuration = $configuration;
$this->repository = $repository;
}
/**
* @param TimesheetEntity $timesheet
* @param Constraint $constraint
*/
public function validate($timesheet, Constraint $constraint)
public function validate(mixed $timesheet, Constraint $constraint): void
{
if (!($constraint instanceof TimesheetOverlapping)) {
throw new UnexpectedTypeException($constraint, TimesheetOverlapping::class);
@@ -64,7 +53,7 @@ final class TimesheetOverlappingValidator extends ConstraintValidator
}
$this->context->buildViolation('You already have an entry for this time.')
->atPath('begin')
->atPath('begin_date')
->setTranslationDomain('validators')
->setCode(TimesheetOverlapping::RECORD_OVERLAPPING)
->addViolation();

View File

@@ -17,9 +17,9 @@ final class TimesheetRestart extends TimesheetConstraint
self::START_DISALLOWED => 'You are not allowed to start this timesheet record.',
];
public $message = 'You are not allowed to start this timesheet record.';
public string $message = 'You are not allowed to start this timesheet record.';
public function getTargets()
public function getTargets(): string|array
{
return self::CLASS_CONSTRAINT;
}

View File

@@ -18,20 +18,15 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
final class TimesheetRestartValidator extends ConstraintValidator
{
private $trackingModeService;
private $security;
public function __construct(Security $security, TrackingModeService $service)
public function __construct(private Security $security, private TrackingModeService $trackingModeService)
{
$this->security = $security;
$this->trackingModeService = $service;
}
/**
* @param TimesheetEntity $timesheet
* @param Constraint $constraint
*/
public function validate($timesheet, Constraint $constraint)
public function validate(mixed $timesheet, Constraint $constraint): void
{
if (!($constraint instanceof TimesheetRestart)) {
throw new UnexpectedTypeException($constraint, TimesheetRestart::class);
@@ -57,10 +52,10 @@ final class TimesheetRestartValidator extends ConstraintValidator
}
$mode = $this->trackingModeService->getActiveMode();
$path = 'start';
$path = 'start_date';
if ($mode->canEditEnd()) {
$path = 'end';
$path = 'end_date';
} elseif ($mode->canEditDuration()) {
$path = 'duration';
}

View File

@@ -17,24 +17,18 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
final class TimesheetValidator extends ConstraintValidator
{
/**
* @var Constraint[]
*/
private $constraints;
/**
* @param Constraint[] $constraints
*/
public function __construct(iterable $constraints)
public function __construct(private iterable $constraints)
{
$this->constraints = $constraints;
}
/**
* @param TimesheetEntity $timesheet
* @param Constraint $constraint
*/
public function validate($timesheet, Constraint $constraint)
public function validate(mixed $timesheet, Constraint $constraint): void
{
if (!($constraint instanceof TimesheetConstraint)) {
throw new UnexpectedTypeException($constraint, Timesheet::class);
@@ -44,11 +38,11 @@ final class TimesheetValidator extends ConstraintValidator
throw new UnexpectedTypeException($timesheet, TimesheetEntity::class);
}
foreach ($this->constraints as $constraint) {
foreach ($this->constraints as $innerConstraint) {
$this->context
->getValidator()
->inContext($this->context)
->validate($timesheet, $constraint, [Constraint::DEFAULT_GROUP]);
->validate($timesheet, $innerConstraint, [Constraint::DEFAULT_GROUP]);
}
}
}

View File

@@ -17,9 +17,9 @@ final class TimesheetZeroDuration extends TimesheetConstraint
self::ZERO_DURATION_ERROR => 'Duration cannot be zero.',
];
public $message = 'Duration cannot be zero.';
public string $message = 'Duration cannot be zero.';
public function getTargets()
public function getTargets(): string|array
{
return self::CLASS_CONSTRAINT;
}

View File

@@ -17,18 +17,15 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
final class TimesheetZeroDurationValidator extends ConstraintValidator
{
private $configuration;
public function __construct(SystemConfiguration $configuration)
public function __construct(private SystemConfiguration $configuration)
{
$this->configuration = $configuration;
}
/**
* @param TimesheetEntity $timesheet
* @param Constraint $constraint
*/
public function validate($timesheet, Constraint $constraint)
public function validate(mixed $timesheet, Constraint $constraint): void
{
if (!($constraint instanceof TimesheetZeroDuration)) {
throw new UnexpectedTypeException($constraint, TimesheetZeroDuration::class);

View File

@@ -9,14 +9,10 @@
namespace App\Validator\Constraints;
use Doctrine\Common\Annotations\Annotation\Target;
use Symfony\Component\Validator\Constraint;
/**
* @Annotation
* @Target({"CLASS", "PROPERTY", "METHOD", "ANNOTATION"})
*/
class User extends Constraint
#[\Attribute(\Attribute::TARGET_CLASS)]
final class User extends Constraint
{
public const USER_EXISTING_EMAIL = 'kimai-user-00';
public const USER_EXISTING_NAME = 'kimai-user-01';
@@ -30,9 +26,9 @@ class User extends Constraint
self::USER_EXISTING_NAME_AS_EMAIL => 'An equal email is already used.',
];
public $message = 'The user has invalid settings.';
public string $message = 'The user has invalid settings.';
public function getTargets()
public function getTargets(): string|array
{
return self::CLASS_CONSTRAINT;
}

View File

@@ -16,20 +16,17 @@ use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
class UserValidator extends ConstraintValidator
final class UserValidator extends ConstraintValidator
{
private $userService;
public function __construct(UserService $userService)
public function __construct(private UserService $userService)
{
$this->userService = $userService;
}
/**
* @param UserEntity $value
* @param Constraint $constraint
*/
public function validate($value, Constraint $constraint)
public function validate(mixed $value, Constraint $constraint): void
{
if (!($constraint instanceof User)) {
throw new UnexpectedTypeException($constraint, User::class);
@@ -42,16 +39,16 @@ class UserValidator extends ConstraintValidator
$this->validateUser($value, $this->context);
}
protected function validateUser(UserEntity $user, ExecutionContextInterface $context)
protected function validateUser(UserEntity $user, ExecutionContextInterface $context): void
{
if ($user->getEmail() !== null) {
if ($user->hasEmail()) {
$this->validateEmailExists($user->getId(), $user->getEmail(), 'email', User::USER_EXISTING_EMAIL, $context);
$this->validateUsernameExists($user->getId(), $user->getEmail(), 'email', User::USER_EXISTING_EMAIL_AS_NAME, $context);
}
if ($user->getUsername() !== null) {
$this->validateEmailExists($user->getId(), $user->getUsername(), 'username', User::USER_EXISTING_NAME_AS_EMAIL, $context);
$this->validateUsernameExists($user->getId(), $user->getUsername(), 'username', User::USER_EXISTING_NAME, $context);
if ($user->hasUsername()) {
$this->validateEmailExists($user->getId(), $user->getUserIdentifier(), 'username', User::USER_EXISTING_NAME_AS_EMAIL, $context);
$this->validateUsernameExists($user->getId(), $user->getUserIdentifier(), 'username', User::USER_EXISTING_NAME, $context);
}
}

View File

@@ -9,7 +9,7 @@
namespace App\Validator;
class ValidationException extends \RuntimeException
final class ValidationException extends \RuntimeException
{
public function __construct(string $message = null)
{

View File

@@ -11,21 +11,14 @@ namespace App\Validator;
use Symfony\Component\Validator\ConstraintViolationListInterface;
class ValidationFailedException extends \RuntimeException
final class ValidationFailedException extends \RuntimeException
{
/**
* @var ConstraintViolationListInterface
*/
private $violations;
public function __construct(ConstraintViolationListInterface $violations, ?string $message = null)
public function __construct(private ConstraintViolationListInterface $violations, ?string $message = null)
{
if ($message === null) {
$message = 'Validation failed';
}
parent::__construct($message, 400);
$this->violations = $violations;
}
public function getViolations(): ConstraintViolationListInterface