Added JsonProperty and BencodeProperty attributes for easier object encoding.
This commit is contained in:
parent
1339de93bf
commit
caf4eef02d
8 changed files with 776 additions and 60 deletions
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
0.2408.401738
|
||||
0.2408.610150
|
||||
|
|
118
composer.lock
generated
118
composer.lock
generated
|
@ -69,16 +69,16 @@
|
|||
},
|
||||
{
|
||||
"name": "nikic/php-parser",
|
||||
"version": "v5.1.0",
|
||||
"version": "v5.3.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/nikic/PHP-Parser.git",
|
||||
"reference": "683130c2ff8c2739f4822ff7ac5c873ec529abd1"
|
||||
"reference": "3abf7425cd284141dc5d8d14a9ee444de3345d1a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/683130c2ff8c2739f4822ff7ac5c873ec529abd1",
|
||||
"reference": "683130c2ff8c2739f4822ff7ac5c873ec529abd1",
|
||||
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3abf7425cd284141dc5d8d14a9ee444de3345d1a",
|
||||
"reference": "3abf7425cd284141dc5d8d14a9ee444de3345d1a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -121,9 +121,9 @@
|
|||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/nikic/PHP-Parser/issues",
|
||||
"source": "https://github.com/nikic/PHP-Parser/tree/v5.1.0"
|
||||
"source": "https://github.com/nikic/PHP-Parser/tree/v5.3.0"
|
||||
},
|
||||
"time": "2024-07-01T20:03:41+00:00"
|
||||
"time": "2024-09-29T13:56:26+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phar-io/manifest",
|
||||
|
@ -245,16 +245,16 @@
|
|||
},
|
||||
{
|
||||
"name": "phpstan/phpstan",
|
||||
"version": "1.11.10",
|
||||
"version": "1.12.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpstan/phpstan.git",
|
||||
"reference": "640410b32995914bde3eed26fa89552f9c2c082f"
|
||||
"reference": "7e6c6cb7cecb0a6254009a1a8a7d54ec99812b17"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/640410b32995914bde3eed26fa89552f9c2c082f",
|
||||
"reference": "640410b32995914bde3eed26fa89552f9c2c082f",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/7e6c6cb7cecb0a6254009a1a8a7d54ec99812b17",
|
||||
"reference": "7e6c6cb7cecb0a6254009a1a8a7d54ec99812b17",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -299,36 +299,36 @@
|
|||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2024-08-08T09:02:50+00:00"
|
||||
"time": "2024-09-26T12:45:22+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-code-coverage",
|
||||
"version": "11.0.5",
|
||||
"version": "11.0.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
|
||||
"reference": "19b6365ab8b59a64438c0c3f4241feeb480c9861"
|
||||
"reference": "ebdffc9e09585dafa71b9bffcdb0a229d4704c45"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/19b6365ab8b59a64438c0c3f4241feeb480c9861",
|
||||
"reference": "19b6365ab8b59a64438c0c3f4241feeb480c9861",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ebdffc9e09585dafa71b9bffcdb0a229d4704c45",
|
||||
"reference": "ebdffc9e09585dafa71b9bffcdb0a229d4704c45",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-dom": "*",
|
||||
"ext-libxml": "*",
|
||||
"ext-xmlwriter": "*",
|
||||
"nikic/php-parser": "^5.0",
|
||||
"nikic/php-parser": "^5.1.0",
|
||||
"php": ">=8.2",
|
||||
"phpunit/php-file-iterator": "^5.0",
|
||||
"phpunit/php-text-template": "^4.0",
|
||||
"sebastian/code-unit-reverse-lookup": "^4.0",
|
||||
"sebastian/complexity": "^4.0",
|
||||
"sebastian/environment": "^7.0",
|
||||
"sebastian/lines-of-code": "^3.0",
|
||||
"sebastian/version": "^5.0",
|
||||
"theseer/tokenizer": "^1.2.0"
|
||||
"phpunit/php-file-iterator": "^5.0.1",
|
||||
"phpunit/php-text-template": "^4.0.1",
|
||||
"sebastian/code-unit-reverse-lookup": "^4.0.1",
|
||||
"sebastian/complexity": "^4.0.1",
|
||||
"sebastian/environment": "^7.2.0",
|
||||
"sebastian/lines-of-code": "^3.0.1",
|
||||
"sebastian/version": "^5.0.1",
|
||||
"theseer/tokenizer": "^1.2.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^11.0"
|
||||
|
@ -340,7 +340,7 @@
|
|||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "11.0-dev"
|
||||
"dev-main": "11.0.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
|
@ -369,7 +369,7 @@
|
|||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
|
||||
"security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.5"
|
||||
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.6"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
@ -377,20 +377,20 @@
|
|||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2024-07-03T05:05:37+00:00"
|
||||
"time": "2024-08-22T04:37:56+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-file-iterator",
|
||||
"version": "5.0.1",
|
||||
"version": "5.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/php-file-iterator.git",
|
||||
"reference": "6ed896bf50bbbfe4d504a33ed5886278c78e4a26"
|
||||
"reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/6ed896bf50bbbfe4d504a33ed5886278c78e4a26",
|
||||
"reference": "6ed896bf50bbbfe4d504a33ed5886278c78e4a26",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/118cfaaa8bc5aef3287bf315b6060b1174754af6",
|
||||
"reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -430,7 +430,7 @@
|
|||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/php-file-iterator/issues",
|
||||
"security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.0.1"
|
||||
"source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
@ -438,7 +438,7 @@
|
|||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2024-07-03T05:06:37+00:00"
|
||||
"time": "2024-08-27T05:02:59+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-invoker",
|
||||
|
@ -626,16 +626,16 @@
|
|||
},
|
||||
{
|
||||
"name": "phpunit/phpunit",
|
||||
"version": "11.3.1",
|
||||
"version": "11.3.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/phpunit.git",
|
||||
"reference": "fe179875ef0c14e90b75617002767eae0a742641"
|
||||
"reference": "d62c45a19c665bb872c2a47023a0baf41a98bb2b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/fe179875ef0c14e90b75617002767eae0a742641",
|
||||
"reference": "fe179875ef0c14e90b75617002767eae0a742641",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d62c45a19c665bb872c2a47023a0baf41a98bb2b",
|
||||
"reference": "d62c45a19c665bb872c2a47023a0baf41a98bb2b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -649,20 +649,20 @@
|
|||
"phar-io/manifest": "^2.0.4",
|
||||
"phar-io/version": "^3.2.1",
|
||||
"php": ">=8.2",
|
||||
"phpunit/php-code-coverage": "^11.0.5",
|
||||
"phpunit/php-file-iterator": "^5.0.1",
|
||||
"phpunit/php-code-coverage": "^11.0.6",
|
||||
"phpunit/php-file-iterator": "^5.1.0",
|
||||
"phpunit/php-invoker": "^5.0.1",
|
||||
"phpunit/php-text-template": "^4.0.1",
|
||||
"phpunit/php-timer": "^7.0.1",
|
||||
"sebastian/cli-parser": "^3.0.2",
|
||||
"sebastian/code-unit": "^3.0.1",
|
||||
"sebastian/comparator": "^6.0.2",
|
||||
"sebastian/comparator": "^6.1.0",
|
||||
"sebastian/diff": "^6.0.2",
|
||||
"sebastian/environment": "^7.2.0",
|
||||
"sebastian/exporter": "^6.1.3",
|
||||
"sebastian/global-state": "^7.0.2",
|
||||
"sebastian/object-enumerator": "^6.0.1",
|
||||
"sebastian/type": "^5.0.1",
|
||||
"sebastian/type": "^5.1.0",
|
||||
"sebastian/version": "^5.0.1"
|
||||
},
|
||||
"suggest": {
|
||||
|
@ -706,7 +706,7 @@
|
|||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
|
||||
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/phpunit/tree/11.3.1"
|
||||
"source": "https://github.com/sebastianbergmann/phpunit/tree/11.3.6"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
@ -722,7 +722,7 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-08-13T06:14:23+00:00"
|
||||
"time": "2024-09-19T10:54:28+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/cli-parser",
|
||||
|
@ -896,16 +896,16 @@
|
|||
},
|
||||
{
|
||||
"name": "sebastian/comparator",
|
||||
"version": "6.0.2",
|
||||
"version": "6.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/comparator.git",
|
||||
"reference": "450d8f237bd611c45b5acf0733ce43e6bb280f81"
|
||||
"reference": "fa37b9e2ca618cb051d71b60120952ee8ca8b03d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/450d8f237bd611c45b5acf0733ce43e6bb280f81",
|
||||
"reference": "450d8f237bd611c45b5acf0733ce43e6bb280f81",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa37b9e2ca618cb051d71b60120952ee8ca8b03d",
|
||||
"reference": "fa37b9e2ca618cb051d71b60120952ee8ca8b03d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -916,12 +916,12 @@
|
|||
"sebastian/exporter": "^6.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^11.0"
|
||||
"phpunit/phpunit": "^11.3"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "6.0-dev"
|
||||
"dev-main": "6.1-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
|
@ -961,7 +961,7 @@
|
|||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/comparator/issues",
|
||||
"security": "https://github.com/sebastianbergmann/comparator/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/comparator/tree/6.0.2"
|
||||
"source": "https://github.com/sebastianbergmann/comparator/tree/6.1.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
@ -969,7 +969,7 @@
|
|||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2024-08-12T06:07:25+00:00"
|
||||
"time": "2024-09-11T15:42:56+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/complexity",
|
||||
|
@ -1538,28 +1538,28 @@
|
|||
},
|
||||
{
|
||||
"name": "sebastian/type",
|
||||
"version": "5.0.1",
|
||||
"version": "5.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/type.git",
|
||||
"reference": "fb6a6566f9589e86661291d13eba708cce5eb4aa"
|
||||
"reference": "461b9c5da241511a2a0e8f240814fb23ce5c0aac"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/type/zipball/fb6a6566f9589e86661291d13eba708cce5eb4aa",
|
||||
"reference": "fb6a6566f9589e86661291d13eba708cce5eb4aa",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/type/zipball/461b9c5da241511a2a0e8f240814fb23ce5c0aac",
|
||||
"reference": "461b9c5da241511a2a0e8f240814fb23ce5c0aac",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^11.0"
|
||||
"phpunit/phpunit": "^11.3"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "5.0-dev"
|
||||
"dev-main": "5.1-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
|
@ -1583,7 +1583,7 @@
|
|||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/type/issues",
|
||||
"security": "https://github.com/sebastianbergmann/type/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/type/tree/5.0.1"
|
||||
"source": "https://github.com/sebastianbergmann/type/tree/5.1.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
@ -1591,7 +1591,7 @@
|
|||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2024-07-03T05:11:49+00:00"
|
||||
"time": "2024-09-17T13:12:04+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/version",
|
||||
|
|
89
src/Bencode/BencodeProperty.php
Normal file
89
src/Bencode/BencodeProperty.php
Normal file
|
@ -0,0 +1,89 @@
|
|||
<?php
|
||||
// BencodeProperty.php
|
||||
// Created: 2024-09-29
|
||||
// Updated: 2024-09-29
|
||||
|
||||
namespace Index\Bencode;
|
||||
|
||||
use Attribute;
|
||||
/**
|
||||
* Defines a class method or property as a Bencoded object property.
|
||||
*/
|
||||
#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_PROPERTY)]
|
||||
final class BencodeProperty {
|
||||
/** @var mixed[] */
|
||||
private array $omitIfValue;
|
||||
|
||||
/**
|
||||
* @param string $name Name of the property in the Bencoded object.
|
||||
* @param int $order Ordering of the property in the Bencoded object.
|
||||
* @param bool $omitIfNull Whether the value should be omitted if its null.
|
||||
* @param mixed|mixed[] $omitIfValue Which values should be omitted.
|
||||
* @param bool $numbersAsString Whether numbers should be cast to strings.
|
||||
*/
|
||||
public function __construct(
|
||||
private ?string $name = null,
|
||||
private int $order = 0,
|
||||
private bool $omitIfNull = true,
|
||||
mixed $omitIfValue = [],
|
||||
private bool $numbersAsString = false
|
||||
) {
|
||||
$this->omitIfValue = is_array($omitIfValue) ? $omitIfValue : [$omitIfValue];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets name for the property value in the Bencoded object.
|
||||
*
|
||||
* @return ?string
|
||||
*/
|
||||
public function getName(): ?string {
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the order value for this property.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getOrder(): int {
|
||||
return $this->order;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether this should be omitted if the value is null.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function shouldOmitIfNull(): bool {
|
||||
return $this->omitIfNull;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets which values should be omitted.
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getOmittedValues(): array {
|
||||
return $this->omitIfValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a given value should be omitted from the Bencoded object.
|
||||
*
|
||||
* @param mixed $value Value to check.
|
||||
* @return bool
|
||||
*/
|
||||
public function shouldBeOmitted(mixed $value): bool {
|
||||
return ($this->omitIfNull && $value === null)
|
||||
|| in_array($value, $this->omitIfValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether numbers should be converted to string.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function shouldConvertNumbersToString(): bool {
|
||||
return $this->numbersAsString;
|
||||
}
|
||||
}
|
93
src/Bencode/BencodeSerialisableTrait.php
Normal file
93
src/Bencode/BencodeSerialisableTrait.php
Normal file
|
@ -0,0 +1,93 @@
|
|||
<?php
|
||||
// BencodeSerialisableTrait.php
|
||||
// Created: 2024-09-29
|
||||
// Updated: 2024-09-29
|
||||
|
||||
namespace Index\Bencode;
|
||||
|
||||
use ReflectionMethod;
|
||||
use ReflectionObject;
|
||||
use ReflectionProperty;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Implements support for the BencodeProperty attribute.
|
||||
*/
|
||||
trait BencodeSerialisableTrait {
|
||||
/**
|
||||
* Constructs Bencode object based on BencodeProperty attributes.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function bencodeSerialise(): mixed {
|
||||
$objectInfo = new ReflectionObject($this);
|
||||
$props = [];
|
||||
|
||||
$propInfos = $objectInfo->getProperties(ReflectionProperty::IS_PUBLIC);
|
||||
foreach($propInfos as $propInfo) {
|
||||
$attrInfos = $propInfo->getAttributes(BencodeProperty::class);
|
||||
if(count($attrInfos) < 1)
|
||||
continue;
|
||||
if(count($attrInfos) > 1)
|
||||
throw new RuntimeException('Properties may only carry a single instance of the BencodeProperty attribute.');
|
||||
|
||||
$info = $attrInfos[0]->newInstance();
|
||||
|
||||
$name = $info->getName() ?? $propInfo->getName();
|
||||
if(array_key_exists($name, $props))
|
||||
throw new RuntimeException('A property with that name has already been defined.');
|
||||
|
||||
$value = $propInfo->getValue($propInfo->isStatic() ? null : $this);
|
||||
if($info->shouldBeOmitted($value))
|
||||
continue;
|
||||
|
||||
if(is_float($value) || (is_int($value) && $info->shouldConvertNumbersToString()))
|
||||
$value = (string)$value;
|
||||
elseif(is_bool($value))
|
||||
$value = $value ? 1 : 0;
|
||||
|
||||
$props[$name] = [
|
||||
'name' => $name,
|
||||
'value' => $value,
|
||||
'order' => $info->getOrder(),
|
||||
];
|
||||
}
|
||||
|
||||
$methodInfos = $objectInfo->getMethods(ReflectionMethod::IS_PUBLIC);
|
||||
foreach($methodInfos as $methodInfo) {
|
||||
if($methodInfo->getNumberOfRequiredParameters() > 0)
|
||||
throw new RuntimeException('Methods marked with the BencodeProperty attribute must not have any required arguments.');
|
||||
|
||||
$attrInfos = $methodInfo->getAttributes(BencodeProperty::class);
|
||||
if(count($attrInfos) < 1)
|
||||
continue;
|
||||
if(count($attrInfos) > 1)
|
||||
throw new RuntimeException('Methods may only carry a single instance of the BencodeProperty attribute.');
|
||||
|
||||
$info = $attrInfos[0]->newInstance();
|
||||
|
||||
$name = $info->getName() ?? $methodInfo->getName();
|
||||
if(array_key_exists($name, $props))
|
||||
throw new RuntimeException('A property with that name has already been defined.');
|
||||
|
||||
$value = $methodInfo->invoke($methodInfo->isStatic() ? null : $this);
|
||||
if($info->shouldBeOmitted($value))
|
||||
continue;
|
||||
|
||||
if(is_float($value) || (is_int($value) && $info->shouldConvertNumbersToString()))
|
||||
$value = (string)$value;
|
||||
elseif(is_bool($value))
|
||||
$value = $value ? 1 : 0;
|
||||
|
||||
$props[$name] = [
|
||||
'name' => $name,
|
||||
'value' => $value,
|
||||
'order' => $info->getOrder(),
|
||||
];
|
||||
}
|
||||
|
||||
usort($props, fn($a, $b) => $a['order'] <=> $b['order']);
|
||||
|
||||
return array_column($props, 'value', 'name');
|
||||
}
|
||||
}
|
89
src/Json/JsonProperty.php
Normal file
89
src/Json/JsonProperty.php
Normal file
|
@ -0,0 +1,89 @@
|
|||
<?php
|
||||
// JsonProperty.php
|
||||
// Created: 2024-09-29
|
||||
// Updated: 2024-09-29
|
||||
|
||||
namespace Index\Json;
|
||||
|
||||
use Attribute;
|
||||
/**
|
||||
* Defines a class method or property as a JSON object property.
|
||||
*/
|
||||
#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_PROPERTY)]
|
||||
final class JsonProperty {
|
||||
/** @var mixed[] */
|
||||
private array $omitIfValue;
|
||||
|
||||
/**
|
||||
* @param string $name Name of the property in the JSON object.
|
||||
* @param int $order Ordering of the property in the JSON object.
|
||||
* @param bool $omitIfNull Whether the value should be omitted if its null.
|
||||
* @param mixed|mixed[] $omitIfValue Which values should be omitted.
|
||||
* @param bool $numbersAsString Whether numbers should be cast to strings.
|
||||
*/
|
||||
public function __construct(
|
||||
private ?string $name = null,
|
||||
private int $order = 0,
|
||||
private bool $omitIfNull = true,
|
||||
mixed $omitIfValue = [],
|
||||
private bool $numbersAsString = false
|
||||
) {
|
||||
$this->omitIfValue = is_array($omitIfValue) ? $omitIfValue : [$omitIfValue];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets name for the property value in the JSON object.
|
||||
*
|
||||
* @return ?string
|
||||
*/
|
||||
public function getName(): ?string {
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the order value for this property.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getOrder(): int {
|
||||
return $this->order;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether this should be omitted if the value is null.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function shouldOmitIfNull(): bool {
|
||||
return $this->omitIfNull;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets which values should be omitted.
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getOmittedValues(): array {
|
||||
return $this->omitIfValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a given value should be omitted from the JSON object.
|
||||
*
|
||||
* @param mixed $value Value to check.
|
||||
* @return bool
|
||||
*/
|
||||
public function shouldBeOmitted(mixed $value): bool {
|
||||
return ($this->omitIfNull && $value === null)
|
||||
|| in_array($value, $this->omitIfValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether numbers should be converted to string.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function shouldConvertNumbersToString(): bool {
|
||||
return $this->numbersAsString;
|
||||
}
|
||||
}
|
89
src/Json/JsonSerializableTrait.php
Normal file
89
src/Json/JsonSerializableTrait.php
Normal file
|
@ -0,0 +1,89 @@
|
|||
<?php
|
||||
// JsonSerializableTrait.php
|
||||
// Created: 2024-09-29
|
||||
// Updated: 2024-09-29
|
||||
|
||||
namespace Index\Json;
|
||||
|
||||
use ReflectionMethod;
|
||||
use ReflectionObject;
|
||||
use ReflectionProperty;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Implements support for the JsonProperty attribute.
|
||||
*/
|
||||
trait JsonSerializableTrait {
|
||||
/**
|
||||
* Constructs JSON object based on JsonProperty attributes.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function jsonSerialize(): mixed {
|
||||
$objectInfo = new ReflectionObject($this);
|
||||
$props = [];
|
||||
|
||||
$propInfos = $objectInfo->getProperties(ReflectionProperty::IS_PUBLIC);
|
||||
foreach($propInfos as $propInfo) {
|
||||
$attrInfos = $propInfo->getAttributes(JsonProperty::class);
|
||||
if(count($attrInfos) < 1)
|
||||
continue;
|
||||
if(count($attrInfos) > 1)
|
||||
throw new RuntimeException('Properties may only carry a single instance of the JsonProperty attribute.');
|
||||
|
||||
$info = $attrInfos[0]->newInstance();
|
||||
|
||||
$name = $info->getName() ?? $propInfo->getName();
|
||||
if(array_key_exists($name, $props))
|
||||
throw new RuntimeException('A property with that name has already been defined.');
|
||||
|
||||
$value = $propInfo->getValue($propInfo->isStatic() ? null : $this);
|
||||
if($info->shouldBeOmitted($value))
|
||||
continue;
|
||||
|
||||
if((is_int($value) || is_float($value)) && $info->shouldConvertNumbersToString())
|
||||
$value = (string)$value;
|
||||
|
||||
$props[$name] = [
|
||||
'name' => $name,
|
||||
'value' => $value,
|
||||
'order' => $info->getOrder(),
|
||||
];
|
||||
}
|
||||
|
||||
$methodInfos = $objectInfo->getMethods(ReflectionMethod::IS_PUBLIC);
|
||||
foreach($methodInfos as $methodInfo) {
|
||||
if($methodInfo->getNumberOfRequiredParameters() > 0)
|
||||
throw new RuntimeException('Methods marked with the JsonProperty attribute must not have any required arguments.');
|
||||
|
||||
$attrInfos = $methodInfo->getAttributes(JsonProperty::class);
|
||||
if(count($attrInfos) < 1)
|
||||
continue;
|
||||
if(count($attrInfos) > 1)
|
||||
throw new RuntimeException('Methods may only carry a single instance of the JsonProperty attribute.');
|
||||
|
||||
$info = $attrInfos[0]->newInstance();
|
||||
|
||||
$name = $info->getName() ?? $methodInfo->getName();
|
||||
if(array_key_exists($name, $props))
|
||||
throw new RuntimeException('A property with that name has already been defined.');
|
||||
|
||||
$value = $methodInfo->invoke($methodInfo->isStatic() ? null : $this);
|
||||
if($info->shouldBeOmitted($value))
|
||||
continue;
|
||||
|
||||
if((is_int($value) || is_float($value)) && $info->shouldConvertNumbersToString())
|
||||
$value = (string)$value;
|
||||
|
||||
$props[$name] = [
|
||||
'name' => $name,
|
||||
'value' => $value,
|
||||
'order' => $info->getOrder(),
|
||||
];
|
||||
}
|
||||
|
||||
usort($props, fn($a, $b) => $a['order'] <=> $b['order']);
|
||||
|
||||
return array_column($props, 'value', 'name');
|
||||
}
|
||||
}
|
179
tests/BencodeSerialisableTest.php
Normal file
179
tests/BencodeSerialisableTest.php
Normal file
|
@ -0,0 +1,179 @@
|
|||
<?php
|
||||
// BencodeSerialisableTest.php
|
||||
// Created: 2024-09-29
|
||||
// Updated: 2024-09-29
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use PHPUnit\Framework\Attributes\{CoversClass,UsesClass};
|
||||
use Index\Bencode\{Bencode,BencodeProperty,BencodeSerialisableTrait,IBencodeSerialisable};
|
||||
|
||||
#[CoversClass(BencodeProperty::class)]
|
||||
#[CoversClass(BencodeSerialisableTrait::class)]
|
||||
#[UsesClass(Bencode::class)]
|
||||
#[UsesClass(IBencodeSerialisable::class)]
|
||||
final class BencodeSerialisableTest extends TestCase {
|
||||
private const BASIC_BENCODED_ENCODED = 'd9:stringVal12:string value6:intVali1234e9:intStrVal4:56788:floatVal5:12.3411:floatStrVal5:56.787:trueVali1e22:truePossiblyOmittedVali1e8:falseVali0e14:nullValPresent10:scalarValsli0ei1234e5:12.343:stre16:stringVal_method12:string value9:getIntVali1234e11:getFloatVal5:12.3417:getFloatStringVal5:56.7810:getTrueVali1e25:getTruePossiblyOmittedVali1e11:getFalseVali0e17:getNullValPresent13:getScalarValsli0ei1234e5:12.343:stre5:innerd3:objd3:str21:wow this is illegibleee15:getIntStringVal4:5678e';
|
||||
|
||||
public function testBasicBencodedObject(): void {
|
||||
$test = new class implements IBencodeSerialisable {
|
||||
use BencodeSerialisableTrait;
|
||||
|
||||
#[BencodeProperty]
|
||||
public string $stringVal = 'string value';
|
||||
|
||||
#[BencodeProperty]
|
||||
public int $intVal = 1234;
|
||||
|
||||
#[BencodeProperty(numbersAsString: true)]
|
||||
public int $intStrVal = 5678;
|
||||
|
||||
#[BencodeProperty]
|
||||
public float $floatVal = 12.34;
|
||||
|
||||
#[BencodeProperty(numbersAsString: true)]
|
||||
public float $floatStrVal = 56.78;
|
||||
|
||||
#[BencodeProperty]
|
||||
public bool $trueVal = true;
|
||||
|
||||
#[BencodeProperty(omitIfValue: false)]
|
||||
public bool $truePossiblyOmittedVal = true;
|
||||
|
||||
#[BencodeProperty]
|
||||
public bool $falseVal = false;
|
||||
|
||||
#[BencodeProperty(omitIfValue: false)]
|
||||
public bool $falseOmittedVal = false;
|
||||
|
||||
#[BencodeProperty]
|
||||
public mixed $nullValOmitted = null;
|
||||
|
||||
#[BencodeProperty(omitIfNull: false)]
|
||||
public mixed $nullValPresent = null;
|
||||
|
||||
#[BencodeProperty]
|
||||
public array $scalarVals = [null, 0, 1234, 12.34, 'str', true, false];
|
||||
|
||||
#[BencodeProperty('stringVal_method')]
|
||||
public function getStringVal(): string {
|
||||
return 'string value';
|
||||
}
|
||||
|
||||
#[BencodeProperty]
|
||||
public function getIntVal(): int {
|
||||
return 1234;
|
||||
}
|
||||
|
||||
#[BencodeProperty(order: 1, numbersAsString: true)]
|
||||
public function getIntStringVal(): int {
|
||||
return 5678;
|
||||
}
|
||||
|
||||
#[BencodeProperty]
|
||||
public function getFloatVal(): float {
|
||||
return 12.34;
|
||||
}
|
||||
|
||||
#[BencodeProperty(numbersAsString: true)]
|
||||
public function getFloatStringVal(): float {
|
||||
return 56.78;
|
||||
}
|
||||
|
||||
#[BencodeProperty]
|
||||
public function getTrueVal(): bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
#[BencodeProperty(omitIfValue: false)]
|
||||
public function getTruePossiblyOmittedVal(): bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
#[BencodeProperty]
|
||||
public function getFalseVal(): bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
#[BencodeProperty(omitIfValue: false)]
|
||||
public function getFalseOmittedVal(): bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
#[BencodeProperty]
|
||||
public function getNullValOmitted(): mixed {
|
||||
return null;
|
||||
}
|
||||
|
||||
#[BencodeProperty(omitIfNull: false)]
|
||||
public function getNullValPresent(): mixed {
|
||||
return null;
|
||||
}
|
||||
|
||||
#[BencodeProperty]
|
||||
public function getScalarVals(): array {
|
||||
return [null, 0, 1234, 12.34, 'str', true, false];
|
||||
}
|
||||
|
||||
#[BencodeProperty('inner')]
|
||||
public function getObject(): object {
|
||||
return new class implements IBencodeSerialisable {
|
||||
use BencodeSerialisableTrait;
|
||||
|
||||
#[BencodeProperty('obj')]
|
||||
public function getObject(): object {
|
||||
return new class implements IBencodeSerialisable {
|
||||
use BencodeSerialisableTrait;
|
||||
|
||||
#[BencodeProperty('str')]
|
||||
public function getString(): string {
|
||||
return 'wow this is illegible';
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
$this->assertEquals(self::BASIC_BENCODED_ENCODED, Bencode::encode($test));
|
||||
}
|
||||
|
||||
public function testDoubleBencodedProperty(): void {
|
||||
$this->expectException(RuntimeException::class);
|
||||
|
||||
$test = new class implements IBencodeSerialisable {
|
||||
use BencodeSerialisableTrait;
|
||||
|
||||
#[BencodeProperty('test1')]
|
||||
#[BencodeProperty('test2')]
|
||||
public string $stringVal = 'string value';
|
||||
|
||||
#[BencodeProperty('test3')]
|
||||
#[BencodeProperty('test4')]
|
||||
public function getIntVal(): int {
|
||||
return 1234;
|
||||
}
|
||||
};
|
||||
|
||||
Bencode::encode($test);
|
||||
}
|
||||
|
||||
public function testDuplicateBencodedProperty(): void {
|
||||
$this->expectException(RuntimeException::class);
|
||||
|
||||
$test = new class implements IBencodeSerialisable {
|
||||
use BencodeSerialisableTrait;
|
||||
|
||||
#[BencodeProperty]
|
||||
public string $test1 = 'string value';
|
||||
|
||||
#[BencodeProperty('test1')]
|
||||
public function getIntVal(): int {
|
||||
return 1234;
|
||||
}
|
||||
};
|
||||
|
||||
Bencode::encode($test);
|
||||
}
|
||||
}
|
177
tests/JsonSerializableTest.php
Normal file
177
tests/JsonSerializableTest.php
Normal file
|
@ -0,0 +1,177 @@
|
|||
<?php
|
||||
// JsonSerializableTest.php
|
||||
// Created: 2024-09-29
|
||||
// Updated: 2024-09-29
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use Index\Json\{JsonProperty,JsonSerializableTrait};
|
||||
|
||||
#[CoversClass(JsonProperty::class)]
|
||||
#[CoversClass(JsonSerializableTrait::class)]
|
||||
final class JsonSerializableTest extends TestCase {
|
||||
private const BASIC_JSON_ENCODED = '{"stringVal":"string value","intVal":1234,"intStrVal":"5678","floatVal":12.34,"floatStrVal":"56.78","trueVal":true,"truePossiblyOmittedVal":true,"falseVal":false,"nullValPresent":null,"scalarVals":[null,0,1234,12.34,"str",true,false],"stringVal_method":"string value","getIntVal":1234,"getFloatVal":12.34,"getFloatStringVal":"56.78","getTrueVal":true,"getTruePossiblyOmittedVal":true,"getFalseVal":false,"getNullValPresent":null,"getScalarVals":[null,0,1234,12.34,"str",true,false],"inner":{"obj":{"str":"wow this is illegible"}},"getIntStringVal":"5678"}';
|
||||
|
||||
public function testBasicJsonObject(): void {
|
||||
$test = new class implements JsonSerializable {
|
||||
use JsonSerializableTrait;
|
||||
|
||||
#[JsonProperty]
|
||||
public string $stringVal = 'string value';
|
||||
|
||||
#[JsonProperty]
|
||||
public int $intVal = 1234;
|
||||
|
||||
#[JsonProperty(numbersAsString: true)]
|
||||
public int $intStrVal = 5678;
|
||||
|
||||
#[JsonProperty]
|
||||
public float $floatVal = 12.34;
|
||||
|
||||
#[JsonProperty(numbersAsString: true)]
|
||||
public float $floatStrVal = 56.78;
|
||||
|
||||
#[JsonProperty]
|
||||
public bool $trueVal = true;
|
||||
|
||||
#[JsonProperty(omitIfValue: false)]
|
||||
public bool $truePossiblyOmittedVal = true;
|
||||
|
||||
#[JsonProperty]
|
||||
public bool $falseVal = false;
|
||||
|
||||
#[JsonProperty(omitIfValue: false)]
|
||||
public bool $falseOmittedVal = false;
|
||||
|
||||
#[JsonProperty]
|
||||
public mixed $nullValOmitted = null;
|
||||
|
||||
#[JsonProperty(omitIfNull: false)]
|
||||
public mixed $nullValPresent = null;
|
||||
|
||||
#[JsonProperty]
|
||||
public array $scalarVals = [null, 0, 1234, 12.34, 'str', true, false];
|
||||
|
||||
#[JsonProperty('stringVal_method')]
|
||||
public function getStringVal(): string {
|
||||
return 'string value';
|
||||
}
|
||||
|
||||
#[JsonProperty]
|
||||
public function getIntVal(): int {
|
||||
return 1234;
|
||||
}
|
||||
|
||||
#[JsonProperty(order: 1, numbersAsString: true)]
|
||||
public function getIntStringVal(): int {
|
||||
return 5678;
|
||||
}
|
||||
|
||||
#[JsonProperty]
|
||||
public function getFloatVal(): float {
|
||||
return 12.34;
|
||||
}
|
||||
|
||||
#[JsonProperty(numbersAsString: true)]
|
||||
public function getFloatStringVal(): float {
|
||||
return 56.78;
|
||||
}
|
||||
|
||||
#[JsonProperty]
|
||||
public function getTrueVal(): bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
#[JsonProperty(omitIfValue: false)]
|
||||
public function getTruePossiblyOmittedVal(): bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
#[JsonProperty]
|
||||
public function getFalseVal(): bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
#[JsonProperty(omitIfValue: false)]
|
||||
public function getFalseOmittedVal(): bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
#[JsonProperty]
|
||||
public function getNullValOmitted(): mixed {
|
||||
return null;
|
||||
}
|
||||
|
||||
#[JsonProperty(omitIfNull: false)]
|
||||
public function getNullValPresent(): mixed {
|
||||
return null;
|
||||
}
|
||||
|
||||
#[JsonProperty]
|
||||
public function getScalarVals(): array {
|
||||
return [null, 0, 1234, 12.34, 'str', true, false];
|
||||
}
|
||||
|
||||
#[JsonProperty('inner')]
|
||||
public function getObject(): object {
|
||||
return new class implements JsonSerializable {
|
||||
use JsonSerializableTrait;
|
||||
|
||||
#[JsonProperty('obj')]
|
||||
public function getObject(): object {
|
||||
return new class implements JsonSerializable {
|
||||
use JsonSerializableTrait;
|
||||
|
||||
#[JsonProperty('str')]
|
||||
public function getString(): string {
|
||||
return 'wow this is illegible';
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
$this->assertEquals(self::BASIC_JSON_ENCODED, json_encode($test));
|
||||
}
|
||||
|
||||
public function testDoubleJsonProperty(): void {
|
||||
$this->expectException(RuntimeException::class);
|
||||
|
||||
$test = new class implements JsonSerializable {
|
||||
use JsonSerializableTrait;
|
||||
|
||||
#[JsonProperty('test1')]
|
||||
#[JsonProperty('test2')]
|
||||
public string $stringVal = 'string value';
|
||||
|
||||
#[JsonProperty('test3')]
|
||||
#[JsonProperty('test4')]
|
||||
public function getIntVal(): int {
|
||||
return 1234;
|
||||
}
|
||||
};
|
||||
|
||||
json_encode($test);
|
||||
}
|
||||
|
||||
public function testDuplicateJsonProperty(): void {
|
||||
$this->expectException(RuntimeException::class);
|
||||
|
||||
$test = new class implements JsonSerializable {
|
||||
use JsonSerializableTrait;
|
||||
|
||||
#[JsonProperty]
|
||||
public string $test1 = 'string value';
|
||||
|
||||
#[JsonProperty('test1')]
|
||||
public function getIntVal(): int {
|
||||
return 1234;
|
||||
}
|
||||
};
|
||||
|
||||
json_encode($test);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue