@@ -58,6 +58,81 @@ public function hydraDocumentationIsValid(): void
5858 self ::assertNotContains ('https://schema.org/Person ' , $ classNames , 'User should not use full IRI as class name ' );
5959 }
6060
61+ /**
62+ * Regression test for api-platform/hydra DocumentationNormalizer bug.
63+ *
64+ * When `serializer.hydra_prefix` is false (the default since API Platform 4.x), the
65+ * `DocumentationNormalizer` emits:
66+ *
67+ * "owl:onProperty": {"@id": "member"}
68+ *
69+ * instead of:
70+ *
71+ * "owl:onProperty": {"@id": "hydra:member"}
72+ *
73+ * The bare term `"member"` cannot be expanded by jsonld.js to
74+ * `http://www.w3.org/ns/hydra/core#member` because `@id` values are expanded
75+ * with `vocab=false`, which skips term definitions. Only compact IRIs (prefix:localname)
76+ * are always resolved. This causes `@api-platform/api-doc-parser`'s `findRelatedClass`
77+ * to throw "Cannot find the class related to …#Entrypoint/book", breaking HydraAdmin.
78+ *
79+ * @see https://github.com/api-platform/hydra/blob/main/src/Serializer/DocumentationNormalizer.php
80+ */
81+ #[Test]
82+ public function entrypointOwlOnPropertyIsMemberCompactIriNotBareTerm (): void
83+ {
84+ $ response = $ this ->client ->request ('GET ' , '/docs.jsonld ' );
85+
86+ self ::assertResponseIsSuccessful ();
87+
88+ $ docs = $ response ->toArray ();
89+
90+ // Find the Entrypoint class in supportedClass
91+ $ entrypointClass = null ;
92+ foreach ($ docs ['supportedClass ' ] as $ class ) {
93+ if ('#Entrypoint ' === ($ class ['@id ' ] ?? null )) {
94+ $ entrypointClass = $ class ;
95+ break ;
96+ }
97+ }
98+
99+ self ::assertNotNull ($ entrypointClass , 'Entrypoint class should exist in supportedClass ' );
100+
101+ $ supportedProperties = $ entrypointClass ['supportedProperty ' ] ?? [];
102+ self ::assertNotEmpty ($ supportedProperties , 'Entrypoint should have supported properties ' );
103+
104+ foreach ($ supportedProperties as $ supportedProperty ) {
105+ $ property = $ supportedProperty ['property ' ] ?? null ;
106+ if (null === $ property ) {
107+ continue ;
108+ }
109+
110+ foreach ($ property ['range ' ] ?? [] as $ rangeEntry ) {
111+ $ onPropertyId = $ rangeEntry ['owl:equivalentClass ' ]['owl:onProperty ' ]['@id ' ] ?? null ;
112+ if (null === $ onPropertyId ) {
113+ continue ;
114+ }
115+
116+ // A bare "member" cannot be expanded to http://www.w3.org/ns/hydra/core#member
117+ // by jsonld.js because @id values are resolved with vocab=false (term definitions
118+ // are ignored). Only compact IRIs like "hydra:member" or the full IRI work.
119+ // Fix in DocumentationNormalizer: replace `$hydraPrefix.'member'` with `'hydra:member'`
120+ // (or ContextBuilderInterface::HYDRA_NS.'member') for owl:onProperty.
121+ self ::assertNotSame (
122+ 'member ' ,
123+ $ onPropertyId ,
124+ \sprintf (
125+ 'owl:onProperty @id for entrypoint property "%s" is the bare term "member". '
126+ .'jsonld.js cannot expand it to http://www.w3.org/ns/hydra/core#member '
127+ .'(@id values use vocab=false, which skips term definitions). '
128+ .'Use "hydra:member" instead. ' ,
129+ $ property ['@id ' ] ?? '? '
130+ )
131+ );
132+ }
133+ }
134+ }
135+
61136 #[Test]
62137 public function booksResourceHasCorrectShortNameAndTypes (): void
63138 {
0 commit comments