Release 2.0.5 (#3888)

- Fixed: mandatory fields / form validation for invoice template
- Added: Duration as calendar title replacer (open: running records)
- Fixed: HTML injection in Calendar
- Fixed: Doctrine Proxies are not initialized (leads to empty customer/project)
- Fixed: tag creation
- Fixed: validation errors do not need to be logged in prod
- Removed: Customer VCard download, as used library is outdated and not maintained
- Added: supporting translations domains in many new places (menu, help_text, page_setup, report, exporter, calendar)
This commit is contained in:
Kevin Papst
2023-03-05 03:08:43 +01:00
committed by GitHub
parent 3b93afabc8
commit 0cbf0053d2
77 changed files with 1306 additions and 874 deletions

View File

@@ -11,14 +11,10 @@ namespace App\Calendar;
interface DragAndDropSource
{
/**
* @return string
*/
public function getTitle(): string;
/**
* @return string
*/
public function getTranslationDomain(): string;
public function getRoute(): string;
/**
@@ -31,9 +27,6 @@ interface DragAndDropSource
*/
public function getRouteReplacer(): array;
/**
* @return string
*/
public function getMethod(): string;
/**

View File

@@ -23,6 +23,11 @@ final class RecentActivitiesSource implements DragAndDropSource
return 'recent.activities';
}
public function getTranslationDomain(): string
{
return 'messages';
}
public function getRoute(): string
{
return 'post_timesheet';

View File

@@ -17,11 +17,11 @@ class Constants
/**
* The current release version
*/
public const VERSION = '2.0.4';
public const VERSION = '2.0.5';
/**
* The current release: major * 10000 + minor * 100 + patch
*/
public const VERSION_ID = 20004;
public const VERSION_ID = 20005;
/**
* The software name
*/

View File

@@ -35,16 +35,12 @@ use App\Repository\Query\CustomerQuery;
use App\Repository\Query\ProjectQuery;
use App\Repository\TeamRepository;
use App\Utils\DataTable;
use App\Utils\FileHelper;
use App\Utils\PageSetup;
use JeroenDesloovere\VCard\VCard;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\ExpressionLanguage\Expression;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Symfony\Component\Intl\Countries;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
@@ -361,82 +357,6 @@ final class CustomerController extends AbstractController
]);
}
#[Route(path: '/{id}/vcard', name: 'customer_vcard', methods: ['GET'])]
#[IsGranted('view', 'customer')]
public function downloadVCard(Customer $customer): Response
{
if ($customer->getName() === null || \strlen($customer->getName()) === 0) {
throw new \Exception('Customer name cannot be null');
}
$vcard = new VCard();
$contact = $customer->getContact();
if ($contact === null || \strlen($contact) === 0) {
$contact = $customer->getName();
}
$contact = explode(' ', $contact);
$lastname = array_pop($contact);
$firstname = \count($contact) > 0 ? $contact[0] : '';
$vcard->addName($lastname, $firstname);
$note = $customer->getComment();
if ($note !== null) {
$note .= PHP_EOL;
}
$address = $customer->getAddress();
if (!empty($note) || !empty($address)) {
$vcard->addNote($note . $address);
}
$country = $customer->getCountry();
if ($country !== null) {
$vcard->addAddress(null, null, null, null, null, null, Countries::getName($country));
}
$company = $customer->getCompany();
if ($company !== null) {
$vcard->addCompany($company);
}
$email = $customer->getEmail();
if ($email !== null) {
$vcard->addEmail($email);
}
$hasPref = false;
if ($customer->getPhone() !== null) {
$hasPref = true;
$vcard->addPhoneNumber($customer->getPhone(), 'PREF;WORK');
}
if ($customer->getMobile() !== null) {
$type = $hasPref ? 'CELL' : 'PREF;CELL';
$vcard->addPhoneNumber($customer->getMobile(), $type);
}
if ($customer->getFax() !== null) {
$vcard->addPhoneNumber($customer->getFax(), 'FAX');
}
if ($customer->getHomepage() !== null) {
$vcard->addURL($customer->getHomepage(), 'WORK');
}
$response = new Response($vcard->getOutput());
$disposition = $response->headers->makeDisposition(
ResponseHeaderBag::DISPOSITION_ATTACHMENT,
FileHelper::convertToAsciiFilename($customer->getName()) . '.vcf'
);
$response->headers->set('Content-Disposition', $disposition);
return $response;
}
#[Route(path: '/{id}/rate/{rate}', name: 'admin_customer_rate_edit', methods: ['GET', 'POST'])]
#[IsGranted('edit', 'customer')]
public function editRateAction(Customer $customer, CustomerRate $rate, Request $request, CustomerRateRepository $repository): Response

View File

@@ -65,8 +65,8 @@ final class ExportController extends AbstractController
];
}
$byCustomer[$cid]['rate'] += $entry->getRate();
$byCustomer[$cid]['internalRate'] += $entry->getInternalRate();
$byCustomer[$cid]['duration'] += $entry->getDuration();
$byCustomer[$cid]['internalRate'] += $entry->getInternalRate() ?? 0.0;
$byCustomer[$cid]['duration'] += $entry->getDuration() ?? 0;
}
} catch (TooManyItemsExportException $ex) {
$tooManyResults = true;

View File

@@ -91,7 +91,6 @@ final class TeamFixtures extends Fixture
$team = new Team($faker->company() . ' ' . $i);
$team->addTeamlead($allUsers[array_rand($allUsers)]);
/* @phpstan-ignore-next-line */
if ($userCount > 0) {
$userKeys = array_rand($allUsers, $userCount);
if (!\is_array($userKeys)) {

View File

@@ -69,7 +69,7 @@ final class AppExtension extends Extension
}
// this should happen always at the end, so bundles do not mess with the base configuration
if ($container->hasParameter('kimai.bundles.config')) { // @phpstan-ignore-line
if ($container->hasParameter('kimai.bundles.config')) {
$bundleConfig = $container->getParameter('kimai.bundles.config');
if (!\is_array($bundleConfig)) {
throw new \Exception('Invalid bundle configuration found, skipping all bundle configuration');

View File

@@ -42,7 +42,7 @@ trait CommentTableTypeTrait
return $this->message;
}
public function setMessage(string $message)
public function setMessage(string $message): void
{
$this->message = $message;
}
@@ -52,7 +52,7 @@ trait CommentTableTypeTrait
return $this->createdBy;
}
public function setCreatedBy(User $createdBy)
public function setCreatedBy(User $createdBy): void
{
$this->createdBy = $createdBy;
}
@@ -62,7 +62,7 @@ trait CommentTableTypeTrait
return $this->createdAt;
}
public function setCreatedAt(\DateTime $createdAt)
public function setCreatedAt(\DateTime $createdAt): void
{
$this->createdAt = $createdAt;
}
@@ -72,7 +72,7 @@ trait CommentTableTypeTrait
return $this->pinned;
}
public function setPinned(bool $pinned)
public function setPinned(bool $pinned): void
{
$this->pinned = $pinned;
}

View File

@@ -190,6 +190,16 @@ class Invoice implements EntityWithMetaFields
public function setModel(InvoiceModel $model): Invoice
{
$template = $model->getTemplate();
if ($template === null) {
throw new \InvalidArgumentException('Missing invoice template');
}
if ($template->getDueDays() === null || $template->getVat() === null) {
throw new \InvalidArgumentException('Missing due-days or vat setting');
}
$this->customer = $model->getCustomer();
$this->user = $model->getUser();
$this->total = $model->getCalculator()->getTotal();
@@ -201,7 +211,6 @@ class Invoice implements EntityWithMetaFields
$this->createdAt = $createdAt;
$this->timezone = $createdAt->getTimezone()->getName();
$template = $model->getTemplate();
$this->dueDays = $template->getDueDays();
$this->vat = $template->getVat();

View File

@@ -42,11 +42,13 @@ class InvoiceTemplate
#[ORM\Column(name: 'contact', type: 'text', nullable: true)]
private ?string $contact = null;
#[ORM\Column(name: 'due_days', type: 'integer', length: 3, nullable: false)]
#[Assert\NotNull]
#[Assert\Range(min: 0, max: 999)]
private int $dueDays = 30;
private ?int $dueDays = 30;
#[ORM\Column(name: 'vat', type: 'float', nullable: false)]
#[Assert\NotNull]
#[Assert\Range(min: 0.0, max: 99.99)]
private float $vat = 0.00;
private ?float $vat = 0.00;
#[ORM\Column(name: 'calculator', type: 'string', length: 20, nullable: false)]
#[Assert\NotBlank]
#[Assert\Length(max: 20)]
@@ -94,7 +96,7 @@ class InvoiceTemplate
return $this->title;
}
public function setTitle(string $title): InvoiceTemplate
public function setTitle(?string $title): InvoiceTemplate
{
$this->title = $title;
@@ -125,24 +127,24 @@ class InvoiceTemplate
return $this;
}
public function getDueDays(): int
public function getDueDays(): ?int
{
return $this->dueDays;
}
public function setDueDays(int $dueDays): InvoiceTemplate
public function setDueDays(?int $dueDays): InvoiceTemplate
{
$this->dueDays = $dueDays;
return $this;
}
public function getVat(): float
public function getVat(): ?float
{
return $this->vat;
}
public function setVat(float $vat): InvoiceTemplate
public function setVat(?float $vat): InvoiceTemplate
{
$this->vat = $vat;
@@ -154,7 +156,7 @@ class InvoiceTemplate
return $this->company;
}
public function setCompany(string $company): InvoiceTemplate
public function setCompany(?string $company): InvoiceTemplate
{
$this->company = $company;

View File

@@ -47,10 +47,6 @@ final class CustomerSubscriber extends AbstractActionsSubscriber
$event->addAction('permissions', ['title' => 'permissions', 'translation_domain' => 'actions', 'url' => $this->path('admin_customer_permissions', ['id' => $customer->getId()]), 'class' => $class]);
}
if ($canView && $event->isIndexView()) {
$event->addAction('vcard', ['title' => 'vcard', 'translation_domain' => 'actions', 'icon' => 'far fa-address-card', 'url' => $this->path('customer_vcard', ['id' => $customer->getId()])]);
}
if ($isListingView) {
if ($customer->isVisible() && $this->isGranted('create_project')) {
$event->addAction('create-project', [

View File

@@ -21,7 +21,7 @@ final class InvoiceDocumentSubscriber extends AbstractActionsSubscriber
public function onActions(PageActionsEvent $event): void
{
/** @var array<string, InvoiceDocument|null|bool> $payload */
/** @var array<string, InvoiceDocument|null|bool|string> $payload */
$payload = $event->getPayload();
if (!\is_array($payload)) {
return;

View File

@@ -14,7 +14,7 @@ final class Expose
{
public string $type = 'string';
public function __construct(public ?string $name = null, public ?string $label = null, string $type = 'string', public ?string $exp = null)
public function __construct(public ?string $name = null, public ?string $label = null, string $type = 'string', public ?string $exp = null, public ?string $translationDomain = null)
{
if (!\in_array($type, ['string', 'datetime', 'date', 'time', 'integer', 'float', 'duration', 'boolean', 'array'])) {
throw new \InvalidArgumentException(sprintf('Unknown type "%s" on annotation "%s".', $type, self::class));

View File

@@ -135,7 +135,7 @@ trait RendererTrait
}
$rate = $exportItem->getRate();
$internalRate = $exportItem->getInternalRate();
$internalRate = $exportItem->getInternalRate() ?? 0;
// rate
$summary[$id]['rate'] += $rate;

View File

@@ -13,6 +13,8 @@ final class ColumnDefinition
{
private $accessor;
private string $translationDomain = 'messages';
public function __construct(private string $label, private string $type, callable $accessor)
{
$this->accessor = $accessor;
@@ -32,4 +34,14 @@ final class ColumnDefinition
{
return $this->accessor;
}
public function getTranslationDomain(): string
{
return $this->translationDomain;
}
public function setTranslationDomain(string $translationDomain): void
{
$this->translationDomain = $translationDomain;
}
}

View File

@@ -67,13 +67,19 @@ final class AnnotationExtractor implements ExtractorInterface
$parsed = $this->expressionLanguage->parse($arguments['exp'], ['object']);
$columns[$arguments['name']] = new ColumnDefinition(
$name = $arguments['name'];
$columns[$name] = new ColumnDefinition(
$arguments['label'],
$arguments['type'] ?? 'string',
function ($obj) use ($parsed) {
return $parsed->getNodes()->evaluate([], ['object' => $obj]);
}
);
if (\array_key_exists('translationDomain', $arguments) && \is_string($arguments['translationDomain'])) {
$columns[$name]->setTranslationDomain($arguments['translationDomain']);
}
}
foreach ($reflectionClass->getProperties() as $property) {
@@ -103,6 +109,10 @@ final class AnnotationExtractor implements ExtractorInterface
return $property->getValue($obj);
}
);
if (\array_key_exists('translationDomain', $arguments) && \is_string($arguments['translationDomain'])) {
$columns[$name]->setTranslationDomain($arguments['translationDomain']);
}
}
}
@@ -137,6 +147,10 @@ final class AnnotationExtractor implements ExtractorInterface
return $method->invoke($obj);
}
);
if (\array_key_exists('translationDomain', $arguments) && \is_string($arguments['translationDomain'])) {
$columns[$name]->setTranslationDomain($arguments['translationDomain']);
}
}
}

View File

@@ -66,7 +66,7 @@ class SpreadsheetExporter
$recordsHeaderRow = 1;
foreach ($columns as $settings) {
$sheet->setCellValue(CellAddress::fromColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow), $this->translator->trans($settings->getLabel()));
$sheet->setCellValue(CellAddress::fromColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow), $this->translator->trans($settings->getLabel(), [], $settings->getTranslationDomain()));
}
$entryHeaderRow = $recordsHeaderRow + 1;

View File

@@ -0,0 +1,36 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Form\Extension;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
final class HelpTranslationDomainExtension extends AbstractTypeExtension
{
public static function getExtendedTypes(): iterable
{
return [FormType::class];
}
public function buildView(FormView $view, FormInterface $form, array $options): void
{
$view->vars['help_translation_domain'] = $options['help_translation_domain'] ?? null;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefined(['help_translation_domain']);
$resolver->setAllowedTypes('help_translation_domain', ['string', 'null']);
$resolver->setDefault('help_translation_domain', null);
}
}

View File

@@ -23,6 +23,7 @@ final class CalendarTitlePatternType extends AbstractType
public const PATTERN_PROJECT = '{project}';
public const PATTERN_ACTIVITY = '{activity}';
public const PATTERN_DESCRIPTION = '{description}';
public const PATTERN_DURATION = '{duration}';
public const SPACER = ' - ';
public const PATTERN_ACTIVITY_DESCRIPTION = self::PATTERN_ACTIVITY . self::SPACER . self::PATTERN_DESCRIPTION;
public const PATTERN_PROJECT_DESCRIPTION = self::PATTERN_PROJECT . self::SPACER . self::PATTERN_DESCRIPTION;
@@ -39,6 +40,7 @@ final class CalendarTitlePatternType extends AbstractType
$project = $this->translator->trans('project');
$activity = $this->translator->trans('activity');
$description = $this->translator->trans('description');
$duration = $this->translator->trans('duration');
$resolver->setDefaults([
'label' => 'choice_pattern',
@@ -47,6 +49,7 @@ final class CalendarTitlePatternType extends AbstractType
$project => CalendarTitlePatternType::PATTERN_PROJECT,
$customer => CalendarTitlePatternType::PATTERN_CUSTOMER,
$description => CalendarTitlePatternType::PATTERN_DESCRIPTION,
$duration => CalendarTitlePatternType::PATTERN_DURATION,
$activity . self::SPACER . $description => CalendarTitlePatternType::PATTERN_ACTIVITY_DESCRIPTION,
$project . self::SPACER . $description => CalendarTitlePatternType::PATTERN_PROJECT_DESCRIPTION,
$customer . self::SPACER . $description => CalendarTitlePatternType::PATTERN_CUSTOMER_DESCRIPTION,

View File

@@ -15,6 +15,9 @@ use App\Repository\TagRepository;
use App\Utils\Color;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\Options;
@@ -25,6 +28,67 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
*/
final class TagsSelectType extends AbstractType
{
public function __construct(private TagRepository $tagRepository)
{
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($options) {
if (!$options['allow_create']) {
return;
}
$tagIds = $event->getData();
if (!\is_array($tagIds)) {
return;
}
$ids = array_filter($tagIds, function ($tagId) {
if (is_numeric($tagId)) {
return true;
}
return false;
});
// get the current tags and find the new ones that should be created
$tags = $this->tagRepository->findBy(['id' => $ids]);
$foundIds = [];
foreach ($tags as $tag) {
$foundIds[] = (string) $tag->getId();
}
$newData = [];
$newNames = [];
foreach ($tagIds as $tag) {
if (!\in_array($tag, $foundIds, true)) {
$newNames[] = $tag;
} else {
$newData[] = $tag;
}
}
// in case someone is using tags like "1234" this can interfere with the ID
$tags = $this->tagRepository->findTagsByName($newNames);
$foundTagNames = [];
foreach ($tags as $tag) {
$newData[] = (string) $tag->getId();
$foundTagNames[] = $tag->getName();
}
/** @var array<string> $newNames */
$newNames = array_diff($newNames, $foundTagNames);
foreach ($newNames as $name) {
$tag = new Tag();
$tag->setName($name);
$this->tagRepository->saveTag($tag);
$newData[] = $tag->getId();
}
$event->setData($newData);
}, 1000);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([

View File

@@ -16,8 +16,10 @@ use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
final class TagsType extends AbstractType
{
public function __construct(private AuthorizationCheckerInterface $auth, private TagRepository $repository)
{
public function __construct(
private AuthorizationCheckerInterface $auth,
private TagRepository $repository
) {
}
public function configureOptions(OptionsResolver $resolver): void

View File

@@ -52,8 +52,8 @@ final class TimePickerType extends AbstractType
// DateTimePickerType
if ($options['input'] === 'array' && \is_array($data)) {
$now = new \DateTime('now', new \DateTimeZone($options['model_timezone']));
$hour = $data['hour'] === '' ? 0 : (int) $data['hour'];
$minute = $data['minute'] === '' ? 0 : (int) $data['minute'];
$hour = $data['hour'] === '' || !is_numeric($data['hour']) ? 0 : (int) $data['hour'];
$minute = $data['minute'] === '' || !is_numeric($data['minute']) ? 0 : (int) $data['minute'];
$now->setTime($hour, $minute, 0);
$data = $now;
}

View File

@@ -22,6 +22,9 @@ final class TimesheetBillableType extends AbstractType
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'documentation' => [
'description' => 'Whether this item should be refundable (yes) or not (no) or if it should be calculated by inherited settings from customer, project and activity (auto).',
],
'label' => 'billable',
'choices' => [
'automatic' => Timesheet::BILLABLE_AUTOMATIC,

View File

@@ -40,7 +40,7 @@ abstract class AbstractCalculator
public function getVat(): float
{
return $this->model->getTemplate()->getVat();
return $this->model->getTemplate()->getVat() ?? 0.00;
}
public function getTax(): float

View File

@@ -174,7 +174,6 @@ class LdapManager
$this->hydrateUserWithAttributesMap($user, $ldapEntry, $attributeMap);
/** @var string|array|null $email */
$email = $user->getEmail();
if (null === $email) {
$user->setEmail($user->getUserIdentifier());

View File

@@ -18,7 +18,7 @@ abstract class AbstractPluginExtension extends Extension
{
$bundleConfig = [$this->getAlias() => $configs];
if ($container->hasParameter('kimai.bundles.config')) { // @phpstan-ignore-line
if ($container->hasParameter('kimai.bundles.config')) {
$config = $container->getParameter('kimai.bundles.config');
if (!\is_array($config)) {
throw new \Exception('Invalid bundle configuration registered for ' . $this->getAlias());

View File

@@ -11,8 +11,13 @@ namespace App\Reporting;
final class Report implements ReportInterface
{
public function __construct(private string $id, private string $route, private string $label, private string $reportIcon)
{
public function __construct(
private string $id,
private string $route,
private string $label,
private string $reportIcon,
private string $translationDomain = 'reporting'
) {
}
public function getRoute(): string
@@ -34,4 +39,9 @@ final class Report implements ReportInterface
{
return $this->reportIcon;
}
public function getTranslationDomain(): string
{
return $this->translationDomain;
}
}

View File

@@ -35,26 +35,6 @@ class ActivityRepository extends EntityRepository
{
use RepositorySearchTrait;
/**
* @param mixed $id
* @param null $lockMode
* @param null $lockVersion
* @return Activity|null
*/
public function find($id, $lockMode = null, $lockVersion = null): ?Activity
{
/** @var Activity|null $activity */
$activity = parent::find($id, $lockMode, $lockVersion);
if (null === $activity) {
return null;
}
$loader = new ActivityLoader($this->getEntityManager(), true);
$loader->loadResults([$activity]);
return $activity;
}
/**
* @param Project $project
* @return Activity[]

View File

@@ -35,26 +35,6 @@ class CustomerRepository extends EntityRepository
{
use RepositorySearchTrait;
/**
* @param mixed $id
* @param null $lockMode
* @param null $lockVersion
* @return Customer|null
*/
public function find($id, $lockMode = null, $lockVersion = null): ?Customer
{
/** @var Customer|null $customer */
$customer = parent::find($id, $lockMode, $lockVersion);
if (null === $customer) {
return null;
}
$loader = new CustomerLoader($this->getEntityManager(), true);
$loader->loadResults([$customer]);
return $customer;
}
/**
* @param int[] $customerIDs
* @return Customer[]

View File

@@ -32,6 +32,9 @@ final class ActivityLoader implements LoaderInterface
$ids = array_map(function ($activity) {
if ($activity instanceof Activity) {
// make sure that this potential doctrine proxy is initialized and filled with all data
$activity->getName();
return $activity->getId();
}

View File

@@ -30,6 +30,9 @@ final class CustomerLoader implements LoaderInterface
$ids = array_map(function ($customer) {
if ($customer instanceof Customer) {
// make sure that this potential doctrine proxy is initialized and filled with all data
$customer->getName();
return $customer->getId();
}

View File

@@ -29,6 +29,9 @@ final class InvoiceLoader implements LoaderInterface
$ids = array_map(function ($invoice) {
if ($invoice instanceof Invoice) {
// make sure that this potential doctrine proxy is initialized and filled with all data
$invoice->getInvoiceNumber();
return $invoice->getId();
}

View File

@@ -31,6 +31,9 @@ final class ProjectLoader implements LoaderInterface
$ids = array_map(function ($project) {
if ($project instanceof Project) {
// make sure that this potential doctrine proxy is initialized and filled with all data
$project->getName();
return $project->getId();
}

View File

@@ -29,6 +29,9 @@ final class TeamLoader implements LoaderInterface
$ids = array_map(function ($team) {
if ($team instanceof Team) {
// make sure that this potential doctrine proxy is initialized and filled with all data
$team->getName();
return $team->getId();
}

View File

@@ -32,6 +32,9 @@ final class TimesheetLoader implements LoaderInterface
$ids = array_map(function ($timesheet) {
if ($timesheet instanceof Timesheet) {
// make sure that this potential doctrine proxy is initialized and filled with all data
$timesheet->getType();
return $timesheet->getId();
}

View File

@@ -30,6 +30,9 @@ final class UserLoader implements LoaderInterface
$ids = array_map(function ($user) {
if ($user instanceof User) {
// make sure that this potential doctrine proxy is initialized and filled with all data
$user->getDisplayName();
return $user->getId();
}

View File

@@ -38,26 +38,6 @@ class ProjectRepository extends EntityRepository
{
use RepositorySearchTrait;
/**
* @param mixed $id
* @param null $lockMode
* @param null $lockVersion
* @return Project|null
*/
public function find($id, $lockMode = null, $lockVersion = null): ?Project
{
/** @var Project|null $project */
$project = parent::find($id, $lockMode, $lockVersion);
if (null === $project) {
return null;
}
$loader = new ProjectLoader($this->getEntityManager(), true);
$loader->loadResults([$project]);
return $project;
}
/**
* @param int[] $projectIds
* @return Project[]

View File

@@ -64,7 +64,7 @@ trait RepositorySearchTrait
$c = 0;
foreach ($searchTerm->getSearchFields() as $metaName => $metaValue) {
$and = $qb->expr()->andX();
/** @var literal-string $alias */
/** @var non-falsy-string&literal-string $alias */
$alias = 'meta' . $a++;
$paramName = 'metaName' . $i++;
$paramValue = 'metaValue' . $c++;

View File

@@ -40,20 +40,6 @@ class TeamRepository extends EntityRepository
return $result;
}
public function find($id, $lockMode = null, $lockVersion = null): ?Team
{
/** @var Team|null $team */
$team = parent::find($id, $lockMode, $lockVersion);
if (null === $team) {
return null;
}
$loader = new TeamLoader($this->getEntityManager());
$loader->loadResults([$team]);
return $team;
}
/**
* @param int[] $teamIds
* @return Team[]

View File

@@ -78,26 +78,6 @@ class TimesheetRepository extends EntityRepository
return $qb->getQuery()->getOneOrNullResult();
}
/**
* @param mixed $id
* @param null $lockMode
* @param null $lockVersion
* @return Timesheet|null
*/
public function find($id, $lockMode = null, $lockVersion = null): ?Timesheet
{
/** @var Timesheet|null $timesheet */
$timesheet = parent::find($id, $lockMode, $lockVersion);
if (null === $timesheet) {
return null;
}
$loader = new TimesheetLoader($this->getEntityManager());
$loader->loadResults([$timesheet]);
return $timesheet;
}
public function delete(Timesheet $timesheet): void
{
$entityManager = $this->getEntityManager();

View File

@@ -46,8 +46,11 @@ trait TwigRendererTrait
{
/** @var TranslationExtension $extension */
$extension = $twig->getExtension(TranslationExtension::class);
/** @var LocaleAwareInterface $translator */
$translator = $extension->getTranslator();
if (!$translator instanceof LocaleAwareInterface) {
throw new \Exception('Translator is expected to be of type LocaleAwareInterface');
}
$previous = $translator->getLocale();
$translator->setLocale($language);

View File

@@ -28,6 +28,7 @@ final class MenuItemModel implements MenuItemInterface
private bool $divider = false;
private bool $lastWasDivider = false;
private bool $expanded = false;
private string $translationDomain = 'messages';
public function __construct(
string $id,
@@ -297,4 +298,14 @@ final class MenuItemModel implements MenuItemInterface
{
$this->expanded = $expanded;
}
public function getTranslationDomain(): string
{
return $this->translationDomain;
}
public function setTranslationDomain(string $translationDomain): void
{
$this->translationDomain = $translationDomain;
}
}

View File

@@ -14,6 +14,7 @@ final class PageSetup
private ?string $help = null;
private ?string $actionName = null;
private string $actionView = 'index';
private string $translationDomain = 'messages';
private array $actionPayload = [];
private ?DataTable $dataTable = null;
@@ -90,4 +91,14 @@ final class PageSetup
{
return \in_array($this->actionView, ['detail', 'custom', 'table']);
}
public function getTranslationDomain(): string
{
return $this->translationDomain;
}
public function setTranslationDomain(string $translationDomain): void
{
$this->translationDomain = $translationDomain;
}
}

View File

@@ -95,11 +95,11 @@ final class TimesheetBudgetUsedValidator extends ConstraintValidator
$timeRate = $this->rateService->calculate($timesheet);
$rate = $timeRate->getRate();
$activityDuration = $duration;
$activityDuration = $duration ?? 0;
$activityRate = $rate;
$projectDuration = $duration;
$projectDuration = $duration ?? 0;
$projectRate = $rate;
$customerDuration = $duration;
$customerDuration = $duration ?? 0;
$customerRate = $rate;
$monthWasChanged = false;