@@ -1226,8 +1226,20 @@ public static function getMimeTypes(string $format): array
12261226
12271227 /**
12281228 * Gets the format associated with the mime type.
1229+ *
1230+ * Resolution order:
1231+ * 1) Exact match on the full MIME type (e.g. "application/json").
1232+ * 2) Match on the canonical MIME type (i.e. before the first ";" parameter).
1233+ * 3) If the type is "application/*+suffix", use the structured syntax suffix
1234+ * mapping (e.g. "application/foo+json" → "json"), when available.
1235+ * 4) If $subtypeFallback is true and no match was found:
1236+ * - return the MIME subtype (without "x-" prefix), provided it does not
1237+ * contain a "+" (e.g. "application/x-yaml" → "yaml", "text/csv" → "csv").
1238+ *
1239+ * @param string|null $mimeType The mime type to check
1240+ * @param bool $subtypeFallback Whether to fall back to the subtype if no exact match is found
12291241 */
1230- public function getFormat (?string $ mimeType ): ?string
1242+ public function getFormat (?string $ mimeType, bool $ subtypeFallback = false ): ?string
12311243 {
12321244 $ canonicalMimeType = null ;
12331245 if ($ mimeType && false !== $ pos = strpos ($ mimeType , '; ' )) {
@@ -1247,6 +1259,27 @@ public function getFormat(?string $mimeType): ?string
12471259 }
12481260 }
12491261
1262+ if (!$ canonicalMimeType ) {
1263+ $ canonicalMimeType = $ mimeType ;
1264+ }
1265+
1266+ if (str_starts_with ($ canonicalMimeType , 'application/ ' ) && str_contains ($ canonicalMimeType , '+ ' )) {
1267+ $ suffix = substr (strrchr ($ canonicalMimeType , '+ ' ), 1 );
1268+ if (isset (static ::getStructuredSuffixFormats ()[$ suffix ])) {
1269+ return static ::getStructuredSuffixFormats ()[$ suffix ];
1270+ }
1271+ }
1272+
1273+ if ($ subtypeFallback && str_contains ($ canonicalMimeType , '/ ' )) {
1274+ [, $ subtype ] = explode ('/ ' , $ canonicalMimeType , 2 );
1275+ if (str_starts_with ($ subtype , 'x- ' )) {
1276+ $ subtype = substr ($ subtype , 2 );
1277+ }
1278+ if (!str_contains ($ subtype , '+ ' )) {
1279+ return $ subtype ;
1280+ }
1281+ }
1282+
12501283 return null ;
12511284 }
12521285
@@ -1919,6 +1952,42 @@ protected static function initializeFormats(): void
19191952 'atom ' => ['application/atom+xml ' ],
19201953 'rss ' => ['application/rss+xml ' ],
19211954 'form ' => ['application/x-www-form-urlencoded ' , 'multipart/form-data ' ],
1955+ 'soap ' => ['application/soap+xml ' ],
1956+ 'problem ' => ['application/problem+json ' ],
1957+ 'hal ' => ['application/hal+json ' , 'application/hal+xml ' ],
1958+ 'jsonapi ' => ['application/vnd.api+json ' ],
1959+ 'yaml ' => ['text/yaml ' , 'application/x-yaml ' ],
1960+ 'wbxml ' => ['application/vnd.wap.wbxml ' ],
1961+ 'pdf ' => ['application/pdf ' ],
1962+ 'csv ' => ['text/csv ' ],
1963+ ];
1964+ }
1965+
1966+ /**
1967+ * Structured MIME suffix fallback formats
1968+ *
1969+ * This mapping is used when no exact MIME match is found in $formats.
1970+ * It enables handling of types like application/soap+xml → 'xml'.
1971+ *
1972+ * @see https://datatracker.ietf.org/doc/html/rfc6839
1973+ * @see https://datatracker.ietf.org/doc/html/rfc7303
1974+ * @see https://www.iana.org/assignments/media-types/media-types.xhtml
1975+ *
1976+ * @return array<string, string>
1977+ */
1978+ private static function getStructuredSuffixFormats (): array
1979+ {
1980+ return [
1981+ 'json ' => 'json ' ,
1982+ 'xml ' => 'xml ' ,
1983+ 'xhtml ' => 'html ' ,
1984+ 'cbor ' => 'cbor ' ,
1985+ 'zip ' => 'zip ' ,
1986+ 'ber ' => 'asn1 ' ,
1987+ 'der ' => 'asn1 ' ,
1988+ 'tlv ' => 'tlv ' ,
1989+ 'wbxml ' => 'xml ' ,
1990+ 'yaml ' => 'yaml ' ,
19221991 ];
19231992 }
19241993
0 commit comments