Skip to content

Commit

Permalink
Merge pull request #619 from nextras/udpate-dependencies
Browse files Browse the repository at this point in the history
Udpate dependencies
  • Loading branch information
hrach committed Apr 15, 2023
2 parents e8a90db + d364f80 commit 2f25c7d
Show file tree
Hide file tree
Showing 7 changed files with 209 additions and 75 deletions.
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@
"ext-json": "*",
"ext-ctype": "*",
"nette/caching": "~3.2 || ~3.1.3",
"nette/utils": "~4.0",
"nette/tokenizer": "~3.0",
"nette/utils": "~3.0 || ~4.0",
"nextras/dbal": "~5.0@dev"
},
"require-dev": {
Expand All @@ -40,6 +39,7 @@
"phpstan/phpstan-nette": "1.2.3",
"phpstan/phpstan-mockery": "1.1.0",
"phpstan/phpstan-strict-rules": "1.4.5",
"nextras/multi-query-parser": "~1.0",
"nextras/orm-phpstan": "~1.0@dev",
"marc-mabe/php-enum-phpstan": "dev-master",
"tracy/tracy": "~2.3"
Expand Down
100 changes: 31 additions & 69 deletions src/Entity/Reflection/ModifierParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,42 +3,22 @@
namespace Nextras\Orm\Entity\Reflection;


use Nette\Tokenizer\Exception;
use Nette\Tokenizer\Stream;
use Nette\Tokenizer\Token;
use Nette\Tokenizer\Tokenizer;
use Nette\Utils\Reflection;
use Nextras\Orm\Entity\Reflection\Parser\Token;
use Nextras\Orm\Entity\Reflection\Parser\TokenLexer;
use Nextras\Orm\Entity\Reflection\Parser\TokenStream;
use Nextras\Orm\Exception\InvalidStateException;
use ReflectionClass;


class ModifierParser
{
private const TOKEN_KEYWORD = 1;
private const TOKEN_STRING = 2;
private const TOKEN_LBRACKET = 3;
private const TOKEN_RBRACKET = 4;
private const TOKEN_EQUAL = 5;
private const TOKEN_SEPARATOR = 6;
private const TOKEN_WHITESPACE = 7;
/** @const regular expression for single & double quoted PHP string */
private const RE_STRING = '\'(?:\\\\.|[^\'\\\\])*\'|"(?:\\\\.|[^"\\\\])*"';

/** @var Tokenizer */
private $tokenizer;
private TokenLexer $tokenLexer;


public function __construct()
{
$this->tokenizer = new Tokenizer([
self::TOKEN_STRING => self::RE_STRING,
self::TOKEN_LBRACKET => '\[',
self::TOKEN_RBRACKET => '\]',
self::TOKEN_EQUAL => '=',
self::TOKEN_KEYWORD => '[a-zA-Z0-9_:$.*>\\\\-]+',
self::TOKEN_SEPARATOR => ',',
self::TOKEN_WHITESPACE => '\s+',
]);
$this->tokenLexer = new TokenLexer();
}


Expand All @@ -51,10 +31,10 @@ public function matchModifiers(string $input): array
preg_match_all('#
\{(
(?:
' . self::RE_STRING . ' |
' . TokenLexer::RE_STRING . ' |
[^}]
)++
)\}#x', $input, $matches);
)}#x', $input, $matches);
return $matches[1];
}

Expand All @@ -66,42 +46,26 @@ public function matchModifiers(string $input): array
*/
public function parse(string $string, ReflectionClass $reflectionClass): array
{
$iterator = $this->lex($string);
$iterator = $this->tokenLexer->lex($string);
return [
$name = $this->processName($iterator),
$this->processArgs($iterator, $reflectionClass, $name, false),
];
}


private function lex(string $input): Stream
{
try {
$tokens = $this->tokenizer->tokenize($input)->tokens;
} catch (Exception $e) {
throw new InvalidModifierDefinitionException('Unable to tokenize the modifier.', 0, $e);
}

$tokens = array_filter($tokens, function ($token): bool {
return $token->type !== self::TOKEN_WHITESPACE;
});
$tokens = array_values($tokens);
return new Stream($tokens);
}


private function processName(Stream $iterator): string
private function processName(TokenStream $iterator): string
{
$iterator->position++;
$currentToken = $iterator->currentToken();
if ($currentToken === null) {
throw new InvalidModifierDefinitionException("Modifier does not have a name.");
}
if ($currentToken->type !== self::TOKEN_KEYWORD) {
if ($currentToken->type !== Token::KEYWORD) {
throw new InvalidModifierDefinitionException("Modifier does not have a name.");
} elseif (isset($iterator->tokens[$iterator->position + 1])) {
$nextToken = $iterator->tokens[$iterator->position + 1];
if ($nextToken->type === self::TOKEN_SEPARATOR) {
if ($nextToken->type === Token::SEPARATOR) {
throw new InvalidModifierDefinitionException("After the {{$currentToken->value}}'s modifier name cannot be a comma separator.");
}
}
Expand All @@ -115,37 +79,37 @@ private function processName(Stream $iterator): string
* @phpstan-return array<int|string, mixed>
*/
private function processArgs(
Stream $iterator,
TokenStream $iterator,
ReflectionClass $reflectionClass,
string $modifierName,
bool $inArray
bool $inArray,
): array
{
$result = [];
while (($currentToken = $iterator->nextToken()) !== null) {
$type = $currentToken->type;

if ($type === self::TOKEN_RBRACKET) {
if ($type === Token::RBRACKET) {
if ($inArray) {
return $result;
} else {
throw new InvalidModifierDefinitionException("Modifier {{$modifierName}} mismatches brackets.");
}
} elseif ($type === self::TOKEN_STRING || $type === self::TOKEN_KEYWORD) {
} elseif ($type === Token::STRING || $type === Token::KEYWORD) {
$iterator->position++;
$nextToken = $iterator->currentToken();
$nextTokenType = $nextToken !== null ? $nextToken->type : null;
$nextTokenType = $nextToken?->type;

if ($nextTokenType === self::TOKEN_EQUAL) {
if ($nextTokenType === Token::EQUAL) {
$iterator->position++;
$nextToken = $iterator->currentToken();
$nextTokenType = $nextToken !== null ? $nextToken->type : null;
$nextTokenType = $nextToken?->type;

if ($nextTokenType === self::TOKEN_LBRACKET) {
if ($nextTokenType === Token::LBRACKET) {
$value = $this->processValue($currentToken, $reflectionClass);
assert(!is_array($value));
$result[$value] = $this->processArgs($iterator, $reflectionClass, $modifierName, true);
} elseif ($nextTokenType === self::TOKEN_STRING || $nextTokenType === self::TOKEN_KEYWORD) {
} elseif ($nextTokenType === Token::STRING || $nextTokenType === Token::KEYWORD) {
$value = $this->processValue($currentToken, $reflectionClass);
assert(!is_array($value));
assert($nextToken !== null);
Expand All @@ -164,18 +128,18 @@ private function processArgs(
$result[] = $value;
}
}
} elseif ($type === self::TOKEN_LBRACKET) {
} elseif ($type === Token::LBRACKET) {
$result[] = $this->processArgs($iterator, $reflectionClass, $modifierName, true);
} else {
throw new InvalidModifierDefinitionException("Modifier {{$modifierName}} has invalid token, expected string, keyword, or array.");
}

$iterator->position++;
$currentToken2 = $iterator->currentToken();
$type = $currentToken2 !== null ? $currentToken2->type : null;
if ($type === self::TOKEN_RBRACKET && $inArray) {
$type = $currentToken2?->type;
if ($type === Token::RBRACKET && $inArray) {
return $result;
} elseif ($type !== null && $type !== self::TOKEN_SEPARATOR) {
} elseif ($type !== null && $type !== Token::SEPARATOR) {
throw new InvalidModifierDefinitionException("Modifier {{$modifierName}} misses argument separator.");
}
}
Expand All @@ -190,13 +154,12 @@ private function processArgs(

/**
* @phpstan-param ReflectionClass<object> $reflectionClass
* @return mixed
*/
private function processValue(Token $token, ReflectionClass $reflectionClass)
private function processValue(Token $token, ReflectionClass $reflectionClass): mixed
{
if ($token->type === self::TOKEN_STRING) {
if ($token->type === Token::STRING) {
return stripslashes(substr($token->value, 1, -1));
} elseif ($token->type === self::TOKEN_KEYWORD) {
} elseif ($token->type === Token::KEYWORD) {
return $this->processKeyword($token->value, $reflectionClass);
} else {
throw new InvalidStateException();
Expand All @@ -206,9 +169,8 @@ private function processValue(Token $token, ReflectionClass $reflectionClass)

/**
* @phpstan-param ReflectionClass<object> $reflectionClass
* @return mixed
*/
private function processKeyword(string $value, ReflectionClass $reflectionClass)
private function processKeyword(string $value, ReflectionClass $reflectionClass): mixed
{
if (strcasecmp($value, 'true') === 0) {
return true;
Expand All @@ -230,7 +192,7 @@ private function processKeyword(string $value, ReflectionClass $reflectionClass)

$enum = [];
$constants = $reflection->getConstants();
if (strpos($const, '*') !== false) {
if (str_contains($const, '*')) {
$prefix = rtrim($const, '*');
$prefixLength = strlen($prefix);
$count = 0;
Expand All @@ -241,11 +203,11 @@ private function processKeyword(string $value, ReflectionClass $reflectionClass)
}
}
if ($count === 0) {
throw new InvalidModifierDefinitionException("No constant matches {$reflection->name}::{$const} pattern.");
throw new InvalidModifierDefinitionException("No constant matches $reflection->name::$const pattern.");
}
} else {
if (!array_key_exists($const, $constants)) {
throw new InvalidModifierDefinitionException("Constant {$reflection->name}::{$const} does not exist.");
throw new InvalidModifierDefinitionException("Constant $reflection->name::$const does not exist.");
}
$value = $reflection->getConstant($const);
$enum[$value] = $value;
Expand Down
26 changes: 26 additions & 0 deletions src/Entity/Reflection/Parser/Token.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php declare(strict_types = 1);

namespace Nextras\Orm\Entity\Reflection\Parser;


/**
* @internal
*/
class Token
{
public const KEYWORD = 1;
public const STRING = 2;
public const LBRACKET = 3;
public const RBRACKET = 4;
public const EQUAL = 5;
public const SEPARATOR = 6;
public const WHITESPACE = 7;

public function __construct(
public readonly string $value,
public readonly int $type,
public readonly int $offset,
)
{
}
}
101 changes: 101 additions & 0 deletions src/Entity/Reflection/Parser/TokenLexer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?php declare(strict_types = 1);

namespace Nextras\Orm\Entity\Reflection\Parser;


use Nextras\Orm\Exception\InvalidStateException;


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

private string $tokenizerRegexp;
/** @var list<int> */
private array $tokenizerTypes;


public function __construct()
{
$patterns = [
Token::STRING => self::RE_STRING,
Token::LBRACKET => '\[',
Token::RBRACKET => '\]',
Token::EQUAL => '=',
Token::KEYWORD => '[a-zA-Z0-9_:$.*>\\\\-]+',
Token::SEPARATOR => ',',
Token::WHITESPACE => '\s+',
];
$this->tokenizerRegexp = '~(' . implode(')|(', $patterns) . ')~A';
$this->tokenizerTypes = array_keys($patterns);
}


public function lex(string $input): TokenStream
{
$tokens = $this->tokenize($input);
$tokens = array_filter($tokens, function ($token): bool {
return $token->type !== Token::WHITESPACE;
});
$tokens = array_values($tokens);
return new TokenStream($tokens);
}


/**
* @return list<Token>
*/
private function tokenize(string $input): array
{
preg_match_all($this->tokenizerRegexp, $input, $tokens, PREG_SET_ORDER);
if (preg_last_error() !== PREG_NO_ERROR) {
throw new InvalidStateException(array_flip(get_defined_constants(true)['pcre'])[preg_last_error()]);
}

$len = 0;
$result = [];
$count = count($this->tokenizerTypes);
foreach ($tokens as $token) {
$type = null;
for ($i = 1; $i <= $count; $i++) {
if (!isset($token[$i])) {
break;
} elseif ($token[$i] !== '') {
$type = $this->tokenizerTypes[$i - 1];
break;
}
}
if ($type === null) {
throw new InvalidStateException("Unknown token type for '$token[0]'.");
}

$token = new Token($token[0], $type, $len);
$result[] = $token;
$len += strlen($token->value);
}

if ($len !== strlen($input)) {
[$line, $col] = self::getCoordinates($input, $len);
$unexpectedToken = str_replace("\n", '\n', substr($input, $len, 10));
throw new InvalidStateException("Unexpected '$unexpectedToken' on line $line, column $col.");
}

return $result;
}


/**
* Returns (line, column) position of token in input string.
* @return array{int, int}
*/
private static function getCoordinates(string $text, int $offset): array
{
$text = substr($text, 0, $offset);
$pos = strrpos("\n" . $text, "\n");
return [substr_count($text, "\n") + 1, $offset - ($pos === false ? 0 : $pos) + 1];
}
}
Loading

0 comments on commit 2f25c7d

Please sign in to comment.