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> // We only want to work with fields created by users.<br> if (!str_starts_with($context['name'], 'field_')) {<br> return FALSE;<br> }<br> $type = $data-&gt;getItemDefinition()-&gt;getFieldDefinition()-&gt;getType();<br> ...<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> if (!parent::supportsNormalization($data, $format)) { // instanceof FieldDefinitionInterface<br> return FALSE;<br> }<br> $type = $data-&gt;getItemDefinition()-&gt;getFieldDefinition()-&gt;getType();<br> ...<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>
— 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 …</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 — 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 — 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 =&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> — 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 — 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'], …)</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 …</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 — a path existing tests never cover — 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> if (!$data instanceof FieldDefinitionInterface) {<br> return FALSE;<br> }<br> if (empty($context['name']) || !str_starts_with($context['name'], 'field_')) {<br> return FALSE;<br> }<br> $type = $data-&gt;getItemDefinition()-&gt;getFieldDefinition()-&gt;getType();<br> $class = $this-&gt;fieldTypeManager-&gt;getPluginClass($type);<br> 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-&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 → 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