Below is a snippet of test code based on my client that works. I can confirm that the PHP JSON encoder does not sort the keys as indicated by the two different thumbprints. The first thumbprint is correct.
<?php
$configargs = [
"digest_alg" => "sha256",
"private_key_bits" => 2048,
"private_key_type" => OPENSSL_KEYTYPE_RSA
];
$privateKeyObject = openssl_pkey_new($configargs);
openssl_pkey_export($privateKeyObject, $privateKey);
$privateKeyDetails = openssl_pkey_get_details($privateKeyObject);
$jwk = [
"e" => strtr(rtrim(base64_encode($privateKeyDetails["rsa"]["e"]), '='), '+/', '-_'),
"kty" => "RSA",
"n" => strtr(rtrim(base64_encode($privateKeyDetails["rsa"]["n"]), '='), '+/', '-_')
];
$thumbprint = strtr(rtrim(base64_encode(openssl_digest(json_encode($jwk, JSON_UNESCAPED_SLASHES), "sha256", true)), '='), '+/', '-_');
$jwk2 = [
"kty" => "RSA",
"n" => strtr(rtrim(base64_encode($privateKeyDetails["rsa"]["n"]), '='), '+/', '-_'),
"e" => strtr(rtrim(base64_encode($privateKeyDetails["rsa"]["e"]), '='), '+/', '-_')
];
$thumbprint2 = strtr(rtrim(base64_encode(openssl_digest(json_encode($jwk2, JSON_UNESCAPED_SLASHES), "sha256", true)), '='), '+/', '-_');
$protected = [
"url" => "account url",
"alg" => "RS256",
"nonce" => "current nonce",
"jwk" => $jwk
];
$payload = [
"termsOfServiceAgreed" => true
];
echo "<pre>";
print_r($thumbprint);
echo "<br><br>";
print_r($thumbprint2);
echo "<br><br>";
print_r($protected);
echo "<br>";
print_r($payload);
echo "</pre>";
?>
Can be tested with: