From caf4eef02dd0a00e6160fce7e0bd555909be30f2 Mon Sep 17 00:00:00 2001 From: flashwave Date: Sun, 29 Sep 2024 23:53:44 +0000 Subject: [PATCH] Added JsonProperty and BencodeProperty attributes for easier object encoding. --- VERSION | 2 +- composer.lock | 118 +++++++-------- src/Bencode/BencodeProperty.php | 89 +++++++++++ src/Bencode/BencodeSerialisableTrait.php | 93 ++++++++++++ src/Json/JsonProperty.php | 89 +++++++++++ src/Json/JsonSerializableTrait.php | 89 +++++++++++ tests/BencodeSerialisableTest.php | 179 +++++++++++++++++++++++ tests/JsonSerializableTest.php | 177 ++++++++++++++++++++++ 8 files changed, 776 insertions(+), 60 deletions(-) create mode 100644 src/Bencode/BencodeProperty.php create mode 100644 src/Bencode/BencodeSerialisableTrait.php create mode 100644 src/Json/JsonProperty.php create mode 100644 src/Json/JsonSerializableTrait.php create mode 100644 tests/BencodeSerialisableTest.php create mode 100644 tests/JsonSerializableTest.php diff --git a/VERSION b/VERSION index 572c349..0bed13c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2408.401738 +0.2408.610150 diff --git a/composer.lock b/composer.lock index 48ed905..5f457ce 100644 --- a/composer.lock +++ b/composer.lock @@ -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", diff --git a/src/Bencode/BencodeProperty.php b/src/Bencode/BencodeProperty.php new file mode 100644 index 0000000..9d8960b --- /dev/null +++ b/src/Bencode/BencodeProperty.php @@ -0,0 +1,89 @@ +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; + } +} diff --git a/src/Bencode/BencodeSerialisableTrait.php b/src/Bencode/BencodeSerialisableTrait.php new file mode 100644 index 0000000..6d203b2 --- /dev/null +++ b/src/Bencode/BencodeSerialisableTrait.php @@ -0,0 +1,93 @@ + + */ + 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'); + } +} diff --git a/src/Json/JsonProperty.php b/src/Json/JsonProperty.php new file mode 100644 index 0000000..088804d --- /dev/null +++ b/src/Json/JsonProperty.php @@ -0,0 +1,89 @@ +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; + } +} diff --git a/src/Json/JsonSerializableTrait.php b/src/Json/JsonSerializableTrait.php new file mode 100644 index 0000000..bab1a78 --- /dev/null +++ b/src/Json/JsonSerializableTrait.php @@ -0,0 +1,89 @@ + + */ + 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'); + } +} diff --git a/tests/BencodeSerialisableTest.php b/tests/BencodeSerialisableTest.php new file mode 100644 index 0000000..cc2df8c --- /dev/null +++ b/tests/BencodeSerialisableTest.php @@ -0,0 +1,179 @@ +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); + } +} diff --git a/tests/JsonSerializableTest.php b/tests/JsonSerializableTest.php new file mode 100644 index 0000000..407b997 --- /dev/null +++ b/tests/JsonSerializableTest.php @@ -0,0 +1,177 @@ +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); + } +}