RelationshipFieldDefinitionNormalizer drops the parent type-gate and reads $context['name'] unguarded
>>> [!note] Migrated issue <!-- Drupal.org comment --> <!-- Migrated from issue #3593342. --> Reported by: [alex ua](https://www.drupal.org/user/110386) Related to !40 >>> <h2>Summary</h2> <p><code>RelationshipFieldDefinitionNormalizer::supportsNormalization()</code><br> (<code>src/Normalizer/jsonapi/RelationshipFieldDefinitionNormalizer.php</code>) overrides<br> the parent (<code>schemata_json_schema</code>) method but drops the parent's type-gate<br> and reads <code>$context['name']</code> before any check on <code>$data</code>:</p> <pre><div class="codeblock"><code class="language-php">public function supportsNormalization($data, $format = NULL, array $context = []): bool {<br>&nbsp; // We only want to work with fields created by users.<br>&nbsp; if (!str_starts_with($context['name'], 'field_')) {<br>&nbsp;&nbsp;&nbsp; return FALSE;<br>&nbsp; }<br>&nbsp; $type = $data-&amp;gt;getItemDefinition()-&amp;gt;getFieldDefinition()-&amp;gt;getType();<br>&nbsp; ...<br>}<p>The parent gates on the field-definition type before touching <code>$data</code>:</p> <pre><div class="codeblock"><code class="language-php">public function supportsNormalization($data, $format = NULL, array $context = []): bool {<br>&nbsp; if (!parent::supportsNormalization($data, $format)) {&nbsp;&nbsp; // instanceof FieldDefinitionInterface<br>&nbsp;&nbsp;&nbsp; return FALSE;<br>&nbsp; }<br>&nbsp; $type = $data-&amp;gt;getItemDefinition()-&amp;gt;getFieldDefinition()-&amp;gt;getType();<br>&nbsp; ...<br>}<p>When <code>supportsNormalization()</code> is called with no <code>'name'</code> key in <code>$context</code>,<br> the override emits:</p> <ul> <li><em>Undefined array key "name"</em> (PHP warning), and</li> <li><em>str_starts_with(): Passing null to parameter #1 ($haystack) of type string is deprecated</em><br> &mdash; which becomes a <strong>TypeError under PHP 9</strong>.</li> </ul> <p>Both fire at the same line. A kernel test triggering this is included with the patch (see Reproduction).</p> <h2>Why this matters</h2> <p>Three concrete reasons to fix it now:</p> <ul> <li><strong>It breaks under PHP 9.</strong> The <code>str_starts_with(): Passing null &hellip;</code> deprecation becomes a hard <code>TypeError</code>. Any call path that reaches <code>supportsNormalization()</code> without a <code>'name'</code> in <code>$context</code> will fatal once the project runs on PHP 9.</li> <li><strong>It fails deprecation-strict CI</strong> the moment any caller invokes the normalizer without pre-seeding <code>$context['name']</code>.</li> <li><strong>It silently diverges from the contract it inherits.</strong> The override dropped the parent's <code>instanceof FieldDefinitionInterface</code> gate, so the class no longer behaves like the normalizer it extends &mdash; a maintenance trap for the next person who touches schema generation.</li> </ul> <h2>Why it is latent today (scope)</h2> <p>Being precise about blast radius &mdash; it tempers urgency without removing it:</p> <ol> <li><strong>The dropped type-gate (calling <code>getItemDefinition()</code> on a non-field object) is not reachable through the serializer.</strong> The parent declares <code>getSupportedTypes(): [FieldDefinitionInterface::class =&amp;gt; TRUE]</code>, which this child inherits. In the Symfony 7 serializer that core uses (<code>DrupalserializationSerializerSerializer</code> is a thin subclass that does not override dispatch), <code>getNormalizer()</code> only routes a normalizer <code>$data</code> whose type matches its <code>getSupportedTypes()</code> map. So <code>supportsNormalization()</code> is only ever called here with a <code>FieldDefinitionInterface</code> &mdash; every one of which has <code>getItemDefinition()</code>. The missing <code>instanceof</code> is therefore a divergence from the parent's contract, not a live crash.</li> <li><strong>The unguarded <code>$context['name']</code> read is not triggered in normal schema generation.</strong> The schemata pipeline always sets <code>$context['name']</code> before normalizing a field definition, so the warning/deprecation does not fire during a normal <code>createSchema()</code> run. (The included schema-generation test for a bundle with an entity-reference field passes cleanly.)</li> </ol> <p>So it does not fire during a normal <code>createSchema()</code> run today &mdash; but the three forcing functions above are why it should be fixed now rather than left for PHP 9 to turn into a fatal.</p> <h2>Reproduction</h2> <p>A kernel test (<code>tests/src/Kernel/RelationshipFieldDefinitionNormalizerTest.php</code>, included with the patch) pins both the behavior and the contract:</p> <ul> <li><code>testSupportsNormalizationContract()</code> calls <code>supportsNormalization()</code> with an empty context. Against the current code this emits, at the <code>str_starts_with($context['name'], &hellip;)</code> line: <ul> <li>1 test triggered 1 PHP warning: <em>Undefined array key "name"</em></li> <li>1 test triggered 1 PHP deprecation: <em>str_starts_with(): Passing null &hellip;</em></li> </ul> </li> <li><code>testSchemaGenerationForReferenceBundle()</code> runs a real <code>createSchema()</code> over a bundle with a custom <code>entity_reference</code> field &mdash; a path existing tests never cover &mdash; and asserts the schema is valid and inlines the relationship. It passes both before and after the fix (confirming point 2 above).</li> </ul> <h2>Proposed resolution</h2> <p>Restore the parent's type-gate and guard the context key, preserving the child's <code>field_</code>-prefix intent:</p> <pre><div class="codeblock"><code class="language-php">public function supportsNormalization($data, $format = NULL, array $context = []): bool {<br>&nbsp; if (!$data instanceof FieldDefinitionInterface) {<br>&nbsp;&nbsp;&nbsp; return FALSE;<br>&nbsp; }<br>&nbsp; if (empty($context['name']) || !str_starts_with($context['name'], 'field_')) {<br>&nbsp;&nbsp;&nbsp; return FALSE;<br>&nbsp; }<br>&nbsp; $type = $data-&amp;gt;getItemDefinition()-&amp;gt;getFieldDefinition()-&amp;gt;getType();<br>&nbsp; $class = $this-&amp;gt;fieldTypeManager-&amp;gt;getPluginClass($type);<br>&nbsp; return $class == EntityReferenceItem::class || is_subclass_of($class, EntityReferenceItem::class);<br>}<p>After the fix the kernel test reports 0 warnings and the line is gone from the deprecation list.</p> <h3>Bundled cleanup (constructor)</h3> <p>The same class's constructor promotes a <code>protected $field_type_manager</code> property, but the parent already defines and sets <code>$this-&amp;gt;fieldTypeManager</code> (the property the method above actually uses). The promotion is a dead shadow. The patch drops the promotion (passing the argument straight to <code>parent::__construct()</code>); no behavior change. Happy to split this into its own issue/MR if maintainers prefer a single-concern change.</p> <h2>Validation</h2> <ul> <li><strong>PHPUnit</strong> (kernel, module test dir): 25 tests / 73 assertions green; the normalizer's warning + <code>str_starts_with</code> deprecation are eliminated (RED &rarr; GREEN demonstrated by reverting the fix).</li> <li><strong>PHPCS</strong> (Drupal+DrupalPractice): clean on both changed files.</li> <li><strong>PHPStan</strong> (level 0, phpstan-drupal): no errors.</li> </ul> </code></div></pre></code></div></pre></code></div></pre>
issue