diff --git a/src/Utils/Json.php b/src/Utils/Json.php index 785786634..5ce45cc4c 100644 --- a/src/Utils/Json.php +++ b/src/Utils/Json.php @@ -25,9 +25,11 @@ final class Json public const ESCAPE_UNICODE = 0b0100; + const FORCE_OBJECT = 0b10000; + /** - * Returns the JSON representation of a value. Accepts flag Json::PRETTY. + * Returns the JSON representation of a value. Accepts flag Json::PRETTY and JSON::FORCE_OBJECT. * @param mixed $value * @throws JsonException */ @@ -36,6 +38,7 @@ public static function encode($value, int $flags = 0): string $flags = ($flags & self::ESCAPE_UNICODE ? 0 : JSON_UNESCAPED_UNICODE) | JSON_UNESCAPED_SLASHES | ($flags & self::PRETTY ? JSON_PRETTY_PRINT : 0) + | ($flags & self::FORCE_OBJECT ? JSON_FORCE_OBJECT : 0) | (defined('JSON_PRESERVE_ZERO_FRACTION') ? JSON_PRESERVE_ZERO_FRACTION : 0); // since PHP 5.6.6 & PECL JSON-C 1.3.7 $json = json_encode($value, $flags); diff --git a/src/Utils/Reflection.php b/src/Utils/Reflection.php index 792197d21..fe7750137 100644 --- a/src/Utils/Reflection.php +++ b/src/Utils/Reflection.php @@ -113,6 +113,7 @@ public static function getPropertyDeclaringClass(\ReflectionProperty $prop): \Re { foreach ($prop->getDeclaringClass()->getTraits() as $trait) { if ($trait->hasProperty($prop->name) + // doc-comment guessing as workaround for insufficient PHP reflection && $trait->getProperty($prop->name)->getDocComment() === $prop->getDocComment() ) { return self::getPropertyDeclaringClass($trait->getProperty($prop->name)); @@ -122,6 +123,40 @@ public static function getPropertyDeclaringClass(\ReflectionProperty $prop): \Re } + /** + * Returns declaring class or trait. + */ + public static function getMethodDeclaringMethod(\ReflectionMethod $method): \ReflectionMethod + { + // file & line guessing as workaround for insufficient PHP reflection + $decl = $method->getDeclaringClass(); + if ($decl->getFileName() === $method->getFileName() + && $decl->getStartLine() <= $method->getStartLine() + && $decl->getEndLine() >= $method->getEndLine() + ) { + return $method; + } + + $hash = [$method->getFileName(), $method->getStartLine(), $method->getEndLine()]; + if (($alias = $decl->getTraitAliases()[$method->name] ?? null) + && ($m = new \ReflectionMethod($alias)) + && [$m->getFileName(), $m->getStartLine(), $m->getEndLine()] === $hash + ) { + return self::getMethodDeclaringMethod($m); + } + + foreach ($decl->getTraits() as $trait) { + if ($trait->hasMethod($method->name) + && ($m = $trait->getMethod($method->name)) + && [$m->getFileName(), $m->getStartLine(), $m->getEndLine()] === $hash + ) { + return self::getMethodDeclaringMethod($m); + } + } + return $method; + } + + /** * Are documentation comments available? */ diff --git a/src/Utils/Validators.php b/src/Utils/Validators.php index 664c8eaa2..3cf23f14a 100644 --- a/src/Utils/Validators.php +++ b/src/Utils/Validators.php @@ -301,12 +301,15 @@ public static function isEmail(string $value): bool { $atom = "[-a-z0-9!#$%&'*+/=?^_`{|}~]"; // RFC 5322 unquoted characters in local-part $alpha = "a-z\x80-\xFF"; // superset of IDN - return (bool) preg_match("(^ - (\"([ !#-[\\]-~]*|\\\\[ -~])+\"|$atom+(\\.$atom+)*) # quoted or unquoted + return (bool) preg_match(<<getDeclaringClass()->name . '::' . $res->name; +} + + +// new ReflectionMethod and getMethod returns different method names, PHP #79636 + +// Method in trait +Assert::same('A::foo', get((new ReflectionClass('E3'))->getMethod('foo3'))); +Assert::same('A::foo', get(new ReflectionMethod('E3', 'foo3'))); + +Assert::same('B2::foo2', get((new ReflectionClass('E3'))->getMethod('foo2'))); +Assert::same('B2::foo2', get(new ReflectionMethod('E3', 'foo2'))); + +Assert::same('A::foo', get((new ReflectionClass('E3'))->getMethod('foo'))); +Assert::same('A::foo', get(new ReflectionMethod('E3', 'foo'))); + +// Method in class +Assert::same('E2::alias', get((new ReflectionClass('E2'))->getMethod('alias'))); +Assert::same('E2::alias', get(new ReflectionMethod('E2', 'alias'))); + +Assert::same('E2::foo2', get((new ReflectionClass('E2'))->getMethod('foo2'))); +Assert::same('E2::foo2', get(new ReflectionMethod('E2', 'foo2'))); + +// Method in trait +Assert::same('A::foo', get((new ReflectionClass('E1'))->getMethod('alias'))); +Assert::same('A::foo', get(new ReflectionMethod('E1', 'alias'))); + +// Method in trait +Assert::same('B2::foo2', get((new ReflectionClass('B2'))->getMethod('foo2'))); +Assert::same('B2::foo2', get(new ReflectionMethod('B2', 'foo2'))); + +Assert::same('A::foo', get((new ReflectionClass('B'))->getMethod('foo2'))); +Assert::same('A::foo', get(new ReflectionMethod('B', 'foo2'))); + +Assert::same('A::foo', get((new ReflectionClass('A'))->getMethod('foo'))); +Assert::same('A::foo', get(new ReflectionMethod('A', 'foo'))); diff --git a/tests/Utils/Reflection.getDeclaringMethod.insteadof.phpt b/tests/Utils/Reflection.getDeclaringMethod.insteadof.phpt new file mode 100644 index 000000000..a1f541d03 --- /dev/null +++ b/tests/Utils/Reflection.getDeclaringMethod.insteadof.phpt @@ -0,0 +1,67 @@ +getDeclaringClass()->name . '::' . $res->name; +} + + +// Method in class +Assert::same('D::foo', get(new ReflectionMethod('D', 'foo'))); + +// Method in trait - uses doccomment & file-line workaround +Assert::same('B::foo', get(new ReflectionMethod('C', 'foo'))); + +// Method in trait +Assert::same('B::foo', get(new ReflectionMethod('B', 'foo'))); + +// Method in trait +Assert::same('A::foo', get(new ReflectionMethod('A', 'foo'))); diff --git a/tests/Utils/Reflection.getDeclaringMethod.overwrite.phpt b/tests/Utils/Reflection.getDeclaringMethod.overwrite.phpt new file mode 100644 index 000000000..cd46fa533 --- /dev/null +++ b/tests/Utils/Reflection.getDeclaringMethod.overwrite.phpt @@ -0,0 +1,66 @@ +getDeclaringClass()->name . '::' . $res->name; +} + + +// Method in class +Assert::same('D::foo', get(new ReflectionMethod('D', 'foo'))); + +// Method in class - uses doccomment & file-line workaround +Assert::same('C::foo', get(new ReflectionMethod('C', 'foo'))); + +// Method in trait - uses doccomment & file-line workaround +Assert::same('B::foo', get(new ReflectionMethod('B', 'foo'))); + +// Method in trait +Assert::same('A::foo', get(new ReflectionMethod('A', 'foo'))); diff --git a/tests/Utils/Reflection.getDeclaringMethod.phpt b/tests/Utils/Reflection.getDeclaringMethod.phpt new file mode 100644 index 000000000..274e86d50 --- /dev/null +++ b/tests/Utils/Reflection.getDeclaringMethod.phpt @@ -0,0 +1,71 @@ +getDeclaringClass()->name . '::' . $res->name; +} + + +// Method in trait +Assert::same('B::foo', get(new ReflectionMethod('D', 'foo'))); + +// Method in parent trait +Assert::same('A::bar', get(new ReflectionMethod('D', 'bar'))); + +// Method in class itself +Assert::same('C::own', get(new ReflectionMethod('D', 'own'))); + +// Method in second trait +Assert::same('E::baz', get(new ReflectionMethod('D', 'baz')));