Compare commits

...
Author SHA1 Message Date
Simon WisselinkandGitHub 139797a165 Support for Laravel Collections style object chaining (#1168)
* Support for Laravel Collections style object chaining for objects return from function calls implemented as modifiers
Fixes #1151

* explain publishing docs
2026-02-11 00:02:52 +01:00
hirosanandGitHub 6709d000cd Fix static analysis warnings for isDot() and remove deprecated APC support (#1164)
* Fix static analysis warnings for isDot()
* Remove deprecated APC support
* Remove redundant isDot() check and fix static analysis warnings
2026-01-08 11:21:57 +01:00
Simon WisselinkandGitHub aa6edc3c0b Document missing inline implementation. Fixed #1152 (#1156) 2025-12-21 22:58:11 +01:00
Simon Wisselink 73da7e90f3 Merge branch 'release/5.7.0' 2025-11-19 22:36:38 +01:00
Simon Wisselink c0d7a36124 version bump 2025-11-19 22:36:36 +01:00
Simon WisselinkandGitHub 8cc9a75964 Php8.5 support (#1138)
* PHP 8.5 support (using RC docker image for php 8.5 unit tests)
2025-11-19 22:33:49 +01:00
Wim WisselinkandGitHub 21d7fbb67e Non-canonical cast (boolean) fix (#1145) 2025-10-17 14:45:38 +02:00
Simon Wisselink 1fc41e385d Merge branch 'release/5.6.0' 2025-10-03 23:22:25 +02:00
Simon Wisselink fe325daec0 version bump 2025-10-03 23:22:23 +02:00
Simon WisselinkandGitHub 3f0f308a7b added changelog 2025-10-03 23:19:22 +02:00
b390e50974 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>
2025-10-03 23:17:55 +02:00
28 changed files with 1527 additions and 997 deletions
+1
View File
@@ -34,6 +34,7 @@ jobs:
- "8.2"
- "8.3"
- "8.4"
- "8.5"
compiler:
- default
+8
View File
@@ -6,6 +6,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [5.7.0] - 2025-11-19
- PHP 8.5 support
## [5.6.0] - 2025-10-03
- Added support for shorttags in functions [#1005](https://github.com/smarty-php/smarty/issues/1005)
## [5.5.2] - 2025-08-26
+2
View File
@@ -115,5 +115,7 @@ If you are a maintainer, you can publish the document using [mike](https://githu
mike deploy 5.x
```
Then, push the `gh-pages` branch.
## Attribution
This guide is based on the **contributing.md**. [Make your own](https://contributing.md/)!
+1 -1
View File
@@ -7,7 +7,7 @@ Smarty is a template engine for PHP, facilitating the separation of presentation
Read the [documentation](https://smarty-php.github.io/smarty/) to find out how to use it.
## Requirements
Smarty v5 can be run with PHP 7.2 to PHP 8.4.
Smarty v5 can be run with PHP 7.2 to PHP 8.5.
## Installation
Smarty versions 3.1.11 or later can be installed with [Composer](https://getcomposer.org/).
+1
View File
@@ -0,0 +1 @@
- Support for Laravel Collections style object chaining for objects return from function calls implemented as modifiers [#1151](https://github.com/smarty-php/smarty/issues/1151)
+1
View File
@@ -0,0 +1 @@
- Update documentation to clarify that include inline is currently not implemented in Smarty v5 [#1152](https://github.com/smarty-php/smarty/issues/1152)
@@ -47,11 +47,13 @@ available within the included template.
## Option Flags
| Name | Description |
|---------|--------------------------------------------------------------------------------------|
| nocache | Disables caching of this subtemplate |
| caching | Enable caching of this subtemplate |
| inline | If set, merge the compile-code of the subtemplate into the compiled calling template |
| Name | Description |
|-----------|--------------------------------------------------------------------------------------|
| nocache | Disables caching of this subtemplate |
| caching | Enable caching of this subtemplate |
| inline \* | If set, merge the compile-code of the subtemplate into the compiled calling template |
\* The `inline` option flag is currently not implemented in Smarty v5. Using it will not trigger an error, however.
## Examples
```smarty
+1 -1
View File
@@ -1,7 +1,7 @@
# Getting started
## Requirements
Smarty can be run with PHP 7.2 to PHP 8.4.
Smarty can be run with PHP 7.2 to PHP 8.5.
## Installation
Smarty can be installed with [Composer](https://getcomposer.org/).
+2 -8
View File
@@ -127,8 +127,6 @@ class File extends Base
&& (!function_exists('ini_get') || strlen(ini_get('opcache.restrict_api'))) < 1
) {
opcache_invalidate($_template->getCached()->filepath, true);
} elseif (function_exists('apc_compile_file')) {
apc_compile_file($_template->getCached()->filepath);
}
$cached = $_template->getCached();
$cached->timestamp = $cached->exists = is_file($cached->filepath);
@@ -223,10 +221,8 @@ class File extends Base
$_filepath = (string)$_file;
// directory ?
if ($_file->isDir()) {
if (!$_cache->isDot()) {
// delete folder if empty
@rmdir($_file->getPathname());
}
// delete folder if empty
@rmdir($_file->getPathname());
} else {
// delete only php files
if (substr($_filepath, -4) !== '.php') {
@@ -279,8 +275,6 @@ class File extends Base
&& (!function_exists('ini_get') || strlen(ini_get("opcache.restrict_api")) < 1)
) {
opcache_invalidate($_filepath, true);
} elseif (function_exists('apc_delete_file')) {
apc_delete_file($_filepath);
}
}
}
+144
View File
@@ -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;
}
}
+6 -78
View File
@@ -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);
}
/**
+20 -16
View File
@@ -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,17 +45,27 @@ class FunctionCallCompiler extends Base {
*/
public function compile($args, Template $compiler, $parameter = [], $tag = null, $function = null): string
{
// check and get attributes
$_attr = $this->getAttributes($compiler, $args);
unset($_attr['nocache']);
$_paramsArray = $this->formatParamsArray($_attr);
$_params = 'array(' . implode(',', $_paramsArray) . ')';
if ($functionHandler = $compiler->getSmarty()->getFunctionHandler($function)) {
$attribute_overrides = [];
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);
unset($_attr['nocache']);
$_paramsArray = $this->formatParamsArray($_attr);
$_params = 'array(' . implode(',', $_paramsArray) . ')';
// not cacheable?
$compiler->tag_nocache = $compiler->tag_nocache || !$functionHandler->isCacheable();
$output = "\$_smarty_tpl->getSmarty()->getFunctionHandler(" . var_export($function, true) . ")";
+77
View File
@@ -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,
];
}
}
@@ -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;
}
File diff suppressed because it is too large Load Diff
+16 -6
View File
@@ -1062,12 +1062,22 @@ object(res) ::= varindexed(vi) objectchain(oc). {
}
}
// optional objectchain - empty
optobjectchain(res) ::= . {
res = '';
}
// optional objectchain - present
optobjectchain(res) ::= objectchain(oc). {
res = oc;
}
// single element
objectchain(res) ::= objectelement(oe). {
res = oe;
}
// chain of elements
// chain of elements
objectchain(res) ::= objectchain(oc) objectelement(oe). {
res = oc.oe;
}
@@ -1111,7 +1121,7 @@ objectelement(res)::= PTR method(f). {
//
// function
//
function(res) ::= ns1(f) OPENP variablelist(v) CLOSEP. {
function(res) ::= ns1(f) OPENP variablelist(v) CLOSEP optobjectchain(oc). {
if (f == 'isset') {
res = '(true';
@@ -1125,15 +1135,15 @@ function(res) ::= ns1(f) OPENP variablelist(v) CLOSEP. {
res .= ' && (' . $value . ' !== null)';
}
}
res .= ')';
res .= ')' . oc;
} elseif (f == 'empty') {
if (count(v) != 1) {
throw new CompilerException("Invalid number of arguments for empty. empty expects at exactly one parameter.");
}
if (is_array(v[0])) {
res .= '( !' . v[0][0] . ' || empty(' . v[0][1] . '))';
res = '( !' . v[0][0] . ' || empty(' . v[0][1] . '))' . oc;
} else {
res = 'false == ' . v[0];
res = 'false == ' . v[0] . oc;
}
} else {
$p = array();
@@ -1144,7 +1154,7 @@ function(res) ::= ns1(f) OPENP variablelist(v) CLOSEP. {
$p[] = $value;
}
}
res = $this->compiler->compileModifierInExpression(f, $p);
res = $this->compiler->compileModifierInExpression(f, $p) . oc;
}
}
+3 -7
View File
@@ -54,7 +54,7 @@ class Smarty extends \Smarty\TemplateBase {
/**
* smarty version
*/
const SMARTY_VERSION = '5.5.2';
const SMARTY_VERSION = '5.7.0';
/**
* define caching modes
@@ -1344,10 +1344,8 @@ class Smarty extends \Smarty\TemplateBase {
}
$_filepath = (string)$_file;
if ($_file->isDir()) {
if (!$_compile->isDot()) {
// delete folder if empty
@rmdir($_file->getPathname());
}
// delete folder if empty
@rmdir($_file->getPathname());
} else {
// delete only php files
if (substr($_filepath, -4) !== '.php') {
@@ -1385,8 +1383,6 @@ class Smarty extends \Smarty\TemplateBase {
&& (!function_exists('ini_get') || strlen(ini_get('opcache.restrict_api')) < 1)
) {
opcache_invalidate($_filepath, true);
} elseif (function_exists('apc_delete_file')) {
apc_delete_file($_filepath);
}
}
}
-2
View File
@@ -251,8 +251,6 @@ class Compiled extends GeneratedPhpFile {
&& (!function_exists('ini_get') || strlen(ini_get("opcache.restrict_api")) < 1)
) {
opcache_invalidate($this->filepath, true);
} elseif (function_exists('apc_compile_file')) {
apc_compile_file($this->filepath);
}
}
if (defined('HHVM_VERSION')) {
+1 -1
View File
@@ -104,7 +104,7 @@ abstract class TemplateBase extends Data {
}
// register the object
$smarty->registered_objects[$object_name] =
[$object, (array)$allowed_methods_properties, (boolean)$format, (array)$block_methods];
[$object, (array)$allowed_methods_properties, (bool)$format, (array)$block_methods];
return $this;
}
+2 -4
View File
@@ -291,10 +291,8 @@ KEY `name` (`name`)
}
// directory ?
if ($file->isDir()) {
if (!$ri->isDot()) {
// delete folder if empty
@rmdir($file->getPathname());
}
// delete folder if empty
@rmdir($file->getPathname());
} else {
unlink($file->getPathname());
}
@@ -1,31 +0,0 @@
<?php
/**
* Smarty PHPunit tests for cache resource Apc
*
* @author Uwe Tews
*/
include_once __DIR__ . '/../Memcache/CacheResourceCustomMemcacheTest.php';
include_once __DIR__ . '/../_shared/PHPunitplugins/cacheresource.apctest.php';
/**
* class for cache resource file tests
*
*
* @preserveGlobalState disabled
*
*/
class CacheResourceCustomApcTest extends CacheResourceCustomMemcacheTest
{
public function setUp(): void
{
if (!function_exists('apc_cache_info') || ini_get('apc.enable_cli')) {
$this->markTestSkipped('APC cache not available');
}
$this->setUpSmarty(__DIR__);
parent::setUp();
$this->smarty->setCachingType('apc');
$this->smarty->registerCacheResource('apc', new Smarty_CacheResource_Apctest());
}
}
@@ -1,33 +0,0 @@
<?php
use Smarty\Smarty;
use Smarty\Template;
use Smarty\Template\Cached;
require_once __DIR__ . '/../../../__shared/cacheresources/cacheresource.apc.php';
class Smarty_CacheResource_Apctest extends Smarty_CacheResource_Apc
{
public $lockTime = 0;
public function hasLock(Smarty $smarty, Cached $cached)
{
if ($this->lockTime) {
$this->lockTime--;
if (!$this->lockTime) {
$this->releaseLock($smarty, $cached);
}
}
return parent::hasLock($smarty, $cached);
}
public function get(Template $_template)
{
$this->contents = array();
$this->timestamps = array();
$t = $this->getContent($_template);
return $t ? $t : null;
}
}
@@ -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']]);
}
}
@@ -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)'
);
}
}
@@ -0,0 +1,208 @@
<?php
/**
* Smarty PHPunit tests for object chain functionality after function calls
* Tests the new feature: {$x = collect($data)->filter()->values()->toJson()}
*
* @author Smarty Vibe
*/
/**
* Helper class to simulate a chainable collection object
*/
class ChainableCollection
{
private $data;
public function __construct($data)
{
$this->data = $data;
}
public function filter()
{
$this->data = array_filter($this->data);
return $this;
}
public function values()
{
$this->data = array_values($this->data);
return $this;
}
public function toJson()
{
return json_encode($this->data);
}
public function toArray()
{
return $this->data;
}
public function count()
{
return count($this->data);
}
}
/**
* Modifier plugin that returns a chainable object (simulates collect())
*/
function smarty_modifier_collect($data)
{
return new ChainableCollection($data);
}
/**
* Modifier plugin that returns an object with methods
*/
function smarty_modifier_create_object($value = null)
{
return new class {
public function getName() {
return 'TestObject';
}
public function getNext() {
return new class {
public function getValue() {
return 'ChainedValue';
}
};
}
};
}
/**
* class for function object chain tests
*
* @preserveGlobalState disabled
*
*/
class FunctionObjectChainTest extends PHPUnit_Smarty
{
public function setUp(): void
{
$this->setUpSmarty(__DIR__);
// Register modifier plugins that return chainable objects
// These are called like functions: {collect($data)}
$this->smarty->registerPlugin(\Smarty\Smarty::PLUGIN_MODIFIER, 'collect', 'smarty_modifier_collect');
$this->smarty->registerPlugin(\Smarty\Smarty::PLUGIN_MODIFIER, 'create_object', 'smarty_modifier_create_object');
}
public function testInit()
{
$this->cleanDirs();
}
/**
* Test the NEW feature: function call followed by method chain
* This is the core test for: {$x = collect($data)->filter()->values()->toJson()}
*/
public function testFunctionCallWithMethodChain()
{
$data = [1, 2, 0, 3, null, 4, '', 5];
$this->smarty->assign('data', $data);
// Test the new syntax: function()->method()->method()->method()
$result = $this->smarty->fetch('string:{collect($data)->filter()->values()->toJson()}');
$this->assertEquals('[1,2,3,4,5]', $result);
}
/**
* Test function call with method chain assigned to a variable
* This tests: {$x = collect($data)->filter()->values()->toJson()}
*/
public function testFunctionCallWithMethodChainAssignment()
{
$data = ['a', 'b', '', 'c', null, 'd'];
$this->smarty->assign('data', $data);
// Test assignment with chained methods
$result = $this->smarty->fetch('string:{$result = collect($data)->filter()->values()->toJson()}{$result}');
$this->assertEquals('["a","b","c","d"]', $result);
}
/**
* Test function call with single method chain
*/
public function testFunctionCallWithSingleMethod()
{
$data = [1, 2, 3];
$this->smarty->assign('data', $data);
$result = $this->smarty->fetch('string:{collect($data)->count()}');
$this->assertEquals('3', $result);
}
/**
* Test function call with nested method chains
*/
public function testFunctionCallWithNestedChain()
{
$result = $this->smarty->fetch('string:{create_object("")->getNext()->getValue()}');
$this->assertEquals('ChainedValue', $result);
}
/**
* Test that old syntax still works (two-step process)
*/
public function testOldSyntaxStillWorks()
{
$data = [1, 2, 0, 3];
$this->smarty->assign('data', $data);
// Old syntax: assign to variable first, then chain
$result = $this->smarty->fetch('string:{$x = collect($data)}{$x->filter()->values()->toJson()}');
$this->assertEquals('[1,2,3]', $result);
}
/**
* Test object chain functionality using template file
*/
public function testFunctionObjectChainFromTemplateFile()
{
$data = ['foo', '', 'bar', null, 'baz'];
$this->smarty->assign('data', $data);
$result = $this->smarty->fetch('test_function_chain.tpl');
// Expected: JSON of filtered array and count
$this->assertStringContainsString('["foo","bar","baz"]', $result);
$this->assertStringContainsString('3', $result);
}
/**
* Test complex chaining with multiple operations
* This demonstrates the power of the new feature
*/
public function testComplexChaining()
{
$data = [1, 2, 0, 3, '', 4, null, 5, false, 6];
$this->smarty->assign('data', $data);
// Complex chain: collect -> filter -> values -> toArray
$result = $this->smarty->fetch('string:{$filtered = collect($data)->filter()->values()->toArray()}{$filtered|@json_encode}');
$this->assertEquals('[1,2,3,4,5,6]', $result);
}
/**
* Test that demonstrates the benefit: one line vs multiple lines
*/
public function testNewSyntaxVsOldSyntax()
{
$data = [10, 20, 0, 30];
$this->smarty->assign('data', $data);
// NEW syntax (single line)
$new = $this->smarty->fetch('string:{collect($data)->filter()->count()}');
// OLD syntax (multiple steps)
$old = $this->smarty->fetch('string:{$temp = collect($data)}{$temp = $temp->filter()}{$temp->count()}');
// Both should produce the same result
$this->assertEquals('3', $new);
$this->assertEquals($new, $old);
}
}
@@ -0,0 +1,4 @@
{* Test template for NEW function object chain feature *}
{* Test: function()->method()->method()->method() *}
{collect($data)->filter()->values()->toJson()}
{collect($data)->filter()->count()}
@@ -1,73 +0,0 @@
<?php
/**
* APC CacheResource
* CacheResource Implementation based on the KeyValueStore API to use
* memcache as the storage resource for Smarty's output caching.
* *
*
* @author Uwe Tews
*/
class Smarty_CacheResource_Apc extends \Smarty\Cacheresource\KeyValueStore
{
/**
* Read values for a set of keys from cache
*
* @param array $keys list of keys to fetch
*
* @return array list of values with the given keys used as indexes
* @return boolean true on success, false on failure
*/
protected function read(array $keys)
{
$_res = array();
$res = apc_fetch($keys);
foreach ($res as $k => $v) {
$_res[ $k ] = $v;
}
return $_res;
}
/**
* Save values for a set of keys to cache
*
* @param array $keys list of values to save
* @param int $expire expiration time
*
* @return boolean true on success, false on failure
*/
protected function write(array $keys, $expire = null)
{
foreach ($keys as $k => $v) {
apc_store($k, $v, $expire);
}
return true;
}
/**
* Remove values from cache
*
* @param array $keys list of keys to delete
*
* @return boolean true on success, false on failure
*/
protected function delete(array $keys)
{
foreach ($keys as $k) {
apc_delete($k);
}
return true;
}
/**
* Remove *all* values from cache
*
* @return boolean true on success, false on failure
*/
protected function purge()
{
return apc_clear_cache('user');
}
}
+10
View File
@@ -0,0 +1,10 @@
FROM php:8.5-rc-cli-bullseye
## Basic utilities
RUN apt-get update -yqq && apt-get install -y curl apt-utils git zip unzip
## Composer
COPY ./utilities/testrunners/shared/install-composer.sh /root/install-composer.sh
WORKDIR /root
RUN sh ./install-composer.sh
RUN mv ./composer.phar /usr/local/bin/composer