diff --git a/formwork/defaults.php b/formwork/defaults.php index d1168984a..d3f648e72 100644 --- a/formwork/defaults.php +++ b/formwork/defaults.php @@ -40,7 +40,12 @@ 'setHandlers' => true ], 'fields' => [ - 'path' => FORMWORK_PATH . 'fields' . DS + 'path' => FORMWORK_PATH . 'fields' . DS, + 'dynamic' => [ + 'vars' => [ + 'file' => FORMWORK_PATH . 'fields' . DS . 'dynamic' . DS . 'vars.php' + ] + ] ], 'files' => [ 'allowedExtensions' => [ diff --git a/formwork/fields/dynamic/vars.php b/formwork/fields/dynamic/vars.php new file mode 100644 index 000000000..588e43d48 --- /dev/null +++ b/formwork/fields/dynamic/vars.php @@ -0,0 +1,21 @@ + Formwork::instance(), + + 'site' => Formwork::instance()->site(), + + 'dateFormats' => [ + 'date' => DateFormats::date(), + 'hour' => DateFormats::hour(), + 'timezones' => DateFormats::timezones() + ], + + 'languages' => [ + 'names' => LanguageCodes::names() + ] +]; diff --git a/formwork/src/Exceptions/RecursionException.php b/formwork/src/Exceptions/RecursionException.php new file mode 100644 index 000000000..393400119 --- /dev/null +++ b/formwork/src/Exceptions/RecursionException.php @@ -0,0 +1,9 @@ +key = $key; + $this->uncomputedValue = $uncomputedValue; + $this->field = $field; + } + + /** + * Create an instance with an already computed value + * (used by field validation) + * + * @internal + */ + public static function withComputed(string $value, self $dynamic): self + { + $instance = clone $dynamic; + $instance->computed = true; + $instance->value = $value; + return $instance; + } + + /** + * Compute dynamic field value + */ + public function compute(): void + { + if ($this->computed) { + return; + } + + if ($this->computing) { + throw new RecursionException(sprintf('Recursion in the computation of dynamic property "%s" of field "%s". Trying to compute "%s"', $this->key, $this->field->name(), $this->uncomputedValue)); + } + + $this->computing = true; + + $this->value = Interpolator::interpolate($this->uncomputedValue, array_merge($this->defaults(), ['this' => $this->field])); + + $this->computed = true; + + $this->computing = false; + } + + /** + * Get the key associated to the dynamic value + */ + public function key() + { + return $this->key; + } + + /** + * Get the computed value + */ + public function value() + { + if (!$this->computed) { + $this->compute(); + } + + return $this->value; + } + + /** + * Return whether the dynamic value has been computed + */ + public function isComputed(): bool + { + return $this->computed; + } + + /** + * Return the field to which the dynamic value belongs + */ + public function field(): Field + { + return $this->field; + } + + /** + * Default vars used in the computation + */ + protected function defaults(): array + { + return PHP::parseFile(Formwork::instance()->config()->get('fields.dynamic.vars.file')); + } +} diff --git a/formwork/src/Fields/Field.php b/formwork/src/Fields/Field.php index f9667f01f..10d9201d7 100644 --- a/formwork/src/Fields/Field.php +++ b/formwork/src/Fields/Field.php @@ -6,6 +6,8 @@ use Formwork\Data\Traits\DataArrayable; use Formwork\Data\Traits\DataMultipleGetter; use Formwork\Data\Traits\DataMultipleSetter; +use Formwork\Exceptions\RecursionException; +use Formwork\Fields\Dynamic\DynamicFieldValue; use Formwork\Fields\Exceptions\ValidationException; use Formwork\Formwork; use Formwork\Traits\Methods; @@ -27,7 +29,7 @@ class Field implements Arrayable } use Methods; - protected const UNTRANSLATABLE_KEYS = ['name', 'type', 'value', 'default', 'translate', 'import']; + protected const UNTRANSLATABLE_KEYS = ['name', 'type', 'value', 'default', 'translate']; /** * Field name @@ -37,17 +39,22 @@ class Field implements Arrayable /** * Parent field collection */ - protected FieldCollection $parent; + protected ?FieldCollection $parent; /** * Field validation status */ protected bool $validated = false; + /** + * Whether the field is being validated + */ + protected bool $validating = false; + /** * Create a new Field instance */ - public function __construct(string $name, array $data = [], FieldCollection $parent) + public function __construct(string $name, array $data = [], ?FieldCollection $parent = null) { $this->name = $name; @@ -55,10 +62,6 @@ public function __construct(string $name, array $data = [], FieldCollection $par $this->setMultiple($data); - if ($this->has('import')) { - $this->importData(); - } - if ($this->has('fields')) { throw new UnexpectedValueException('Fields may not have other fields inside'); } @@ -86,7 +89,7 @@ public function name(): string /** * Return the parent field collection */ - public function parent(): FieldCollection + public function parent(): ?FieldCollection { return $this->parent; } @@ -184,19 +187,38 @@ public function isHidden(): bool */ public function validate(): static { + if ($this->validating) { + throw new RecursionException(sprintf('Recursion in the validation of field "%s" of type "%s"', $this->name(), $this->type())); + } + + $this->validating = true; + $value = $this->value(); + $dynamic = $value instanceof DynamicFieldValue ? $value : null; + + if ($dynamic !== null) { + $value = $value->value(); + } + if ($this->isRequired() && Constraint::isEmpty($value)) { throw new ValidationException(sprintf('Required field "%s" of type "%s" cannot be empty', $this->name(), $this->type())); } if ($this->hasMethod('validate')) { $value = $this->callMethod('validate', [$value]); + + if ($dynamic !== null) { + $value = DynamicFieldValue::withComputed($value, $dynamic); + } + $this->set('value', $value); } $this->validated = true; + $this->validating = false; + return $this; } @@ -233,6 +255,14 @@ public function get(string $key, $default = null) { $value = $this->baseGet($key, $default); + if ($value instanceof DynamicFieldValue) { + if ($key === 'value' && !$value->isComputed()) { + $this->validated = false; + } + + $value = $value->value(); + } + if ($this->isTranslatable($key)) { $value = $this->translate($value); } @@ -242,6 +272,11 @@ public function get(string $key, $default = null) public function set(string $key, $value): void { + if (Str::endsWith($key, '@')) { + $key = Str::beforeLast($key, '@'); + $value = new DynamicFieldValue($key, $value, $this); + } + if ($key === 'value') { $this->validated = false; } @@ -270,26 +305,6 @@ protected function loadMethods(): void } } - /** - * Import data helper - */ - protected function importData(): void - { - foreach ((array) $this->data['import'] as $key => $value) { - if ($key === 'import') { - throw new UnexpectedValueException('Invalid key for import'); - } - - $callback = explode('::', $value, 2); - - if (!is_callable($callback)) { - throw new UnexpectedValueException(sprintf('Invalid import callback "%s"', $value)); - } - - $this->data[$key] = $callback(); - } - } - /** * Return whether a field key is translatable */ diff --git a/formwork/src/Interpolator/Errors/SyntaxError.php b/formwork/src/Interpolator/Errors/SyntaxError.php new file mode 100644 index 000000000..d512a93db --- /dev/null +++ b/formwork/src/Interpolator/Errors/SyntaxError.php @@ -0,0 +1,9 @@ +interpolate(); + } +} diff --git a/formwork/src/Interpolator/NodeInterpolator.php b/formwork/src/Interpolator/NodeInterpolator.php new file mode 100644 index 000000000..61979b272 --- /dev/null +++ b/formwork/src/Interpolator/NodeInterpolator.php @@ -0,0 +1,244 @@ +node = $node; + $this->vars = $vars; + } + + /** + * Return the value interpolated from the node + */ + public function interpolate() + { + switch ($this->node->type()) { + case IdentifierNode::TYPE: + return $this->interpolateIdentifierNode($this->node); + + default: + throw new InterpolationException('Unexpected ' . $this->node); + } + } + + /** + * Interpolate a node + */ + protected function interpolateNode(AbstractNode $node) + { + switch ($node->type()) { + case IdentifierNode::TYPE: + return $this->interpolateIdentifierNode($node); + + case ArrayNode::TYPE: + return $this->interpolateArrayNode($node); + + default: + return $node->value(); + } + } + + /** + * Interpolate an identifier node + */ + protected function interpolateIdentifierNode(IdentifierNode $node, array|object|null $parent = null) + { + $name = $node->value(); + + $arguments = []; + + $traverse = $node->traverse(); + + if ($node->arguments() !== null) { + foreach ($node->arguments()->value() as $argument) { + $arguments[] = $this->interpolateNode($argument); + } + } + + if ($parent === null) { + if (!array_key_exists($name, $this->vars)) { + throw new InterpolationException(sprintf('Undefined variable "%s"', $name)); + } + $value = $this->vars[$name]; + } else { + if (is_array($parent)) { + if (!array_key_exists($name, $parent)) { + throw new InterpolationException(sprintf('Undefined array key "%s"', $name)); + } + $value = $parent[$name]; + } elseif (is_object($parent)) { + switch (true) { + case property_exists($parent, $name) && $node->arguments() === null: + $value = $parent->{$name}; + break; + + case is_callable([$parent, '__get']) && $node->arguments() === null: + $value = $parent->__get($name); + break; + + case method_exists($parent, $name) && $node->arguments() !== null: + $value = $parent->{$name}(...$arguments); + break; + + case is_callable([$parent, '__call']) && $node->arguments() !== null: + $value = $parent->__call($name, $arguments); + break; + + case defined($parent::class . '::' . $name) && $node->arguments() === null: + $value = constant($parent::class . '::' . $name); + break; + + default: + throw new InterpolationException(sprintf('Undefined class method, property or constant %s::%s', $parent::class, $name)); + } + } else { + throw new InvalidArgumentException(sprintf('%s() accepts only arrays and objects as $parent argument', __METHOD__)); + } + } + + if ($traverse !== null) { + if (is_scalar($value)) { + throw new InterpolationException(sprintf('Scalar value "%s" cannot be traversed like arrays or objects', $value)); + } + + if (is_resource($value)) { + throw new InterpolationException(sprintf('%s cannot be traversed like arrays or objects', $value)); + } + + switch ($traverse->type()) { + case NumberNode::TYPE: + case StringNode::TYPE: + $key = $this->validateArrayKey($traverse->value()); + + if (!is_array($value) || !array_key_exists($key, $value)) { + throw new InterpolationException(sprintf('Undefined array key "%s"', $key)); + } + + $value = $value[$key]; + break; + + case IdentifierNode::TYPE: + $value = $this->interpolateIdentifierNode($traverse, $value); + break; + + default: + throw new InterpolationException(sprintf( + 'Invalid %s of type "%s"', + is_object($value) ? 'class method, property or constant name' : 'array key' + )); + } + } + + // Call closures if arguments (zero or more) are given + if ($value instanceof Closure && $node->arguments() !== null) { + return $value(...$arguments); + } + + return $value; + } + + /** + * Interpolate an array node + */ + protected function interpolateArrayNode(ArrayNode $node): array + { + $result = []; + $keys = $this->interpolateArrayKeysNode($node->keys()); + + foreach ($node->value() as $i => $value) { + $key = $keys[$i]; + $result[$key] = $this->interpolateNode($value); + } + + return $result; + } + + /** + * Interpolate an array keys node + */ + protected function interpolateArrayKeysNode(ArrayKeysNode $node): array + { + $offset = -1; + + $result = []; + + foreach ($node->value() as $key) { + switch ($key->type()) { + case ImplicitArrayKeyNode::TYPE: + $offset++; + $result[] = $offset; + continue 2; // break out of the switch and continue the foreach loop + + case NumberNode::TYPE: + case StringNode::TYPE: + $value = $key->value(); + break; + + case IdentifierNode::TYPE: + $value = $this->interpolateIdentifierNode($key); + break; + + default: + throw new InterpolationException(sprintf('Invalid array key type "%s"', $key->type())); + } + + $value = $this->validateArrayKey($value); + + if (is_int($value)) { + $offset = $value; + } + + $result[] = $value; + } + + return $result; + } + + /** + * Validate array key + * + * @see https://www.php.net/manual/en/language.types.array.php + */ + protected function validateArrayKey($key): int|string + { + switch (true) { + case is_int($key): + return $key; + break; + + case is_bool($key): + case is_float($key): + case is_string($key) && ctype_digit($key) && $key[0] !== '0': + return (int) $key; + + case is_string($key): + return $key; + break; + + case $key === null: + return ''; + break; + + default: + throw new InterpolationException('Invalid non-scalar array key'); + } + } +} diff --git a/formwork/src/Interpolator/Nodes/AbstractNode.php b/formwork/src/Interpolator/Nodes/AbstractNode.php new file mode 100644 index 000000000..18e0b9fa5 --- /dev/null +++ b/formwork/src/Interpolator/Nodes/AbstractNode.php @@ -0,0 +1,37 @@ +value; + } +} diff --git a/formwork/src/Interpolator/Nodes/ArgumentsNode.php b/formwork/src/Interpolator/Nodes/ArgumentsNode.php new file mode 100644 index 000000000..ffd8e9965 --- /dev/null +++ b/formwork/src/Interpolator/Nodes/ArgumentsNode.php @@ -0,0 +1,16 @@ +value = $value; + } +} diff --git a/formwork/src/Interpolator/Nodes/ArrayKeysNode.php b/formwork/src/Interpolator/Nodes/ArrayKeysNode.php new file mode 100644 index 000000000..ba9a9353f --- /dev/null +++ b/formwork/src/Interpolator/Nodes/ArrayKeysNode.php @@ -0,0 +1,16 @@ +value = $value; + } +} diff --git a/formwork/src/Interpolator/Nodes/ArrayNode.php b/formwork/src/Interpolator/Nodes/ArrayNode.php new file mode 100644 index 000000000..471ebfc23 --- /dev/null +++ b/formwork/src/Interpolator/Nodes/ArrayNode.php @@ -0,0 +1,27 @@ +value = $value; + $this->keys = $keys; + } + + /** + * Get the array keys node + */ + public function keys(): ArrayKeysNode + { + return $this->keys; + } +} diff --git a/formwork/src/Interpolator/Nodes/IdentifierNode.php b/formwork/src/Interpolator/Nodes/IdentifierNode.php new file mode 100644 index 000000000..b60114f2a --- /dev/null +++ b/formwork/src/Interpolator/Nodes/IdentifierNode.php @@ -0,0 +1,44 @@ +value = $value; + $this->arguments = $arguments; + $this->traverse = $traverse; + } + + /** + * Return node arguments + */ + public function arguments(): ?ArgumentsNode + { + return $this->arguments; + } + + /** + * Return the node used to traverse + */ + public function traverse(): ?AbstractNode + { + return $this->traverse; + } +} diff --git a/formwork/src/Interpolator/Nodes/ImplicitArrayKeyNode.php b/formwork/src/Interpolator/Nodes/ImplicitArrayKeyNode.php new file mode 100644 index 000000000..bac71fd2d --- /dev/null +++ b/formwork/src/Interpolator/Nodes/ImplicitArrayKeyNode.php @@ -0,0 +1,11 @@ +value = $value; + } +} diff --git a/formwork/src/Interpolator/Nodes/StringNode.php b/formwork/src/Interpolator/Nodes/StringNode.php new file mode 100644 index 000000000..95540317a --- /dev/null +++ b/formwork/src/Interpolator/Nodes/StringNode.php @@ -0,0 +1,16 @@ +value = $value; + } +} diff --git a/formwork/src/Interpolator/Parser.php b/formwork/src/Interpolator/Parser.php new file mode 100644 index 000000000..4d8ed90cc --- /dev/null +++ b/formwork/src/Interpolator/Parser.php @@ -0,0 +1,211 @@ +stream = $stream; + } + + /** + * Parse the tokens + */ + public function parse(): AbstractNode + { + $node = $this->parseIdentifierToken(); + $this->stream->expectEnd(); + return $node; + } + + /** + * Parse a given TokenStream object + */ + public static function parseTokenStream(TokenStream $stream): AbstractNode + { + $parser = new static($stream); + return $parser->parse(); + } + + /** + * Parse an identifier token + */ + protected function parseIdentifierToken(): IdentifierNode + { + $token = $this->stream->expect(Token::TYPE_IDENTIFIER); + + $traverse = null; + + $arguments = null; + + if ($this->stream->current()->test(Token::TYPE_PUNCTUATION, '(')) { + $arguments = $this->parseArguments(); + } + + if ($this->stream->current()->test(Token::TYPE_PUNCTUATION, '.')) { + $traverse = $this->parseDotNotation(); + } + + if ($this->stream->current()->test(Token::TYPE_PUNCTUATION, '[')) { + $traverse = $this->parseBracketsNotation(); + } + + return new IdentifierNode($token->value(), $arguments, $traverse); + } + + /** + * Parse a number token + */ + protected function parseNumberToken(): NumberNode + { + $token = $this->stream->expect(Token::TYPE_NUMBER); + return new NumberNode($token->value() + 0); + } + + /** + * Parse a string token + */ + protected function parseStringToken(): StringNode + { + $token = $this->stream->expect(Token::TYPE_STRING); + return new StringNode(stripcslashes(trim($token->value(), '\'"'))); + } + + /** + * Parse dot notation + */ + protected function parseDotNotation(): IdentifierNode + { + $this->stream->expect(Token::TYPE_PUNCTUATION, '.'); + return $this->parseIdentifierToken(false); + } + + /** + * Parse brackets notation + */ + protected function parseBracketsNotation(): AbstractNode + { + $this->stream->expect(Token::TYPE_PUNCTUATION, '['); + + $token = $this->stream->current(); + + switch ($token->type()) { + case Token::TYPE_NUMBER: + $key = $this->parseNumberToken(); + break; + + case Token::TYPE_STRING: + $key = $this->parseStringToken(); + break; + + default: + throw new SyntaxError(sprintf('Unexpected %s at position %d', $token, $token->position())); + } + + $this->stream->expect(Token::TYPE_PUNCTUATION, ']'); + + return $key; + } + + /** + * Parse arguments + */ + protected function parseArguments(): ArgumentsNode + { + $this->stream->expect(Token::TYPE_PUNCTUATION, '('); + + $arguments = []; + + while (!$this->stream->current()->test(Token::TYPE_PUNCTUATION, ')')) { + if ($arguments !== []) { + $this->stream->expect(Token::TYPE_PUNCTUATION, ','); + } + $arguments[] = $this->parseExpression(); + } + + $this->stream->expect(Token::TYPE_PUNCTUATION, ')'); + + return new ArgumentsNode($arguments); + } + + /** + * Parse expression + */ + protected function parseExpression(): AbstractNode + { + $token = $this->stream->current(); + + switch ($token->type()) { + case Token::TYPE_IDENTIFIER: + return $this->parseIdentifierToken(); + + case Token::TYPE_NUMBER: + return $this->parseNumberToken(); + + case Token::TYPE_STRING: + return $this->parseStringToken(); + + case Token::TYPE_PUNCTUATION: + if ($token->value() === '[') { + return $this->parseArrayExpression(); + } + // no break for other punctuation characters + + default: + throw new SyntaxError(sprintf('Unexpected %s at position %d', $token, $token->position())); + } + } + + /** + * Parse array expression + */ + protected function parseArrayExpression(): ArrayNode + { + $this->stream->expect(Token::TYPE_PUNCTUATION, '['); + + $elements = []; + + $keys = []; + + while (!$this->stream->current()->test(Token::TYPE_PUNCTUATION, ']')) { + if ($elements !== []) { + $this->stream->expect(Token::TYPE_PUNCTUATION, ','); + } + + $value = $this->parseExpression(); + + if ($this->stream->current()->test(Token::TYPE_ARROW)) { + $arrow = $this->stream->consume(); + + if ($value->type() === ArrayNode::TYPE) { + throw new SyntaxError(sprintf('Unexpected %s at position %d', $arrow, $arrow->position())); + } + + $key = $value; + $value = $this->parseExpression(); + } else { + $key = new ImplicitArrayKeyNode(); + } + + $elements[] = $value; + $keys[] = $key; + } + + $this->stream->expect(Token::TYPE_PUNCTUATION, ']'); + + return new ArrayNode($elements, new ArrayKeysNode($keys)); + } +} diff --git a/formwork/src/Interpolator/Token.php b/formwork/src/Interpolator/Token.php new file mode 100644 index 000000000..f49860cff --- /dev/null +++ b/formwork/src/Interpolator/Token.php @@ -0,0 +1,102 @@ +type = $type; + $this->value = $value; + $this->position = $position; + } + + public function __toString() + { + return sprintf( + 'token%s of type %s', + $this->value === null ? '' : ' "' . $this->value . '"', + $this->type + ); + } + + /** + * Get token type + */ + public function type(): string + { + return $this->type; + } + + /** + * Get token value + */ + public function value(): ?string + { + return $this->value; + } + + /** + * Get token position + */ + public function position(): int + { + return $this->position; + } + + /** + * Test if token matches the given type and value + */ + public function test(string $type, ?string $value = null): bool + { + if (func_num_args() < 2) { + return $this->type === $type; + } + return $this->type === $type && $this->value === $value; + } +} diff --git a/formwork/src/Interpolator/TokenStream.php b/formwork/src/Interpolator/TokenStream.php new file mode 100644 index 000000000..f541f82b5 --- /dev/null +++ b/formwork/src/Interpolator/TokenStream.php @@ -0,0 +1,73 @@ +tokens = $tokens; + $this->count = count($tokens); + } + + /** + * Get the current token + */ + public function current(): Token + { + return $this->tokens[$this->pointer]; + } + + /** + * Get the current token and advance the pointer + */ + public function consume(): Token + { + if (!isset($this->tokens[$this->pointer + 1])) { + throw new SyntaxError(sprintf('Unexpected end at position %d', $this->pointer + 1)); + } + return $this->tokens[$this->pointer++]; + } + + /** + * Consume the current token only if it matches the given type or value. Throw a SyntaxError otherwise + */ + public function expect(string $type, ?string $value = null): Token + { + $token = $this->current(); + $test = func_num_args() < 2 ? $token->test($type) : $token->test($type, $value); + if (!$test) { + $expectedToken = new Token($type, $value, $token->position()); + throw new SyntaxError(sprintf('Unexpected %s, expected %s at position %d', $token, $expectedToken, $token->position())); + } + return $this->consume(); + } + + /** + * Expect the end of stream. Throw a SyntaxError otherwise + */ + public function expectEnd(): void + { + $token = $this->current(); + if (!$token->test(Token::TYPE_END)) { + throw new SyntaxError(sprintf('Unexpected %s, expected end at position %d', $token, $token->position())); + } + } +} diff --git a/formwork/src/Interpolator/Tokenizer.php b/formwork/src/Interpolator/Tokenizer.php new file mode 100644 index 000000000..a160a78bf --- /dev/null +++ b/formwork/src/Interpolator/Tokenizer.php @@ -0,0 +1,120 @@ +'; + + /** + * Tokenizer input string + */ + protected string $input; + + /** + * Tokenizer input length + */ + protected int $length = 0; + + /** + * Current position within the input string + */ + protected int $position = 0; + + public function __construct(string $input) + { + $this->input = $input; + $this->length = strlen($input); + } + + /** + * Tokenize input + */ + public function tokenize(): TokenStream + { + $tokens = []; + + while ($this->position < $this->length) { + switch (true) { + case preg_match(self::IDENTIFIER_REGEX, $this->input, $matches, 0, $this->position): + $value = array_shift($matches); + $tokens[] = new Token(Token::TYPE_IDENTIFIER, $value, $this->position); + $this->position += strlen($value); + break; + + case preg_match(self::NUMBER_REGEX, $this->input, $matches, 0, $this->position): + $value = array_shift($matches); + $tokens[] = new Token(Token::TYPE_NUMBER, $value, $this->position); + $this->position += strlen($value); + break; + + case preg_match(self::SINGLE_QUOTE_STRING_REGEX, $this->input, $matches, 0, $this->position): + case preg_match(self::DOUBLE_QUOTE_STRING_REGEX, $this->input, $matches, 0, $this->position): + $value = array_shift($matches); + $tokens[] = new Token(Token::TYPE_STRING, $value, $this->position); + $this->position += strlen($value); + break; + + case substr($this->input, $this->position, strlen(self::ARROW_SEQUENCE)) === self::ARROW_SEQUENCE: + $tokens[] = new Token(Token::TYPE_ARROW, self::ARROW_SEQUENCE, $this->position); + $this->position += strlen(self::ARROW_SEQUENCE); + break; + + case strpos(self::PUNCTUATION_CHARACTERS, $this->input[$this->position]) !== false: + $tokens[] = new Token(Token::TYPE_PUNCTUATION, $this->input[$this->position], $this->position); + $this->position++; + break; + + case ctype_space($this->input[$this->position]): + $this->position++; + break; + + default: + throw new SyntaxError(sprintf('Unexpected character "%s" at position %d', $this->input[$this->position], $this->position)); + } + } + + $tokens[] = new Token(Token::TYPE_END, null, $this->position); + + return new TokenStream($tokens); + } + + /** + * Tokenize a string + */ + public static function tokenizeString(string $string): TokenStream + { + $tokenizer = new static($string); + return $tokenizer->tokenize(); + } +} diff --git a/formwork/src/Panel/Users/UserCollection.php b/formwork/src/Panel/Users/UserCollection.php index 618539978..df23d28c2 100644 --- a/formwork/src/Panel/Users/UserCollection.php +++ b/formwork/src/Panel/Users/UserCollection.php @@ -35,7 +35,7 @@ public static function load(): self /** * Get all available roles */ - public static function availableRoles(): array + public function availableRoles(): array { $roles = []; foreach (static::$roles as $role => $data) { diff --git a/panel/schemes/user.yml b/panel/schemes/user.yml index 1b638b6d2..ce8d7b2f9 100644 --- a/panel/schemes/user.yml +++ b/panel/schemes/user.yml @@ -32,15 +32,13 @@ fields: label: '{{panel.user.language}}' required: true translate: [label] - import: - options: 'Formwork\Panel\Panel::availableTranslations' + options@: formwork.panel().availableTranslations() role: type: select label: '{{panel.user.role}}' disabled: true - import: - options: 'Formwork\Panel\Users\UserCollection::availableRoles' + options@: formwork.panel().users().availableRoles() colorScheme: diff --git a/site/config/schemes/system.yml b/site/config/schemes/system.yml index d28c3a83e..1bec54058 100644 --- a/site/config/schemes/system.yml +++ b/site/config/schemes/system.yml @@ -42,20 +42,17 @@ fields: date.format: type: select label: '{{panel.options.system.dateAndTime.dateFormat}}' - import: - options: 'Formwork\Panel\Utils\DateFormats::date' + options@: dateFormats.date date.timeFormat: type: select label: '{{panel.options.system.dateAndTime.hourFormat}}' - import: - options: 'Formwork\Panel\Utils\DateFormats::hour' + options@: dateFormats.hour date.timezone: type: select label: '{{panel.options.system.dateAndTime.timezone}}' - import: - options: 'Formwork\Panel\Utils\DateFormats::timezones' + options@: dateFormats.timezones date.weekStarts: type: select @@ -70,8 +67,7 @@ fields: placeholder: '{{panel.options.system.languages.availableLanguages.noLanguages}}' pattern: '^[a-z]{2,3}$' translate: [label, placeholder] - import: - options: 'Formwork\Languages\LanguageCodes::names' + options@: languages.names languages.httpPreferred: type: togglegroup @@ -106,8 +102,7 @@ fields: type: select label: '{{panel.options.system.adminPanel.defaultLanguage}}' translate: [label] - import: - options: 'Formwork\Panel\Panel::availableTranslations' + options@: formwork.panel().availableTranslations() panel.logoutRedirect: type: togglegroup