summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorpharixces <pharixces@gmail.com>2025-10-03 23:17:55 +0200
committerGitHub <noreply@github.com>2025-10-03 23:17:55 +0200
commitb390e509747c42e940008b042fb0d3137dbbf5df (patch)
treedb1f36591f2ebdc3db94d3bf85602bb223de447d
parent0e46ae3add2c6213612951464b4edc44828fbc01 (diff)
downloadsmarty-b390e509747c42e940008b042fb0d3137dbbf5df.tar.gz
smarty-b390e509747c42e940008b042fb0d3137dbbf5df.tar.bz2
smarty-b390e509747c42e940008b042fb0d3137dbbf5df.zip
Add support for shorttags in functions (#1142)
* Add support for shorttags in functions Co-authored-by: Anne Zijlstra <a.zijlstra@iwink.nl> Co-authored-by: Simon Wisselink <s.wisselink@iwink.nl>
-rw-r--r--src/Compile/AttributeCompiler.php144
-rw-r--r--src/Compile/Base.php84
-rw-r--r--src/Compile/FunctionCallCompiler.php30
-rw-r--r--src/FunctionHandler/AttributeBase.php77
-rw-r--r--src/FunctionHandler/AttributeFunctionHandlerInterface.php15
-rw-r--r--tests/UnitTests/Compile/AttributeCompilerTest.php193
-rw-r--r--tests/UnitTests/Compile/FunctionCallCompilerTest.php62
7 files changed, 514 insertions, 91 deletions
diff --git a/src/Compile/AttributeCompiler.php b/src/Compile/AttributeCompiler.php
new file mode 100644
index 00000000..077c4cfc
--- /dev/null
+++ b/src/Compile/AttributeCompiler.php
@@ -0,0 +1,144 @@
+<?php
+
+namespace Smarty\Compile;
+
+/**
+ * This class handles compiling the attributes.
+ */
+class AttributeCompiler
+{
+ /**
+ * Array of names of required attributes required by tag
+ *
+ * @var array
+ */
+ protected $required_attributes = [];
+
+ /**
+ * Array of names of optional attribute required by tag
+ * use array('_any') if there is no restriction of attributes names
+ *
+ * @var array
+ */
+ protected $optional_attributes = [];
+
+ /**
+ * Shorttag attribute order defined by its names
+ *
+ * @var array
+ */
+ protected $shorttag_order = [];
+
+ /**
+ * Array of names of valid option flags
+ *
+ * @var array
+ */
+ protected $option_flags = [];
+
+ public function __construct(
+ array $required_attributes = [],
+ array $optional_attributes = [],
+ array $shorttag_order = [],
+ array $option_flags = []
+ ) {
+ $this->required_attributes = $required_attributes;
+ $this->optional_attributes = $optional_attributes;
+ $this->shorttag_order = $shorttag_order;
+ $this->option_flags = $option_flags;
+ }
+
+ /**
+ * This function checks if the attributes passed are valid
+ * The attributes passed for the tag to compile are checked against the list of required and
+ * optional attributes. Required attributes must be present. Optional attributes are check against
+ * the corresponding list. The keyword '_any' specifies that any attribute will be accepted
+ * as valid
+ *
+ * @param object $compiler compiler object
+ * @param array $attributes attributes applied to the tag
+ *
+ * @return array of mapped attributes for further processing
+ */
+ public function getAttributes($compiler, $attributes)
+ {
+ $_indexed_attr = [];
+ $options = array_fill_keys($this->option_flags, true);
+ foreach ($attributes as $key => $mixed) {
+ // shorthand ?
+ if (!is_array($mixed)) {
+ // options flag ?
+ if (isset($options[trim($mixed, '\'"')])) {
+ $_indexed_attr[trim($mixed, '\'"')] = true;
+ // shorthand attribute ?
+ } elseif (isset($this->shorttag_order[$key])) {
+ $_indexed_attr[$this->shorttag_order[$key]] = $mixed;
+ } else {
+ // too many shorthands
+ $compiler->trigger_template_error('too many shorthand attributes', null, true);
+ }
+ // named attribute
+ } else {
+ foreach ($mixed as $k => $v) {
+ // options flag?
+ if (isset($options[$k])) {
+ if (is_bool($v)) {
+ $_indexed_attr[$k] = $v;
+ } else {
+ if (is_string($v)) {
+ $v = trim($v, '\'" ');
+ }
+
+ // Mapping array for boolean option value
+ static $optionMap = [1 => true, 0 => false, 'true' => true, 'false' => false];
+
+ if (isset($optionMap[$v])) {
+ $_indexed_attr[$k] = $optionMap[$v];
+ } else {
+ $compiler->trigger_template_error(
+ "illegal value '" . var_export($v, true) .
+ "' for options flag '{$k}'",
+ null,
+ true
+ );
+ }
+ }
+ // must be named attribute
+ } else {
+ $_indexed_attr[$k] = $v;
+ }
+ }
+ }
+ }
+ // check if all required attributes present
+ foreach ($this->required_attributes as $attr) {
+ if (!isset($_indexed_attr[$attr])) {
+ $compiler->trigger_template_error("missing '{$attr}' attribute", null, true);
+ }
+ }
+ // check for not allowed attributes
+ if ($this->optional_attributes !== ['_any']) {
+ $allowedAttributes = array_fill_keys(
+ array_merge(
+ $this->required_attributes,
+ $this->optional_attributes,
+ $this->option_flags
+ ),
+ true
+ );
+ foreach ($_indexed_attr as $key => $dummy) {
+ if (!isset($allowedAttributes[$key]) && $key !== 0) {
+ $compiler->trigger_template_error("unexpected '{$key}' attribute", null, true);
+ }
+ }
+ }
+ // default 'false' for all options flags not set
+ foreach ($this->option_flags as $flag) {
+ if (!isset($_indexed_attr[$flag])) {
+ $_indexed_attr[$flag] = false;
+ }
+ }
+
+ return $_indexed_attr;
+ }
+}
diff --git a/src/Compile/Base.php b/src/Compile/Base.php
index 2d5c0c0e..10153501 100644
--- a/src/Compile/Base.php
+++ b/src/Compile/Base.php
@@ -82,84 +82,12 @@ abstract class Base implements CompilerInterface {
* @return array of mapped attributes for further processing
*/
protected function getAttributes($compiler, $attributes) {
- $_indexed_attr = [];
- $options = array_fill_keys($this->option_flags, true);
- foreach ($attributes as $key => $mixed) {
- // shorthand ?
- if (!is_array($mixed)) {
- // options flag ?
- if (isset($options[trim($mixed, '\'"')])) {
- $_indexed_attr[trim($mixed, '\'"')] = true;
- // shorthand attribute ?
- } elseif (isset($this->shorttag_order[$key])) {
- $_indexed_attr[$this->shorttag_order[$key]] = $mixed;
- } else {
- // too many shorthands
- $compiler->trigger_template_error('too many shorthand attributes', null, true);
- }
- // named attribute
- } else {
- foreach ($mixed as $k => $v) {
- // options flag?
- if (isset($options[$k])) {
- if (is_bool($v)) {
- $_indexed_attr[$k] = $v;
- } else {
- if (is_string($v)) {
- $v = trim($v, '\'" ');
- }
-
- // Mapping array for boolean option value
- static $optionMap = [1 => true, 0 => false, 'true' => true, 'false' => false];
-
- if (isset($optionMap[$v])) {
- $_indexed_attr[$k] = $optionMap[$v];
- } else {
- $compiler->trigger_template_error(
- "illegal value '" . var_export($v, true) .
- "' for options flag '{$k}'",
- null,
- true
- );
- }
- }
- // must be named attribute
- } else {
- $_indexed_attr[$k] = $v;
- }
- }
- }
- }
- // check if all required attributes present
- foreach ($this->required_attributes as $attr) {
- if (!isset($_indexed_attr[$attr])) {
- $compiler->trigger_template_error("missing '{$attr}' attribute", null, true);
- }
- }
- // check for not allowed attributes
- if ($this->optional_attributes !== ['_any']) {
- $allowedAttributes = array_fill_keys(
- array_merge(
- $this->required_attributes,
- $this->optional_attributes,
- $this->option_flags
- ),
- true
- );
- foreach ($_indexed_attr as $key => $dummy) {
- if (!isset($allowedAttributes[$key]) && $key !== 0) {
- $compiler->trigger_template_error("unexpected '{$key}' attribute", null, true);
- }
- }
- }
- // default 'false' for all options flags not set
- foreach ($this->option_flags as $flag) {
- if (!isset($_indexed_attr[$flag])) {
- $_indexed_attr[$flag] = false;
- }
- }
-
- return $_indexed_attr;
+ return (new AttributeCompiler(
+ $this->required_attributes,
+ $this->optional_attributes,
+ $this->shorttag_order,
+ $this->option_flags
+ ))->getAttributes($compiler, $attributes);
}
/**
diff --git a/src/Compile/FunctionCallCompiler.php b/src/Compile/FunctionCallCompiler.php
index 107dd98b..3ce50da2 100644
--- a/src/Compile/FunctionCallCompiler.php
+++ b/src/Compile/FunctionCallCompiler.php
@@ -3,24 +3,18 @@
* Smarty Internal Plugin Compile Registered Function
* Compiles code for the execution of a registered function
*
-
-
* @author Uwe Tews
*/
namespace Smarty\Compile;
use Smarty\Compiler\Template;
-use Smarty\CompilerException;
+use Smarty\FunctionHandler\AttributeFunctionHandlerInterface;
/**
* Smarty Internal Plugin Compile Registered Function Class
- *
-
-
*/
class FunctionCallCompiler extends Base {
-
/**
* Attribute definition: Overwrites base class.
*
@@ -51,16 +45,26 @@ class FunctionCallCompiler extends Base {
*/
public function compile($args, Template $compiler, $parameter = [], $tag = null, $function = null): string
{
+ if ($functionHandler = $compiler->getSmarty()->getFunctionHandler($function)) {
- // check and get attributes
- $_attr = $this->getAttributes($compiler, $args);
- unset($_attr['nocache']);
+ $attribute_overrides = [];
- $_paramsArray = $this->formatParamsArray($_attr);
- $_params = 'array(' . implode(',', $_paramsArray) . ')';
+ if ($functionHandler instanceof AttributeFunctionHandlerInterface) {
+ $attribute_overrides = $functionHandler->getSupportedAttributes();
+ }
+ // check and get attributes
+ $_attr = (new AttributeCompiler(
+ $attribute_overrides['required_attributes'] ?? $this->required_attributes,
+ $attribute_overrides['optional_attributes'] ?? $this->optional_attributes,
+ $attribute_overrides['shorttag_order'] ?? $this->shorttag_order,
+ $attribute_overrides['option_flags'] ?? $this->option_flags
+ ))->getAttributes($compiler, $args);
- if ($functionHandler = $compiler->getSmarty()->getFunctionHandler($function)) {
+ unset($_attr['nocache']);
+
+ $_paramsArray = $this->formatParamsArray($_attr);
+ $_params = 'array(' . implode(',', $_paramsArray) . ')';
// not cacheable?
$compiler->tag_nocache = $compiler->tag_nocache || !$functionHandler->isCacheable();
diff --git a/src/FunctionHandler/AttributeBase.php b/src/FunctionHandler/AttributeBase.php
new file mode 100644
index 00000000..71a035dc
--- /dev/null
+++ b/src/FunctionHandler/AttributeBase.php
@@ -0,0 +1,77 @@
+<?php
+
+namespace Smarty\FunctionHandler;
+
+use Smarty\Template;
+
+/**
+ * Abstract implementation for function handlers which support custom attributes
+ */
+abstract class AttributeBase implements AttributeFunctionHandlerInterface
+{
+ /**
+ * Array of names of required attribute required by tag
+ *
+ * @var array
+ */
+ protected array $required_attributes = [];
+
+ /**
+ * Array of names of optional attribute required by tag
+ * use array('_any') if there is no restriction of attributes names
+ *
+ * @var array
+ */
+ protected array $optional_attributes = [];
+
+ /**
+ * Shorttag attribute order defined by its names
+ *
+ * @var array
+ */
+ protected array $shorttag_order = [];
+
+ /**
+ * Array of names of valid option flags
+ *
+ * @var array
+ */
+ protected array $option_flags = [];
+
+ /**
+ * Return whether the output is cacheable.
+ * @var bool
+ */
+ protected bool $cacheable = true;
+
+ /**
+ * Return whether the output is cacheable.
+ * @return bool
+ */
+ public function isCacheable(): bool
+ {
+ return $this->cacheable;
+ }
+
+ /**
+ * Function body
+ * @param mixed $params The supplied parameters.
+ * @param Smarty\Template $template
+ * @return mixed
+ */
+ abstract public function handle($params, Template $template): ?string;
+
+ /**
+ * Return the support attributes for this function.
+ * @return array<string, array>
+ */
+ public function getSupportedAttributes(): array
+ {
+ return [
+ 'required_attributes' => $this->required_attributes,
+ 'optional_attributes' => $this->optional_attributes,
+ 'shorttag_order' => $this->shorttag_order,
+ 'option_flags' => $this->option_flags,
+ ];
+ }
+}
diff --git a/src/FunctionHandler/AttributeFunctionHandlerInterface.php b/src/FunctionHandler/AttributeFunctionHandlerInterface.php
new file mode 100644
index 00000000..f76259ec
--- /dev/null
+++ b/src/FunctionHandler/AttributeFunctionHandlerInterface.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Smarty\FunctionHandler;
+
+/**
+ * Function handler interface with support for specifying supported properties
+ */
+interface AttributeFunctionHandlerInterface extends FunctionHandlerInterface
+{
+ /**
+ * Returns an array with the supported attributes, flags, and shorttags
+ * @return array<string, array>
+ */
+ public function getSupportedAttributes(): array;
+}
diff --git a/tests/UnitTests/Compile/AttributeCompilerTest.php b/tests/UnitTests/Compile/AttributeCompilerTest.php
new file mode 100644
index 00000000..9a8fbf98
--- /dev/null
+++ b/tests/UnitTests/Compile/AttributeCompilerTest.php
@@ -0,0 +1,193 @@
+<?php
+
+use Smarty\Compile\AttributeCompiler;
+use Smarty\Compiler\Template;
+
+class AttributeCompilerTest extends PHPUnit\Framework\TestCase
+{
+ /**
+ * The template compiler.
+ */
+ private $template_compiler;
+
+ /**
+ * The attributes
+ */
+ private $attributes = [];
+
+ /**
+ * @inheritDoc
+ * Set up attribute compiler class
+ */
+ protected function setUp(): void
+ {
+ $this->template_compiler = $this->createMock(Template::class);
+
+ // reset attributes to empty arrays
+ $this->attributes = [
+ 'required_attributes' => [],
+ 'optional_attributes' => [],
+ 'shorttag_order' => [],
+ 'option_flags' => [],
+ ];
+ }
+
+ /**
+ * Create the attribute compiler for testing.
+ */
+ private function createAttributeCompiler() {
+ return new AttributeCompiler(
+ $this->attributes['required_attributes'],
+ $this->attributes['optional_attributes'],
+ $this->attributes['shorttag_order'],
+ $this->attributes['option_flags']
+ );
+ }
+
+ /**
+ * Tests shorthand attribute compiling.
+ */
+ public function testAttributeCompiler(): void
+ {
+ $this->attributes['shorttag_order'] = ['shorttag'];
+ $this->attributes['required_attributes'] = ['required'];
+ $this->attributes['option_flags'] = ['option', 'option_two'];
+
+ $payload = [
+ 0 => 'shorttag value',
+ 1 => [
+ 'required' => 'required_value'
+ ],
+ 2 => 'option',
+ ];
+
+ $this->assertEquals(
+ $this->createAttributeCompiler()
+ ->getAttributes($this->template_compiler, $payload),
+ [
+ 'shorttag' => 'shorttag value',
+ 'required' => 'required_value',
+ 'option' => true,
+ 'option_two' => false,
+ ]
+ );
+ }
+
+ /**
+ * Tests normal optional attribute compiling.
+ */
+ public function testAttributeCompilerOptionalArguments(): void
+ {
+ $this->attributes['optional_attributes'] = ['optional'];
+
+ $payload = [
+ 0 => [
+ 'optional' => 'optional value'
+ ],
+ ];
+
+ $this->assertEquals(
+ $this->createAttributeCompiler()
+ ->getAttributes($this->template_compiler, $payload),
+ [
+ 'optional' => 'optional value',
+ ]
+ );
+
+ $this->assertEquals(
+ $this->createAttributeCompiler()
+ ->getAttributes($this->template_compiler, []),
+ []
+ );
+ }
+
+ /**
+ * Tests any attribute compiling.
+ */
+ public function testAttributeCompilerAnyOptionalArguments(): void
+ {
+ $this->attributes['optional_attributes'] = ['_any'];
+
+ $payload = [
+ 0 => [
+ 'optional' => 'optional value'
+ ],
+ 1 => [
+ 'optional_two' => 'optional value two'
+ ],
+ ];
+
+ $this->assertEquals(
+ $this->createAttributeCompiler()
+ ->getAttributes($this->template_compiler, $payload),
+ [
+ 'optional' => 'optional value',
+ 'optional_two' => 'optional value two',
+ ]
+ );
+ }
+
+ /**
+ * Test if the attribute compiler tries to throw a too many shorthand attributes error.
+ */
+ public function testAttributeCompilerTooManyShorthands(): void
+ {
+ $payload = [
+ 0 => 'option one',
+ ];
+
+ $this->template_compiler
+ ->expects(self::once())
+ ->method('trigger_template_error')
+ ->with('too many shorthand attributes', null, true);
+
+ $this->createAttributeCompiler()
+ ->getAttributes($this->template_compiler, $payload);
+ }
+
+ /**
+ * Test if the attribute compiler tries to throw a missing required attribute error.
+ */
+ public function testAttributeCompilerWithMissingRequiredAttributes(): void
+ {
+ $this->attributes['required_attributes'] = ['required'];
+
+ $this->template_compiler
+ ->expects(self::once())
+ ->method('trigger_template_error')
+ ->with('missing \'required\' attribute', null, true);
+
+ $this->createAttributeCompiler()
+ ->getAttributes($this->template_compiler, []);
+ }
+
+ /**
+ * Test if the attribute compiler tries to throw a illegal value template error.
+ */
+ public function testAttributeCompilerWithInvalidOptionAttribute(): void
+ {
+ $this->attributes['option_flags'] = ['option'];
+
+ $this->template_compiler
+ ->expects(self::once())
+ ->method('trigger_template_error')
+ ->with('illegal value \'\'foo\'\' for options flag \'option\'', null, true);
+
+ $this->createAttributeCompiler()
+ ->getAttributes($this->template_compiler, [0 => ['option' => 'foo']]);
+ }
+
+ /**
+ * Test if the attribute compiler tries to throw an unexpected attribute error.
+ */
+ public function testAttributeCompilerWithInvalidUnexpectedAttribute(): void
+ {
+ $this->template_compiler
+ ->expects(self::once())
+ ->method('trigger_template_error')
+ ->with('unexpected \'unexpected\' attribute', null, true);
+
+ $this->createAttributeCompiler()
+ ->getAttributes($this->template_compiler, [0 => ['unexpected' => 'bar']]);
+ }
+}
diff --git a/tests/UnitTests/Compile/FunctionCallCompilerTest.php b/tests/UnitTests/Compile/FunctionCallCompilerTest.php
new file mode 100644
index 00000000..2eb0e5a1
--- /dev/null
+++ b/tests/UnitTests/Compile/FunctionCallCompilerTest.php
@@ -0,0 +1,62 @@
+<?php
+
+use Smarty\Compile\FunctionCallCompiler;
+use Smarty\Compiler\Template;
+use Smarty\FunctionHandler\AttributeFunctionHandlerInterface;
+use Smarty\Smarty;
+
+class FunctionCallCompilerTest extends PHPUnit\Framework\TestCase
+{
+ /**
+ * @inheritDoc
+ * Set up attribute compiler class
+ */
+ protected function setUp(): void
+ {
+ $this->smarty = $this->createMock(Smarty::class);
+ $this->template_compiler = $this->createMock(Template::class);
+ $this->template_compiler
+ ->expects(self::once())
+ ->method('getSmarty')
+ ->willReturn($this->smarty);
+ }
+
+ public function testAttributeFunctionHandlerInterface(): void
+ {
+ $attribute_function_handler = $this->createMock(AttributeFunctionHandlerInterface::class);
+
+ $attribute_function_handler
+ ->expects(self::once())
+ ->method('getSupportedAttributes')
+ ->willReturn([
+ 'required_attributes' => ['required'],
+ 'optional_attributes' => ['optional'],
+ 'shorttag_order' => ['short'],
+ 'option_flags' => ['option'],
+ ]);
+
+ $args = [
+ 0 => 'short',
+ 1 => 'option',
+ 2 => [
+ 'optional' => 'optional',
+ ],
+ 3 => [
+ 'required' => 'required',
+ ],
+ ];
+
+ $this->smarty
+ ->expects(self::once())
+ ->method('getFunctionHandler')
+ ->with('method')
+ ->willReturn($attribute_function_handler);
+
+ $function_call_compiler = new FunctionCallCompiler();
+
+ $this->assertEquals(
+ $function_call_compiler->compile($args, $this->template_compiler, [], null, 'method'),
+ '$_smarty_tpl->getSmarty()->getFunctionHandler(\'method\')->handle(array(\'short\'=>short,\'option\'=>1,\'optional\'=>optional,\'required\'=>required), $_smarty_tpl)'
+ );
+ }
+}