The CP exercise (Juin N-1 → Mai N) is annual and continuous across contract-signature changes within the same leave rule (e.g. 35h → 39h, isDriver flip, weeklyHours bump). Capping `from` at the phase start truncated the accrual to just the months under the latest phase, producing wrong "en cours d'acquisition" values and dropping presence days from earlier months on the leave-tab calendar. For Damien GUILLOT (35h until 2025-10-31, then 39h), this gave 15 days acquired (6 months Nov→Apr) instead of the expected 27.5 days (11 months Jun→Apr at 2.5/month). After this fix, the H39 view shows the full annual accrual as expected. FORFAIT phases keep the from cap: the 218-day target is calendar-year scoped and only counts the FORFAIT portion of the year. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
478 lines
21 KiB
PHP
478 lines
21 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\State;
|
|
|
|
use App\Dto\Contracts\ContractPhase;
|
|
use App\Entity\Contract;
|
|
use App\Entity\Employee;
|
|
use App\Entity\EmployeeContractPeriod;
|
|
use App\Enum\ContractNature;
|
|
use App\Enum\ContractType;
|
|
use App\Enum\TrackingMode;
|
|
use App\Service\Contracts\EmployeeContractPhaseResolver;
|
|
use App\Service\Exercise\ExerciseYearResolver;
|
|
use App\State\EmployeeLeaveSummaryProvider;
|
|
use DateTimeImmutable;
|
|
use PHPUnit\Framework\TestCase;
|
|
use ReflectionClass;
|
|
use ReflectionProperty;
|
|
use Symfony\Component\HttpFoundation\Request;
|
|
use Symfony\Component\HttpFoundation\RequestStack;
|
|
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
|
|
|
|
/**
|
|
* @internal
|
|
*/
|
|
final class EmployeeLeaveSummaryProviderTest extends TestCase
|
|
{
|
|
// -----------------------------------------------------------------------
|
|
// Existing tests (unchanged) — verify accrual prorating arithmetic.
|
|
// -----------------------------------------------------------------------
|
|
|
|
public function testComputeAccruedDaysFromStartProratesPartialFirstMonth(): void
|
|
{
|
|
$provider = new ReflectionClass(EmployeeLeaveSummaryProvider::class)->newInstanceWithoutConstructor();
|
|
$method = new ReflectionClass(EmployeeLeaveSummaryProvider::class)->getMethod('computeAccruedDaysFromStart');
|
|
|
|
$result = $method->invoke(
|
|
$provider,
|
|
25.0,
|
|
25.0 / 12.0,
|
|
new DateTimeImmutable('2025-06-10'),
|
|
new DateTimeImmutable('2026-02-28')
|
|
);
|
|
|
|
self::assertEqualsWithDelta(18.125, $result, 0.0001);
|
|
}
|
|
|
|
public function testComputeAccruingDaysTotalMatchesAlainCase(): void
|
|
{
|
|
$provider = new ReflectionClass(EmployeeLeaveSummaryProvider::class)->newInstanceWithoutConstructor();
|
|
$method = new ReflectionClass(EmployeeLeaveSummaryProvider::class)->getMethod('computeAccruedDaysFromStart');
|
|
|
|
$days = $method->invoke(
|
|
$provider,
|
|
25.0,
|
|
25.0 / 12.0,
|
|
new DateTimeImmutable('2025-06-10'),
|
|
new DateTimeImmutable('2026-02-28')
|
|
);
|
|
$saturdays = $method->invoke(
|
|
$provider,
|
|
5.0,
|
|
5.0 / 12.0,
|
|
new DateTimeImmutable('2025-06-10'),
|
|
new DateTimeImmutable('2026-02-28')
|
|
);
|
|
|
|
self::assertEqualsWithDelta(21.75, $days + $saturdays, 0.0001);
|
|
}
|
|
|
|
public function testComputeAccruedDaysFromStartIncludesLastDayOfMonth(): void
|
|
{
|
|
$provider = new ReflectionClass(EmployeeLeaveSummaryProvider::class)->newInstanceWithoutConstructor();
|
|
$method = new ReflectionClass(EmployeeLeaveSummaryProvider::class)->getMethod('computeAccruedDaysFromStart');
|
|
|
|
$result = $method->invoke(
|
|
$provider,
|
|
25.0,
|
|
25.0 / 12.0,
|
|
new DateTimeImmutable('2026-02-01 12:50:18'),
|
|
new DateTimeImmutable('2026-02-28 00:00:00')
|
|
);
|
|
|
|
self::assertEqualsWithDelta(25.0 / 12.0, $result, 0.0001);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Phase resolution tests (Task 3 — phaseId support).
|
|
// The repository / service dependencies are typed against final classes
|
|
// which PHPUnit cannot double, so phase resolution is exercised via
|
|
// reflection on private methods to avoid instantiating the full DI graph.
|
|
// -----------------------------------------------------------------------
|
|
|
|
public function testResolveTargetPhasePicksH39PhaseFromPhaseId(): void
|
|
{
|
|
$employee = $this->buildEmployeeWithTransition('2020-06-01', '2026-04-30', '2026-05-01');
|
|
$phases = new EmployeeContractPhaseResolver()->resolvePhases($employee);
|
|
$h39Phase = $phases[1]; // oldest = 39h
|
|
|
|
$provider = $this->buildProvider(['phaseId' => (string) $h39Phase->id]);
|
|
$resolved = $this->invokePrivate($provider, 'resolveTargetPhase', $employee);
|
|
|
|
self::assertInstanceOf(ContractPhase::class, $resolved);
|
|
self::assertSame($h39Phase->id, $resolved->id);
|
|
self::assertSame(ContractType::H39, $resolved->contractType);
|
|
self::assertFalse($resolved->isCurrent);
|
|
}
|
|
|
|
public function testResolveTargetPhaseDefaultsToCurrentPhaseWhenPhaseIdAbsent(): void
|
|
{
|
|
$employee = $this->buildEmployeeWithTransition('2020-06-01', '2026-04-30', '2026-05-01');
|
|
$phases = new EmployeeContractPhaseResolver()->resolvePhases($employee);
|
|
$currentPhase = $phases[0]; // most recent = FORFAIT
|
|
|
|
$provider = $this->buildProvider([]);
|
|
$resolved = $this->invokePrivate($provider, 'resolveTargetPhase', $employee);
|
|
|
|
self::assertSame($currentPhase->id, $resolved->id);
|
|
self::assertSame(ContractType::FORFAIT, $resolved->contractType);
|
|
self::assertTrue($resolved->isCurrent);
|
|
}
|
|
|
|
public function testPastH39PhaseAppliesNonForfaitRuleCodeEvenWhenCurrentIsForfait(): void
|
|
{
|
|
// Verifies resolveLeavePolicy uses the phase's contractType (not the current contract).
|
|
$employee = $this->buildEmployeeWithTransition('2020-06-01', '2026-04-30', '2026-05-01');
|
|
$phases = new EmployeeContractPhaseResolver()->resolvePhases($employee);
|
|
$h39Phase = $phases[1];
|
|
|
|
$provider = $this->buildProvider(['phaseId' => (string) $h39Phase->id]);
|
|
$from = new DateTimeImmutable('2025-06-01');
|
|
$to = new DateTimeImmutable('2026-04-30');
|
|
$leavePolicy = $this->invokePrivate($provider, 'resolveLeavePolicy', $employee, $h39Phase, $from, $to);
|
|
|
|
self::assertNotNull($leavePolicy);
|
|
self::assertSame('CDI_CDD_NON_FORFAIT', $leavePolicy['ruleCode']);
|
|
self::assertSame(25.0, $leavePolicy['acquiredDays']);
|
|
self::assertEqualsWithDelta(25.0 / 12.0, $leavePolicy['accrualPerMonth'], 0.0001);
|
|
}
|
|
|
|
public function testResolvePeriodBoundsCapsAtPhaseEndDate(): void
|
|
{
|
|
// 39h phase (June 2020 → April 30 2026). Exercise 2026 spans June 2025 → May 31 2026.
|
|
// The phase cap should clip the upper bound to April 30 2026.
|
|
$employee = $this->buildEmployeeWithTransition('2020-06-01', '2026-04-30', '2026-05-01');
|
|
$phases = new EmployeeContractPhaseResolver()->resolvePhases($employee);
|
|
$h39Phase = $phases[1];
|
|
|
|
$provider = $this->buildProvider(['phaseId' => (string) $h39Phase->id, 'year' => '2026']);
|
|
[$from, $to] = $this->invokePrivate($provider, 'resolvePeriodBounds', $employee, 2026, $h39Phase);
|
|
|
|
self::assertSame('2025-06-01', $from->format('Y-m-d'));
|
|
self::assertSame('2026-04-30', $to->format('Y-m-d'));
|
|
}
|
|
|
|
public function testTransitionExerciseOnH39PhaseAccruesAround22Point9Days(): void
|
|
{
|
|
// 11 full months of accrual at 25/12 ≈ 22.917 days.
|
|
$employee = $this->buildEmployeeWithTransition('2020-06-01', '2026-04-30', '2026-05-01');
|
|
$phases = new EmployeeContractPhaseResolver()->resolvePhases($employee);
|
|
$h39Phase = $phases[1];
|
|
|
|
$provider = $this->buildProvider(['phaseId' => (string) $h39Phase->id, 'year' => '2026']);
|
|
$method = new ReflectionClass(EmployeeLeaveSummaryProvider::class)->getMethod('computeAccruedDaysFromStart');
|
|
|
|
// Period bounds for exercise 2026 on H39 phase = June 1 2025 → April 30 2026.
|
|
[$from, $to] = $this->invokePrivate($provider, 'resolvePeriodBounds', $employee, 2026, $h39Phase);
|
|
$acquired = $method->invoke($provider, 25.0, 25.0 / 12.0, $from, $to);
|
|
|
|
self::assertEqualsWithDelta(22.92, $acquired, 0.1);
|
|
}
|
|
|
|
public function testNonForfaitPhaseStartingMidExerciseUsesFullExerciseFromAsStart(): void
|
|
{
|
|
// Scenario: 35h CDI from 2014-07-01 to 2025-10-31, then 39h CDI from 2025-11-01.
|
|
// Both phases are non-forfait (same leave rule CDI_CDD_NON_FORFAIT).
|
|
// Viewing exercise 2026 on the current 39h phase, accrual must run from the
|
|
// exercise start (June 1, 2025), NOT from the phase start (November 1, 2025).
|
|
// Otherwise the 5 months of June-October under 35h would be lost from the
|
|
// annual CP accrual, which is wrong (CP exercise is annual, not per-phase).
|
|
$employee = $this->buildH35ToH39Transition('2014-07-01', '2025-10-31', '2025-11-01');
|
|
$phases = new EmployeeContractPhaseResolver()->resolvePhases($employee);
|
|
$h39Phase = $phases[0]; // current
|
|
|
|
$provider = $this->buildProvider(['phaseId' => (string) $h39Phase->id, 'year' => '2026']);
|
|
|
|
[$from, $to] = $this->invokePrivate($provider, 'resolvePeriodBounds', $employee, 2026, $h39Phase);
|
|
|
|
self::assertSame('2025-06-01', $from->format('Y-m-d'));
|
|
self::assertSame('2026-05-31', $to->format('Y-m-d'));
|
|
}
|
|
|
|
public function testForfaitPhaseStartingMidYearCapsFromAtPhaseStart(): void
|
|
{
|
|
// Scenario: 39h CDI ends 2026-04-30, FORFAIT from 2026-05-01.
|
|
// Viewing year 2026 on the FORFAIT phase, the period must be capped at
|
|
// phase start (May 1) so that only the FORFAIT portion of the calendar
|
|
// year is counted.
|
|
$employee = $this->buildEmployeeWithTransition('2020-06-01', '2026-04-30', '2026-05-01');
|
|
$phases = new EmployeeContractPhaseResolver()->resolvePhases($employee);
|
|
$forfaitPhase = $phases[0]; // current FORFAIT
|
|
|
|
$provider = $this->buildProvider(['phaseId' => (string) $forfaitPhase->id, 'year' => '2026']);
|
|
|
|
[$from, $to] = $this->invokePrivate($provider, 'resolvePeriodBounds', $employee, 2026, $forfaitPhase);
|
|
|
|
self::assertSame('2026-05-01', $from->format('Y-m-d'));
|
|
self::assertSame('2026-12-31', $to->format('Y-m-d'));
|
|
}
|
|
|
|
public function testYearOutsidePhaseRangeIsSilentlyClampedToPhaseLastExercise(): void
|
|
{
|
|
$employee = $this->buildEmployeeWithTransition('2020-06-01', '2026-04-30', '2026-05-01');
|
|
$phases = new EmployeeContractPhaseResolver()->resolvePhases($employee);
|
|
$h39Phase = $phases[1];
|
|
|
|
$provider = $this->buildProvider(['phaseId' => (string) $h39Phase->id, 'year' => '2030']);
|
|
$year = $this->invokePrivate($provider, 'resolveYear', $employee, $h39Phase);
|
|
|
|
self::assertSame(2026, $year);
|
|
}
|
|
|
|
public function testYearBeforePhaseIsClampedToPhaseFirstExercise(): void
|
|
{
|
|
$employee = $this->buildEmployeeWithTransition('2020-06-01', '2026-04-30', '2026-05-01');
|
|
$phases = new EmployeeContractPhaseResolver()->resolvePhases($employee);
|
|
$h39Phase = $phases[1];
|
|
|
|
// Phase starts 2020-06-01 → first exercise (non-forfait) = 2021 (since month >=6 = year+1).
|
|
$provider = $this->buildProvider(['phaseId' => (string) $h39Phase->id, 'year' => '2010']);
|
|
$year = $this->invokePrivate($provider, 'resolveYear', $employee, $h39Phase);
|
|
|
|
self::assertSame(2021, $year);
|
|
}
|
|
|
|
public function testInvalidPhaseIdReturns422(): void
|
|
{
|
|
$employee = $this->buildEmployeeWithTransition('2020-06-01', '2026-04-30', '2026-05-01');
|
|
$provider = $this->buildProvider(['phaseId' => '99999']);
|
|
|
|
$this->expectException(UnprocessableEntityHttpException::class);
|
|
$this->invokePrivate($provider, 'resolveTargetPhase', $employee);
|
|
}
|
|
|
|
public function testNonNumericPhaseIdReturns422(): void
|
|
{
|
|
$employee = $this->buildEmployeeWithTransition('2020-06-01', '2026-04-30', '2026-05-01');
|
|
$provider = $this->buildProvider(['phaseId' => 'abc']);
|
|
|
|
$this->expectException(UnprocessableEntityHttpException::class);
|
|
$this->invokePrivate($provider, 'resolveTargetPhase', $employee);
|
|
}
|
|
|
|
public function testDefaultYearForPhaseIdOnClosedPhaseUsesPhaseEndDate(): void
|
|
{
|
|
// No `year` param + explicit phaseId → default year is derived from $phase->endDate.
|
|
// H39 phase ends 2026-04-30 → non-forfait exercise containing that date = 2026.
|
|
$employee = $this->buildEmployeeWithTransition('2020-06-01', '2026-04-30', '2026-05-01');
|
|
$phases = new EmployeeContractPhaseResolver()->resolvePhases($employee);
|
|
$h39Phase = $phases[1];
|
|
|
|
$provider = $this->buildProvider(['phaseId' => (string) $h39Phase->id]);
|
|
$year = $this->invokePrivate($provider, 'resolveYear', $employee, $h39Phase);
|
|
|
|
self::assertSame(2026, $year);
|
|
}
|
|
|
|
public function testNoQueryParamsKeepsLegacyYearDefaulting(): void
|
|
{
|
|
$employee = $this->buildEmployeeWithTransition('2020-06-01', '2026-04-30', '2026-05-01');
|
|
$phases = new EmployeeContractPhaseResolver()->resolvePhases($employee);
|
|
$currentPhase = $phases[0];
|
|
|
|
$provider = $this->buildProvider([]);
|
|
$year = $this->invokePrivate($provider, 'resolveYear', $employee, $currentPhase);
|
|
|
|
// Today is 2026-05-19, FORFAIT phase → year is the current calendar year (2026).
|
|
self::assertSame(2026, $year);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Regression: terminated-employee path through `computeYearSummary` without
|
|
// an explicit phase (legacy callers: LeaveRecapRowBuilder,
|
|
// DumpVerificationSnapshotCommand). Before the phase-aware refactor, the
|
|
// period bounds were NOT capped at the contract end for terminated
|
|
// employees (because Employee::getCurrentContractEndDate() returns null
|
|
// when no period covers "today"). The new code resolves a fallback phase
|
|
// whose `isCurrent` is false, which would otherwise cap `to` at the phase
|
|
// end — a behavior change for legacy callers. The flag `applyPhaseEndCap`
|
|
// toggles this cap so legacy callers get the pre-refactor behavior.
|
|
// -----------------------------------------------------------------------
|
|
|
|
public function testTerminatedEmployeeWithoutExplicitPhaseSkipsPhaseEndCap(): void
|
|
{
|
|
// Terminated employee: H39 phase ending 2024-12-31 (well in the past).
|
|
$employee = $this->buildTerminatedEmployee('2020-06-01', '2024-12-31');
|
|
$phases = new EmployeeContractPhaseResolver()->resolvePhases($employee);
|
|
self::assertCount(1, $phases);
|
|
$phase = $phases[0];
|
|
self::assertFalse($phase->isCurrent, 'Sanity: terminated phase must not be flagged as current.');
|
|
|
|
$provider = $this->buildProvider([]);
|
|
|
|
// applyPhaseEndCap=false → mimics legacy callers (no explicit phase):
|
|
// the upper bound MUST stay at the natural leave-year end (May 31).
|
|
[$fromLegacy, $toLegacy] = $this->invokePrivate($provider, 'resolvePeriodBounds', $employee, 2025, $phase, false);
|
|
self::assertSame('2024-06-01', $fromLegacy->format('Y-m-d'));
|
|
self::assertSame('2025-05-31', $toLegacy->format('Y-m-d'));
|
|
|
|
// applyPhaseEndCap=true → explicit-phase callers get the cap at phase end.
|
|
[$fromCap, $toCap] = $this->invokePrivate($provider, 'resolvePeriodBounds', $employee, 2025, $phase, true);
|
|
self::assertSame('2024-06-01', $fromCap->format('Y-m-d'));
|
|
self::assertSame('2024-12-31', $toCap->format('Y-m-d'));
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Test harness helpers.
|
|
// -----------------------------------------------------------------------
|
|
|
|
/**
|
|
* Build a terminated-employee fixture: a single H39 period ending before today.
|
|
*/
|
|
private function buildTerminatedEmployee(string $start, string $end): Employee
|
|
{
|
|
$employee = new Employee();
|
|
$this->setEntityId($employee, 2);
|
|
|
|
$contract = new Contract();
|
|
$contract->setName('39H');
|
|
$contract->setTrackingMode(TrackingMode::TIME->value);
|
|
$contract->setWeeklyHours(39);
|
|
|
|
$period = new EmployeeContractPeriod();
|
|
$this->setEntityId($period, 10);
|
|
$period->setEmployee($employee);
|
|
$period->setContract($contract);
|
|
$period->setStartDate(new DateTimeImmutable($start));
|
|
$period->setEndDate(new DateTimeImmutable($end));
|
|
$period->setContractNature(ContractNature::CDI);
|
|
$period->setIsDriver(false);
|
|
|
|
$employee->getContractPeriods()->add($period);
|
|
|
|
return $employee;
|
|
}
|
|
|
|
/**
|
|
* Build a two-period employee transitioning from H39 to FORFAIT.
|
|
*/
|
|
private function buildH35ToH39Transition(string $h35Start, string $h35End, string $h39Start): Employee
|
|
{
|
|
$employee = new Employee();
|
|
$this->setEntityId($employee, 1);
|
|
|
|
$h35Contract = new Contract();
|
|
$h35Contract->setName('35H');
|
|
$h35Contract->setTrackingMode(TrackingMode::TIME->value);
|
|
$h35Contract->setWeeklyHours(35);
|
|
|
|
$h39Contract = new Contract();
|
|
$h39Contract->setName('39H');
|
|
$h39Contract->setTrackingMode(TrackingMode::TIME->value);
|
|
$h39Contract->setWeeklyHours(39);
|
|
|
|
$h35Period = new EmployeeContractPeriod();
|
|
$this->setEntityId($h35Period, 1);
|
|
$h35Period->setEmployee($employee);
|
|
$h35Period->setContract($h35Contract);
|
|
$h35Period->setStartDate(new DateTimeImmutable($h35Start));
|
|
$h35Period->setEndDate(new DateTimeImmutable($h35End));
|
|
$h35Period->setContractNature(ContractNature::CDI);
|
|
$h35Period->setIsDriver(false);
|
|
|
|
$h39Period = new EmployeeContractPeriod();
|
|
$this->setEntityId($h39Period, 2);
|
|
$h39Period->setEmployee($employee);
|
|
$h39Period->setContract($h39Contract);
|
|
$h39Period->setStartDate(new DateTimeImmutable($h39Start));
|
|
$h39Period->setEndDate(null);
|
|
$h39Period->setContractNature(ContractNature::CDI);
|
|
$h39Period->setIsDriver(false);
|
|
|
|
$employee->getContractPeriods()->add($h35Period);
|
|
$employee->getContractPeriods()->add($h39Period);
|
|
|
|
return $employee;
|
|
}
|
|
|
|
private function buildEmployeeWithTransition(string $h39Start, string $h39End, string $forfaitStart): Employee
|
|
{
|
|
$employee = new Employee();
|
|
$this->setEntityId($employee, 1);
|
|
|
|
$h39Contract = new Contract();
|
|
$h39Contract->setName('39H');
|
|
$h39Contract->setTrackingMode(TrackingMode::TIME->value);
|
|
$h39Contract->setWeeklyHours(39);
|
|
|
|
$forfaitContract = new Contract();
|
|
$forfaitContract->setName('Forfait');
|
|
$forfaitContract->setTrackingMode(TrackingMode::PRESENCE->value);
|
|
$forfaitContract->setWeeklyHours(null);
|
|
|
|
$h39Period = new EmployeeContractPeriod();
|
|
$this->setEntityId($h39Period, 1);
|
|
$h39Period->setEmployee($employee);
|
|
$h39Period->setContract($h39Contract);
|
|
$h39Period->setStartDate(new DateTimeImmutable($h39Start));
|
|
$h39Period->setEndDate(new DateTimeImmutable($h39End));
|
|
$h39Period->setContractNature(ContractNature::CDI);
|
|
$h39Period->setIsDriver(false);
|
|
|
|
$forfaitPeriod = new EmployeeContractPeriod();
|
|
$this->setEntityId($forfaitPeriod, 2);
|
|
$forfaitPeriod->setEmployee($employee);
|
|
$forfaitPeriod->setContract($forfaitContract);
|
|
$forfaitPeriod->setStartDate(new DateTimeImmutable($forfaitStart));
|
|
$forfaitPeriod->setEndDate(null);
|
|
$forfaitPeriod->setContractNature(ContractNature::CDI);
|
|
$forfaitPeriod->setIsDriver(false);
|
|
|
|
$employee->getContractPeriods()->add($h39Period);
|
|
$employee->getContractPeriods()->add($forfaitPeriod);
|
|
|
|
return $employee;
|
|
}
|
|
|
|
/**
|
|
* Build an uninitialized provider with a RequestStack pre-loaded with the given query.
|
|
*
|
|
* The provider's repository/service dependencies are typed against final classes
|
|
* (EmployeeRepository, LeaveBalanceComputationService, etc.) which PHPUnit cannot
|
|
* double. We bypass full instantiation by using newInstanceWithoutConstructor and
|
|
* only setting the properties that the tested private methods actually read:
|
|
* `requestStack` and `phaseResolver`. Tests targeting heavier code paths exercise
|
|
* private methods directly (resolveTargetPhase, resolvePeriodBounds, etc.).
|
|
*
|
|
* @param array<string, string> $request query parameters (year, phaseId, ...)
|
|
*/
|
|
private function buildProvider(array $request = []): EmployeeLeaveSummaryProvider
|
|
{
|
|
$requestStack = new RequestStack();
|
|
$requestStack->push(new Request(query: $request));
|
|
|
|
$reflection = new ReflectionClass(EmployeeLeaveSummaryProvider::class);
|
|
$provider = $reflection->newInstanceWithoutConstructor();
|
|
|
|
$this->setReadonlyProperty($provider, 'requestStack', $requestStack);
|
|
$this->setReadonlyProperty($provider, 'phaseResolver', new EmployeeContractPhaseResolver());
|
|
$this->setReadonlyProperty($provider, 'exerciseYearResolver', new ExerciseYearResolver());
|
|
$this->setReadonlyProperty($provider, 'dataStartDate', null);
|
|
|
|
return $provider;
|
|
}
|
|
|
|
private function invokePrivate(object $obj, string $method, mixed ...$args): mixed
|
|
{
|
|
$reflection = new ReflectionClass($obj::class);
|
|
$m = $reflection->getMethod($method);
|
|
|
|
return $m->invoke($obj, ...$args);
|
|
}
|
|
|
|
private function setReadonlyProperty(object $obj, string $property, mixed $value): void
|
|
{
|
|
$reflection = new ReflectionProperty($obj::class, $property);
|
|
$reflection->setValue($obj, $value);
|
|
}
|
|
|
|
private function setEntityId(object $entity, int $id): void
|
|
{
|
|
$reflection = new ReflectionProperty($entity::class, 'id');
|
|
$reflection->setValue($entity, $id);
|
|
}
|
|
}
|