fix(listener): the meeting-folder listener has never once run (#471) - #472
Merged
Merged
Conversation
Two independent defects, and repairing either one alone leaves the listener dead. A four-arm control over one production-shaped entity, printed: A origin/development (method_exists) ......... '' no match B probe swapped to is_callable() ............. THREW no match C probe fixed to property_exists() only ...... '93' no match D this change ................................ 'meeting' match 1. The probe. ObjectEntity declares getSchema() only as an @method docblock tag; Entity::__call serves it, so method_exists() is false for it and the getter tier was skipped for every entity that actually has a schema. is_callable() is not the remedy: arm B shows it is true for any name and the call then raises BadFunctionCallException. Entity::__call routes get* to Entity::getter(), which decides on property_exists(), so that is the instrument used here, with the call additionally made exception-safe. 2. The value. MagicMapper and SaveObject stamp the schema's numeric database id onto every entity they materialise, so arm C reads '93' where the guard compares against the slug 'meeting'. The id is now resolved back to its slug through OpenRegister's SchemaMapper, memoised per request because the unfiltered-registration fallback invokes listeners on every object write. Why the suite was green over a listener that could not fire: the stub is faithful — tests/Stubs/Db/ObjectEntity.php honours the decidesk#399 parity contract and does not declare the magic accessors — but every fixture fed a _schemaSlug key that OpenRegister never emits. The parity contract constrains the double; it does not constrain the payload. The new tests build the entity the way MagicMapper does and assert through the real resolver. Both halves are proven able to fail. Removing the id-to-slug resolution fails exactly 5 tests; restoring method_exists as the only probe fails exactly 6. Both predictions were written before the reverts and matched. Behaviour change: meeting creates now get their Files folder tree, which they never have. The call is fail-soft and bounded (one folder tree per meeting create), and an unresolvable schema fails closed - no tree is created for an object that might not be a meeting. Not touched, same defect, filed separately: SubmissionDeadlineListener and GovernanceRoleProjectionListener resolve their slug the same dead way. Waking those two changes what the write path permits rather than what it creates, so they are sequenced behind their own verification.
rubenvdlinde
requested review from
WilcoLouwerse,
bbrands02 and
rjzondervan
as code owners
August 11, 2026 23:41
Contributor
Quality Report — ConductionNL/decidesk @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| composer | ✅ | ✅ 100/100 | |||
| npm | ✅ | ✅ 548/548 | |||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-12 00:07 UTC
Download the full PDF report from the workflow artifacts.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #471.
What was wrong
MeetingFolderListener::resolveSchemaSlug()could not returnmeetingfor any object OpenRegister has ever dispatched, for two independent reasons. Repairing either one alone leaves the listener dead — which is why this change does both.A four-arm control over a single production-shaped entity (numeric schema id on the entity, no schema key on the payload), all four values printed:
meeting?origin/development,method_exists()''is_callable()BadFunctionCallExceptionproperty_exists(), value untouched'93''meeting'1. The probe.
ObjectEntitydeclaresgetSchema()only as an@methoddocblock tag;Entity::__call()serves it.method_exists()is therefore false for it and the getter tier was skipped for every entity that actually has a schema. Arm B is why the probe was not simply swapped:is_callable()is true for any name on a__callclass, so it converts a silently-false guard into a silently-true one and the call then raises.Entity::__call()routesget*toEntity::getter(), which decides onproperty_exists()— so that is the instrument used here, with the call additionally made exception-safe in the same edit.2. The value.
MagicMapperandSaveObjectstamp the schema's numeric database id onto every entity they materialise (setSchema((string) $schema->getId())). Arm C shows the correctly-probed read returning'93'against a guard comparing to the slugmeeting. The id is now resolved back to its slug through OpenRegister'sSchemaMapper, memoised per request — the unfiltered-registration fallback inObjectListenerRegistrarinvokes listeners on every object write instance-wide, so an unmemoised lookup would be a database read per write.Decidesk's own
PortalCreateOpenParentGuardListenerdocblock already recorded reason 2 ("the REALObjectEntity::getSchema()returns the schema's numeric database id, not its slug"), which is why that listener carries a Tier-2 signature fallback and this one does not.Why the unit suite was green over a listener that could not fire
The stub is faithful —
tests/Stubs/Db/ObjectEntity.phphonours the decidesk#399 signature parity contract and deliberately does not declare the magic accessors. The fixtures were not: every existing test fed a_schemaSlugkey that OpenRegister never emits. The parity contract constrains the double; it does not constrain the payload. The new tests build the entity the wayMagicMapperdoes and drive the real resolver rather than a mocked answer.tests/Stubs/Db/Schema.phpgains a concretesetSlug()— concrete in production atlib/Db/Schema.php:1714— and a note recording thatgetSlug()stays magic on purpose.Proven able to fail
Both halves were reverted separately with the prediction written first, and both predictions matched exactly: removing the id-to-slug resolution fails five named tests; restoring
method_exists()as the only probe fails six. Reverts were made with an editor, not a script.Behaviour change
Meeting creates now get their Files folder tree, which they never have. The call is fail-soft and bounded — one tree per meeting create, not a fan-out — and an unresolvable schema fails closed, so no tree is created for an object that might not be a meeting. Nothing that previously errored now succeeds; the change adds a side effect, it does not open a path.
Deliberately not in this change
SubmissionDeadlineListenerandGovernanceRoleProjectionListenerresolve their slug the same dead way and are dead for the same two reasons. Waking those changes what the write path permits — a pre-write veto and a set of RBAC scope projections respectively — rather than what it creates, so they are sequenced behind their own verification instead of being carried in on the back of this one.Local verification: lint, phpcs (0 errors), phpmd and the full unit suite pass. Psalm and PHPStan cannot run in a host checkout here because
vendor/nextcloud/ocp/OCPis a symlink into the container; both are left to CI.