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
15 changed files with 987 additions and 899 deletions
+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
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
+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);
}
}
}
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;
}
}
+2 -6
View File
@@ -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')) {
+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,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');
}
}