Release 2.13 (#4659)

This commit is contained in:
Kevin Papst
2024-03-10 15:35:59 +01:00
committed by GitHub
parent a78e8ed8c4
commit dee90bb15e
79 changed files with 1019 additions and 932 deletions

View File

@@ -21,7 +21,7 @@ version-resolver:
- 'translation'
default: patch
template: |
[Upgrade Kimai](https://www.kimai.org/documentation/updates.html) - [Install Kimai](https://www.kimai.org/documentation/installation.html) - [Docker](https://tobybatch.github.io/kimai2/)
[Upgrade Kimai](https://www.kimai.org/documentation/updates.html) - [Install Kimai](https://www.kimai.org/documentation/installation.html) - [Docker](https://www.kimai.org/documentation/docker.html)
**Compatible with PHP 8.1 to 8.3**

View File

@@ -106,7 +106,7 @@ jobs:
- name: Upload code coverage
if: matrix.php == '8.2'
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage.xml

View File

@@ -47,8 +47,8 @@ Support for PHP 8.2 and 8.3.
## Installation
- [Recommended setup](https://www.kimai.org/documentation/installation.html#recommended-setup) — with Git and Composer
- [Docker](https://hub.docker.com/r/kimai/kimai2) — containerized by [@tobybatch](https://github.com/tobybatch/kimai2)
- [Native setup](https://www.kimai.org/documentation/installation.html) — with Git and Composer
- [Docker](https://hub.docker.com/r/kimai/kimai2) — containerized by @tobybatch
There are also documentations for:
- [developer setups](https://www.kimai.org/documentation/developers.html) — on your local machine

View File

@@ -10,7 +10,7 @@ Perform EACH version specific task between your version and the new one, otherwi
## [2.0.30](https://github.com/kimai/kimai/releases/tag/2.0.30)
The `DATABASE_URL` in your environment settings ([.env](https://github.com/kimai/kimai/issues/4246) or in your [Docker](https://github.com/tobybatch/kimai2/issues/531) or webserver config)
The `DATABASE_URL` in your environment settings (e.g. [.env](https://github.com/kimai/kimai/issues/4246), [docker-compose.yaml](https://github.com/tobybatch/kimai2/issues/531) or webserver config)
now requires the `charset` and `serverVersion` params, e.g.: `DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=10.5.8-MariaDB` (examples in `.env`).
## [2.0](https://github.com/kimai/kimai/releases/tag/2.0)

812
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1107,11 +1107,6 @@ parameters:
count: 1
path: src/Controller/Security/PasswordResetController.php
-
message: "#^Parameter \\#1 \\$usernameOrEmail of method App\\\\User\\\\UserService\\:\\:findUserByUsernameOrEmail\\(\\) expects string, bool\\|float\\|int\\|string\\|null given\\.$#"
count: 1
path: src/Controller/Security/PasswordResetController.php
-
message: "#^Method App\\\\Controller\\\\Security\\\\SelfRegistrationController\\:\\:getTargetUrlFromSession\\(\\) should return string\\|null but returns mixed\\.$#"
count: 1
@@ -4812,11 +4807,6 @@ parameters:
count: 1
path: src/Saml/SamlLoginAttributes.php
-
message: "#^Method App\\\\Saml\\\\SamlLogoutSubscriber\\:\\:logout\\(\\) has no return type specified\\.$#"
count: 1
path: src/Saml/SamlLogoutSubscriber.php
-
message: "#^Parameter \\#4 \\$sessionIndex of method OneLogin\\\\Saml2\\\\Auth\\:\\:logout\\(\\) expects string\\|null, mixed given\\.$#"
count: 1
@@ -4827,11 +4817,6 @@ parameters:
count: 1
path: src/Saml/SamlProvider.php
-
message: "#^Method App\\\\Saml\\\\SamlProvider\\:\\:getPropertyValue\\(\\) has no return type specified\\.$#"
count: 1
path: src/Saml/SamlProvider.php
-
message: "#^Method App\\\\Saml\\\\SamlProvider\\:\\:getPropertyValue\\(\\) has parameter \\$attribute with no type specified\\.$#"
count: 1
@@ -5557,11 +5542,6 @@ parameters:
count: 1
path: src/Validator/Constraints/TimesheetBudgetUsedValidator.php
-
message: "#^Parameter \\#1 \\$timesheet \\(App\\\\Entity\\\\Timesheet\\) of method App\\\\Validator\\\\Constraints\\\\TimesheetBudgetUsedValidator\\:\\:validate\\(\\) should be contravariant with parameter \\$value \\(mixed\\) of method Symfony\\\\Component\\\\Validator\\\\ConstraintValidatorInterface\\:\\:validate\\(\\)$#"
count: 2
path: src/Validator/Constraints/TimesheetBudgetUsedValidator.php
-
message: "#^Parameter \\#4 \\$duration of method App\\\\Validator\\\\Constraints\\\\TimesheetBudgetUsedValidator\\:\\:checkBudgets\\(\\) expects int, float\\|int\\|null given\\.$#"
count: 3
@@ -5572,31 +5552,11 @@ parameters:
count: 2
path: src/Validator/Constraints/TimesheetBudgetUsedValidator.php
-
message: "#^Parameter \\#1 \\$timesheet \\(App\\\\Entity\\\\Timesheet\\) of method App\\\\Validator\\\\Constraints\\\\TimesheetExportedValidator\\:\\:validate\\(\\) should be contravariant with parameter \\$value \\(mixed\\) of method Symfony\\\\Component\\\\Validator\\\\ConstraintValidatorInterface\\:\\:validate\\(\\)$#"
count: 2
path: src/Validator/Constraints/TimesheetExportedValidator.php
-
message: "#^Cannot call method getTimezone\\(\\) on DateTime\\|null\\.$#"
count: 1
path: src/Validator/Constraints/TimesheetFutureTimesValidator.php
-
message: "#^Parameter \\#1 \\$value \\(App\\\\Entity\\\\Timesheet\\) of method App\\\\Validator\\\\Constraints\\\\TimesheetFutureTimesValidator\\:\\:validate\\(\\) should be contravariant with parameter \\$value \\(mixed\\) of method Symfony\\\\Component\\\\Validator\\\\ConstraintValidatorInterface\\:\\:validate\\(\\)$#"
count: 2
path: src/Validator/Constraints/TimesheetFutureTimesValidator.php
-
message: "#^Parameter \\#1 \\$timesheet \\(App\\\\Entity\\\\Timesheet\\) of method App\\\\Validator\\\\Constraints\\\\TimesheetLockdownValidator\\:\\:validate\\(\\) should be contravariant with parameter \\$value \\(mixed\\) of method Symfony\\\\Component\\\\Validator\\\\ConstraintValidatorInterface\\:\\:validate\\(\\)$#"
count: 2
path: src/Validator/Constraints/TimesheetLockdownValidator.php
-
message: "#^Parameter \\#1 \\$timesheet \\(App\\\\Entity\\\\Timesheet\\) of method App\\\\Validator\\\\Constraints\\\\TimesheetLongRunningValidator\\:\\:validate\\(\\) should be contravariant with parameter \\$value \\(mixed\\) of method Symfony\\\\Component\\\\Validator\\\\ConstraintValidatorInterface\\:\\:validate\\(\\)$#"
count: 2
path: src/Validator/Constraints/TimesheetLongRunningValidator.php
-
message: "#^Parameter \\#2 \\$value of method Symfony\\\\Component\\\\Validator\\\\Violation\\\\ConstraintViolationBuilderInterface\\:\\:setParameter\\(\\) expects string, string\\|null given\\.$#"
count: 1
@@ -5607,26 +5567,6 @@ parameters:
count: 1
path: src/Validator/Constraints/TimesheetMultiUpdateValidator.php
-
message: "#^Parameter \\#1 \\$timesheet \\(App\\\\Entity\\\\Timesheet\\) of method App\\\\Validator\\\\Constraints\\\\TimesheetOverlappingValidator\\:\\:validate\\(\\) should be contravariant with parameter \\$value \\(mixed\\) of method Symfony\\\\Component\\\\Validator\\\\ConstraintValidatorInterface\\:\\:validate\\(\\)$#"
count: 2
path: src/Validator/Constraints/TimesheetOverlappingValidator.php
-
message: "#^Parameter \\#1 \\$timesheet \\(App\\\\Entity\\\\Timesheet\\) of method App\\\\Validator\\\\Constraints\\\\TimesheetRestartValidator\\:\\:validate\\(\\) should be contravariant with parameter \\$value \\(mixed\\) of method Symfony\\\\Component\\\\Validator\\\\ConstraintValidatorInterface\\:\\:validate\\(\\)$#"
count: 2
path: src/Validator/Constraints/TimesheetRestartValidator.php
-
message: "#^Parameter \\#1 \\$timesheet \\(App\\\\Entity\\\\Timesheet\\) of method App\\\\Validator\\\\Constraints\\\\TimesheetValidator\\:\\:validate\\(\\) should be contravariant with parameter \\$value \\(mixed\\) of method Symfony\\\\Component\\\\Validator\\\\ConstraintValidatorInterface\\:\\:validate\\(\\)$#"
count: 2
path: src/Validator/Constraints/TimesheetValidator.php
-
message: "#^Parameter \\#1 \\$timesheet \\(App\\\\Entity\\\\Timesheet\\) of method App\\\\Validator\\\\Constraints\\\\TimesheetZeroDurationValidator\\:\\:validate\\(\\) should be contravariant with parameter \\$value \\(mixed\\) of method Symfony\\\\Component\\\\Validator\\\\ConstraintValidatorInterface\\:\\:validate\\(\\)$#"
count: 2
path: src/Validator/Constraints/TimesheetZeroDurationValidator.php
-
message: "#^Parameter \\#1 \\$value \\(App\\\\Entity\\\\User\\) of method App\\\\Validator\\\\Constraints\\\\UserValidator\\:\\:validate\\(\\) should be contravariant with parameter \\$value \\(mixed\\) of method Symfony\\\\Component\\\\Validator\\\\ConstraintValidatorInterface\\:\\:validate\\(\\)$#"
count: 2

View File

@@ -44,10 +44,10 @@ final class ActivityController extends BaseApiController
public const GROUPS_RATE = ['Default', 'Entity', 'Activity_Rate'];
public function __construct(
private ViewHandlerInterface $viewHandler,
private ActivityRepository $repository,
private EventDispatcherInterface $dispatcher,
private ActivityRateRepository $activityRateRepository
private readonly ViewHandlerInterface $viewHandler,
private readonly ActivityRepository $repository,
private readonly EventDispatcherInterface $dispatcher,
private readonly ActivityRateRepository $activityRateRepository
) {
}
@@ -128,6 +128,7 @@ final class ActivityController extends BaseApiController
#[Route(methods: ['GET'], path: '/{id}', name: 'get_activity', requirements: ['id' => '\d+'])]
#[ApiSecurity(name: 'apiUser')]
#[ApiSecurity(name: 'apiToken')]
#[IsGranted('view', 'activity')]
public function getAction(Activity $activity): Response
{
$view = new View($activity, 200);

View File

@@ -44,10 +44,10 @@ final class CustomerController extends BaseApiController
public const GROUPS_RATE = ['Default', 'Entity', 'Customer_Rate'];
public function __construct(
private ViewHandlerInterface $viewHandler,
private CustomerRepository $repository,
private EventDispatcherInterface $dispatcher,
private CustomerRateRepository $customerRateRepository
private readonly ViewHandlerInterface $viewHandler,
private readonly CustomerRepository $repository,
private readonly EventDispatcherInterface $dispatcher,
private readonly CustomerRateRepository $customerRateRepository
) {
}
@@ -105,6 +105,7 @@ final class CustomerController extends BaseApiController
#[Route(methods: ['GET'], path: '/{id}', name: 'get_customer', requirements: ['id' => '\d+'])]
#[ApiSecurity(name: 'apiUser')]
#[ApiSecurity(name: 'apiToken')]
#[IsGranted('view', 'customer')]
public function getAction(Customer $customer): Response
{
$view = new View($customer, 200);

View File

@@ -46,11 +46,11 @@ final class ProjectController extends BaseApiController
public const GROUPS_RATE = ['Default', 'Entity', 'Project_Rate'];
public function __construct(
private ViewHandlerInterface $viewHandler,
private ProjectRepository $repository,
private EventDispatcherInterface $dispatcher,
private ProjectRateRepository $projectRateRepository,
private ProjectService $projectService
private readonly ViewHandlerInterface $viewHandler,
private readonly ProjectRepository $repository,
private readonly EventDispatcherInterface $dispatcher,
private readonly ProjectRateRepository $projectRateRepository,
private readonly ProjectService $projectService
) {
}
@@ -159,6 +159,7 @@ final class ProjectController extends BaseApiController
#[Route(methods: ['GET'], path: '/{id}', name: 'get_project', requirements: ['id' => '\d+'])]
#[ApiSecurity(name: 'apiUser')]
#[ApiSecurity(name: 'apiToken')]
#[IsGranted('view', 'project')]
public function getAction(Project $project): Response
{
$view = new View($project, 200);

View File

@@ -54,11 +54,11 @@ final class TimesheetController extends BaseApiController
public const GROUPS_COLLECTION_FULL = ['Default', 'Collection', 'Timesheet', 'Expanded'];
public function __construct(
private ViewHandlerInterface $viewHandler,
private TimesheetRepository $repository,
private TagRepository $tagRepository,
private EventDispatcherInterface $dispatcher,
private TimesheetService $service
private readonly ViewHandlerInterface $viewHandler,
private readonly TimesheetRepository $repository,
private readonly TagRepository $tagRepository,
private readonly EventDispatcherInterface $dispatcher,
private readonly TimesheetService $service
) {
}
@@ -99,6 +99,7 @@ final class TimesheetController extends BaseApiController
public function cgetAction(ParamFetcherInterface $paramFetcher, CustomerRepository $customerRepository, ProjectRepository $projectRepository, ActivityRepository $activityRepository, UserRepository $userRepository): Response
{
$query = new TimesheetQuery(false);
$query->setCurrentUser($this->getUser());
$seeAll = false;
if ($this->isGranted('view_other_timesheet')) {

View File

@@ -17,11 +17,11 @@ class Constants
/**
* The current release version
*/
public const VERSION = '2.12.0';
public const VERSION = '2.13.0';
/**
* The current release: major * 10000 + minor * 100 + patch
*/
public const VERSION_ID = 21200;
public const VERSION_ID = 21300;
/**
* The software name
*/

View File

@@ -67,7 +67,7 @@ final class CustomerController extends AbstractController
$query->setCurrentUser($this->getUser());
$query->setPage($page);
$form = $this->getToolbarForm($query);
$form = $this->getToolbarForm($query, $request);
if ($this->handleSearch($form, $request)) {
return $this->redirectToRoute('admin_customer');
}
@@ -473,7 +473,7 @@ final class CustomerController extends AbstractController
$query = new CustomerQuery();
$query->setCurrentUser($this->getUser());
$form = $this->getToolbarForm($query);
$form = $this->getToolbarForm($query, $request);
$form->setData($query);
$form->submit($request->query->all(), false);
@@ -526,12 +526,12 @@ final class CustomerController extends AbstractController
}
/**
* @param CustomerQuery $query
* @return FormInterface<CustomerQuery>
*/
private function getToolbarForm(CustomerQuery $query): FormInterface
private function getToolbarForm(CustomerQuery $query, Request $request): FormInterface
{
return $this->createSearchForm(CustomerToolbarForm::class, $query, [
'locale' => $request->getLocale(),
'action' => $this->generateUrl('admin_customer', [
'page' => $query->getPage(),
])
@@ -539,7 +539,6 @@ final class CustomerController extends AbstractController
}
/**
* @param CustomerComment $comment
* @return FormInterface<CustomerComment>
*/
private function getCommentForm(CustomerComment $comment): FormInterface
@@ -555,7 +554,6 @@ final class CustomerController extends AbstractController
}
/**
* @param Customer $customer
* @return FormInterface<Customer>
*/
private function createEditForm(Customer $customer): FormInterface

View File

@@ -45,21 +45,28 @@ final class PluginController extends AbstractController
private function getPluginInformation(HttpClientInterface $client, CacheInterface $cache): array
{
return $cache->get('kimai.marketplace_extensions', function (ItemInterface $item) use ($client) {
$response = $client->request('GET', 'https://www.kimai.org/plugins.json');
try {
$response = $client->request('GET', 'https://www.kimai.org/plugins.json');
if ($response->getStatusCode() !== 200) {
return [];
if ($response->getStatusCode() !== 200) {
return [];
}
$json = json_decode($response->getContent(), true);
if ($json === null) {
return [];
}
$item->expiresAfter(86400); // one day
return $response->toArray();
} catch (\Exception $exception) {
$this->logException($exception);
$this->flashError('Could not download plugin information');
}
$json = json_decode($response->getContent(), true);
if ($json === null) {
return [];
}
$item->expiresAfter(86400); // one day
return $response->toArray();
return [];
});
}
}

View File

@@ -32,9 +32,9 @@ use Symfony\Contracts\Translation\TranslatorInterface;
final class PasswordResetController extends AbstractController
{
public function __construct(
private EventDispatcherInterface $eventDispatcher,
private UserService $userService,
private SystemConfiguration $configuration
private readonly EventDispatcherInterface $eventDispatcher,
private readonly UserService $userService,
private readonly SystemConfiguration $configuration
) {
}
@@ -62,6 +62,10 @@ final class PasswordResetController extends AbstractController
}
$username = $request->request->get('username');
if (!\is_string($username) || trim($username) === '') {
throw $this->createAccessDeniedException('Username cannot be empty');
}
$user = $this->userService->findUserByUsernameOrEmail($username);
if (!$user->isPasswordRequestNonExpired($this->configuration->getPasswordResetRetryLifetime())) {

View File

@@ -18,7 +18,10 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
final class MenuSubscriber implements EventSubscriberInterface
{
public function __construct(private Security $security, private ContextHelper $helper)
public function __construct(
private readonly Security $security,
private readonly ContextHelper $helper
)
{
}
@@ -134,19 +137,19 @@ final class MenuSubscriber implements EventSubscriberInterface
$menu = $event->getAdminMenu();
if ($auth->isGranted('view_customer') || $auth->isGranted('view_teamlead_customer') || $auth->isGranted('view_team_customer')) {
$customers = new MenuItemModel('customer_admin', 'customers', 'admin_customer', [], 'customer');
$customers = new MenuItemModel('customers', 'customers', 'admin_customer', [], 'customer');
$customers->setChildRoutes(['admin_customer_create', 'admin_customer_permissions', 'customer_details', 'admin_customer_edit', 'admin_customer_delete']);
$menu->addChild($customers);
}
if ($auth->isGranted('view_project') || $auth->isGranted('view_teamlead_project') || $auth->isGranted('view_team_project')) {
$projects = new MenuItemModel('project_admin', 'projects', 'admin_project', [], 'project');
$projects = new MenuItemModel('projects', 'projects', 'admin_project', [], 'project');
$projects->setChildRoutes(['admin_project_permissions', 'admin_project_create', 'project_details', 'admin_project_edit', 'admin_project_delete']);
$menu->addChild($projects);
}
if ($auth->isGranted('view_activity') || $auth->isGranted('view_teamlead_activity') || $auth->isGranted('view_team_activity')) {
$activities = new MenuItemModel('activity_admin', 'activities', 'admin_activity', [], 'activity');
$activities = new MenuItemModel('activities', 'activities', 'admin_activity', [], 'activity');
$activities->setChildRoutes(['admin_activity_create', 'activity_details', 'admin_activity_edit', 'admin_activity_delete']);
$menu->addChild($activities);
}
@@ -163,18 +166,18 @@ final class MenuSubscriber implements EventSubscriberInterface
$menu = $event->getSystemMenu();
if ($auth->isGranted('view_user')) {
$users = new MenuItemModel('user_admin', 'users', 'admin_user', [], 'users');
$users = new MenuItemModel('users', 'users', 'admin_user', [], 'users');
$users->setChildRoutes(['admin_user_create', 'admin_user_delete', 'user_profile', 'user_profile_edit', 'user_profile_password', 'user_profile_api_token', 'user_profile_roles', 'user_profile_teams', 'user_profile_preferences', 'user_profile_2fa']);
$menu->addChild($users);
}
if ($auth->isGranted('role_permissions')) {
$users = new MenuItemModel('admin_user_permissions', 'profile.roles', 'admin_user_permissions', [], 'permissions');
$users = new MenuItemModel('roles', 'profile.roles', 'admin_user_permissions', [], 'permissions');
$menu->addChild($users);
}
if ($auth->isGranted('view_team')) {
$teams = new MenuItemModel('user_team', 'teams', 'admin_team', [], 'team');
$teams = new MenuItemModel('teams', 'teams', 'admin_team', [], 'team');
$teams->setChildRoutes(['admin_team_create', 'admin_team_edit']);
$menu->addChild($teams);
}
@@ -190,7 +193,7 @@ final class MenuSubscriber implements EventSubscriberInterface
}
if ($auth->isGranted('system_configuration')) {
$systemConfig = new MenuItemModel('system_configuration', 'menu.system_configuration', 'system_configuration', [], 'configuration');
$systemConfig = new MenuItemModel('configurations', 'menu.system_configuration', 'system_configuration', [], 'configuration');
$systemConfig->setChildRoutes(['system_configuration_update', 'system_configuration_section']);
$menu->addChild($systemConfig);
}

View File

@@ -9,9 +9,12 @@
namespace App\Form\Toolbar;
use App\Repository\CustomerRepository;
use App\Repository\Query\CustomerQuery;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Intl\Countries;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
@@ -22,9 +25,38 @@ final class CustomerToolbarForm extends AbstractType
{
use ToolbarFormTrait;
public function __construct(private readonly CustomerRepository $customerRepository)
{
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$this->addSearchTermInputField($builder);
// fetch countries
$qb = $this->customerRepository->createQueryBuilder('c');
$qb
->select('c.country')
->distinct(true);
$countries = $qb->getQuery()->getSingleColumnResult();
$choices = [];
foreach ($countries as $country) {
if (\is_string($country) && \is_string($options['locale'])) {
$choices[$country] = Countries::getName($country, $options['locale']);
}
}
if (\count($choices) > 0) {
$choices = array_flip($choices);
ksort($choices);
$builder->add('country', ChoiceType::class, [
'label' => 'country',
'choices' => $choices,
'required' => false,
]);
}
$this->addVisibilityChoice($builder);
$this->addPageSizeChoice($builder);
$this->addHiddenPagination($builder);
@@ -37,6 +69,8 @@ final class CustomerToolbarForm extends AbstractType
$resolver->setDefaults([
'data_class' => CustomerQuery::class,
'csrf_protection' => false,
'locale' => locale_get_default(),
]);
$resolver->setAllowedTypes('locale', ['string']);
}
}

View File

@@ -19,8 +19,8 @@ final class TagsType extends AbstractType
private ?int $count = null;
public function __construct(
private AuthorizationCheckerInterface $auth,
private TagRepository $repository
private readonly AuthorizationCheckerInterface $auth,
private readonly TagRepository $repository
) {
}

View File

@@ -19,7 +19,10 @@ final class ConfigurableNumberGenerator implements NumberGeneratorInterface
{
private ?InvoiceModel $model = null;
public function __construct(private InvoiceRepository $repository, private SystemConfiguration $configuration)
public function __construct(
private readonly InvoiceRepository $repository,
private readonly SystemConfiguration $configuration
)
{
}
@@ -33,9 +36,6 @@ final class ConfigurableNumberGenerator implements NumberGeneratorInterface
$this->model = $model;
}
/**
* @return string
*/
public function getInvoiceNumber(): string
{
$format = $this->configuration->find('invoice.number_format');
@@ -68,6 +68,10 @@ final class ConfigurableNumberGenerator implements NumberGeneratorInterface
'm' => $invoiceDate->format('n'),
'D' => $invoiceDate->format('d'),
'd' => $invoiceDate->format('j'),
'YY' => (int) $invoiceDate->format('Y') + $increaseBy,
'yy' => (int) $invoiceDate->format('y') + $increaseBy,
'MM' => (int) $invoiceDate->format('m') + $increaseBy,
'DD' => (int) $invoiceDate->format('d') + $increaseBy,
'date' => $invoiceDate->format('ymd'),
'cc' => $this->repository->getCounterForCustomerAllTime($this->model->getCustomer()) + $increaseBy,
'ccy' => $this->repository->getCounterForYear($invoiceDate, $this->model->getCustomer()) + $increaseBy,
@@ -92,7 +96,7 @@ final class ConfigurableNumberGenerator implements NumberGeneratorInterface
do {
$result = $numberGenerator->getNumber($increaseBy);
$increaseBy++;
} while ($this->repository->hasInvoice($result) && $loops++ < 99);
} while ((int) $result < 0 || ($this->repository->hasInvoice($result) && $loops++ < 99));
return $result;
}

View File

@@ -130,7 +130,7 @@ final class MPdfConverter implements HtmlToPdfConverter
// lowercase all font names, otherwise they cannot be loaded
// see https://github.com/kimai/www.kimai.org/issues/280
if (\array_key_exists('fonts', $options)) {
if (\array_key_exists('fonts', $options) && \is_array($options['fonts'])) {
$fonts = [];
foreach ($options['fonts'] as $name => $values) {
$fonts[strtolower($name)] = $values;

View File

@@ -20,15 +20,15 @@ use Doctrine\ORM\Query\Expr\Join;
final class CustomerMonthlyProjectsRepository
{
public function __construct(private TimesheetRepository $repository, private EntityManagerInterface $entityManager)
public function __construct(
private readonly TimesheetRepository $repository,
private readonly EntityManagerInterface $entityManager
)
{
}
/**
* @param DateTime $begin
* @param DateTime $end
* @param User[] $users
* @param Customer|null $customer
* @return array
* @internal
*/

View File

@@ -55,10 +55,6 @@ class CustomerRepository extends EntityRepository
return $customers;
}
/**
* @param Customer $customer
* @throws ORMException
*/
public function saveCustomer(Customer $customer): void
{
$entityManager = $this->getEntityManager();
@@ -75,7 +71,7 @@ class CustomerRepository extends EntityRepository
return $this->count([]);
}
private function addPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = []): void
public function addPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = []): void
{
$permissions = $this->getPermissionCriteria($qb, $user, $teams);
if ($permissions->count() > 0) {
@@ -124,9 +120,6 @@ class CustomerRepository extends EntityRepository
/**
* Returns a query builder that is used for CustomerType and your own 'query_builder' option.
*
* @param CustomerFormTypeQuery $query
* @return QueryBuilder
*/
public function getQueryBuilderForFormType(CustomerFormTypeQuery $query): QueryBuilder
{
@@ -177,6 +170,13 @@ class CustomerRepository extends EntityRepository
->from(Customer::class, 'c')
;
if ($query->getCountry() !== null) {
$qb
->andWhere($qb->expr()->eq('c.country', ':country'))
->setParameter('country', $query->getCountry())
;
}
foreach ($query->getOrderGroups() as $orderBy => $order) {
switch ($orderBy) {
case 'vat_id':

View File

@@ -18,11 +18,24 @@ class CustomerQuery extends BaseQuery implements VisibilityInterface
'phone', 'currency', 'address', 'contact', 'company', 'vat_id', 'budget', 'timeBudget', 'visible'
];
private ?string $country = null;
public function __construct()
{
$this->setDefaults([
'orderBy' => 'name',
'visibility' => VisibilityInterface::SHOW_VISIBLE,
'country' => null,
]);
}
public function getCountry(): ?string
{
return $this->country;
}
public function setCountry(?string $country): void
{
$this->country = $country;
}
}

View File

@@ -20,8 +20,8 @@ use Symfony\Component\HttpFoundation\RequestStack;
class SamlAuthFactory
{
public function __construct(
private RequestStack $request,
private SamlConfigurationInterface $configuration
private readonly RequestStack $request,
private readonly SamlConfigurationInterface $configuration
) {
}

View File

@@ -35,12 +35,12 @@ class SamlAuthenticator extends AbstractAuthenticator
];
public function __construct(
private HttpUtils $httpUtils,
private SamlAuthenticationSuccessHandler $successHandler,
private SamlAuthenticationFailureHandler $failureHandler,
private SamlAuthFactory $samlAuthFactory,
private SamlProvider $samlProvider,
private SamlConfigurationInterface $configuration
private readonly HttpUtils $httpUtils,
private readonly SamlAuthenticationSuccessHandler $successHandler,
private readonly SamlAuthenticationFailureHandler $failureHandler,
private readonly SamlAuthFactory $samlAuthFactory,
private readonly SamlProvider $samlProvider,
private readonly SamlConfigurationInterface $configuration
) {
}

View File

@@ -13,7 +13,7 @@ use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface;
final class SamlBadge implements BadgeInterface
{
public function __construct(private SamlLoginAttributes $samlToken)
public function __construct(private readonly SamlLoginAttributes $samlToken)
{
}

View File

@@ -15,7 +15,7 @@ use Symfony\Component\Security\Http\Event\LogoutEvent;
final class SamlLogoutSubscriber implements EventSubscriberInterface
{
public function __construct(private SamlAuthFactory $samlAuth)
public function __construct(private readonly SamlAuthFactory $samlAuth)
{
}
@@ -26,7 +26,7 @@ final class SamlLogoutSubscriber implements EventSubscriberInterface
];
}
public function logout(LogoutEvent $event)
public function logout(LogoutEvent $event): void
{
$token = $event->getToken();

View File

@@ -22,9 +22,9 @@ final class SamlProvider
* @param UserProviderInterface<User> $userProvider
*/
public function __construct(
private UserRepository $repository,
private UserProviderInterface $userProvider,
private SamlConfigurationInterface $configuration
private readonly UserRepository $repository,
private readonly UserProviderInterface $userProvider,
private readonly SamlConfigurationInterface $configuration
) {
}
@@ -123,7 +123,7 @@ final class SamlProvider
$user->setAuth(User::AUTH_SAML);
}
private function getPropertyValue(SamlLoginAttributes $token, $attribute)
private function getPropertyValue(SamlLoginAttributes $token, $attribute): string
{
$results = [];
$attributes = $token->getAttributes();

View File

@@ -29,11 +29,14 @@ final class RolePermissionManager
private bool $isInitialized = false;
/**
* @param PermissionService $service
* @param array<string, array<string, bool>> $permissions as defined in kimai.yaml
* @param array<string, bool> $permissionNames as defined in kimai.yaml
*/
public function __construct(private PermissionService $service, private array $permissions, private array $permissionNames)
public function __construct(
private readonly PermissionService $service,
private array $permissions,
private readonly array $permissionNames
)
{
}
@@ -47,11 +50,6 @@ final class RolePermissionManager
$perm = (string) $item['permission'];
$role = (string) $item['role'];
// these permissions may not be revoked at any time, because super admin would lose the ability to reactivate any permission
if ($role === User::ROLE_SUPER_ADMIN && \array_key_exists($perm, self::SUPER_ADMIN_PERMISSIONS)) {
continue;
}
if (!\array_key_exists($role, $this->permissions)) {
$this->permissions[$role] = [];
}
@@ -59,6 +57,7 @@ final class RolePermissionManager
$this->permissions[$role][$perm] = (bool) $item['allowed'];
}
// these permissions may not be revoked at any time, because super admin would lose the ability to reactivate any permission
foreach (self::SUPER_ADMIN_PERMISSIONS as $perm => $value) {
$this->permissions[User::ROLE_SUPER_ADMIN][$perm] = $value;
}
@@ -68,14 +67,9 @@ final class RolePermissionManager
/**
* Only permissions which were registered through the Symfony configuration stack will be acknowledged here.
*
* @param string $permission
* @return bool
*/
public function isRegisteredPermission(string $permission): bool
{
$this->init();
return \array_key_exists($permission, $this->permissionNames);
}
@@ -89,7 +83,7 @@ final class RolePermissionManager
return false;
}
return \array_key_exists($permission, $this->permissions[$role]) ? $this->permissions[$role][$permission] : false;
return \array_key_exists($permission, $this->permissions[$role]) && $this->permissions[$role][$permission];
}
public function hasRolePermission(User $user, string $permission): bool
@@ -112,8 +106,6 @@ final class RolePermissionManager
*/
public function getPermissions(): array
{
$this->init();
return array_keys($this->permissionNames);
}
}

View File

@@ -18,7 +18,7 @@ use DateTimeInterface;
final class TimesheetStatisticService
{
public function __construct(private TimesheetRepository $repository)
public function __construct(private readonly TimesheetRepository $repository)
{
}
@@ -53,10 +53,10 @@ final class TimesheetStatisticService
->addSelect('MONTH(t.date) as month')
->addSelect('YEAR(t.date) as year')
->where($qb->expr()->isNotNull('t.end'))
->andWhere($qb->expr()->between('t.begin', ':begin', ':end'))
->andWhere($qb->expr()->between('t.date', ':begin', ':end'))
->andWhere($qb->expr()->in('t.user', ':user'))
->setParameter('begin', $begin)
->setParameter('end', $end)
->setParameter('begin', $begin->format('Y-m-d'))
->setParameter('end', $end->format('Y-m-d'))
->setParameter('user', $users)
->groupBy('year')
->addGroupBy('month')
@@ -125,10 +125,10 @@ final class TimesheetStatisticService
->addSelect('IDENTITY(t.activity) as activity')
->addSelect('DATE(t.date) as date')
->where($qb->expr()->isNotNull('t.end'))
->andWhere($qb->expr()->between('t.begin', ':begin', ':end'))
->andWhere($qb->expr()->between('t.date', ':begin', ':end'))
->andWhere($qb->expr()->in('t.user', ':user'))
->setParameter('begin', $begin)
->setParameter('end', $end)
->setParameter('begin', $begin->format('Y-m-d'))
->setParameter('end', $end->format('Y-m-d'))
->setParameter('user', $users)
->groupBy('date')
->addGroupBy('project')
@@ -173,8 +173,6 @@ final class TimesheetStatisticService
/**
* @internal only for core development
* @param DateTimeInterface $begin
* @param DateTimeInterface $end
* @param User[] $users
* @return array
*/
@@ -203,10 +201,10 @@ final class TimesheetStatisticService
->addSelect('YEAR(t.date) as year')
->addSelect('MONTH(t.date) as month')
->where($qb->expr()->isNotNull('t.end'))
->andWhere($qb->expr()->between('t.begin', ':begin', ':end'))
->andWhere($qb->expr()->between('t.date', ':begin', ':end'))
->andWhere($qb->expr()->in('t.user', ':user'))
->setParameter('begin', $begin)
->setParameter('end', $end)
->setParameter('begin', $begin->format('Y-m-d'))
->setParameter('end', $end->format('Y-m-d'))
->setParameter('user', $users)
->groupBy('year')
->addGroupBy('month')
@@ -253,7 +251,7 @@ final class TimesheetStatisticService
public function findFirstRecordDate(User $user): ?\DateTimeImmutable
{
$result = $this->repository->createQueryBuilder('t')
->select('MIN(t.begin)')
->select('MIN(t.date)')
->where('t.user = :user')
->setParameter('user', $user)
->getQuery()
@@ -295,10 +293,10 @@ final class TimesheetStatisticService
->addSelect('YEAR(t.date) as year')
->addSelect('IDENTITY(t.user) as user')
->where($qb->expr()->isNotNull('t.end'))
->andWhere($qb->expr()->between('t.begin', ':begin', ':end'))
->andWhere($qb->expr()->between('t.date', ':begin', ':end'))
->andWhere($qb->expr()->in('t.user', ':user'))
->setParameter('begin', $begin)
->setParameter('end', $end)
->setParameter('begin', $begin->format('Y-m-d'))
->setParameter('end', $end->format('Y-m-d'))
->setParameter('user', $users)
->groupBy('year')
->addGroupBy('month')

View File

@@ -192,6 +192,8 @@ final class LocaleFormatExtensions extends AbstractExtension implements LocaleAw
public function getJavascriptConfiguration(User $user): array
{
return [
'locale' => $this->locale,
'language' => $user->getLanguage(),
'formatDuration' => $this->localeService->getDurationFormat($this->locale),
'formatDate' => $this->localeService->getDateFormat($this->locale),
'defaultColor' => Constants::DEFAULT_COLOR,

View File

@@ -76,7 +76,7 @@ final class PaginationExtension extends AbstractExtension
$propertyAccessor = PropertyAccess::createPropertyAccessor();
$propertyAccessor->setValue($routeParams, $pagePropertyPath, $page);
return $router->generate($routeName, $routeParams);
return $router->generate($routeName, $routeParams); // @phpstan-ignore-line
};
}
}

View File

@@ -38,9 +38,7 @@ final class QuickEntryTimesheetValidator extends ConstraintValidator
throw new UnexpectedTypeException($value, TimesheetEntity::class);
}
$timesheet = $value;
if ($timesheet->getId() === null && $timesheet->getDuration(false) === null) {
if ($value->getId() === null && $value->getDuration(false) === null) {
return;
}
@@ -49,7 +47,7 @@ final class QuickEntryTimesheetValidator extends ConstraintValidator
->getValidator()
->inContext($this->context)
->atPath('duration')
->validate($timesheet, $innerConstraint, [Constraint::DEFAULT_GROUP]);
->validate($value, $innerConstraint, [Constraint::DEFAULT_GROUP]);
}
}
}

View File

@@ -18,7 +18,7 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
final class TimesheetBasicValidator extends ConstraintValidator
{
public function __construct(private SystemConfiguration $systemConfiguration)
public function __construct(private readonly SystemConfiguration $systemConfiguration)
{
}
@@ -36,10 +36,6 @@ final class TimesheetBasicValidator extends ConstraintValidator
$this->validateActivityAndProject($value, $this->context);
}
/**
* @param TimesheetEntity $timesheet
* @param ExecutionContextInterface $context
*/
protected function validateBeginAndEnd(TimesheetEntity $timesheet, ExecutionContextInterface $context): void
{
$begin = $timesheet->getBegin();
@@ -64,10 +60,6 @@ final class TimesheetBasicValidator extends ConstraintValidator
}
}
/**
* @param TimesheetEntity $timesheet
* @param ExecutionContextInterface $context
*/
protected function validateActivityAndProject(TimesheetEntity $timesheet, ExecutionContextInterface $context): void
{
$activity = $timesheet->getActivity();

View File

@@ -13,7 +13,7 @@ use App\Activity\ActivityStatisticService;
use App\Configuration\LocaleService;
use App\Configuration\SystemConfiguration;
use App\Customer\CustomerStatisticService;
use App\Entity\Timesheet;
use App\Entity\Timesheet as TimesheetEntity;
use App\Model\BudgetStatisticModel;
use App\Project\ProjectStatisticService;
use App\Repository\TimesheetRepository;
@@ -29,53 +29,49 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
final class TimesheetBudgetUsedValidator extends ConstraintValidator
{
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
private readonly SystemConfiguration $configuration,
private readonly CustomerStatisticService $customerStatisticService,
private readonly ProjectStatisticService $projectStatisticService,
private readonly ActivityStatisticService $activityStatisticService,
private readonly TimesheetRepository $timesheetRepository,
private readonly RateServiceInterface $rateService,
private readonly AuthorizationCheckerInterface $security,
private readonly LocaleService $localeService
) {
}
/**
* @param Timesheet $timesheet
* @param Constraint $constraint
*/
public function validate(mixed $timesheet, Constraint $constraint): void
public function validate(mixed $value, Constraint $constraint): void
{
if (!($constraint instanceof TimesheetBudgetUsed)) {
throw new UnexpectedTypeException($constraint, TimesheetBudgetUsed::class);
}
if (!\is_object($timesheet) || !($timesheet instanceof Timesheet)) {
throw new UnexpectedTypeException($timesheet, Timesheet::class);
if (!\is_object($value) || !($value instanceof TimesheetEntity)) {
throw new UnexpectedTypeException($value, TimesheetEntity::class);
}
if ($this->configuration->isTimesheetAllowOverbookingBudget()) {
return;
}
$begin = $timesheet->getBegin();
$begin = $value->getBegin();
// we can only work with stopped entries
if ($begin === null || $timesheet->getEnd() === null || $timesheet->getUser() === null) {
if ($begin === null || $value->getEnd() === null || $value->getUser() === null) {
return;
}
// budgets need only be calculated for billable records
if (!$timesheet->isBillable()) {
if (!$value->isBillable()) {
return;
}
// this validator needs a project to calculate the rates
if ($timesheet->getProject() === null) {
if ($value->getProject() === null) {
return;
}
$id = $timesheet->getId();
$id = $value->getId();
// when changing the date via the calendar and/or the API, the duration will not be reset by the
// duration calculator (which runs after validation!) so we manually reset the duration before
@@ -92,9 +88,9 @@ final class TimesheetBudgetUsedValidator extends ConstraintValidator
// $timesheet->setDuration(null);
// $duration = $timesheet->getDuration();
$duration = $timesheet->getCalculatedDuration();
$duration = $value->getCalculatedDuration();
$timeRate = $this->rateService->calculate($timesheet);
$timeRate = $this->rateService->calculate($value);
$rate = $timeRate->getRate();
$activityDuration = $duration ?? 0;
@@ -116,10 +112,10 @@ final class TimesheetBudgetUsedValidator extends ConstraintValidator
// this could for example happen when export flag is changed OR if "prevent overbooking" config was recently activated and this is an old entry
if ($duration === $rawData['duration'] &&
$rate === $rawData['rate'] &&
$timesheet->isBillable() === $rawData['billable'] &&
$value->isBillable() === $rawData['billable'] &&
$begin->format('Y.m.d') === $rawData['begin']->format('Y.m.d') &&
$timesheet->getProject()->getId() === $projectId &&
($timesheet->getActivity() === null || $timesheet->getActivity()->getId() === $activityId)
$value->getProject()->getId() === $projectId &&
($value->getActivity() === null || $value->getActivity()->getId() === $activityId)
) {
return;
}
@@ -129,18 +125,18 @@ final class TimesheetBudgetUsedValidator extends ConstraintValidator
// only subtract the previously logged data in case the record was billable
// if it wasn't billable, then its values are not included in the statistic models used later on
if ($rawData['billable']) {
if (null !== $timesheet->getActivity() && $activityId === $timesheet->getActivity()->getId()) {
if (null !== $value->getActivity() && $activityId === $value->getActivity()->getId()) {
$activityDuration -= $rawData['duration'];
$activityRate -= $rawData['rate'];
}
if (null !== $timesheet->getProject()) {
if ($projectId === $timesheet->getProject()->getId()) {
if (null !== $value->getProject()) {
if ($projectId === $value->getProject()->getId()) {
$projectDuration -= $rawData['duration'];
$projectRate -= $rawData['rate'];
}
if ($customerId === $timesheet->getProject()->getCustomer()->getId()) {
if ($customerId === $value->getProject()->getCustomer()->getId()) {
$customerDuration -= $rawData['duration'];
$customerRate -= $rawData['rate'];
}
@@ -153,23 +149,23 @@ final class TimesheetBudgetUsedValidator extends ConstraintValidator
$now = new DateTime('now', $begin->getTimezone());
$recordDate = $begin;
if (null !== ($activity = $timesheet->getActivity()) && $activity->hasBudgets()) {
if (null !== ($activity = $value->getActivity()) && $activity->hasBudgets()) {
$dateTime = $activity->isMonthlyBudget() ? $recordDate : $now;
if ($activity->isMonthlyBudget() && $monthWasChanged) {
$activityDuration = $duration;
}
$stat = $this->activityStatisticService->getBudgetStatisticModel($activity, $dateTime);
$this->checkBudgets($constraint, $stat, $timesheet, $activityDuration, $activityRate, 'activity');
$this->checkBudgets($constraint, $stat, $value, $activityDuration, $activityRate, 'activity');
}
if (null !== ($project = $timesheet->getProject())) {
if (null !== ($project = $value->getProject())) {
if ($project->hasBudgets()) {
$dateTime = $project->isMonthlyBudget() ? $recordDate : $now;
if ($project->isMonthlyBudget() && $monthWasChanged) {
$projectDuration = $duration;
}
$stat = $this->projectStatisticService->getBudgetStatisticModel($project, $dateTime);
$this->checkBudgets($constraint, $stat, $timesheet, $projectDuration, $projectRate, 'project');
$this->checkBudgets($constraint, $stat, $value, $projectDuration, $projectRate, 'project');
}
if (null !== ($customer = $project->getCustomer()) && $customer->hasBudgets()) {
$dateTime = $customer->isMonthlyBudget() ? $recordDate : $now;
@@ -177,12 +173,12 @@ final class TimesheetBudgetUsedValidator extends ConstraintValidator
$customerDuration = $duration;
}
$stat = $this->customerStatisticService->getBudgetStatisticModel($customer, $dateTime);
$this->checkBudgets($constraint, $stat, $timesheet, $customerDuration, $customerRate, 'customer');
$this->checkBudgets($constraint, $stat, $value, $customerDuration, $customerRate, 'customer');
}
}
}
private function checkBudgets(TimesheetBudgetUsed $constraint, BudgetStatisticModel $stat, Timesheet $timesheet, int $duration, float $rate, string $field): bool
private function checkBudgets(TimesheetBudgetUsed $constraint, BudgetStatisticModel $stat, TimesheetEntity $timesheet, int $duration, float $rate, string $field): bool
{
$fullRate = ($stat->getBudgetSpent() + $rate);
@@ -203,7 +199,7 @@ final class TimesheetBudgetUsedValidator extends ConstraintValidator
return false;
}
private function addBudgetViolation(TimesheetBudgetUsed $constraint, Timesheet $timesheet, string $field, float $budget, float $rate): void
private function addBudgetViolation(TimesheetBudgetUsed $constraint, TimesheetEntity $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 LocaleFormatter($this->localeService, $timesheet->getUser()?->getLocale() ?? 'en');

View File

@@ -17,29 +17,25 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
final class TimesheetExportedValidator extends ConstraintValidator
{
public function __construct(private Security $security)
public function __construct(private readonly Security $security)
{
}
/**
* @param TimesheetEntity $timesheet
* @param Constraint $constraint
*/
public function validate(mixed $timesheet, Constraint $constraint): void
public function validate(mixed $value, Constraint $constraint): void
{
if (!($constraint instanceof TimesheetExported)) {
throw new UnexpectedTypeException($constraint, TimesheetExported::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);
}
if ($timesheet->getId() === null) {
if ($value->getId() === null) {
return;
}
if (!$timesheet->isExported()) {
if (!$value->isExported()) {
return;
}
@@ -47,7 +43,7 @@ final class TimesheetExportedValidator extends ConstraintValidator
// 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)) {
if (null !== $this->security->getUser() && $this->security->isGranted('edit_export', $value)) {
return;
}

View File

@@ -17,14 +17,10 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
final class TimesheetFutureTimesValidator extends ConstraintValidator
{
public function __construct(private SystemConfiguration $configuration)
public function __construct(private readonly SystemConfiguration $configuration)
{
}
/**
* @param TimesheetEntity $value
* @param Constraint $constraint
*/
public function validate(mixed $value, Constraint $constraint): void
{
if (!($constraint instanceof TimesheetFutureTimes)) {

View File

@@ -18,29 +18,28 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
final class TimesheetLockdownValidator extends ConstraintValidator
{
public function __construct(private Security $security, private LockdownService $lockdownService)
public function __construct(
private readonly Security $security,
private readonly LockdownService $lockdownService
)
{
}
/**
* @param TimesheetEntity $timesheet
* @param Constraint $constraint
*/
public function validate(mixed $timesheet, Constraint $constraint): void
public function validate(mixed $value, Constraint $constraint): void
{
if (!($constraint instanceof TimesheetLockdown)) {
throw new UnexpectedTypeException($constraint, TimesheetLockdown::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);
}
if (!$this->lockdownService->isLockdownActive()) {
return;
}
if (null === ($timesheetStart = $timesheet->getBegin())) {
if (null === ($timesheetStart = $value->getBegin())) {
return;
}
@@ -67,7 +66,7 @@ final class TimesheetLockdownValidator extends ConstraintValidator
$allowEditInGracePeriod = true;
}
if ($this->lockdownService->isEditable($timesheet, $now, $allowEditInGracePeriod)) {
if ($this->lockdownService->isEditable($value, $now, $allowEditInGracePeriod)) {
return;
}

View File

@@ -17,30 +17,26 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
final class TimesheetLongRunningValidator extends ConstraintValidator
{
public function __construct(private SystemConfiguration $systemConfiguration)
public function __construct(private readonly SystemConfiguration $systemConfiguration)
{
}
/**
* @param TimesheetEntity $timesheet
* @param Constraint $constraint
*/
public function validate(mixed $timesheet, Constraint $constraint): void
public function validate(mixed $value, Constraint $constraint): void
{
if (!($constraint instanceof TimesheetLongRunning)) {
throw new UnexpectedTypeException($constraint, TimesheetLongRunning::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);
}
if ($timesheet->isRunning()) {
if ($value->isRunning()) {
return;
}
/** @var int $duration */
$duration = $timesheet->getCalculatedDuration();
$duration = $value->getCalculatedDuration();
// one year is currently the maximum that can be logged (which is already not logically)
// the database column could hold more data, but let's limit it here

View File

@@ -18,26 +18,25 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
final class TimesheetOverlappingValidator extends ConstraintValidator
{
public function __construct(private SystemConfiguration $configuration, private TimesheetRepository $repository)
public function __construct(
private readonly SystemConfiguration $configuration,
private readonly TimesheetRepository $repository
)
{
}
/**
* @param TimesheetEntity $timesheet
* @param Constraint $constraint
*/
public function validate(mixed $timesheet, Constraint $constraint): void
public function validate(mixed $value, Constraint $constraint): void
{
if (!($constraint instanceof TimesheetOverlapping)) {
throw new UnexpectedTypeException($constraint, TimesheetOverlapping::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);
}
$begin = $timesheet->getBegin();
$end = $timesheet->getEnd();
$begin = $value->getBegin();
$end = $value->getEnd();
// this case is handled in TimesheetValidator and should not raise a second validation
if ($begin !== null && $end !== null && $begin > $end) {
@@ -48,7 +47,7 @@ final class TimesheetOverlappingValidator extends ConstraintValidator
return;
}
if (!$this->repository->hasRecordForTime($timesheet)) {
if (!$this->repository->hasRecordForTime($value)) {
return;
}

View File

@@ -18,28 +18,27 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
final class TimesheetRestartValidator extends ConstraintValidator
{
public function __construct(private Security $security, private TrackingModeService $trackingModeService)
public function __construct(
private readonly Security $security,
private readonly TrackingModeService $trackingModeService
)
{
}
/**
* @param TimesheetEntity $timesheet
* @param Constraint $constraint
*/
public function validate(mixed $timesheet, Constraint $constraint): void
public function validate(mixed $value, Constraint $constraint): void
{
if (!($constraint instanceof TimesheetRestart)) {
throw new UnexpectedTypeException($constraint, TimesheetRestart::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);
}
// special case that would otherwise need to be validated in several controllers:
// an entry is edited and the end date is removed (or duration deleted) would restart the record,
// which might be disallowed for the current user
if (null !== $timesheet->getEnd()) {
if (null !== $value->getEnd()) {
return;
}
@@ -47,7 +46,7 @@ final class TimesheetRestartValidator extends ConstraintValidator
return;
}
if (null !== $this->security->getUser() && $this->security->isGranted('start', $timesheet)) {
if (null !== $this->security->getUser() && $this->security->isGranted('start', $value)) {
return;
}

View File

@@ -28,18 +28,14 @@ final class TimesheetValidator extends ConstraintValidator
{
}
/**
* @param TimesheetEntity $timesheet
* @param Constraint $constraint
*/
public function validate(mixed $timesheet, Constraint $constraint): void
public function validate(mixed $value, Constraint $constraint): void
{
if (!($constraint instanceof TimesheetEntityConstraint)) {
throw new UnexpectedTypeException($constraint, TimesheetEntityConstraint::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);
}
$groups = [Constraint::DEFAULT_GROUP];
@@ -51,7 +47,7 @@ final class TimesheetValidator extends ConstraintValidator
$this->context
->getValidator()
->inContext($this->context)
->validate($timesheet, $innerConstraint, $groups);
->validate($value, $innerConstraint, $groups);
}
}
}

View File

@@ -17,35 +17,31 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
final class TimesheetZeroDurationValidator extends ConstraintValidator
{
public function __construct(private SystemConfiguration $configuration)
public function __construct(private readonly SystemConfiguration $configuration)
{
}
/**
* @param TimesheetEntity $timesheet
* @param Constraint $constraint
*/
public function validate(mixed $timesheet, Constraint $constraint): void
public function validate(mixed $value, Constraint $constraint): void
{
if (!($constraint instanceof TimesheetZeroDuration)) {
throw new UnexpectedTypeException($constraint, TimesheetZeroDuration::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);
}
if ($this->configuration->isTimesheetAllowZeroDuration()) {
return;
}
if ($timesheet->isRunning()) {
if ($value->isRunning()) {
return;
}
$duration = 0;
if ($timesheet->getEnd() !== null && $timesheet->getBegin() !== null) {
$duration = $timesheet->getCalculatedDuration();
if ($value->getEnd() !== null && $value->getBegin() !== null) {
$duration = $value->getCalculatedDuration();
}
if ($duration <= 0) {

View File

@@ -35,21 +35,23 @@ final class ActivityVoter extends Voter
'permissions',
];
public function __construct(private RolePermissionManager $permissionManager)
public function __construct(private readonly RolePermissionManager $permissionManager)
{
}
public function supportsAttribute(string $attribute): bool
{
return \in_array($attribute, self::ALLOWED_ATTRIBUTES, true);
}
public function supportsType(string $subjectType): bool
{
return str_contains($subjectType, Activity::class);
}
protected function supports(string $attribute, mixed $subject): bool
{
if (!($subject instanceof Activity)) {
return false;
}
if (!\in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
return false;
}
return true;
return $subject instanceof Activity && $this->supportsAttribute($attribute);
}
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
@@ -65,7 +67,7 @@ final class ActivityVoter extends Voter
}
// those cannot be assigned to teams
if (\in_array($attribute, ['create', 'delete'])) {
if (\in_array($attribute, ['create', 'delete'], true)) {
return false;
}

View File

@@ -39,21 +39,23 @@ final class CustomerVoter extends Voter
'access',
];
public function __construct(private RolePermissionManager $permissionManager)
public function __construct(private readonly RolePermissionManager $permissionManager)
{
}
public function supportsAttribute(string $attribute): bool
{
return \in_array($attribute, self::ALLOWED_ATTRIBUTES, true);
}
public function supportsType(string $subjectType): bool
{
return str_contains($subjectType, Customer::class);
}
protected function supports(string $attribute, mixed $subject): bool
{
if (!($subject instanceof Customer)) {
return false;
}
if (!\in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
return false;
}
return true;
return $subject instanceof Customer && $this->supportsAttribute($attribute);
}
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool

View File

@@ -38,17 +38,22 @@ final class EntityMultiRoleVoter extends Voter
'activity',
];
public function __construct(private RolePermissionManager $permissionManager)
public function __construct(private readonly RolePermissionManager $permissionManager)
{
}
public function supportsAttribute(string $attribute): bool
{
return \in_array($attribute, self::ALLOWED_ATTRIBUTES, true);
}
protected function supports(string $attribute, mixed $subject): bool
{
if (!\in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
if (!$this->supportsAttribute($attribute)) {
return false;
}
if (\is_string($subject) && \in_array($subject, self::ALLOWED_SUBJECTS)) {
if (\is_string($subject) && \in_array($subject, self::ALLOWED_SUBJECTS, true)) {
return true;
}
@@ -69,7 +74,7 @@ final class EntityMultiRoleVoter extends Voter
$suffix = null;
if (\is_string($subject) && \in_array($subject, self::ALLOWED_SUBJECTS)) {
if (\is_string($subject) && \in_array($subject, self::ALLOWED_SUBJECTS, true)) {
$suffix = $subject;
} elseif ($subject instanceof Activity) {
$suffix = 'activity';

View File

@@ -37,21 +37,23 @@ final class ProjectVoter extends Voter
'details',
];
public function __construct(private RolePermissionManager $permissionManager)
public function __construct(private readonly RolePermissionManager $permissionManager)
{
}
public function supportsAttribute(string $attribute): bool
{
return \in_array($attribute, self::ALLOWED_ATTRIBUTES, true);
}
public function supportsType(string $subjectType): bool
{
return str_contains($subjectType, Project::class);
}
protected function supports(string $attribute, mixed $subject): bool
{
if (!($subject instanceof Project)) {
return false;
}
if (!\in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
return false;
}
return true;
return $subject instanceof Project && $this->supportsAttribute($attribute);
}
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool

View File

@@ -20,13 +20,21 @@ use Symfony\Component\Security\Core\Authorization\Voter\Voter;
*/
final class QuickEntryVoter extends Voter
{
public function __construct(private RolePermissionManager $permissionManager, private TrackingModeService $trackingModeService)
public function __construct(
private readonly RolePermissionManager $permissionManager,
private readonly TrackingModeService $trackingModeService
)
{
}
public function supportsAttribute(string $attribute): bool
{
return 'quick-entry' === $attribute;
}
protected function supports(string $attribute, mixed $subject): bool
{
return 'quick-entry' === $attribute;
return $this->supportsAttribute($attribute);
}
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool

View File

@@ -26,13 +26,23 @@ final class ReportingVoter extends Voter
'report:user',
];
public function __construct(private RolePermissionManager $permissionManager)
public function __construct(private readonly RolePermissionManager $permissionManager)
{
}
public function supportsAttribute(string $attribute): bool
{
return \in_array($attribute, self::ALLOWED_ATTRIBUTES, true);
}
public function supportsType(string $subjectType): bool
{
return $subjectType === 'null';
}
protected function supports(string $attribute, mixed $subject): bool
{
return $subject === null && \in_array($attribute, self::ALLOWED_ATTRIBUTES);
return $subject === null && $this->supportsAttribute($attribute);
}
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool

View File

@@ -21,18 +21,24 @@ use Symfony\Component\Security\Core\Authorization\Voter\Voter;
*/
final class RolePermissionVoter extends Voter
{
public function __construct(private RolePermissionManager $permissionManager)
public function __construct(private readonly RolePermissionManager $permissionManager)
{
}
public function supportsAttribute(string $attribute): bool
{
return $this->permissionManager->isRegisteredPermission($attribute);
}
public function supportsType(string $subjectType): bool
{
// we only work on single strings that have no subject
return $subjectType === 'null';
}
protected function supports(string $attribute, mixed $subject): bool
{
// we only work on single strings that have no subject
if (null !== $subject) {
return false;
}
return $this->permissionManager->isRegisteredPermission($attribute);
return $subject === null && $this->supportsAttribute($attribute);
}
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool

View File

@@ -29,21 +29,23 @@ final class TeamVoter extends Voter
'delete',
];
public function __construct(private RolePermissionManager $permissionManager)
public function __construct(private readonly RolePermissionManager $permissionManager)
{
}
public function supportsAttribute(string $attribute): bool
{
return \in_array($attribute, self::ALLOWED_ATTRIBUTES, true);
}
public function supportsType(string $subjectType): bool
{
return str_contains($subjectType, Team::class);
}
protected function supports(string $attribute, mixed $subject): bool
{
if (!($subject instanceof Team)) {
return false;
}
if (!\in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
return false;
}
return true;
return $subject instanceof Team && $this->supportsAttribute($attribute);
}
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool

View File

@@ -11,6 +11,7 @@ namespace App\Voter;
use App\Entity\Timesheet;
use App\Entity\User;
use App\Form\Model\MultiUserTimesheet;
use App\Security\RolePermissionManager;
use App\Timesheet\LockdownService;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
@@ -55,21 +56,26 @@ final class TimesheetVoter extends Voter
private ?bool $editExported = null;
private ?\DateTime $now = null;
public function __construct(private RolePermissionManager $permissionManager, private LockdownService $lockdownService)
public function __construct(
private readonly RolePermissionManager $permissionManager,
private readonly LockdownService $lockdownService
)
{
}
public function supportsAttribute(string $attribute): bool
{
return \in_array($attribute, self::ALLOWED_ATTRIBUTES, true);
}
public function supportsType(string $subjectType): bool
{
return str_contains($subjectType, Timesheet::class) || str_contains($subjectType, MultiUserTimesheet::class);
}
protected function supports(string $attribute, mixed $subject): bool
{
if (!($subject instanceof Timesheet)) {
return false;
}
if (!\in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
return false;
}
return true;
return $subject instanceof Timesheet && $this->supportsAttribute($attribute);
}
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool

View File

@@ -38,21 +38,23 @@ final class UserVoter extends Voter
'supervisor',
];
public function __construct(private RolePermissionManager $permissionManager)
public function __construct(private readonly RolePermissionManager $permissionManager)
{
}
public function supportsAttribute(string $attribute): bool
{
return \in_array($attribute, self::ALLOWED_ATTRIBUTES, true);
}
public function supportsType(string $subjectType): bool
{
return str_contains($subjectType, User::class);
}
protected function supports(string $attribute, mixed $subject): bool
{
if (!($subject instanceof User)) {
return false;
}
if (!\in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
return false;
}
return true;
return $subject instanceof User && $this->supportsAttribute($attribute);
}
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool

View File

@@ -25,7 +25,7 @@ use DateTimeInterface;
*/
class DailyWorkingTimeChartProvider
{
public function __construct(private TimesheetRepository $repository)
public function __construct(private readonly TimesheetRepository $repository)
{
}
@@ -33,9 +33,6 @@ class DailyWorkingTimeChartProvider
* In case this method is called with one timezone and the results are from another timezone,
* it might return rows outside the time-range.
*
* @param DateTimeInterface $begin
* @param DateTimeInterface $end
* @param User|null $user
* @return array<mixed>
*/
protected function getDailyData(DateTimeInterface $begin, DateTimeInterface $end, ?User $user = null): array

View File

@@ -144,10 +144,10 @@ final class WorkingTimeService
->select('COALESCE(SUM(t.duration), 0) as duration')
->addSelect('DATE(t.date) as day')
->where($qb->expr()->isNotNull('t.end'))
->andWhere($qb->expr()->between('t.begin', ':begin', ':end'))
->andWhere($qb->expr()->between('t.date', ':begin', ':end'))
->andWhere($qb->expr()->eq('t.user', ':user'))
->setParameter('begin', $begin)
->setParameter('end', $end)
->setParameter('begin', $begin->format('Y-m-d'))
->setParameter('end', $end->format('Y-m-d'))
->setParameter('user', $user->getId())
->addGroupBy('day')
;

View File

@@ -20,18 +20,6 @@
"ref": "56eaa387b5e48ebcc7c95a893b47dfa1ad51449c"
}
},
"doctrine/annotations": {
"version": "1.13",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "master",
"version": "1.0",
"ref": "a2759dd6123694c8d901d0ec80006e044c2e6457"
},
"files": [
"config/routes/annotations.yaml"
]
},
"doctrine/cache": {
"version": "v1.7.1"
},

View File

@@ -14,7 +14,7 @@
{% import "project/actions.html.twig" as projectActions %}
{% block box_attributes %}id="activity_details_box"{% endblock %}
{% block box_title %}
{{ widgets.label_name(activity.name, activity.color) }}
{{ widgets.label_dot(activity.name, activity.color) }}
{% endblock %}
{% block box_tools %}
{% if can_edit %}

View File

@@ -8,7 +8,7 @@
{% import "macros/widgets.html.twig" as widgets %}
{% block box_attributes %}id="customer_details_box"{% endblock %}
{% block box_title %}
{{ widgets.label_customer(customer) }}
{{ widgets.label_dot(customer.name, customer.color) }}
{% if customer.company is not empty %} &ndash; {{ customer.company }}{% endif %}
{% endblock %}
{% block box_tools %}

View File

@@ -176,7 +176,6 @@
{% set format = locale_format('date')|js_format %}
{% set attr = attr|merge({'data-daterangepicker': 'on', 'autocomplete': 'off', 'data-format': format, 'placeholder': format ~ attr['data-separator'] ~ format}) -%}
<div class="input-group">
{{ block('form_widget_simple') }}
{% if ranges is defined %}
<button type="button" class="input-group-text dropdown-toggle btn-duration-preset" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></button>
<div class="dropdown-menu dropdown-menu-start pre-scrollable">
@@ -185,6 +184,7 @@
{% endfor %}
</div>
{% endif %}
{{ block('form_widget_simple') }}
</div>
{% endblock daterange_widget %}

View File

@@ -1,7 +1,6 @@
{% macro init_frontend_loader() %}
{% set configurations = javascript_configurations(app.user)|merge({
login: path('login'),
locale: app.request.locale,
direction: tabler_bundle.rightToLeft ? 'rtl' : 'ltr',
first_dow_iso: iso_day_by_name(app.user.firstDayOfWeek),
}) %}

View File

@@ -14,7 +14,7 @@
{% endif %}
{% endblock %}
{% block box_title %}
{{ widgets.label_name(project.name, project.color) }}
{{ widgets.label_dot(project.name, project.color) }}
{% endblock %}
{% block box_body_class %}p-0{% endblock %}
{% block box_body %}

View File

@@ -168,7 +168,10 @@ class ActivityControllerTest extends APIControllerBaseTest
}
$this->assertAccessIsGranted($client, $url, 'GET', $parameters);
$result = json_decode($client->getResponse()->getContent(), true);
$content = $client->getResponse()->getContent();
$this->assertIsString($content);
$result = json_decode($content, true);
$this->assertIsArray($result);
$this->assertNotEmpty($result);
@@ -208,7 +211,10 @@ class ActivityControllerTest extends APIControllerBaseTest
$query = ['order' => 'ASC', 'orderBy' => 'project'];
$this->assertAccessIsGranted($client, '/api/activities', 'GET', $query);
$result = json_decode($client->getResponse()->getContent(), true);
$content = $client->getResponse()->getContent();
$this->assertIsString($content);
$result = json_decode($content, true);
$this->assertIsArray($result);
$this->assertNotEmpty($result);
@@ -219,11 +225,19 @@ class ActivityControllerTest extends APIControllerBaseTest
$this->assertEquals($imports[1]->getId(), $result[2]['project']);
}
public function testGetEntityIsSecure(): void
{
$this->assertUrlIsSecuredForRole(User::ROLE_USER, '/api/activities/1');
}
public function testGetEntity(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->assertAccessIsGranted($client, '/api/activities/1');
$result = json_decode($client->getResponse()->getContent(), true);
$content = $client->getResponse()->getContent();
$this->assertIsString($content);
$result = json_decode($content, true);
$this->assertIsArray($result);
self::assertApiResponseTypeStructure('ActivityEntity', $result);
@@ -247,7 +261,10 @@ class ActivityControllerTest extends APIControllerBaseTest
$this->request($client, '/api/activities', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$content = $client->getResponse()->getContent();
$this->assertIsString($content);
$result = json_decode($content, true);
$this->assertIsArray($result);
self::assertApiResponseTypeStructure('ActivityEntity', $result);
$this->assertNotEmpty($result['id']);
@@ -262,7 +279,10 @@ class ActivityControllerTest extends APIControllerBaseTest
$this->request($client, '/api/activities', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$content = $client->getResponse()->getContent();
$this->assertIsString($content);
$result = json_decode($content, true);
$this->assertIsArray($result);
self::assertApiResponseTypeStructure('ActivityEntity', $result);
$this->assertNotEmpty($result['id']);
@@ -308,7 +328,10 @@ class ActivityControllerTest extends APIControllerBaseTest
$this->request($client, '/api/activities/1', 'PATCH', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$content = $client->getResponse()->getContent();
$this->assertIsString($content);
$result = json_decode($content, true);
$this->assertIsArray($result);
self::assertApiResponseTypeStructure('ActivityEntity', $result);
$this->assertNotEmpty($result['id']);
@@ -330,7 +353,10 @@ class ActivityControllerTest extends APIControllerBaseTest
$this->request($client, '/api/activities/' . $imports[2]->getId(), 'PATCH', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$content = $client->getResponse()->getContent();
$this->assertIsString($content);
$result = json_decode($content, true);
$this->assertIsArray($result);
self::assertApiResponseTypeStructure('ActivityEntity', $result);
$this->assertNotEmpty($result['id']);

View File

@@ -96,7 +96,10 @@ class CustomerControllerTest extends APIControllerBaseTest
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->assertAccessIsGranted($client, '/api/customers');
$result = json_decode($client->getResponse()->getContent(), true);
$content = $client->getResponse()->getContent();
$this->assertIsString($content);
$result = json_decode($content, true);
$this->assertIsArray($result);
$this->assertNotEmpty($result);
@@ -109,7 +112,10 @@ class CustomerControllerTest extends APIControllerBaseTest
$query = ['order' => 'ASC', 'orderBy' => 'name', 'visible' => 3, 'term' => 'test'];
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->assertAccessIsGranted($client, '/api/customers', 'GET', $query);
$result = json_decode($client->getResponse()->getContent(), true);
$content = $client->getResponse()->getContent();
$this->assertIsString($content);
$result = json_decode($content, true);
$this->assertIsArray($result);
$this->assertNotEmpty($result);
@@ -117,11 +123,19 @@ class CustomerControllerTest extends APIControllerBaseTest
self::assertApiResponseTypeStructure('CustomerCollection', $result[0]);
}
public function testGetEntityIsSecure(): void
{
$this->assertUrlIsSecuredForRole(User::ROLE_USER, '/api/customers/1');
}
public function testGetEntity(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->assertAccessIsGranted($client, '/api/customers/1');
$result = json_decode($client->getResponse()->getContent(), true);
$content = $client->getResponse()->getContent();
$this->assertIsString($content);
$result = json_decode($content, true);
$this->assertIsArray($result);
self::assertApiResponseTypeStructure('CustomerEntity', $result);
@@ -129,7 +143,7 @@ class CustomerControllerTest extends APIControllerBaseTest
public function testGetEntityWithFullResponse(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$em = $this->getEntityManager();
@@ -165,7 +179,10 @@ class CustomerControllerTest extends APIControllerBaseTest
$em->flush();
$this->assertAccessIsGranted($client, '/api/customers/1');
$result = json_decode($client->getResponse()->getContent(), true);
$content = $client->getResponse()->getContent();
$this->assertIsString($content);
$result = json_decode($content, true);
$this->assertIsArray($result);
self::assertApiResponseTypeStructure('CustomerEntity', $result);
@@ -191,7 +208,10 @@ class CustomerControllerTest extends APIControllerBaseTest
$this->request($client, '/api/customers', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$content = $client->getResponse()->getContent();
$this->assertIsString($content);
$result = json_decode($content, true);
$this->assertIsArray($result);
self::assertApiResponseTypeStructure('CustomerEntity', $result);
$this->assertNotEmpty($result['id']);
@@ -209,7 +229,10 @@ class CustomerControllerTest extends APIControllerBaseTest
$this->request($client, '/api/customers', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$content = $client->getResponse()->getContent();
$this->assertIsString($content);
$result = json_decode($content, true);
$this->assertIsArray($result);
self::assertApiResponseTypeStructure('CustomerEntity', $result);
$this->assertNotEmpty($result['id']);
@@ -262,7 +285,10 @@ class CustomerControllerTest extends APIControllerBaseTest
$this->request($client, '/api/customers/1', 'PATCH', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$content = $client->getResponse()->getContent();
$this->assertIsString($content);
$result = json_decode($content, true);
$this->assertIsArray($result);
self::assertApiResponseTypeStructure('CustomerEntity', $result);
$this->assertNotEmpty($result['id']);

View File

@@ -97,7 +97,10 @@ class ProjectControllerTest extends APIControllerBaseTest
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->assertAccessIsGranted($client, '/api/projects');
$result = json_decode($client->getResponse()->getContent(), true);
$content = $client->getResponse()->getContent();
$this->assertIsString($content);
$result = json_decode($content, true);
$this->assertIsArray($result);
$this->assertNotEmpty($result);
@@ -206,7 +209,10 @@ class ProjectControllerTest extends APIControllerBaseTest
}
$this->assertAccessIsGranted($client, $url, 'GET', $parameters);
$result = json_decode($client->getResponse()->getContent(), true);
$content = $client->getResponse()->getContent();
$this->assertIsString($content);
$result = json_decode($content, true);
$this->assertIsArray($result);
$this->assertEquals(\count($expected), \count($result), 'Found wrong amount of projects');
@@ -242,9 +248,27 @@ class ProjectControllerTest extends APIControllerBaseTest
yield ['/api/projects', 1, ['customers' => ['2', '2'], 'visible' => VisibilityInterface::SHOW_HIDDEN, 'start' => '2010-12-11', 'end' => '2030-12-11'], []];
}
public function testGetEntityIsSecure(): void
{
$this->assertUrlIsSecuredForRole(User::ROLE_USER, '/api/projects/1');
}
public function testGetEntity(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->assertAccessIsGranted($client, '/api/projects/1');
$content = $client->getResponse()->getContent();
$this->assertIsString($content);
$result = json_decode($content, true);
$this->assertIsArray($result);
self::assertApiResponseTypeStructure('ProjectEntity', $result);
}
public function testGetEntityComplex(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$em = $this->getEntityManager();
$customer = new Customer('first one');
@@ -268,7 +292,10 @@ class ProjectControllerTest extends APIControllerBaseTest
$em->flush();
$this->assertAccessIsGranted($client, '/api/projects/' . $project->getId());
$result = json_decode($client->getResponse()->getContent(), true);
$content = $client->getResponse()->getContent();
$this->assertIsString($content);
$result = json_decode($content, true);
$this->assertIsArray($result);
self::assertApiResponseTypeStructure('ProjectEntity', $result);
@@ -318,7 +345,10 @@ class ProjectControllerTest extends APIControllerBaseTest
$this->request($client, '/api/projects', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$content = $client->getResponse()->getContent();
$this->assertIsString($content);
$result = json_decode($content, true);
$this->assertIsArray($result);
self::assertApiResponseTypeStructure('ProjectEntity', $result);
$this->assertNotEmpty($result['id']);
@@ -344,7 +374,10 @@ class ProjectControllerTest extends APIControllerBaseTest
$this->request($client, '/api/projects', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$content = $client->getResponse()->getContent();
$this->assertIsString($content);
$result = json_decode($content, true);
$this->assertIsArray($result);
self::assertApiResponseTypeStructure('ProjectEntity', $result);
$this->assertNotEmpty($result['id']);
@@ -367,7 +400,10 @@ class ProjectControllerTest extends APIControllerBaseTest
$this->request($client, '/api/projects', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$content = $client->getResponse()->getContent();
$this->assertIsString($content);
$result = json_decode($content, true);
$this->assertIsArray($result);
self::assertApiResponseTypeStructure('ProjectEntity', $result);
$this->assertNotEmpty($result['id']);
@@ -390,7 +426,10 @@ class ProjectControllerTest extends APIControllerBaseTest
$this->request($client, '/api/projects', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$content = $client->getResponse()->getContent();
$this->assertIsString($content);
$result = json_decode($content, true);
$this->assertIsArray($result);
self::assertApiResponseTypeStructure('ProjectEntity', $result);
$this->assertNotEmpty($result['id']);
@@ -410,7 +449,10 @@ class ProjectControllerTest extends APIControllerBaseTest
$this->request($client, '/api/projects', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$content = $client->getResponse()->getContent();
$this->assertIsString($content);
$result = json_decode($content, true);
$this->assertIsArray($result);
self::assertApiResponseTypeStructure('ProjectEntity', $result);
$this->assertNotEmpty($result['id']);
@@ -461,7 +503,10 @@ class ProjectControllerTest extends APIControllerBaseTest
$this->request($client, '/api/projects/1', 'PATCH', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$content = $client->getResponse()->getContent();
$this->assertIsString($content);
$result = json_decode($content, true);
$this->assertIsArray($result);
self::assertApiResponseTypeStructure('ProjectEntity', $result);
$this->assertNotEmpty($result['id']);

View File

@@ -20,20 +20,14 @@ use App\Entity\User;
*/
trait RateControllerTestTrait
{
/**
* @param string|int $id
* @param string|int|null $rateId
* @return string
*/
abstract protected function getRateUrl($id = '1', $rateId = null): string;
abstract protected function getRateUrl(string|int $id = '1', string|int|null $rateId = null): string;
abstract protected function getRateUrlByRate(RateInterface $rate, bool $isCollection): string;
/**
* @param string|int $id
* @return RateInterface[]
*/
abstract protected function importTestRates($id): array;
abstract protected function importTestRates(string|int $id): array;
public function testAddRateMissingEntityAction(): void
{
@@ -89,7 +83,10 @@ trait RateControllerTestTrait
$this->request($client, $this->getRateUrl(), 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$content = $client->getResponse()->getContent();
$this->assertIsString($content);
$result = json_decode($content, true);
$this->assertIsArray($result);
$this->assertRateStructure($result, null);
$this->assertNotEmpty($result['id']);
@@ -110,7 +107,10 @@ trait RateControllerTestTrait
$this->request($client, $this->getRateUrl(), 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$content = $client->getResponse()->getContent();
$this->assertIsString($content);
$result = json_decode($content, true);
$this->assertIsArray($result);
$this->assertRateStructure($result, 1);
$this->assertNotEmpty($result['id']);
@@ -125,7 +125,10 @@ trait RateControllerTestTrait
$this->request($client, $this->getRateUrl(1));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$content = $client->getResponse()->getContent();
$this->assertIsString($content);
$result = json_decode($content, true);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
@@ -138,7 +141,10 @@ trait RateControllerTestTrait
$this->request($client, $this->getRateUrl(1));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$content = $client->getResponse()->getContent();
$this->assertIsString($content);
$result = json_decode($content, true);
$this->assertIsArray($result);
$this->assertNotEmpty($result);
$this->assertEquals(\count($expectedRates), \count($result));
@@ -171,7 +177,10 @@ trait RateControllerTestTrait
$this->request($client, $this->getRateUrlByRate($expectedRates[0], true));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$content = $client->getResponse()->getContent();
$this->assertIsString($content);
$result = json_decode($content, true);
$this->assertIsArray($result);
$this->assertEquals(\count($expectedRates) - 1, \count($result));
}

View File

@@ -118,7 +118,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]);
}
public function testGetCollectionForAllUser(): void
public function testGetCollectionForAllUserIsSecure(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
$this->importFixtureForUser(User::ROLE_USER);
@@ -136,6 +136,53 @@ class TimesheetControllerTest extends APIControllerBaseTest
self::assertIsString($content);
$result = json_decode($content, true);
self::assertIsArray($result);
self::assertEmpty($result);
self::assertEquals(0, \count($result));
}
public function testGetCollectionForAllUserIsSecureForUser(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->importFixtureForUser(User::ROLE_USER);
$fixture = new TimesheetFixtures($this->getUserByRole(User::ROLE_ADMIN), 7);
$fixture->setFixedRate(true);
$fixture->setHourlyRate(true);
$fixture->setStartDate(new \DateTime('-10 days'));
$this->importFixture($fixture);
$query = ['user' => 'all'];
$this->assertAccessIsGranted($client, '/api/timesheets', 'GET', $query);
$content = $client->getResponse()->getContent();
self::assertIsString($content);
$result = json_decode($content, true);
self::assertIsArray($result);
self::assertNotEmpty($result);
self::assertEquals(10, \count($result));
self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]);
}
public function testGetCollectionForAllUser(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->importFixtureForUser(User::ROLE_USER);
$fixture = new TimesheetFixtures($this->getUserByRole(User::ROLE_SUPER_ADMIN), 7);
$fixture->setFixedRate(true);
$fixture->setHourlyRate(true);
$fixture->setStartDate(new \DateTime('-10 days'));
$this->importFixture($fixture);
$query = ['user' => 'all'];
$this->assertAccessIsGranted($client, '/api/timesheets', 'GET', $query);
$content = $client->getResponse()->getContent();
self::assertIsString($content);
$result = json_decode($content, true);
self::assertIsArray($result);
self::assertNotEmpty($result);
self::assertEquals(17, \count($result));

View File

@@ -19,9 +19,9 @@ trait EntityValidationTestTrait
{
/**
* @param object $entity
* @param array|string $fieldNames
* @param array<string>|string $fieldNames
*/
public function assertHasViolationForField(object $entity, $fieldNames, $groups = null): void
public function assertHasViolationForField(object $entity, array|string $fieldNames, $groups = null): void
{
self::bootKernel();
/** @var ValidatorInterface $validator */

View File

@@ -24,7 +24,7 @@ class TimesheetValidationTest extends KernelTestCase
{
use EntityValidationTestTrait;
protected function getEntity()
protected function getEntity(): Timesheet
{
$customer = new Customer('Test Customer');

View File

@@ -61,6 +61,8 @@ class ConfigurableNumberGeneratorTest extends TestCase
public function getTestData(): array
{
$invoiceDate = new \DateTime();
$invoiceDateFixed = new \DateTime('2021-03-07');
$invoiceDateFixed1 = new \DateTime('2021-03-27');
return [
// simple tests for single calls
@@ -109,6 +111,7 @@ class ConfigurableNumberGeneratorTest extends TestCase
['{c+13,3}', '014', $invoiceDate],
['{c+13,2}', '14', $invoiceDate],
['{c+13}', '14', $invoiceDate],
['{c-20}', '0', $invoiceDate],
['{cd+111}', '112', $invoiceDate],
['{cd+111,5}', '00112', $invoiceDate],
['{cd+111,2}', '113', $invoiceDate, 2],
@@ -128,7 +131,7 @@ class ConfigurableNumberGeneratorTest extends TestCase
['{Y,5}/{cy,5}', '0' . $invoiceDate->format('Y') . '/00002', $invoiceDate],
// with decrementing counter
['{c-1,2}', '00', $invoiceDate],
['{c-2,2}', '-1', $invoiceDate],
['{c-2,2}', '00', $invoiceDate],
// with incrementing and decrementing counter
['{c-5+13,1}', '9', $invoiceDate],
['{c+13-5,2}', '09', $invoiceDate],
@@ -142,6 +145,43 @@ class ConfigurableNumberGeneratorTest extends TestCase
['{cm+22+2+2-1-13-4}', '9', $invoiceDate],
['{cm+22+2-2-22}', '6', $invoiceDate, 5],
['{cm-21+22+2-2}', '6', $invoiceDate, 5],
// increment dates
['{YY}', '2022', $invoiceDateFixed],
['{YY+1}', '2022', $invoiceDateFixed],
['{YY+2}', '2023', $invoiceDateFixed],
['{YY+3}', '2024', $invoiceDateFixed],
['{YY-1}', '2020', $invoiceDateFixed],
['{YY-2}', '2019', $invoiceDateFixed],
['{YY-3}', '2018', $invoiceDateFixed],
['{yy}', '22', $invoiceDateFixed],
['{yy+1}', '22', $invoiceDateFixed],
['{yy+2}', '23', $invoiceDateFixed],
['{yy+3}', '24', $invoiceDateFixed],
['{yy-1}', '20', $invoiceDateFixed],
['{yy-2}', '19', $invoiceDateFixed],
['{yy-3}', '18', $invoiceDateFixed],
['{MM}', '4', $invoiceDateFixed], // cast to int removes leading zero
['{MM+1}', '4', $invoiceDateFixed], // cast to int removes leading zero
['{MM+2}', '5', $invoiceDateFixed], // cast to int removes leading zero
['{MM+3}', '6', $invoiceDateFixed], // cast to int removes leading zero
['{MM-1}', '2', $invoiceDateFixed], // cast to int removes leading zero
['{MM-2}', '1', $invoiceDateFixed], // cast to int removes leading zero
['{MM-3}', '0', $invoiceDateFixed], // cast to int removes leading zero
['{MM-4}', '0', $invoiceDateFixed], // cast to int removes leading zero
['{DD}', '8', $invoiceDateFixed], // cast to int removes leading zero
['{DD+1}', '8', $invoiceDateFixed], // cast to int removes leading zero
['{DD+2}', '9', $invoiceDateFixed], // cast to int removes leading zero
['{DD+3}', '10', $invoiceDateFixed], // cast to int removes leading zero
['{DD-1}', '6', $invoiceDateFixed], // cast to int removes leading zero
['{DD-2}', '5', $invoiceDateFixed], // cast to int removes leading zero
['{DD-3}', '4', $invoiceDateFixed], // cast to int removes leading zero
['{DD}', '28', $invoiceDateFixed1], // cast to int removes leading zero
['{DD+1}', '28', $invoiceDateFixed1], // cast to int removes leading zero
['{DD+2}', '29', $invoiceDateFixed1], // cast to int removes leading zero
['{DD+3}', '30', $invoiceDateFixed1], // cast to int removes leading zero
['{DD-1}', '26', $invoiceDateFixed1], // cast to int removes leading zero
['{DD-2}', '25', $invoiceDateFixed1], // cast to int removes leading zero
['{DD-3}', '24', $invoiceDateFixed1], // cast to int removes leading zero
];
}

View File

@@ -463,6 +463,8 @@ class LocaleFormatExtensionsTest extends TestCase
'twentyFourHours' => false,
'updateBrowserTitle' => false,
'timezone' => 'America/Edmonton',
'locale' => 'en',
'language' => 'de',
'user' => [
'id' => 1,
'name' => null,
@@ -473,6 +475,7 @@ class LocaleFormatExtensionsTest extends TestCase
$user = $this->createMock(User::class);
$user->method('getId')->willReturn(1);
$user->method('getName')->willReturn('Testing');
$user->method('getLanguage')->willReturn('de');
$user->method('isAdmin')->willReturn(false);
$user->method('isSuperAdmin')->willReturn(false);
$user->method('getTimezone')->willReturn('America/Edmonton');

View File

@@ -121,7 +121,7 @@ class TimesheetBudgetUsedValidatorTest extends ConstraintValidatorTestCase
{
$this->expectException(UnexpectedTypeException::class);
$this->validator->validate('foo', new TimesheetBudgetUsed()); // @phpstan-ignore-line
$this->validator->validate('foo', new TimesheetBudgetUsed());
}
public function testWithMissingEnd(): void

View File

@@ -59,7 +59,7 @@ class TimesheetExportedValidatorTest extends ConstraintValidatorTestCase
{
$this->expectException(UnexpectedTypeException::class);
$this->validator->validate(new NotBlank(), new TimesheetExported(['message' => 'myMessage'])); // @phpstan-ignore-line
$this->validator->validate(new NotBlank(), new TimesheetExported(['message' => 'myMessage']));
}
public function testTriggersOnMissingPermission(): void

View File

@@ -60,7 +60,7 @@ class TimesheetFutureTimesValidatorTest extends ConstraintValidatorTestCase
{
$this->expectException(UnexpectedTypeException::class);
$this->validator->validate(new NotBlank(), new TimesheetFutureTimes(['message' => 'myMessage'])); // @phpstan-ignore-line
$this->validator->validate(new NotBlank(), new TimesheetFutureTimes(['message' => 'myMessage']));
}
public function testFutureBeginIsDisallowed(): void

View File

@@ -75,7 +75,7 @@ class TimesheetLockdownValidatorTest extends ConstraintValidatorTestCase
{
$this->expectException(UnexpectedTypeException::class);
$this->validator->validate(new NotBlank(), new TimesheetLockdown(['message' => 'myMessage'])); // @phpstan-ignore-line
$this->validator->validate(new NotBlank(), new TimesheetLockdown(['message' => 'myMessage']));
}
public function testValidatorWithoutNowConstraint(): void

View File

@@ -55,7 +55,7 @@ class TimesheetLongRunningValidatorTest extends ConstraintValidatorTestCase
{
$this->expectException(UnexpectedTypeException::class);
$this->validator->validate(new NotBlank(), new TimesheetLongRunning(['message' => 'myMessage'])); // @phpstan-ignore-line
$this->validator->validate(new NotBlank(), new TimesheetLongRunning(['message' => 'myMessage']));
}
public function testLongRunningTriggers(): void

View File

@@ -58,7 +58,7 @@ class TimesheetOverlappingValidatorTest extends ConstraintValidatorTestCase
{
$this->expectException(UnexpectedTypeException::class);
$this->validator->validate(new NotBlank(), new TimesheetOverlapping(['message' => 'myMessage'])); // @phpstan-ignore-line
$this->validator->validate(new NotBlank(), new TimesheetOverlapping(['message' => 'myMessage']));
}
public function testOverlappingDisallowedWithRecords(): void

View File

@@ -57,7 +57,7 @@ class TimesheetRestartValidatorTest extends ConstraintValidatorTestCase
{
$this->expectException(UnexpectedTypeException::class);
$this->validator->validate(new NotBlank(), new TimesheetOverlapping(['message' => 'myMessage'])); // @phpstan-ignore-line
$this->validator->validate(new NotBlank(), new TimesheetOverlapping(['message' => 'myMessage']));
}
/**

View File

@@ -44,6 +44,6 @@ class TimesheetValidatorTest extends ConstraintValidatorTestCase
{
$this->expectException(UnexpectedTypeException::class);
$this->validator->validate(new NotBlank(), new TimesheetConstraint(['message' => 'myMessage'])); // @phpstan-ignore-line
$this->validator->validate(new NotBlank(), new TimesheetConstraint(['message' => 'myMessage']));
}
}

View File

@@ -55,7 +55,7 @@ class TimesheetZeroDurationValidatorTest extends ConstraintValidatorTestCase
{
$this->expectException(UnexpectedTypeException::class);
$this->validator->validate(new NotBlank(), new TimesheetZeroDuration(['message' => 'Duration cannot be zero.'])); // @phpstan-ignore-line
$this->validator->validate(new NotBlank(), new TimesheetZeroDuration(['message' => 'Duration cannot be zero.']));
}
private function prepareTimesheet()

View File

@@ -202,11 +202,6 @@ parameters:
count: 1
path: API/ActivityControllerTest.php
-
message: "#^Parameter \\#1 \\$json of function json_decode expects string, string\\|false given\\.$#"
count: 12
path: API/ActivityControllerTest.php
-
message: "#^Parameter \\#5 \\$content of method App\\\\Tests\\\\API\\\\APIControllerBaseTest\\:\\:request\\(\\) expects string\\|null, string\\|false given\\.$#"
count: 13
@@ -297,11 +292,6 @@ parameters:
count: 1
path: API/CustomerControllerTest.php
-
message: "#^Parameter \\#1 \\$json of function json_decode expects string, string\\|false given\\.$#"
count: 12
path: API/CustomerControllerTest.php
-
message: "#^Parameter \\#5 \\$content of method App\\\\Tests\\\\API\\\\APIControllerBaseTest\\:\\:request\\(\\) expects string\\|null, string\\|false given\\.$#"
count: 13
@@ -382,11 +372,6 @@ parameters:
count: 1
path: API/ProjectControllerTest.php
-
message: "#^Parameter \\#1 \\$json of function json_decode expects string, string\\|false given\\.$#"
count: 14
path: API/ProjectControllerTest.php
-
message: "#^Parameter \\#5 \\$content of method App\\\\Tests\\\\API\\\\APIControllerBaseTest\\:\\:request\\(\\) expects string\\|null, string\\|false given\\.$#"
count: 15
@@ -1657,21 +1642,11 @@ parameters:
count: 1
path: Entity/TimesheetValidationTest.php
-
message: "#^Method App\\\\Tests\\\\Entity\\\\TimesheetValidationTest\\:\\:assertHasViolationForField\\(\\) has parameter \\$fieldNames with no value type specified in iterable type array\\.$#"
count: 1
path: Entity/TimesheetValidationTest.php
-
message: "#^Method App\\\\Tests\\\\Entity\\\\TimesheetValidationTest\\:\\:assertHasViolationForField\\(\\) has parameter \\$groups with no type specified\\.$#"
count: 1
path: Entity/TimesheetValidationTest.php
-
message: "#^Method App\\\\Tests\\\\Entity\\\\TimesheetValidationTest\\:\\:getEntity\\(\\) has no return type specified\\.$#"
count: 1
path: Entity/TimesheetValidationTest.php
-
message: "#^Cannot access offset 0 on iterable\\<App\\\\Entity\\\\Team\\>\\.$#"
count: 1
@@ -1692,11 +1667,6 @@ parameters:
count: 1
path: Entity/UserValidationTest.php
-
message: "#^Method App\\\\Tests\\\\Entity\\\\UserValidationTest\\:\\:assertHasViolationForField\\(\\) has parameter \\$fieldNames with no value type specified in iterable type array\\.$#"
count: 1
path: Entity/UserValidationTest.php
-
message: "#^Method App\\\\Tests\\\\Entity\\\\UserValidationTest\\:\\:assertHasViolationForField\\(\\) has parameter \\$groups with no type specified\\.$#"
count: 1