openssl_sign(), openssl_verify() and the openssl_pkey_* functions are thin wrappers around the OpenSSL that PHP was built against — 3.5.7 in the official php:8.5.11 image — so curves and behaviour follow it: DER signatures, random nonces, no low-S normalization, and verification that accepts high-S. The hash always comes from the algorithm argument; there is no function that signs a precomputed digest.
The return value of openssl_verify() is the classic PHP pitfall. It returns 1 for a valid signature, 0 for an invalid one and -1 when the signature cannot even be parsed — which is what a 64-byte raw r‖s signature from WebCrypto or a JWT produced in the check. In PHP -1 is truthy, so if (openssl_verify(...)) accepts garbage; compare with === 1 as the sample does. Raw signatures have to be wrapped in DER first, which is what every PHP JWT library does internally; the DER ⇄ raw converter shows the two forms.
Keys are passed as PEM strings, certificates or key resources: SPKI PEM and X.509 PEM worked, while raw SPKI DER bytes were rejected outright. Private keys load from PKCS#8 (plain or encrypted) and from SEC1 EC PRIVATE KEY, and openssl_pkey_export() writes PKCS#8. Curves covered P-224 to P-521, secp256k1 and Brainpool, and Ed25519 and Ed448 keys work through OPENSSL_KEYTYPE_ED25519 and OPENSSL_KEYTYPE_ED448 with algorithm 0.
Post-quantum algorithms are not reachable from PHP code even though the linked OpenSSL implements them: the CLI in the same image listed ML-DSA, but the extension defines no key type for it. For JWTs use firebase/php-jwt, web-token or lcobucci/jwt rather than assembling tokens by hand.