From 2998f43e6dfe50b2303e5b55cd17d89f1a8b7568 Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Wed, 29 Jul 2026 15:43:03 +0200 Subject: [PATCH 1/6] Entries referencing deleted users or tickets no longer fail the whole request. Added userId to timesheets and stopped resolving ticket status against the session project when a ticket has no project. Added PHPUnit setup and a Taskfile for running tests. --- .claude/settings.json | 9 ++ .github/workflows/pr.yml | 22 ++++ .gitignore | 2 + CHANGELOG.md | 6 + Model/DeletedData.php | 4 +- Model/MilestoneData.php | 4 +- Model/ProjectData.php | 2 +- Model/TicketData.php | 4 +- Model/TimesheetData.php | 9 +- Repositories/ApiDataRepository.php | 2 +- Services/APIData.php | 29 +++-- Taskfile.yml | 62 +++++++++ bin/release-exclude.txt | 5 + compose.yml | 1 + composer.json | 13 ++ phpunit.xml.dist | 21 +++ tests/Model/DeletedDataTest.php | 25 ++++ tests/Model/MilestoneDataTest.php | 25 ++++ tests/Model/ProjectDataTest.php | 22 ++++ tests/Model/TicketDataTest.php | 45 +++++++ tests/Model/TimesheetDataTest.php | 97 ++++++++++++++ tests/Service/APIDataTest.php | 197 +++++++++++++++++++++++++++++ tests/Stub/LeantimeTickets.php | 40 ++++++ 23 files changed, 624 insertions(+), 22 deletions(-) create mode 100644 .claude/settings.json create mode 100644 Taskfile.yml create mode 100644 phpunit.xml.dist create mode 100644 tests/Model/DeletedDataTest.php create mode 100644 tests/Model/MilestoneDataTest.php create mode 100644 tests/Model/ProjectDataTest.php create mode 100644 tests/Model/TicketDataTest.php create mode 100644 tests/Model/TimesheetDataTest.php create mode 100644 tests/Service/APIDataTest.php create mode 100644 tests/Stub/LeantimeTickets.php diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..56fc68a --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "Bash(task test:*)", + "Bash(task composer:*)", + "Bash(task lint:*)" + ] + } +} diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 4ed1f3c..a32163b 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -15,3 +15,25 @@ jobs: - name: Check that changelog has been updated. run: git diff --exit-code origin/${{ github.base_ref }} -- CHANGELOG.md && exit 1 || exit 0 + + test: + runs-on: ubuntu-latest + name: Unit tests + strategy: + fail-fast: false + steps: + - name: Checkout + uses: actions/checkout@v4 + + # Matches the PHP version in Dockerfile (itkdev/php8.3-fpm). + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + coverage: none + + - name: Install dependencies + run: composer install --no-interaction --prefer-dist --no-progress + + - name: Run tests + run: vendor/bin/phpunit diff --git a/.gitignore b/.gitignore index e0d067c..a3fd9f3 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ composer.lock release/ checksum.txt *.tar.gz + +economics diff --git a/CHANGELOG.md b/CHANGELOG.md index 0bfa34e..e6c8054 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ## [Unreleased] +* [PR-14](https://github.com/ITK-Leantime/data-api/pull/14) + * Allowed null values in API models, so entries referencing deleted users or deleted tickets no longer fail the whole request. + * Added userId to timesheets, so hours logged by a deleted user stay attributable. + * Stopped resolving ticket status against the session's project when a ticket has no project. + * Added PHPUnit test setup and a Taskfile for running it. + ## [0.1.2] - 2026-03-06 * [PR-12](https://github.com/ITK-Leantime/data-api/pull/12) diff --git a/Model/DeletedData.php b/Model/DeletedData.php index 74e379c..9433a0c 100644 --- a/Model/DeletedData.php +++ b/Model/DeletedData.php @@ -7,7 +7,7 @@ class DeletedData { public function __construct( - public int $id, - public CarbonInterface $deletedDate, + public ?int $id, + public ?CarbonInterface $deletedDate, ) {} } diff --git a/Model/MilestoneData.php b/Model/MilestoneData.php index 0a06bd2..f222b7c 100644 --- a/Model/MilestoneData.php +++ b/Model/MilestoneData.php @@ -8,8 +8,8 @@ { public function __construct( public int $id, - public int $projectId, - public string $name, + public ?int $projectId, + public ?string $name, public ?CarbonInterface $modified, ) {} } diff --git a/Model/ProjectData.php b/Model/ProjectData.php index 0c07e16..388ada8 100644 --- a/Model/ProjectData.php +++ b/Model/ProjectData.php @@ -8,7 +8,7 @@ { public function __construct( public int $id, - public string $name, + public ?string $name, public ?CarbonInterface $modified, ) {} } diff --git a/Model/TicketData.php b/Model/TicketData.php index 8826a6a..6ff561f 100644 --- a/Model/TicketData.php +++ b/Model/TicketData.php @@ -8,8 +8,8 @@ { public function __construct( public int $id, - public int $projectId, - public string $name, + public ?int $projectId, + public ?string $name, public ?string $status, public ?int $milestoneId, public array $tags, diff --git a/Model/TimesheetData.php b/Model/TimesheetData.php index b14c145..cd726b5 100644 --- a/Model/TimesheetData.php +++ b/Model/TimesheetData.php @@ -8,13 +8,14 @@ { public function __construct( public int $id, - public int $ticketId, - public int $projectId, + public ?int $ticketId, + public ?int $projectId, public ?string $description, public float $hours, - public string $username, + public ?int $userId, + public ?string $username, + public ?string $kind, public ?CarbonInterface $workDate = null, public ?CarbonInterface $modified = null, - public string $kind, ) {} } diff --git a/Repositories/ApiDataRepository.php b/Repositories/ApiDataRepository.php index f5cc80a..e6460db 100644 --- a/Repositories/ApiDataRepository.php +++ b/Repositories/ApiDataRepository.php @@ -65,7 +65,7 @@ public function getTimesheets(int $startId, int $limit, ?int $modifiedAfter = nu { return $this->query() ->from("zp_timesheets", "timesheet") - ->select(["timesheet.id", "timesheet.description", "timesheet.hours", "timesheet.workDate", "timesheet.modified", "timesheet.ticketId", "timesheet.kind", "user.username", "ticket.projectId"]) + ->select(["timesheet.id", "timesheet.description", "timesheet.hours", "timesheet.workDate", "timesheet.modified", "timesheet.ticketId", "timesheet.userId", "timesheet.kind", "user.username", "ticket.projectId"]) ->where("timesheet.id", ">=", $startId) ->whereNotNull("timesheet.hours") ->leftJoin('zp_user as user', "user.id", "=", "timesheet.userId") diff --git a/Services/APIData.php b/Services/APIData.php index 8929f87..af28091 100644 --- a/Services/APIData.php +++ b/Services/APIData.php @@ -144,7 +144,12 @@ public function getTickets(int $startId, int $limit, int $modifiedAfter = null, $values = $this->apiDataRepository->getTickets($startId, $limit, $modifiedAfter, $ids, $projectIds); return array_map(function ($value) { - $projectStatuses = $this->ticketRepository->getStateLabels($value->projectId); + // Asked for labels without a project id, Leantime falls back to + // session('currentProject'), which would resolve the status against + // an unrelated project. + $projectStatuses = $value->projectId !== null + ? $this->ticketRepository->getStateLabels($value->projectId) + : []; return new TicketData( $value->id, @@ -168,16 +173,20 @@ public function getTimesheets(int $startId, int $limit, ?int $modifiedAfter = nu $values = $this->apiDataRepository->getTimesheets($startId, $limit, $modifiedAfter, $ids, $projectIds); return array_map(function ($value) { + // Named arguments: CarbonImmutable has a __toString(), so a + // mis-ordered date would be coerced into one of the string + // parameters instead of raising a TypeError. return new TimesheetData( - $value->id, - $value->ticketId, - $value->projectId, - $value->description, - $value->hours, - $value->username, - $this->getCarbonFromDatabaseValue($value->workDate), - $this->getCarbonFromDatabaseValue($value->modified), - $value->kind, + id: $value->id, + ticketId: $value->ticketId, + projectId: $value->projectId, + description: $value->description, + hours: $value->hours, + userId: $value->userId, + username: $value->username, + kind: $value->kind, + workDate: $this->getCarbonFromDatabaseValue($value->workDate), + modified: $this->getCarbonFromDatabaseValue($value->modified), ); }, $values); } diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 0000000..4601cfe --- /dev/null +++ b/Taskfile.yml @@ -0,0 +1,62 @@ +# https://taskfile.dev — install with `brew install go-task` (or see docs). +# Run `task` (or `task --list-all`) to see all available commands. +# +# This plugin has no long-running stack, so everything runs in a one-off +# container built from the local Dockerfile (itkdev/php8.3-fpm). + +version: "3" + +vars: + # https://taskfile.dev/reference/templating/ + DOCKER_COMPOSE: '{{ .TASK_DOCKER_COMPOSE | default "docker compose" }}' + +tasks: + default: + desc: List all tasks + cmds: + - task --list-all + silent: true + + # -------------------------------------------------------------- Wrappers --- + + compose: + desc: "Run a docker compose command. Example: task compose -- build php." + cmds: + - "{{ .DOCKER_COMPOSE }} {{ .CLI_ARGS }}" + + php: + desc: "Run a command in a one-off php container. Example: task php -- php --version." + cmds: + - task compose -- run --rm --no-deps php {{ .CLI_ARGS }} + silent: true + + composer: + desc: "Run a composer command. Example: task composer -- install." + cmds: + - task php -- composer {{ .CLI_ARGS }} + silent: true + + # ------------------------------------------------------------ Lifecycle --- + + setup: + desc: Build the php image and install dev dependencies. + cmds: + - task compose -- build php + - task composer -- install + + # ------------------------------------------------------------ PHP tests --- + + test: + desc: "Run the PHP test suite. Example: task test -- --filter TimesheetData." + cmds: + - task php -- vendor/bin/phpunit {{ .CLI_ARGS }} + silent: true + + lint: + desc: Syntax-check all PHP files. + cmds: + - >- + task php -- sh -c + 'find Controllers Model Repositories Services -name "*.php" -print0 + | xargs -0 -n1 -- php -l' + silent: true diff --git a/bin/release-exclude.txt b/bin/release-exclude.txt index 3331c0c..0e9974d 100755 --- a/bin/release-exclude.txt +++ b/bin/release-exclude.txt @@ -1,6 +1,7 @@ *.tar.gz .git* .php-cs-fixer.dist.php +.phpunit.cache .twig-cs-fixer.dist.php bin checksum.txt @@ -8,3 +9,7 @@ compose.yaml composer.lock vendor Dockerfile +phpunit.xml +phpunit.xml.dist +Taskfile.yml +tests diff --git a/compose.yml b/compose.yml index b40cbb9..908a2c5 100644 --- a/compose.yml +++ b/compose.yml @@ -1,5 +1,6 @@ services: php: build: . + working_dir: /app volumes: - .:/app diff --git a/composer.json b/composer.json index bb27426..f1e60a9 100644 --- a/composer.json +++ b/composer.json @@ -12,9 +12,22 @@ ], "homepage": "https://github.com/ITK-Leantime/leantime-data-api", "require-dev": { + "illuminate/database": "^11.0", + "nesbot/carbon": "^2.72.2 || ^3.0", + "phpunit/phpunit": "^11.0" }, "config": { }, + "autoload-dev": { + "psr-4": { + "Leantime\\Plugins\\APIData\\": "", + "Leantime\\Plugins\\APIData\\Tests\\": "tests/" + }, + "classmap": [ + "tests/Stub/" + ] + }, "scripts": { + "test": "phpunit" } } diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..2a306e4 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,21 @@ + + + + + tests + tests/Stub + + + + + Model + Services + + + diff --git a/tests/Model/DeletedDataTest.php b/tests/Model/DeletedDataTest.php new file mode 100644 index 0000000..26e42d2 --- /dev/null +++ b/tests/Model/DeletedDataTest.php @@ -0,0 +1,25 @@ +assertNull($deleted->id); + $this->assertNull($deleted->deletedDate); + } +} diff --git a/tests/Model/MilestoneDataTest.php b/tests/Model/MilestoneDataTest.php new file mode 100644 index 0000000..3ebb4ac --- /dev/null +++ b/tests/Model/MilestoneDataTest.php @@ -0,0 +1,25 @@ +assertSame(5, $milestone->id); + $this->assertNull($milestone->projectId); + $this->assertNull($milestone->name); + } +} diff --git a/tests/Model/ProjectDataTest.php b/tests/Model/ProjectDataTest.php new file mode 100644 index 0000000..859a12d --- /dev/null +++ b/tests/Model/ProjectDataTest.php @@ -0,0 +1,22 @@ +assertSame(92, $project->id); + $this->assertNull($project->name); + } +} diff --git a/tests/Model/TicketDataTest.php b/tests/Model/TicketDataTest.php new file mode 100644 index 0000000..069fd5b --- /dev/null +++ b/tests/Model/TicketDataTest.php @@ -0,0 +1,45 @@ +makeTicket(['projectId' => null, 'name' => null]); + + $this->assertNull($ticket->projectId); + $this->assertNull($ticket->name); + } + + /** + * @param array $overrides + */ + private function makeTicket(array $overrides = []): TicketData + { + return new TicketData(...[ + 'id' => 4711, + 'projectId' => 92, + 'name' => 'Fix the thing', + 'status' => 'NEW', + 'milestoneId' => null, + 'tags' => [], + 'worker' => 'anne@aarhus.dk', + 'plannedHours' => null, + 'remainingHours' => null, + 'dueDate' => null, + 'resolutionDate' => null, + 'modified' => null, + ...$overrides, + ]); + } +} diff --git a/tests/Model/TimesheetDataTest.php b/tests/Model/TimesheetDataTest.php new file mode 100644 index 0000000..12ac0c9 --- /dev/null +++ b/tests/Model/TimesheetDataTest.php @@ -0,0 +1,97 @@ +makeTimesheet(['username' => null]); + + $this->assertNull($timesheet->username); + $this->assertSame(7.5, $timesheet->hours); + } + + /** + * `ticket.projectId` comes from the other left join, so a timesheet whose + * ticket was deleted has neither a ticket id nor a project id. + */ + public function testAcceptsNullTicketIdAndProjectIdWhenTheTicketWasDeleted(): void + { + $timesheet = $this->makeTimesheet(['ticketId' => null, 'projectId' => null]); + + $this->assertNull($timesheet->ticketId); + $this->assertNull($timesheet->projectId); + } + + public function testAcceptsNullKind(): void + { + $timesheet = $this->makeTimesheet(['kind' => null]); + + $this->assertNull($timesheet->kind); + } + + /** + * Hours logged by a deleted user stay attributable through the user id, so + * a consumer can group them per departed worker instead of collapsing them + * into one anonymous pile. + */ + public function testExposesUserIdSoOrphanedHoursStayAttributable(): void + { + $timesheet = $this->makeTimesheet(['userId' => 57, 'username' => null]); + + $this->assertSame(57, $timesheet->userId); + $this->assertNull($timesheet->username); + } + + /** + * `$kind` has to move ahead of the optional date parameters, since a + * required parameter after an optional one is deprecated. CarbonImmutable + * implements __toString() and this codebase does not use strict_types, so a + * date landing in a string parameter would be coerced silently instead of + * raising a TypeError. Pin the declarations so a swap cannot go unnoticed. + */ + public function testKindHoldsAStatusStringAndDatesHoldCarbonInstances(): void + { + $timesheet = $this->makeTimesheet([ + 'kind' => 'GENERAL_BILLABLE', + 'workDate' => CarbonImmutable::parse('2026-03-02 09:00:00'), + ]); + + $this->assertSame('GENERAL_BILLABLE', $timesheet->kind); + $this->assertInstanceOf(CarbonInterface::class, $timesheet->workDate); + $this->assertSame('2026-03-02 09:00:00', $timesheet->workDate->format('Y-m-d H:i:s')); + } + + /** + * @param array $overrides + */ + private function makeTimesheet(array $overrides = []): TimesheetData + { + return new TimesheetData(...[ + 'id' => 1, + 'ticketId' => 2, + 'projectId' => 3, + 'description' => 'Worked on stuff', + 'hours' => 7.5, + 'userId' => 57, + 'username' => 'anne@aarhus.dk', + 'kind' => 'GENERAL_BILLABLE', + 'workDate' => null, + 'modified' => null, + ...$overrides, + ]); + } +} diff --git a/tests/Service/APIDataTest.php b/tests/Service/APIDataTest.php new file mode 100644 index 0000000..6d1435d --- /dev/null +++ b/tests/Service/APIDataTest.php @@ -0,0 +1,197 @@ +makeServiceReturningTimesheets([ + $this->timesheetRow(['username' => null, 'userId' => 57]), + ]); + + $timesheets = $service->getTimesheets(0, 100); + + $this->assertCount(1, $timesheets); + $this->assertNull($timesheets[0]->username); + $this->assertSame(57, $timesheets[0]->userId); + $this->assertSame(7.5, $timesheets[0]->hours); + } + + /** + * Guards the constructor argument order. `$kind` moved ahead of the two + * optional date parameters, and CarbonImmutable implements __toString(), so + * a mis-ordered date would land in `$kind` as a coerced string instead of + * raising a TypeError. Only asserting each field individually catches that. + */ + public function testGetTimesheetsMapsEveryFieldToItsOwnProperty(): void + { + $service = $this->makeServiceReturningTimesheets([$this->timesheetRow()]); + + $timesheet = $service->getTimesheets(0, 100)[0]; + + $this->assertSame(4711, $timesheet->id); + $this->assertSame(12, $timesheet->ticketId); + $this->assertSame(92, $timesheet->projectId); + $this->assertSame('Worked on stuff', $timesheet->description); + $this->assertSame(7.5, $timesheet->hours); + $this->assertSame(57, $timesheet->userId); + $this->assertSame('anne@aarhus.dk', $timesheet->username); + $this->assertSame('GENERAL_BILLABLE', $timesheet->kind); + + $this->assertInstanceOf(CarbonInterface::class, $timesheet->workDate); + $this->assertSame('2026-03-02 09:00:00', $timesheet->workDate->format('Y-m-d H:i:s')); + $this->assertSame('UTC', $timesheet->workDate->timezoneName); + + $this->assertInstanceOf(CarbonInterface::class, $timesheet->modified); + $this->assertSame('2026-03-03 11:30:00', $timesheet->modified->format('Y-m-d H:i:s')); + } + + /** + * Leantime stores "no date" as the zero date rather than null. Carbon throws + * on a format mismatch, so this guard has to stay in place. + */ + public function testGetTimesheetsTreatsTheZeroDateSentinelAsNull(): void + { + $service = $this->makeServiceReturningTimesheets([ + $this->timesheetRow(['workDate' => '0000-00-00 00:00:00', 'modified' => null]), + ]); + + $timesheet = $service->getTimesheets(0, 100)[0]; + + $this->assertNull($timesheet->workDate); + $this->assertNull($timesheet->modified); + } + + /** + * Leantime's `getStateLabels()` falls back to `session('currentProject')` + * when it is handed a null project id, so asking it for labels would return + * some unrelated project's status list and put a wrong status on the ticket. + * A ticket without a project has no status to resolve. + */ + public function testGetTicketsDoesNotLookUpStatusLabelsForATicketWithoutAProject(): void + { + $ticketRepository = $this->createMock(TicketRepository::class); + $ticketRepository->expects($this->never())->method('getStateLabels'); + + $repository = $this->createMock(ApiDataRepository::class); + $repository->method('getTickets')->willReturn([$this->ticketRow(['projectId' => null])]); + + $tickets = (new APIData($ticketRepository, $repository))->getTickets(0, 100); + + $this->assertNull($tickets[0]->projectId); + $this->assertNull($tickets[0]->status); + } + + /** + * The normal path still has to resolve the status against the ticket's own + * project. + */ + public function testGetTicketsResolvesStatusFromTheProjectsOwnLabels(): void + { + $ticketRepository = $this->createMock(TicketRepository::class); + $ticketRepository->expects($this->once()) + ->method('getStateLabels') + ->with(92) + ->willReturn($this->stateLabels()); + + $repository = $this->createMock(ApiDataRepository::class); + $repository->method('getTickets')->willReturn([ + $this->ticketRow(['projectId' => 92, 'status' => 3]), + ]); + + $tickets = (new APIData($ticketRepository, $repository))->getTickets(0, 100); + + $this->assertSame(92, $tickets[0]->projectId); + $this->assertSame('NEW', $tickets[0]->status); + } + + /** + * @param list $rows + */ + private function makeServiceReturningTimesheets(array $rows): APIData + { + $repository = $this->createMock(ApiDataRepository::class); + $repository->method('getTimesheets')->willReturn($rows); + + return new APIData($this->createMock(TicketRepository::class), $repository); + } + + /** + * A row as the repository returns it — the select list in + * `ApiDataRepository::getTimesheets()` decides these keys. + * + * @param array $overrides + */ + private function timesheetRow(array $overrides = []): object + { + return (object) array_merge([ + 'id' => 4711, + 'ticketId' => 12, + 'projectId' => 92, + 'description' => 'Worked on stuff', + 'hours' => 7.5, + 'userId' => 57, + 'username' => 'anne@aarhus.dk', + 'kind' => 'GENERAL_BILLABLE', + 'workDate' => '2026-03-02 09:00:00', + 'modified' => '2026-03-03 11:30:00', + ], $overrides); + } + + /** + * A row as `ApiDataRepository::getTickets()` returns it. + * + * @param array $overrides + */ + private function ticketRow(array $overrides = []): object + { + return (object) array_merge([ + 'id' => 4711, + 'projectId' => 92, + 'headline' => 'Fix the thing', + 'status' => 3, + 'milestoneid' => 0, + 'tags' => '', + 'username' => 'anne@aarhus.dk', + 'planHours' => null, + 'hourRemaining' => null, + 'dateToFinish' => null, + 'editTo' => null, + 'modified' => null, + ], $overrides); + } + + /** + * Mirrors the shape of Leantime v3.9.7's `$statusListSeed`: keyed by the + * integer status id, each entry carrying a `statusType`. + * + * @return array> + */ + private function stateLabels(): array + { + return [ + 3 => [ + 'name' => 'status.new', + 'class' => 'label-info', + 'statusType' => 'NEW', + 'kanbanCol' => true, + 'sortKey' => 1, + ], + ]; + } +} diff --git a/tests/Stub/LeantimeTickets.php b/tests/Stub/LeantimeTickets.php new file mode 100644 index 0000000..be2abf8 --- /dev/null +++ b/tests/Stub/LeantimeTickets.php @@ -0,0 +1,40 @@ + [ + * 'name' => 'status.new', + * 'class' => 'label-info', + * 'statusType' => 'NEW', + * 'kanbanCol' => true, + * 'sortKey' => 1, + * ] + * + * Note that Leantime falls back to `session('currentProject')` when $projectId + * is null, which is why `APIData::getTickets()` must not call this with a null + * project id. + */ +class Tickets +{ + public function getStateLabels($projectId = null): array + { + throw new \LogicException( + 'Leantime\Domain\Tickets\Repositories\Tickets is a test stub and must be mocked.' + ); + } +} From cb0fdb4f7e8bd664775a41e9e701d40ff9d31436 Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Thu, 30 Jul 2026 07:41:36 +0200 Subject: [PATCH 2/6] docs: updated changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6c8054..2d0ae4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## [Unreleased] -* [PR-14](https://github.com/ITK-Leantime/data-api/pull/14) +* [PR-18](https://github.com/ITK-Leantime/data-api/pull/18) * Allowed null values in API models, so entries referencing deleted users or deleted tickets no longer fail the whole request. * Added userId to timesheets, so hours logged by a deleted user stay attributable. * Stopped resolving ticket status against the session's project when a ticket has no project. From 54c9ddf7eab1955aba1542cbe0e57a9fe793ff8f Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Tue, 4 Aug 2026 07:53:37 +0200 Subject: [PATCH 3/6] chore: removed economics fro gitignore --- .gitignore | 2 -- 1 file changed, 2 deletions(-) diff --git a/.gitignore b/.gitignore index a3fd9f3..e0d067c 100644 --- a/.gitignore +++ b/.gitignore @@ -7,5 +7,3 @@ composer.lock release/ checksum.txt *.tar.gz - -economics From 93379437deb10214255886b3007dc81b0a1b3299 Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Tue, 11 Aug 2026 08:45:31 +0000 Subject: [PATCH 4/6] fix: addressed review comments on PR-18 Allowed a missing worker name, and stopped returning a whitespace-only name for a worker whose firstname and lastname are both blank. Ticket status labels are looked up once per project instead of once per ticket, and the remaining implicit-nullable parameters are now explicit. Committed composer.lock and pinned illuminate/database and nesbot/carbon to the versions Leantime v3.9.7 locks, so the tests run against the code Leantime itself runs. PHPUnit moved to 12, the newest release that still runs on PHP 8.3. The pull request test job runs in the project's Docker Compose stack, so the PHP version comes from the Dockerfile rather than being repeated in the workflow. Releases no longer ship .claude, .idea or compose.yml. --- .github/workflows/pr.yml | 22 +- .gitignore | 1 - CHANGELOG.md | 5 +- Model/DeletedData.php | 2 +- Model/WorkerData.php | 12 +- README.md | 21 + Repositories/ApiDataRepository.php | 9 +- Services/APIData.php | 13 +- bin/create-release | 11 +- bin/release-exclude.txt | 4 +- composer.json | 6 +- composer.lock | 3200 ++++++++++++++++++++++++++++ phpunit.xml.dist | 7 +- tests/Model/WorkerDataTest.php | 27 + tests/Service/APIDataTest.php | 131 +- 15 files changed, 3427 insertions(+), 44 deletions(-) create mode 100644 composer.lock create mode 100644 tests/Model/WorkerDataTest.php diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index a32163b..63567a8 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -4,11 +4,9 @@ jobs: changelog: runs-on: ubuntu-latest name: Changelog should be updated - strategy: - fail-fast: false steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Git fetch run: git fetch @@ -19,21 +17,17 @@ jobs: test: runs-on: ubuntu-latest name: Unit tests - strategy: - fail-fast: false steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 - # Matches the PHP version in Dockerfile (itkdev/php8.3-fpm). - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: '8.3' - coverage: none + # Tests run in the project's own compose stack, so the PHP version comes + # from the Dockerfile rather than being repeated here. + - name: Build the php image + run: docker compose build php - name: Install dependencies - run: composer install --no-interaction --prefer-dist --no-progress + run: docker compose run --rm --no-deps php composer install --no-interaction --prefer-dist --no-progress - name: Run tests - run: vendor/bin/phpunit + run: docker compose run --rm --no-deps php vendor/bin/phpunit diff --git a/.gitignore b/.gitignore index e0d067c..13361a9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ vendor .DS_Store *.cache -composer.lock # Release artifacts release/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d0ae4b..690e5ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,10 @@ * Allowed null values in API models, so entries referencing deleted users or deleted tickets no longer fail the whole request. * Added userId to timesheets, so hours logged by a deleted user stay attributable. * Stopped resolving ticket status against the session's project when a ticket has no project. - * Added PHPUnit test setup and a Taskfile for running it. + * Allowed a missing worker name, and stopped returning a whitespace-only name for a worker without one. + * Looked up ticket status labels once per project instead of once per ticket. + * Pinned the development dependencies to the Leantime release the plugin targets. + * Added PHPUnit test setup and a Taskfile for running it, and ran the tests in the project's Docker Compose stack on pull requests. ## [0.1.2] - 2026-03-06 diff --git a/Model/DeletedData.php b/Model/DeletedData.php index 9433a0c..976c19e 100644 --- a/Model/DeletedData.php +++ b/Model/DeletedData.php @@ -4,7 +4,7 @@ use Carbon\CarbonInterface; -class DeletedData +readonly class DeletedData { public function __construct( public ?int $id, diff --git a/Model/WorkerData.php b/Model/WorkerData.php index 41f47f4..ef1f96f 100644 --- a/Model/WorkerData.php +++ b/Model/WorkerData.php @@ -1,16 +1,12 @@ toArray(); } - public function getMilestones(int $startId, int $limit, int $modifiedAfter = null, ?array $ids = null, ?array $projectIds = null): array + public function getMilestones(int $startId, int $limit, ?int $modifiedAfter = null, ?array $ids = null, ?array $projectIds = null): array { return $this->query() ->select(["id", "headline", "projectId", "modified"]) @@ -44,7 +44,7 @@ public function getMilestones(int $startId, int $limit, int $modifiedAfter = nul ->toArray(); } - public function getTickets(int $startId, int $limit, int $modifiedAfter = null, array $ids = null, ?array $projectIds = null): array + public function getTickets(int $startId, int $limit, ?int $modifiedAfter = null, ?array $ids = null, ?array $projectIds = null): array { return $this->query() ->select(["ticket.id", "ticket.headline", "ticket.projectId", "ticket.status", "ticket.planHours", "ticket.hourRemaining", "ticket.tags", "ticket.dateToFinish", "ticket.editTo", "ticket.milestoneid", "ticket.modified", "user.username"]) @@ -83,7 +83,10 @@ public function getWorkers(int $startId, int $limit, ?int $modifiedAfter = null, { return $this->query() ->from("zp_user", "worker") - ->select(["worker.id", "worker.username", DB::raw("CONCAT(worker.firstname, ' ', worker.lastname) as name")]) + // CONCAT_WS skips a missing name part, so a worker with only a + // firstname keeps a usable name. NULLIF turns an all-blank name into + // null rather than a string of whitespace. + ->select(["worker.id", "worker.username", DB::raw("NULLIF(TRIM(CONCAT_WS(' ', worker.firstname, worker.lastname)), '') as name")]) ->where("worker.id", ">=", $startId) ->where("worker.source", "<>", "api") ->when($modifiedAfter !== null, fn ($query) => $query->where("worker.modified", ">=", CarbonImmutable::createFromTimestamp($modifiedAfter)->format(APIData::DATE_FORMAT))) diff --git a/Services/APIData.php b/Services/APIData.php index af28091..17b9116 100644 --- a/Services/APIData.php +++ b/Services/APIData.php @@ -125,7 +125,7 @@ public function getProjects(int $startId, int $limit, ?int $modifiedAfter = null }, $values); } - public function getMilestones(int $startId, int $limit, int $modifiedAfter = null, ?array $ids = null, ?array $projectIds = null): array + public function getMilestones(int $startId, int $limit, ?int $modifiedAfter = null, ?array $ids = null, ?array $projectIds = null): array { $values = $this->apiDataRepository->getMilestones($startId, $limit, $modifiedAfter, $ids, $projectIds); @@ -139,16 +139,21 @@ public function getMilestones(int $startId, int $limit, int $modifiedAfter = nul }, $values); } - public function getTickets(int $startId, int $limit, int $modifiedAfter = null, array $ids = null, ?array $projectIds = null): array + public function getTickets(int $startId, int $limit, ?int $modifiedAfter = null, ?array $ids = null, ?array $projectIds = null): array { $values = $this->apiDataRepository->getTickets($startId, $limit, $modifiedAfter, $ids, $projectIds); - return array_map(function ($value) { + // Tickets arrive in batches from the same handful of projects, so the + // labels are looked up once per project instead of once per ticket. Kept + // local to the call, since labels can change between requests. + $statusesByProject = []; + + return array_map(function ($value) use (&$statusesByProject) { // Asked for labels without a project id, Leantime falls back to // session('currentProject'), which would resolve the status against // an unrelated project. $projectStatuses = $value->projectId !== null - ? $this->ticketRepository->getStateLabels($value->projectId) + ? $statusesByProject[$value->projectId] ??= $this->ticketRepository->getStateLabels($value->projectId) : []; return new TicketData( diff --git a/bin/create-release b/bin/create-release index 9f47f4d..a6ade89 100755 --- a/bin/create-release +++ b/bin/create-release @@ -37,13 +37,18 @@ plugin_name="$plugin_name" # Configuration end git config --global --add safe.directory "$PWD" -if ! git diff --exit-code composer.json; then - (>&2 echo; echo composer.json is changed. Aborting.) +# Both files are restored with `git checkout` at the end, so refuse to run with +# uncommitted changes in them. +if ! git diff --exit-code composer.json composer.lock; then + (>&2 echo; echo composer.json or composer.lock is changed. Aborting.) exit 1 fi # Make sure that `composer.json` has the correct version. composer config version "${tag}" +# Writing the version invalidates the lock file's content hash, so refresh it to +# keep `composer install` from warning about an out of date lock. +composer update --lock --no-install --no-scripts # Script defined in local.create-release to build project-specific assets buildRelease @@ -68,7 +73,7 @@ sha256sum "$name" >| checksum.txt # Clean up rm -fr "$release_dir" -git checkout composer.json +git checkout composer.json composer.lock echo echo "Release content ($name)" diff --git a/bin/release-exclude.txt b/bin/release-exclude.txt index 0e9974d..dd231cf 100755 --- a/bin/release-exclude.txt +++ b/bin/release-exclude.txt @@ -1,11 +1,13 @@ *.tar.gz +.claude .git* +.idea .php-cs-fixer.dist.php .phpunit.cache .twig-cs-fixer.dist.php bin checksum.txt -compose.yaml +compose.yml composer.lock vendor Dockerfile diff --git a/composer.json b/composer.json index f1e60a9..3b3772b 100644 --- a/composer.json +++ b/composer.json @@ -12,9 +12,9 @@ ], "homepage": "https://github.com/ITK-Leantime/leantime-data-api", "require-dev": { - "illuminate/database": "^11.0", - "nesbot/carbon": "^2.72.2 || ^3.0", - "phpunit/phpunit": "^11.0" + "illuminate/database": "11.45.1", + "nesbot/carbon": "3.10.1", + "phpunit/phpunit": "^12.0" }, "config": { }, diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..be93f7b --- /dev/null +++ b/composer.lock @@ -0,0 +1,3200 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "bc479de7cb56c5fdc896005e60d56dee", + "packages": [], + "packages-dev": [ + { + "name": "brick/math", + "version": "0.12.3", + "source": { + "type": "git", + "url": "https://github.com/brick/math.git", + "reference": "866551da34e9a618e64a819ee1e01c20d8a588ba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/brick/math/zipball/866551da34e9a618e64a819ee1e01c20d8a588ba", + "reference": "866551da34e9a618e64a819ee1e01c20d8a588ba", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.2", + "phpunit/phpunit": "^10.1", + "vimeo/psalm": "6.8.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Brick\\Math\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Arbitrary-precision arithmetic library", + "keywords": [ + "Arbitrary-precision", + "BigInteger", + "BigRational", + "arithmetic", + "bigdecimal", + "bignum", + "bignumber", + "brick", + "decimal", + "integer", + "math", + "mathematics", + "rational" + ], + "support": { + "issues": "https://github.com/brick/math/issues", + "source": "https://github.com/brick/math/tree/0.12.3" + }, + "funding": [ + { + "url": "https://github.com/BenMorel", + "type": "github" + } + ], + "time": "2025-02-28T13:11:00+00:00" + }, + { + "name": "carbonphp/carbon-doctrine-types", + "version": "3.2.0", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon-doctrine-types.git", + "reference": "18ba5ddfec8976260ead6e866180bd5d2f71aa1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon-doctrine-types/zipball/18ba5ddfec8976260ead6e866180bd5d2f71aa1d", + "reference": "18ba5ddfec8976260ead6e866180bd5d2f71aa1d", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "conflict": { + "doctrine/dbal": "<4.0.0 || >=5.0.0" + }, + "require-dev": { + "doctrine/dbal": "^4.0.0", + "nesbot/carbon": "^2.71.0 || ^3.0.0", + "phpunit/phpunit": "^10.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Carbon\\Doctrine\\": "src/Carbon/Doctrine/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KyleKatarn", + "email": "kylekatarnls@gmail.com" + } + ], + "description": "Types to use Carbon in Doctrine", + "keywords": [ + "carbon", + "date", + "datetime", + "doctrine", + "time" + ], + "support": { + "issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues", + "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/3.2.0" + }, + "funding": [ + { + "url": "https://github.com/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/nesbot/carbon", + "type": "tidelift" + } + ], + "time": "2024-02-09T16:56:22+00:00" + }, + { + "name": "doctrine/inflector", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^12.0 || ^13.0", + "phpstan/phpstan": "^1.12 || ^2.0", + "phpstan/phpstan-phpunit": "^1.4 || ^2.0", + "phpstan/phpstan-strict-rules": "^1.6 || ^2.0", + "phpunit/phpunit": "^8.5 || ^12.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Inflector\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", + "homepage": "https://www.doctrine-project.org/projects/inflector.html", + "keywords": [ + "inflection", + "inflector", + "lowercase", + "manipulation", + "php", + "plural", + "singular", + "strings", + "uppercase", + "words" + ], + "support": { + "issues": "https://github.com/doctrine/inflector/issues", + "source": "https://github.com/doctrine/inflector/tree/2.1.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", + "type": "tidelift" + } + ], + "time": "2025-08-10T19:31:58+00:00" + }, + { + "name": "illuminate/collections", + "version": "v11.51.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/collections.git", + "reference": "856b1da953e46281ba61d7c82d337072d3ee1825" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/collections/zipball/856b1da953e46281ba61d7c82d337072d3ee1825", + "reference": "856b1da953e46281ba61d7c82d337072d3ee1825", + "shasum": "" + }, + "require": { + "illuminate/conditionable": "^11.0", + "illuminate/contracts": "^11.0", + "illuminate/macroable": "^11.0", + "php": "^8.2" + }, + "suggest": { + "symfony/var-dumper": "Required to use the dump method (^7.0)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "11.x-dev" + } + }, + "autoload": { + "files": [ + "functions.php", + "helpers.php" + ], + "psr-4": { + "Illuminate\\Support\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Collections package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-03-24T11:54:20+00:00" + }, + { + "name": "illuminate/conditionable", + "version": "v11.51.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/conditionable.git", + "reference": "319b717e0587bd7c8a3b44464f0e27867b4bcda9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/conditionable/zipball/319b717e0587bd7c8a3b44464f0e27867b4bcda9", + "reference": "319b717e0587bd7c8a3b44464f0e27867b4bcda9", + "shasum": "" + }, + "require": { + "php": "^8.0.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "11.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Support\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Conditionable package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-03-24T11:54:20+00:00" + }, + { + "name": "illuminate/container", + "version": "v11.51.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/container.git", + "reference": "79bf9149ad7ddd7e14326ebcdd41197d2c4ee36a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/container/zipball/79bf9149ad7ddd7e14326ebcdd41197d2c4ee36a", + "reference": "79bf9149ad7ddd7e14326ebcdd41197d2c4ee36a", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^11.0", + "php": "^8.2", + "psr/container": "^1.1.1|^2.0.1" + }, + "provide": { + "psr/container-implementation": "1.1|2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "11.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Container\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Container package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-03-24T11:54:20+00:00" + }, + { + "name": "illuminate/contracts", + "version": "v11.51.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/contracts.git", + "reference": "4787042340aae19a7ea0fa82f4073c4826204a48" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/contracts/zipball/4787042340aae19a7ea0fa82f4073c4826204a48", + "reference": "4787042340aae19a7ea0fa82f4073c4826204a48", + "shasum": "" + }, + "require": { + "php": "^8.2", + "psr/container": "^1.1.1|^2.0.1", + "psr/simple-cache": "^1.0|^2.0|^3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "11.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Contracts\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Contracts package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-11-27T16:16:07+00:00" + }, + { + "name": "illuminate/database", + "version": "v11.45.1", + "source": { + "type": "git", + "url": "https://github.com/illuminate/database.git", + "reference": "0b3a1d429fb44c8bc2ed6afcc431872cbf3a85b2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/database/zipball/0b3a1d429fb44c8bc2ed6afcc431872cbf3a85b2", + "reference": "0b3a1d429fb44c8bc2ed6afcc431872cbf3a85b2", + "shasum": "" + }, + "require": { + "brick/math": "^0.9.3|^0.10.2|^0.11|^0.12", + "ext-pdo": "*", + "illuminate/collections": "^11.0", + "illuminate/container": "^11.0", + "illuminate/contracts": "^11.0", + "illuminate/macroable": "^11.0", + "illuminate/support": "^11.0", + "laravel/serializable-closure": "^1.3|^2.0", + "php": "^8.2" + }, + "suggest": { + "ext-filter": "Required to use the Postgres database driver.", + "fakerphp/faker": "Required to use the eloquent factory builder (^1.24).", + "illuminate/console": "Required to use the database commands (^11.0).", + "illuminate/events": "Required to use the observers with Eloquent (^11.0).", + "illuminate/filesystem": "Required to use the migrations (^11.0).", + "illuminate/pagination": "Required to paginate the result set (^11.0).", + "symfony/finder": "Required to use Eloquent model factories (^7.0)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "11.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Database\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Database package.", + "homepage": "https://laravel.com", + "keywords": [ + "database", + "laravel", + "orm", + "sql" + ], + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-03-24T11:54:20+00:00" + }, + { + "name": "illuminate/macroable", + "version": "v11.51.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/macroable.git", + "reference": "e1cb9e51b9ed5d3c9bc1ab431d0a52fe42a990ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/macroable/zipball/e1cb9e51b9ed5d3c9bc1ab431d0a52fe42a990ed", + "reference": "e1cb9e51b9ed5d3c9bc1ab431d0a52fe42a990ed", + "shasum": "" + }, + "require": { + "php": "^8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "11.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Support\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Macroable package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2024-06-28T20:10:30+00:00" + }, + { + "name": "illuminate/support", + "version": "v11.51.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/support.git", + "reference": "11cc8bd46991e277f6af19eb9dee5d4c1f65c7d2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/support/zipball/11cc8bd46991e277f6af19eb9dee5d4c1f65c7d2", + "reference": "11cc8bd46991e277f6af19eb9dee5d4c1f65c7d2", + "shasum": "" + }, + "require": { + "doctrine/inflector": "^2.0", + "ext-ctype": "*", + "ext-filter": "*", + "ext-mbstring": "*", + "illuminate/collections": "^11.0", + "illuminate/conditionable": "^11.0", + "illuminate/contracts": "^11.0", + "illuminate/macroable": "^11.0", + "nesbot/carbon": "^2.72.6|^3.8.4", + "php": "^8.2", + "voku/portable-ascii": "^2.0.2" + }, + "conflict": { + "tightenco/collect": "<5.5.33" + }, + "replace": { + "spatie/once": "*" + }, + "suggest": { + "illuminate/filesystem": "Required to use the Composer class (^11.0).", + "laravel/serializable-closure": "Required to use the once function (^1.3|^2.0).", + "league/commonmark": "Required to use Str::markdown() and Stringable::markdown() (^2.7).", + "league/uri": "Required to use the Uri class (^7.5.1).", + "ramsey/uuid": "Required to use Str::uuid() (^4.7).", + "symfony/process": "Required to use the Composer class (^7.0).", + "symfony/uid": "Required to use Str::ulid() (^7.0).", + "symfony/var-dumper": "Required to use the dd function (^7.0).", + "vlucas/phpdotenv": "Required to use the Env class and env helper (^5.6.1)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "11.x-dev" + } + }, + "autoload": { + "files": [ + "functions.php", + "helpers.php" + ], + "psr-4": { + "Illuminate\\Support\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Support package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2026-01-20T15:25:35+00:00" + }, + { + "name": "laravel/serializable-closure", + "version": "v2.0.15", + "source": { + "type": "git", + "url": "https://github.com/laravel/serializable-closure.git", + "reference": "dccd8bcb851bb03fcc005df650b708b57cc52661" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/dccd8bcb851bb03fcc005df650b708b57cc52661", + "reference": "dccd8bcb851bb03fcc005df650b708b57cc52661", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "illuminate/support": "^10.0|^11.0|^12.0|^13.0", + "nesbot/carbon": "^2.67|^3.0", + "pestphp/pest": "^2.36|^3.0|^4.0", + "phpstan/phpstan": "^2.0", + "symfony/var-dumper": "^6.2.0|^7.0.0|^8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\SerializableClosure\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "nuno@laravel.com" + } + ], + "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.", + "keywords": [ + "closure", + "laravel", + "serializable" + ], + "support": { + "issues": "https://github.com/laravel/serializable-closure/issues", + "source": "https://github.com/laravel/serializable-closure" + }, + "time": "2026-07-21T16:49:22+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "nesbot/carbon", + "version": "3.10.1", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon.git", + "reference": "1fd1935b2d90aef2f093c5e35f7ae1257c448d00" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/1fd1935b2d90aef2f093c5e35f7ae1257c448d00", + "reference": "1fd1935b2d90aef2f093c5e35f7ae1257c448d00", + "shasum": "" + }, + "require": { + "carbonphp/carbon-doctrine-types": "<100.0", + "ext-json": "*", + "php": "^8.1", + "psr/clock": "^1.0", + "symfony/clock": "^6.3.12 || ^7.0", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "require-dev": { + "doctrine/dbal": "^3.6.3 || ^4.0", + "doctrine/orm": "^2.15.2 || ^3.0", + "friendsofphp/php-cs-fixer": "^3.75.0", + "kylekatarnls/multi-tester": "^2.5.3", + "phpmd/phpmd": "^2.15.0", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.17", + "phpunit/phpunit": "^10.5.46", + "squizlabs/php_codesniffer": "^3.13.0" + }, + "bin": [ + "bin/carbon" + ], + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Carbon\\Laravel\\ServiceProvider" + ] + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev", + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Carbon\\": "src/Carbon/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Nesbitt", + "email": "brian@nesbot.com", + "homepage": "https://markido.com" + }, + { + "name": "kylekatarnls", + "homepage": "https://github.com/kylekatarnls" + } + ], + "description": "An API extension for DateTime that supports 281 different languages.", + "homepage": "https://carbon.nesbot.com", + "keywords": [ + "date", + "datetime", + "time" + ], + "support": { + "docs": "https://carbon.nesbot.com/docs", + "issues": "https://github.com/CarbonPHP/carbon/issues", + "source": "https://github.com/CarbonPHP/carbon" + }, + "funding": [ + { + "url": "https://github.com/sponsors/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon#sponsor", + "type": "opencollective" + }, + { + "url": "https://tidelift.com/subscription/pkg/packagist-nesbot-carbon?utm_source=packagist-nesbot-carbon&utm_medium=referral&utm_campaign=readme", + "type": "tidelift" + } + ], + "time": "2025-06-21T15:19:35+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.8.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "044a6a392ff8ad0d61f14370a5fbbd0a0107152f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/044a6a392ff8ad0d61f14370a5fbbd0a0107152f", + "reference": "044a6a392ff8ad0d61f14370a5fbbd0a0107152f", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.8.0" + }, + "time": "2026-07-04T14:30:18+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "12.5.7", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "186dab580576598076de6818596d12b61801880e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/186dab580576598076de6818596d12b61801880e", + "reference": "186dab580576598076de6818596d12b61801880e", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.7.0", + "php": ">=8.3", + "phpunit/php-text-template": "^5.0", + "sebastian/complexity": "^5.0", + "sebastian/environment": "^8.1.2", + "sebastian/lines-of-code": "^4.0.1", + "sebastian/version": "^6.0", + "theseer/tokenizer": "^2.0.1" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.28" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "12.5.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.7" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" + } + ], + "time": "2026-06-01T13:24:19+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", + "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/6.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", + "type": "tidelift" + } + ], + "time": "2026-02-02T14:04:18+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^12.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:58:58+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/e1367a453f0eda562eedb4f659e13aa900d66c53", + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:59:16+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "8.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/8.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:59:38+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "12.5.33", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "b98e028a26c5c5ba7e4a54be96ccf35f2914d184" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b98e028a26c5c5ba7e4a54be96ccf35f2914d184", + "reference": "b98e028a26c5c5ba7e4a54be96ccf35f2914d184", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-filter": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.3", + "phpunit/php-code-coverage": "^12.5.7", + "phpunit/php-file-iterator": "^6.0.1", + "phpunit/php-invoker": "^6.0.0", + "phpunit/php-text-template": "^5.0.0", + "phpunit/php-timer": "^8.0.0", + "sebastian/cli-parser": "^4.2.1", + "sebastian/comparator": "^7.1.8", + "sebastian/diff": "^7.0.0", + "sebastian/environment": "^8.1.2", + "sebastian/exporter": "^7.0.3", + "sebastian/global-state": "^8.0.3", + "sebastian/object-enumerator": "^7.0.0", + "sebastian/recursion-context": "^7.0.1", + "sebastian/type": "^6.0.4", + "sebastian/version": "^6.0.0", + "staabm/side-effects-detector": "^1.0.5" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "12.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.33" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsoring.html", + "type": "other" + } + ], + "time": "2026-07-28T13:58:09+00:00" + }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/simple-cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" + }, + "time": "2021-10-29T13:26:27+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "4.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "7d05781b13f7dec9043a629a21d086ed74582a15" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/7d05781b13f7dec9043a629a21d086ed74582a15", + "reference": "7d05781b13f7dec9043a629a21d086ed74582a15", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/4.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/cli-parser", + "type": "tidelift" + } + ], + "time": "2026-05-17T05:29:34+00:00" + }, + { + "name": "sebastian/comparator", + "version": "7.1.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "7c65c1e79836812819705b473a90c12399542485" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/7c65c1e79836812819705b473a90c12399542485", + "reference": "7c65c1e79836812819705b473a90c12399542485", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.3", + "sebastian/diff": "^7.0", + "sebastian/exporter": "^7.0.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.25" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/7.1.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-05-21T04:45:25+00:00" + }, + { + "name": "sebastian/complexity", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/bad4316aba5303d0221f43f8cee37eb58d384bbb", + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:55:25+00:00" + }, + { + "name": "sebastian/diff", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "7ab1ea946c012266ca32390913653d844ecd085f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7ab1ea946c012266ca32390913653d844ecd085f", + "reference": "7ab1ea946c012266ca32390913653d844ecd085f", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0", + "symfony/process": "^7.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:55:46+00:00" + }, + { + "name": "sebastian/environment", + "version": "8.1.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "9d32c685773823b1983e256ae4ecd48a10d6e439" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/9d32c685773823b1983e256ae4ecd48a10d6e439", + "reference": "9d32c685773823b1983e256ae4ecd48a10d6e439", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.26" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/8.1.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" + } + ], + "time": "2026-05-25T13:40:20+00:00" + }, + { + "name": "sebastian/exporter", + "version": "7.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23", + "reference": "c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.3", + "sebastian/recursion-context": "^7.0.1" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/7.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2026-05-20T04:37:17+00:00" + }, + { + "name": "sebastian/global-state", + "version": "8.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "b164d3274d6537ab462591c5755f76a8f5b1aae9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b164d3274d6537ab462591c5755f76a8f5b1aae9", + "reference": "b164d3274d6537ab462591c5755f76a8f5b1aae9", + "shasum": "" + }, + "require": { + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0.1" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^12.5.28" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/8.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", + "type": "tidelift" + } + ], + "time": "2026-06-01T15:10:33+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "d543b8ef219dcd8da262cbb958639a96bedba10e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d543b8ef219dcd8da262cbb958639a96bedba10e", + "reference": "d543b8ef219dcd8da262cbb958639a96bedba10e", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.7.0", + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/lines-of-code", + "type": "tidelift" + } + ], + "time": "2026-05-19T16:22:07+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "shasum": "" + }, + "require": { + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:57:48+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "4bfa827c969c98be1e527abd576533293c634f6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/4bfa827c969c98be1e527abd576533293c634f6a", + "reference": "4bfa827c969c98be1e527abd576533293c634f6a", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:58:17+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "7.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/7.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-13T04:44:59+00:00" + }, + { + "name": "sebastian/type", + "version": "6.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "82ff822c2edc46724be9f7411d3163021f602773" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/82ff822c2edc46724be9f7411d3163021f602773", + "reference": "82ff822c2edc46724be9f7411d3163021f602773", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/6.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" + } + ], + "time": "2026-05-20T06:45:45+00:00" + }, + { + "name": "sebastian/version", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/3e6ccf7657d4f0a59200564b08cead899313b53c", + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T05:00:38+00:00" + }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00" + }, + { + "name": "symfony/clock", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/clock.git", + "reference": "674fa3b98e21531dd040e613479f5f6fa8f32111" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/clock/zipball/674fa3b98e21531dd040e613479f5f6fa8f32111", + "reference": "674fa3b98e21531dd040e613479f5f6fa8f32111", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/clock": "^1.0", + "symfony/polyfill-php83": "^1.28" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/now.php" + ], + "psr-4": { + "Symfony\\Component\\Clock\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Decouples applications from the system clock", + "homepage": "https://symfony.com", + "keywords": [ + "clock", + "psr20", + "time" + ], + "support": { + "source": "https://github.com/symfony/clock/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.7.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/f3202fa1b5097b0af062dc978b32ecf63404e31d", + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-05T06:23:12+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.38.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-27T06:59:30+00:00" + }, + { + "name": "symfony/polyfill-php83", + "version": "v1.41.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "5ea99087fb99c273a9b9236ed4c31e78b16103c6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/5ea99087fb99c273a9b9236ed4c31e78b16103c6", + "reference": "5ea99087fb99c273a9b9236ed4c31e78b16103c6", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php83/tree/v1.41.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-07-01T12:47:55+00:00" + }, + { + "name": "symfony/translation", + "version": "v7.4.16", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "501e0ff4553c744209ca1a68790d8a4541563710" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/501e0ff4553c744209ca1a68790d8a4541563710", + "reference": "501e0ff4553c744209ca1a68790d8a4541563710", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/translation-contracts": "^2.5.3|^3.3" + }, + "conflict": { + "nikic/php-parser": "<5.0", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<6.4", + "symfony/service-contracts": "<2.5", + "symfony/twig-bundle": "<6.4", + "symfony/yaml": "<6.4" + }, + "provide": { + "symfony/translation-implementation": "2.3|3.0" + }, + "require-dev": { + "nikic/php-parser": "^5.0", + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/http-client-contracts": "^2.5|^3.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to internationalize your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/translation/tree/v7.4.16" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-07-30T12:37:26+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v3.7.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "ccb206b98faccc511ebae8e5fad50f2dc0b30621" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/ccb206b98faccc511ebae8e5fad50f2dc0b30621", + "reference": "ccb206b98faccc511ebae8e5fad50f2dc0b30621", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-contracts/tree/v3.7.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-05T06:23:12+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^8.1" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/2.0.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-12-08T11:19:18+00:00" + }, + { + "name": "voku/portable-ascii", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/voku/portable-ascii.git", + "reference": "8e1051fe39379367aecf014f41744ce7539a856f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/voku/portable-ascii/zipball/8e1051fe39379367aecf014f41744ce7539a856f", + "reference": "8e1051fe39379367aecf014f41744ce7539a856f", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpunit/phpunit": "~8.5 || ~9.6 || ~10.5 || ~11.5" + }, + "suggest": { + "ext-intl": "Use Intl for transliterator_transliterate() support" + }, + "type": "library", + "autoload": { + "psr-4": { + "voku\\": "src/voku/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lars Moelleken", + "homepage": "https://www.moelleken.org/" + } + ], + "description": "Portable ASCII library - performance optimized (ascii) string functions for php.", + "homepage": "https://github.com/voku/portable-ascii", + "keywords": [ + "ascii", + "clean", + "php" + ], + "support": { + "issues": "https://github.com/voku/portable-ascii/issues", + "source": "https://github.com/voku/portable-ascii/tree/2.1.1" + }, + "funding": [ + { + "url": "https://www.paypal.me/moelleken", + "type": "custom" + }, + { + "url": "https://github.com/voku", + "type": "github" + }, + { + "url": "https://opencollective.com/portable-ascii", + "type": "open_collective" + }, + { + "url": "https://www.patreon.com/voku", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/voku/portable-ascii", + "type": "tidelift" + } + ], + "time": "2026-04-26T05:33:54+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": {}, + "platform-dev": {}, + "plugin-api-version": "2.9.0" +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 2a306e4..748b90e 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -5,7 +5,12 @@ cacheDirectory=".phpunit.cache" colors="true" failOnWarning="true" - failOnNotice="true"> + failOnNotice="true" + failOnDeprecation="true" + failOnPhpunitDeprecation="true" + failOnPhpunitNotice="true" + displayDetailsOnTestsThatTriggerDeprecations="true" + displayDetailsOnPhpunitNotices="true"> tests diff --git a/tests/Model/WorkerDataTest.php b/tests/Model/WorkerDataTest.php new file mode 100644 index 0000000..bc41797 --- /dev/null +++ b/tests/Model/WorkerDataTest.php @@ -0,0 +1,27 @@ +assertSame(57, $worker->id); + $this->assertNull($worker->email); + $this->assertNull($worker->name); + } +} diff --git a/tests/Service/APIDataTest.php b/tests/Service/APIDataTest.php index 6d1435d..b301712 100644 --- a/tests/Service/APIDataTest.php +++ b/tests/Service/APIDataTest.php @@ -88,7 +88,7 @@ public function testGetTicketsDoesNotLookUpStatusLabelsForATicketWithoutAProject $ticketRepository = $this->createMock(TicketRepository::class); $ticketRepository->expects($this->never())->method('getStateLabels'); - $repository = $this->createMock(ApiDataRepository::class); + $repository = $this->createStub(ApiDataRepository::class); $repository->method('getTickets')->willReturn([$this->ticketRow(['projectId' => null])]); $tickets = (new APIData($ticketRepository, $repository))->getTickets(0, 100); @@ -109,7 +109,7 @@ public function testGetTicketsResolvesStatusFromTheProjectsOwnLabels(): void ->with(92) ->willReturn($this->stateLabels()); - $repository = $this->createMock(ApiDataRepository::class); + $repository = $this->createStub(ApiDataRepository::class); $repository->method('getTickets')->willReturn([ $this->ticketRow(['projectId' => 92, 'status' => 3]), ]); @@ -120,15 +120,110 @@ public function testGetTicketsResolvesStatusFromTheProjectsOwnLabels(): void $this->assertSame('NEW', $tickets[0]->status); } + /** + * Tickets arrive in batches from a handful of projects, so the labels are + * looked up once per project rather than once per ticket. + */ + public function testGetTicketsLooksUpStatusLabelsOncePerProject(): void + { + $ticketRepository = $this->createMock(TicketRepository::class); + $ticketRepository->expects($this->exactly(2)) + ->method('getStateLabels') + ->willReturn($this->stateLabels()); + + $repository = $this->createStub(ApiDataRepository::class); + $repository->method('getTickets')->willReturn([ + $this->ticketRow(['id' => 1, 'projectId' => 92]), + $this->ticketRow(['id' => 2, 'projectId' => 92]), + $this->ticketRow(['id' => 3, 'projectId' => 93]), + ]); + + $tickets = (new APIData($ticketRepository, $repository))->getTickets(0, 100); + + $this->assertCount(3, $tickets); + $this->assertSame('NEW', $tickets[0]->status); + $this->assertSame('NEW', $tickets[1]->status); + } + + public function testGetWorkersMapsEveryFieldToItsOwnProperty(): void + { + $worker = $this->makeServiceReturningWorkers([$this->workerRow()])->getWorkers(0, 100)[0]; + + $this->assertSame(57, $worker->id); + $this->assertSame('anne@aarhus.dk', $worker->email); + $this->assertSame('Anne Andersen', $worker->name); + } + + /** + * `ApiDataRepository::getWorkers()` maps an all-blank name to null, so the + * mapping has to carry that through instead of failing the whole request. + */ + public function testGetWorkersMapsABlankNameToNull(): void + { + $worker = $this->makeServiceReturningWorkers([ + $this->workerRow(['name' => null]), + ])->getWorkers(0, 100)[0]; + + $this->assertNull($worker->name); + $this->assertSame('anne@aarhus.dk', $worker->email); + } + + public function testGetDeletedMapsEntryIdAndDeletedDate(): void + { + $deleted = $this->makeServiceReturningDeleted([$this->deletedRow()]) + ->getDeleted(APIData::TYPE_TICKETS)[0]; + + $this->assertSame(4711, $deleted->id); + $this->assertInstanceOf(CarbonInterface::class, $deleted->deletedDate); + $this->assertSame('2026-03-04 08:15:00', $deleted->deletedDate->format('Y-m-d H:i:s')); + $this->assertSame('UTC', $deleted->deletedDate->timezoneName); + } + + /** + * The delete triggers write `dateDeleted` through the same column type as the + * rest of Leantime, so the zero date sentinel has to be handled here too. + */ + public function testGetDeletedTreatsTheZeroDateSentinelAsNull(): void + { + $deleted = $this->makeServiceReturningDeleted([ + $this->deletedRow(['entryId' => null, 'dateDeleted' => '0000-00-00 00:00:00']), + ])->getDeleted(APIData::TYPE_TICKETS)[0]; + + $this->assertNull($deleted->id); + $this->assertNull($deleted->deletedDate); + } + /** * @param list $rows */ private function makeServiceReturningTimesheets(array $rows): APIData { - $repository = $this->createMock(ApiDataRepository::class); + $repository = $this->createStub(ApiDataRepository::class); $repository->method('getTimesheets')->willReturn($rows); - return new APIData($this->createMock(TicketRepository::class), $repository); + return new APIData($this->createStub(TicketRepository::class), $repository); + } + + /** + * @param list $rows + */ + private function makeServiceReturningWorkers(array $rows): APIData + { + $repository = $this->createStub(ApiDataRepository::class); + $repository->method('getWorkers')->willReturn($rows); + + return new APIData($this->createStub(TicketRepository::class), $repository); + } + + /** + * @param list $rows + */ + private function makeServiceReturningDeleted(array $rows): APIData + { + $repository = $this->createStub(ApiDataRepository::class); + $repository->method('getDeleted')->willReturn($rows); + + return new APIData($this->createStub(TicketRepository::class), $repository); } /** @@ -176,6 +271,34 @@ private function ticketRow(array $overrides = []): object ], $overrides); } + /** + * A row as `ApiDataRepository::getWorkers()` returns it. `name` is the + * `CONCAT_WS` expression from the select list, not a column. + * + * @param array $overrides + */ + private function workerRow(array $overrides = []): object + { + return (object) array_merge([ + 'id' => 57, + 'username' => 'anne@aarhus.dk', + 'name' => 'Anne Andersen', + ], $overrides); + } + + /** + * A row as `ApiDataRepository::getDeleted()` returns it. + * + * @param array $overrides + */ + private function deletedRow(array $overrides = []): object + { + return (object) array_merge([ + 'entryId' => 4711, + 'dateDeleted' => '2026-03-04 08:15:00', + ], $overrides); + } + /** * Mirrors the shape of Leantime v3.9.7's `$statusListSeed`: keyed by the * integer status id, each entry carrying a `statusType`. From 2f9cb04ce5983830172034d780525f3a2a9fc59a Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Tue, 11 Aug 2026 09:11:39 +0000 Subject: [PATCH 5/6] ci: ran pull request tests on the itkdev image directly The container ran as `deploy` (uid 1000) while the CI checkout belongs to the runner's uid, so composer could not create `vendor`. The compose user now comes from COMPOSE_USER, which the workflow sets to the checkout owner; the image carries a `runner` user at that uid, so it still has a writable home. Only `bin/create-release` needs the rsync the Dockerfile adds, so tests and linting run on itkdev/php8.3-fpm directly and no longer build an image. The built image moved to a `php-release` service, used by the release workflow alone. --- .github/workflows/pr.yml | 14 ++++++++------ .github/workflows/release.yml | 4 ++-- Dockerfile | 2 +- README.md | 8 ++++++-- Taskfile.yml | 9 ++++----- compose.yml | 17 ++++++++++++++++- 6 files changed, 37 insertions(+), 17 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 63567a8..367e2fd 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -21,13 +21,15 @@ jobs: - name: Checkout uses: actions/checkout@v7 - # Tests run in the project's own compose stack, so the PHP version comes - # from the Dockerfile rather than being repeated here. - - name: Build the php image - run: docker compose build php + # The checkout belongs to the runner's uid, so the container has to write + # to the bind mount as that uid rather than as the image's `deploy` user. + - name: Run the container as the checkout owner + run: echo "COMPOSE_USER=$(id -u):$(id -g)" >> "$GITHUB_ENV" + # Tests run in the project's own compose stack, so the PHP version comes + # from compose.yml rather than being repeated here. - name: Install dependencies - run: docker compose run --rm --no-deps php composer install --no-interaction --prefer-dist --no-progress + run: docker compose run --rm --no-deps phpfpm composer install --no-interaction --prefer-dist --no-progress - name: Run tests - run: docker compose run --rm --no-deps php vendor/bin/phpunit + run: docker compose run --rm --no-deps phpfpm vendor/bin/phpunit diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e2c24b9..f73c81d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,7 +14,7 @@ jobs: release: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Build and create release env: @@ -34,7 +34,7 @@ jobs: release_name="release-${tag_name}" fi - docker compose run --user root --rm php bin/create-release "$tag_name" + docker compose run --user root --rm php-release bin/create-release "$tag_name" # Delete release if it already exists. gh release view "$release_name" > /dev/null 2>&1 && gh release delete "$release_name" --yes diff --git a/Dockerfile b/Dockerfile index 08ef1cc..ff2de27 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM itkdev/php8.3-fpm:latest USER root -# Add rsync +# Add rsync, which `bin/create-release` needs and the base image does not carry. RUN apt-get update && apt-get --yes install rsync # Clean up diff --git a/README.md b/README.md index 78aec65..14c9ebc 100644 --- a/README.md +++ b/README.md @@ -65,8 +65,8 @@ curl https://leantime.local.itkdev.dk/apidata/api/deleted ## Development -The plugin has no long-running stack, so everything runs in a one-off container -built from the local `Dockerfile`. Install [Task](https://taskfile.dev), then: +The plugin has no long-running stack, so everything runs in a one-off +`itkdev/php8.3-fpm` container. Install [Task](https://taskfile.dev), then: ```shell task setup @@ -76,6 +76,10 @@ task lint Run `task --list-all` to see the remaining commands. +The `Dockerfile` exists only for releases: `bin/create-release` needs `rsync`, +which the base image does not carry. It backs the `php-release` compose service +and is not used for tests or linting. + Leantime core is not a Composer dependency of this plugin, so the development dependencies stand in for it. `illuminate/database` and `nesbot/carbon` are pinned to the exact versions the targeted Leantime release locks, so the tests diff --git a/Taskfile.yml b/Taskfile.yml index 4601cfe..d44d170 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -2,7 +2,7 @@ # Run `task` (or `task --list-all`) to see all available commands. # # This plugin has no long-running stack, so everything runs in a one-off -# container built from the local Dockerfile (itkdev/php8.3-fpm). +# itkdev/php8.3-fpm container. version: "3" @@ -20,14 +20,14 @@ tasks: # -------------------------------------------------------------- Wrappers --- compose: - desc: "Run a docker compose command. Example: task compose -- build php." + desc: "Run a docker compose command. Example: task compose -- config." cmds: - "{{ .DOCKER_COMPOSE }} {{ .CLI_ARGS }}" php: desc: "Run a command in a one-off php container. Example: task php -- php --version." cmds: - - task compose -- run --rm --no-deps php {{ .CLI_ARGS }} + - task compose -- run --rm --no-deps phpfpm {{ .CLI_ARGS }} silent: true composer: @@ -39,9 +39,8 @@ tasks: # ------------------------------------------------------------ Lifecycle --- setup: - desc: Build the php image and install dev dependencies. + desc: Install dev dependencies. cmds: - - task compose -- build php - task composer -- install # ------------------------------------------------------------ PHP tests --- diff --git a/compose.yml b/compose.yml index 908a2c5..b190d7f 100644 --- a/compose.yml +++ b/compose.yml @@ -1,5 +1,20 @@ services: - php: + # Tests, linting and composer run straight on the itkdev image — nothing in + # that path needs anything added to it. + phpfpm: + image: itkdev/php8.3-fpm:latest + working_dir: /app + # `deploy` is right locally, where Docker Desktop masks the ownership of the + # bind mount. On a Linux CI runner the checkout belongs to the runner's own + # uid, so CI sets COMPOSE_USER to it — otherwise composer cannot create + # `vendor`. The image carries a `runner` user at uid 1001, so the overridden + # uid still resolves to a writable home. + user: ${COMPOSE_USER:-deploy} + volumes: + - .:/app + + # Only `bin/create-release` needs the built image, for the rsync it adds. + php-release: build: . working_dir: /app volumes: From db81c43b3029cdb5b2965b93b1314eae0d324860 Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Tue, 11 Aug 2026 09:28:36 +0000 Subject: [PATCH 6/6] ci: set the compose user via a workflow env variable Matches how itk-dev/economics does it, and drops a shell step. --- .github/workflows/pr.yml | 12 +++++++----- compose.yml | 6 ++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 367e2fd..a890d81 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -1,5 +1,12 @@ on: pull_request + name: Review + +# The checkout belongs to the runner, so the container writes to the bind mount +# as `runner` rather than as the image's `deploy` user. +env: + COMPOSE_USER: runner + jobs: changelog: runs-on: ubuntu-latest @@ -21,11 +28,6 @@ jobs: - name: Checkout uses: actions/checkout@v7 - # The checkout belongs to the runner's uid, so the container has to write - # to the bind mount as that uid rather than as the image's `deploy` user. - - name: Run the container as the checkout owner - run: echo "COMPOSE_USER=$(id -u):$(id -g)" >> "$GITHUB_ENV" - # Tests run in the project's own compose stack, so the PHP version comes # from compose.yml rather than being repeated here. - name: Install dependencies diff --git a/compose.yml b/compose.yml index b190d7f..4426757 100644 --- a/compose.yml +++ b/compose.yml @@ -5,10 +5,8 @@ services: image: itkdev/php8.3-fpm:latest working_dir: /app # `deploy` is right locally, where Docker Desktop masks the ownership of the - # bind mount. On a Linux CI runner the checkout belongs to the runner's own - # uid, so CI sets COMPOSE_USER to it — otherwise composer cannot create - # `vendor`. The image carries a `runner` user at uid 1001, so the overridden - # uid still resolves to a writable home. + # bind mount. On a Linux CI runner the checkout belongs to `runner`, so CI + # sets COMPOSE_USER to it — otherwise composer cannot create `vendor`. user: ${COMPOSE_USER:-deploy} volumes: - .:/app