Implement extend-schemas-in-register-service: honour _extend on RegisterService end-to-end (#1428)#1430
Conversation
Quality Report — ConductionNL/openregister @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| composer | ✅ | ✅ 147/147 | |||
| npm | ✅ | ✅ 598/598 | |||
| PHPUnit | ❌ | ||||
| Newman | ❌ | ||||
| Playwright | ⏭️ |
Quality workflow — 2026-05-05 18:37 UTC
Download the full PDF report from the workflow artifacts.
| - **softwarecatalog:** repo not cloned in this workspace; cross-app audit deferred to a separate ticket against that repo. | ||
| - [x] 7.2 Grep `docudesk/lib/Service/RegisterDiscoveryService.php` — confirm it calls `$registerService->findAll(_extend: ['schemas'])` and expects entities. Document that DocuDesk's follow-up PR will swap its inline `$register->jsonSerialize()` for a `RegisterSerializer::serialize($register, ['schemas'])` call. | ||
| - Confirmed: `RegisterDiscoveryService::fetchAvailableRegisters()` (around line 75) calls `findAll(_extend: ['schemas'])` and then `array_map([$this, 'serializeRegister'], ...)` which calls `$register->jsonSerialize()` per register. After this change merges, that path returns ID-only schemas (entity contract is unchanged). The DocuDesk follow-up PR replaces the call with `RegisterService::findAllSerialized(_extend: ['schemas'])` (or injects `RegisterSerializer` and serializes per register). | ||
|
|
There was a problem hiding this comment.
🟡 Concern — softwarecatalog cross-repo audit deferred without a tracking issue number
Task 7.1 notes:
softwarecatalog: repo not cloned in this workspace; cross-app audit deferred to a separate ticket against that repo.
The wire-format breaking change (orphan schema IDs now appear in the schemas array as bare IDs instead of being silently dropped) is a behavioural change for any consumer that calls GET /api/registers?_extend=schemas. The DocuDesk follow-up has a filed issue number (ConductionNL/docudesk#120). The softwarecatalog audit is left without a corresponding tracking issue, making it easy to forget.
Impact: If softwarecatalog depends on schemas being a homogeneous array of objects and has not yet audited its []Schema decoder, it will silently misparse registers with orphan schema references after this merges.
Suggested fix: File a tracking issue against softwarecatalog (or the relevant cross-repo tracker) and link it here, matching the pattern used for DocuDesk in task 8.3.
There was a problem hiding this comment.
Fair point — leaving the softwarecatalog audit without a tracking issue makes it easy to forget, especially given the same DocuDesk follow-up (#120) is filed and visible. I'll open the cross-repo tracking issue against softwarecatalog as a follow-up to this PR and link the issue here once filed, mirroring the DocuDesk task-8.3 pattern.
To set expectations on scope: the audit itself is "grep the codebase for RegisterService::findAll*/RegisterMapper::findAll* callers passing _extend, then check that any matches handle the heterogeneous schemas array shape." Should be quick once we have the repo cloned.
There was a problem hiding this comment.
🟡 Still tracking — the agreed cross-repo tracking issue against softwarecatalog has not been filed (or at least not linked here). Task 7.1 in tasks.md is checkmarked, but the audit is explicitly post-PR work and needs a real issue number to survive the merge. Please file the issue and post the URL in this thread before merging — mirroring how the DocuDesk #120 follow-up is tracked. Will resolve once linked.
WilcoLouwerse
left a comment
There was a problem hiding this comment.
Strict-mode review — 3 blockers, 4 concerns, 3 minors.
Top blockers:
- 🔴 Wrong namespace in RegisterSerializerTest — CI PHPUnit failu
- 🔴 serialize() passes $data['schemas'] to expandSchemas() witho
- 🔴 Type-key mismatch: service builds schemaStatsByRegisterId wi
Strict mode requires REQUEST_CHANGES on any 🔴 or 🟡. Please address blockers and concerns before merge.
Five targeted fixes from WilcoLouwerse's review:
1. Test namespace: tests/Unit/Service/Serializer/RegisterSerializerTest.php
declared `namespace Unit\Service\Serializer;` which the project bootstrap
does not resolve. The 10 new unit tests therefore did not execute in CI.
Corrected to `OCA\OpenRegister\Tests\Unit\Service\Serializer` to match
the rest of the test suite and the bootstrap's namespace-to-path map.
2. Exception coverage in RegisterSerializer::expandSchemas(): catch
MultipleObjectsReturnedException alongside DoesNotExistException so an
ambiguous schema record is treated like an orphan ID (logged + retained
in place) instead of propagating a 500 to the controller.
3. Consistent typing in RegisterSerializer::serializeMany(): cast
$register->getId() to int so the key type matches what
RegisterService::findAllSerialized() stores. PHP's array-key coercion
already handled the mismatch, but the explicit cast makes the contract
obvious and prevents drift if the entity layer ever widens getId().
4. N+1 query characteristic documented in two places:
- RegisterService::findAllSerialized() docblock notes that
`@self.stats` alongside `schemas` triggers one getSchemaObjectCounts()
query per register.
- docs/development/Services/serializers.md gains a "Performance note"
section pointing DI callers at findAll() when finer control is needed.
5. New controller unit test
`testIndexForwardsExtendValueToFindAllSerialized` asserts that the
`_extend` value parsed from the request is forwarded verbatim to the
service. The other index tests use `$this->anything()` for `_extend`,
which would not catch a regression that drops or mutates the value
before the service call.
Pushed back on blocker 2 (null guard) and blocker 3 (type-key mismatch)
in the GitHub replies: Register::jsonSerialize() guarantees an array via
array_filter, and PHP's array-key coercion already handles numeric-string
keys. Wilco's suggested code change for blocker 3 is applied for clarity;
the underlying claim is not a runtime bug.
Static analysis on touched lib files: PHPCS 0 errors / 5 pre-existing
warnings, PHPStan 0 errors, Psalm 0 errors.
|
Addressed Wilco's review in 2776afa — five targeted fixes plus a merge with Fixed:
Style consistency applied without claiming a bug:
Pushed back on:
Tracked as follow-up:
Static analysis on touched lib files: PHPCS 0 errors / 5 pre-existing warnings, PHPStan 0 errors, Psalm 0 errors. CI will run shortly. |
Five targeted fixes from WilcoLouwerse's review:
1. Test namespace: tests/Unit/Service/Serializer/RegisterSerializerTest.php
declared `namespace Unit\Service\Serializer;` which the project bootstrap
does not resolve. The 10 new unit tests therefore did not execute in CI.
Corrected to `OCA\OpenRegister\Tests\Unit\Service\Serializer` to match
the rest of the test suite and the bootstrap's namespace-to-path map.
2. Exception coverage in RegisterSerializer::expandSchemas(): catch
MultipleObjectsReturnedException alongside DoesNotExistException so an
ambiguous schema record is treated like an orphan ID (logged + retained
in place) instead of propagating a 500 to the controller.
3. Consistent typing in RegisterSerializer::serializeMany(): cast
$register->getId() to int so the key type matches what
RegisterService::findAllSerialized() stores. PHP's array-key coercion
already handled the mismatch, but the explicit cast makes the contract
obvious and prevents drift if the entity layer ever widens getId().
4. N+1 query characteristic documented in two places:
- RegisterService::findAllSerialized() docblock notes that
`@self.stats` alongside `schemas` triggers one getSchemaObjectCounts()
query per register.
- docs/development/Services/serializers.md gains a "Performance note"
section pointing DI callers at findAll() when finer control is needed.
5. New controller unit test
`testIndexForwardsExtendValueToFindAllSerialized` asserts that the
`_extend` value parsed from the request is forwarded verbatim to the
service. The other index tests use `$this->anything()` for `_extend`,
which would not catch a regression that drops or mutates the value
before the service call.
Pushed back on blocker 2 (null guard) and blocker 3 (type-key mismatch)
in the GitHub replies: Register::jsonSerialize() guarantees an array via
array_filter, and PHP's array-key coercion already handles numeric-string
keys. Wilco's suggested code change for blocker 3 is applied for clarity;
the underlying claim is not a runtime bug.
Static analysis on touched lib files: PHPCS 0 errors / 5 pre-existing
warnings, PHPStan 0 errors, Psalm 0 errors.
25346a6 to
7a97601
Compare
|
Rebased on top of latest
Resolved 3 conflicts:
Force-pushed with lease. |
Five targeted fixes from WilcoLouwerse's review:
1. Test namespace: tests/Unit/Service/Serializer/RegisterSerializerTest.php
declared `namespace Unit\Service\Serializer;` which the project bootstrap
does not resolve. The 10 new unit tests therefore did not execute in CI.
Corrected to `OCA\OpenRegister\Tests\Unit\Service\Serializer` to match
the rest of the test suite and the bootstrap's namespace-to-path map.
2. Exception coverage in RegisterSerializer::expandSchemas(): catch
MultipleObjectsReturnedException alongside DoesNotExistException so an
ambiguous schema record is treated like an orphan ID (logged + retained
in place) instead of propagating a 500 to the controller.
3. Consistent typing in RegisterSerializer::serializeMany(): cast
$register->getId() to int so the key type matches what
RegisterService::findAllSerialized() stores. PHP's array-key coercion
already handled the mismatch, but the explicit cast makes the contract
obvious and prevents drift if the entity layer ever widens getId().
4. N+1 query characteristic documented in two places:
- RegisterService::findAllSerialized() docblock notes that
`@self.stats` alongside `schemas` triggers one getSchemaObjectCounts()
query per register.
- docs/development/Services/serializers.md gains a "Performance note"
section pointing DI callers at findAll() when finer control is needed.
5. New controller unit test
`testIndexForwardsExtendValueToFindAllSerialized` asserts that the
`_extend` value parsed from the request is forwarded verbatim to the
service. The other index tests use `$this->anything()` for `_extend`,
which would not catch a regression that drops or mutates the value
before the service call.
Pushed back on blocker 2 (null guard) and blocker 3 (type-key mismatch)
in the GitHub replies: Register::jsonSerialize() guarantees an array via
array_filter, and PHP's array-key coercion already handles numeric-string
keys. Wilco's suggested code change for blocker 3 is applied for clarity;
the underlying claim is not a runtime bug.
Static analysis on touched lib files: PHPCS 0 errors / 5 pre-existing
warnings, PHPStan 0 errors, Psalm 0 errors.
7a97601 to
a6704e3
Compare
Honour `_extend` on `RegisterService` end-to-end so DI consumers receive the same expanded schemas as HTTP callers. Schema expansion + per-schema `@self.stats` move from the controller into a new `RegisterSerializer`. What changed - New `OCA\OpenRegister\Service\Serializer\RegisterSerializer` (197 lines) with `serialize()` + `serializeMany()`. Recognises `schemas` and `@self.stats`. Preserves orphan schema IDs in original position (deliberate divergence from the pre-refactor controller, documented in proposal/design/spec). Preserves `properties` on expanded schemas; consumer-side stripping stays in the consumer (e.g. DocuDesk). - `RegisterService::findSerialized()` and `findAllSerialized()` — DI-accessible methods that orchestrate the stats fetch and delegate to the serializer. - `RegistersController::index()` now calls `findAllSerialized` and the inline expansion + per-schema stats blocks are gone. Register-level `@self.stats` (object/log/file counts) stays in the controller — out of scope for the serializer. - `RegisterMapper::find()` and `::findAll()` drop the unused `_extend` parameter; the `@SuppressWarnings(PHPMD.UnusedFormalParameter)` pragmas are removed. Service-level signatures keep `_extend` as a documented no-op placeholder for compatibility. - `SchemaMapper`'s `@method find(...)` annotation extended so PHPStan sees the full signature. - `RegisterSerializerTest` (10 unit tests) and `RegisterSerializerIntegrationTest` (4 integration tests) cover all spec scenarios. `RegisterServiceTest` and `RegistersControllerTest` updated for the new constructor + delegation pattern. - Docs: new `docs/development/Services/serializers.md`, CHANGELOG entry (Added + Breaking). Quality - PHPCS / Psalm / PHPStan clean on all touched files - Unit suite for affected files: 120/120 green inside the dev container - Integration tests: 4/4 green Follow-up - DocuDesk follow-up issue filed at ConductionNL/docudesk#120 — opens once openregister cuts a release containing this change. Closes openregister#1428
Five targeted fixes from WilcoLouwerse's review:
1. Test namespace: tests/Unit/Service/Serializer/RegisterSerializerTest.php
declared `namespace Unit\Service\Serializer;` which the project bootstrap
does not resolve. The 10 new unit tests therefore did not execute in CI.
Corrected to `OCA\OpenRegister\Tests\Unit\Service\Serializer` to match
the rest of the test suite and the bootstrap's namespace-to-path map.
2. Exception coverage in RegisterSerializer::expandSchemas(): catch
MultipleObjectsReturnedException alongside DoesNotExistException so an
ambiguous schema record is treated like an orphan ID (logged + retained
in place) instead of propagating a 500 to the controller.
3. Consistent typing in RegisterSerializer::serializeMany(): cast
$register->getId() to int so the key type matches what
RegisterService::findAllSerialized() stores. PHP's array-key coercion
already handled the mismatch, but the explicit cast makes the contract
obvious and prevents drift if the entity layer ever widens getId().
4. N+1 query characteristic documented in two places:
- RegisterService::findAllSerialized() docblock notes that
`@self.stats` alongside `schemas` triggers one getSchemaObjectCounts()
query per register.
- docs/development/Services/serializers.md gains a "Performance note"
section pointing DI callers at findAll() when finer control is needed.
5. New controller unit test
`testIndexForwardsExtendValueToFindAllSerialized` asserts that the
`_extend` value parsed from the request is forwarded verbatim to the
service. The other index tests use `$this->anything()` for `_extend`,
which would not catch a regression that drops or mutates the value
before the service call.
Pushed back on blocker 2 (null guard) and blocker 3 (type-key mismatch)
in the GitHub replies: Register::jsonSerialize() guarantees an array via
array_filter, and PHP's array-key coercion already handles numeric-string
keys. Wilco's suggested code change for blocker 3 is applied for clarity;
the underlying claim is not a runtime bug.
Static analysis on touched lib files: PHPCS 0 errors / 5 pre-existing
warnings, PHPStan 0 errors, Psalm 0 errors.
a6704e3 to
41b6bad
Compare
- EntityOrganisationAssignmentTest: add RegisterSerializer to RegisterService ctor (7th arg added by PR 1430). - RegisterSerializerTest: assert new warning message 'Schema unresolvable for expansion' (production was updated). Note: 28 'Unknown named parameter $_extend' errors remain — these trace to a production-code bug in lib/Service/Configuration/ImportHandler.php where line 645 still calls registerMapper->find(_extend: []) but PR 1430 removed _extend from RegisterMapper::find()'s signature. Not fixed here because per agent instructions production code is off-limits.
The PR's review-feedback commit (41b6bad) removed `_extend` from `RegisterMapper::find()` / `findAll()` signatures as a no-op param, but missed this call site in ImportHandler. Result: 24 unit-test errors + 4 expectException failures all stack-tracing to ImportHandler.php:645 with "Unknown named parameter \$_extend". The companion call at line 3138 uses `schemaMapper->find(_extend: [])` which is fine — SchemaMapper still has the param. Verified locally: ImportHandler tests 231/231 pass, full PHPUnit suite 12,226 tests / 25,949 assertions / 0 errors / 0 failures.
This PR's review-feedback commit (41b6bad) removed `\$_extend` from `RegisterMapper::find()` / `findAll()` as a no-op param. But one positional call site in MagicMapper at line 6426 still passed the old 5-arg shape `(id, [], null, true, false)` — the `[]` (former `\$_extend`) now lands on `\$published: ?bool` and triggers a TypeError at runtime. That broke `POST /api/objects/{register}/{schema}/{id}/files` (Advanced File Operations Newman tests 1-6) which routes through MagicMapper's `findRegisterAndSchemaForEntity` helper. File 1 returned 500; tests 2-6 cascaded as 404. Switched the call to named arguments matching the new signature: \$this->registerMapper->find(id: (int) \$entity->getRegister(), _multitenancy: false); Verified locally: file POST against /api/objects/.../files now returns HTTP 200 with the created file row.
|
🟡 CI heads-up — The failing job is the docs build. The two failures are in files this PR doesn't touch:
Neither file appears in this PR's diff. The check is not marked required ( |
WilcoLouwerse
left a comment
There was a problem hiding this comment.
No blockers. All three 🔴 prior findings fixed; pushback on the TypeError concern verified correct against Register::jsonSerialize()'s upstream contract.
One 🟡 remains: please file (or link) the softwarecatalog cross-repo tracking issue in that thread before merge — task 7.1 is checkmarked but the audit is post-PR work that needs a real follow-up. The failing deploy / Build and validate check is on pre-existing docs MDX issues unrelated to this PR.
…ng issue per PR #1430 review Resolves the outstanding 🟡 from Wilco's approval of PR #1430: > One 🟡 remains: please file (or link) the softwarecatalog cross-repo > tracking issue in that thread before merge — task 7.1 is checkmarked > but the audit is post-PR work that needs a real follow-up. Filed against the softwarecatalog repo as ConductionNL/softwarecatalog#272 with the full wire-format breaking- change context: orphan schema IDs now appear in the `schemas` array as bare integers alongside the resolved Schema objects (was: silently dropped). Statically-typed clients decoding `schemas` as a homogeneous `[]Schema` need to switch to a discriminated decoder. Linked from tasks.md task 7.1 next to the existing DocuDesk follow-up (`ConductionNL/docudesk#120`) so both downstream audits live in the same place.
Summary
Honours
_extendonRegisterServiceend-to-end so DI consumers (DocuDesk, OpenCatalogi, softwarecatalog) receive the same expanded schemas as the HTTPGET /api/registers?_extend=schemasendpoint. Schema expansion + per-schema@self.statsmove fromRegistersController::index()into a newRegisterSerializerin a newlib/Service/Serializer/namespace.Closes #1428.
What changed
New code
lib/Service/Serializer/RegisterSerializer.php— entity serializer withserialize()+serializeMany(). Recognisesschemasand@self.stats. Preserves orphan schema IDs in their original position (deliberate divergence from the pre-refactor controller, documented in the proposal/design/spec). Preservespropertieson expanded schemas; consumer-side stripping stays in the consumer (e.g. DocuDesk).RegisterService::findSerialized()andRegisterService::findAllSerialized()— DI-accessible methods that orchestrate the stats fetch and delegate to the serializer.Refactored
RegistersController::index()now callsfindAllSerializedfor the schema-expansion path. Inline expansion + per-schema stats loops removed. Register-level@self.stats(object/log/file counts on the register itself) stays in the controller — out of scope for the serializer.RegisterMapper::find()/::findAll()drop the unused_extendparameter; the@SuppressWarnings(PHPMD.UnusedFormalParameter)pragmas are removed. Service-levelfind()/findAll()keep_extendas a documented no-op placeholder for signature compatibility.SchemaMapper's@method find(...)annotation extended so PHPStan sees the full signature (was previouslyfind(int|string $id)only).Tests + docs
RegisterSerializerTest— 10 unit tests covering all spec scenarios (no extend, schemas, orphan-ID retention, type preservation, stats with/without orphans, unknown-key tolerance, entity-contract preservation, plus aserializeManyper-register stats routing test).RegisterSerializerIntegrationTest— 4 integration tests against a live DB (DI parity, orphan retention, stats attachment, no-extend baseline).RegisterServiceTestandRegistersControllerTestupdated for the new constructor + delegation pattern. Caught a pre-existing constructor-arg-count drift in the controller test (IGroupManagermock was missing) and fixed that too.docs/development/Services/serializers.md.CHANGELOG.mdAdded + Breaking entries.Wire-format breaking change (orphan IDs)
When a register references a schema that has been deleted, the response's
schemasfield is now a heterogeneous array of objects + bare IDs (in original order) instead of silently dropping the orphan. JSON consumers in statically-typed clients (Go, Java, Kotlin) that decodeschemasas[]Schemaneed a discriminated decoder. The DocuDesk frontend already filters viatypeof schema === 'object'and is unaffected. Documented under Risks indesign.mdand called out in the changelog.Quality
@SuppressWarnings+ reasons; pre-existing warnings on unrelated code left as-is)RegisterServiceTest21,RegistersControllerTest87,RegisterMapperTest2,RegisterSerializerTest10)RegisterSerializerIntegrationTest)Test plan
GET /api/registers?_extend=schemasagainst a register with all schemas resolvable — response is byte-equivalent to the pre-refactor happy pathGET /api/registers?_extend=schemasagainst a register with at least one deleted schema — orphan ID is retained in its original position, log warning is emittedGET /api/registers?_extend=schemas&_extend=@self.stats— each expanded schema hasstats.objects.total; the register hasstats.objects/logs/filesCross-repo follow-ups
RegisterDiscoveryService::fetchAvailableRegisters()to callfindAllSerializedonce openregister cuts a release containing this change. The one-line swap + theserializeRegister → filterSchemasrename are documented in that issue._extendtoRegisterService::findAll/findwere found in opencatalogi; softwarecatalog wasn't audited locally (separate ticket).🤖 Generated with Claude Code