Skip to content

Commit

Permalink
Refactor: moved annotation parse from contributte/utils to this pkg
Browse files Browse the repository at this point in the history
  • Loading branch information
f3l1x committed Feb 5, 2024
1 parent 6e68a47 commit b90e7fd
Show file tree
Hide file tree
Showing 3 changed files with 196 additions and 2 deletions.
1 change: 0 additions & 1 deletion composer.json
Expand Up @@ -19,7 +19,6 @@
],
"require": {
"php": ">=8.1",
"contributte/utils": "^0.6 || ^0.7",
"nette/di": "^3.1.8",
"nextras/orm": "^4.0.7"
},
Expand Down
2 changes: 1 addition & 1 deletion src/DI/NextrasOrmEventsExtension.php
Expand Up @@ -11,7 +11,7 @@
use Contributte\Nextras\Orm\Events\Listeners\BeforeRemoveListener;
use Contributte\Nextras\Orm\Events\Listeners\BeforeUpdateListener;
use Contributte\Nextras\Orm\Events\Listeners\FlushListener;
use Contributte\Utils\Annotations;
use Contributte\Nextras\Orm\Events\Utils\Annotations;
use Nette\DI\CompilerExtension;
use Nette\DI\Definitions\ServiceDefinition;
use Nette\DI\ServiceCreationException;
Expand Down
195 changes: 195 additions & 0 deletions src/Utils/Annotations.php
@@ -0,0 +1,195 @@
<?php declare(strict_types = 1);

namespace Contributte\Nextras\Orm\Events\Utils;

use LogicException;
use Nette\StaticClass;
use Nette\Utils\ArrayHash;
use Nette\Utils\Strings;
use ReflectionClass;
use ReflectionException;
use ReflectionFunction;
use ReflectionMethod;
use ReflectionProperty;
use Reflector;

/**
* @creator David Grudl https://github.com/nette/reflection
*/
class Annotations
{

use StaticClass;

/** @internal single & double quoted PHP string */
public const RE_STRING = '\'(?:\\\\.|[^\'\\\\])*\'|"(?:\\\\.|[^"\\\\])*"';

/** @internal identifier */
public const RE_IDENTIFIER = '[_a-zA-Z\x7F-\xFF][_a-zA-Z0-9\x7F-\xFF-\\\]*';

public static ?bool $useReflection = null;

public static bool $autoRefresh = true;

/** @var string[] */
public static array $inherited = ['description', 'param', 'return'];

/** @var array<string, array<string, array<string, array<mixed>>>> */
private static array $cache;

/**
* @param ReflectionClass<object>|ReflectionMethod|ReflectionProperty|ReflectionFunction $r
*/
public static function hasAnnotation(Reflector $r, string $name): bool
{
return self::getAnnotation($r, $name) !== null;
}

/**
* @param ReflectionClass<object>|ReflectionMethod|ReflectionProperty|ReflectionFunction $r
*/
public static function getAnnotation(Reflector $r, string $name): mixed
{
$res = self::getAnnotations($r);

if ($res === []) {
return null;
}

return isset($res[$name]) ? end($res[$name]) : null;
}

/**
* @param ReflectionClass<object>|ReflectionMethod|ReflectionProperty|ReflectionFunction $r
* @return array<array<mixed>>
*/
public static function getAnnotations(Reflector $r): array
{
if ($r instanceof ReflectionClass) {
$type = $r->getName();
$member = 'class';

} elseif ($r instanceof ReflectionMethod) {
$type = $r->getDeclaringClass()->getName();
$member = $r->getName();

} elseif ($r instanceof ReflectionFunction) {
$type = null;
$member = $r->getName();

} else {
$type = $r->getDeclaringClass()->getName();
$member = '$' . $r->getName();
}

if (self::$useReflection === null) { // detects whether is reflection available
self::$useReflection = (bool) (new ReflectionClass(self::class))->getDocComment();
}

if (isset(self::$cache[$type][$member])) { // is value cached?
return self::$cache[$type][$member];
}

$annotations = self::$useReflection ? self::parseComment((string) $r->getDocComment()) : [];

// @phpstan-ignore-next-line
if ($r instanceof ReflectionMethod && !$r->isPrivate() && (!$r->isConstructor() || !empty($annotations['inheritdoc'][0]))
) {
try {
// @phpstan-ignore-next-line
$inherited = self::getAnnotations(new ReflectionMethod((string) get_parent_class($type), $member));
} catch (ReflectionException $e) {
try {
$inherited = self::getAnnotations($r->getPrototype());
} catch (ReflectionException $e) {
$inherited = [];
}
}

$annotations += array_intersect_key($inherited, array_flip(self::$inherited));
}

return self::$cache[$type][$member] = $annotations;
}

/**
* @return array<array<string|int,mixed[]>>
*/
private static function parseComment(string $comment): array
{
static $tokens = ['true' => true, 'false' => false, 'null' => null, '' => true];

$res = [];
$comment = (string) preg_replace('#^\s*\*\s?#ms', '', trim($comment, '/*'));
$parts = preg_split('#^\s*(?=@' . self::RE_IDENTIFIER . ')#m', $comment, 2);

if ($parts === false) {
throw new LogicException('Cannot split comment');
}

$description = trim($parts[0]);
if ($description !== '') {
$res['description'] = [$description];
}

$matches = Strings::matchAll(
$parts[1] ?? '',
'~
(?<=\s|^)@(' . self::RE_IDENTIFIER . ')[ \t]* ## annotation
(
\((?>' . self::RE_STRING . '|[^\'")@]+)+\)| ## (value)
[^(@\r\n][^@\r\n]*|) ## value
~xi'
);

foreach ($matches as $match) {
[, $name, $value] = $match;

if (substr($value, 0, 1) === '(') {
$items = [];
$key = '';
$val = true;
$value[0] = ',';
while ($m = Strings::match($value, '#\s*,\s*(?>(' . self::RE_IDENTIFIER . ')\s*=\s*)?(' . self::RE_STRING . '|[^\'"),\s][^\'"),]*)#A')) {
$value = substr($value, strlen($m[0]));
[, $key, $val] = $m;
$val = rtrim($val);
if ($val[0] === "'" || $val[0] === '"') {
$val = substr($val, 1, -1);

} elseif (is_numeric($val)) {
$val = 1 * $val;

} else {
$lval = strtolower($val);
$val = array_key_exists($lval, $tokens) ? $tokens[$lval] : $val;
}

if ($key === '') {
$items[] = $val;

} else {
$items[$key] = $val;
}
}

$value = count($items) < 2 && $key === '' ? $val : $items;

} else {
$value = trim($value);
if (is_numeric($value)) {
$value = 1 * $value;

} else {
$lval = strtolower($value);
$value = array_key_exists($lval, $tokens) ? $tokens[$lval] : $value;
}
}

$res[$name][] = is_array($value) ? ArrayHash::from($value) : $value;
}

return $res;
}

}

0 comments on commit b90e7fd

Please sign in to comment.