├── phpstan.neon ├── ecs.php ├── src ├── twig │ └── ClosureExpressionParser.php ├── helpers │ └── Reflection.php └── Closure.php ├── LICENSE.md ├── CHANGELOG.md ├── composer.json └── README.md /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - %currentWorkingDirectory%/vendor/craftcms/phpstan/phpstan.neon 3 | 4 | parameters: 5 | level: 5 6 | paths: 7 | - src 8 | -------------------------------------------------------------------------------- /ecs.php: -------------------------------------------------------------------------------- 1 | paths([ 8 | __DIR__ . '/src', 9 | __FILE__, 10 | ]); 11 | $ecsConfig->parallel(); 12 | $ecsConfig->sets([SetList::CRAFT_CMS_4]); 13 | }; 14 | -------------------------------------------------------------------------------- /src/twig/ClosureExpressionParser.php: -------------------------------------------------------------------------------- 1 | = `3.15.0` because "arrow functions everywhere" is baked in. Also log a message to that effect 8 | 9 | ## 1.0.6 - 2024.07.31 10 | ### Changed 11 | * Use the new (as of Craft `^4.3.0`) event `View::EVENT_AFTER_CREATE_TWIG` to hook Closure in, so it will work with any Craft-created Twig environment (not just when rendering page templates) ([#11774](https://github.com/craftcms/cms/pull/11774)) 12 | * Require `craftcms/cms` `^4.3.0 || ^5.0.0` 13 | 14 | ## 1.0.5 - 2024.05.20 15 | ### Added 16 | * Converted `addClosure()` to a public method so it is callable by anyone ([#1](https://github.com/nystudio107/craft-closure/pull/1)) 17 | 18 | ## 1.0.4 - 2024.04.15 19 | ### Added 20 | * Stable release for Craft CMS 5 21 | * Add `create-release.yml` for automated releases 22 | 23 | ## 1.0.3 - 2024.01.31 24 | ### Added 25 | * Add `phpstan` and `ecs` code linting 26 | * Add `code-analysis.yaml` GitHub action 27 | 28 | ### Changed 29 | * PHPstan code cleanup 30 | * ECS code cleanup 31 | 32 | ## 1.0.2 - 2024.01.27 33 | ### Fixed 34 | * Fix semver to `|| ^5.0.0-alpha.1` 35 | 36 | ## 1.0.1 - 2024.01.26 37 | ### Added 38 | * Add Craft CMS 5 compatibility 39 | 40 | ## 1.0.0 - 2022.09.09 41 | ### Added 42 | * Initial release 43 | 44 | ## 1.0.0-beta.1 - 2022.08.09 45 | ### Added 46 | * Initial beta release 47 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nystudio107/craft-closure", 3 | "description": "Allows you to use arrow function closures in Twig", 4 | "type": "yii2-extension", 5 | "version": "1.0.7", 6 | "keywords": [ 7 | "craft", 8 | "cms", 9 | "craftcms", 10 | "twig", 11 | "closure", 12 | "closures", 13 | "arrow", 14 | "function" 15 | ], 16 | "support": { 17 | "docs": "https://github.com/nystudio107/craft-closure/blob/v1/README.md", 18 | "issues": "https://github.com/nystudio107/craft-closure/issues", 19 | "source": "https://github.com/nystudio107/craft-closure" 20 | }, 21 | "license": "MIT", 22 | "authors": [ 23 | { 24 | "name": "nystudio107", 25 | "homepage": "https://nystudio107.com" 26 | } 27 | ], 28 | "require": { 29 | "php": "^8.0", 30 | "craftcms/cms": "^4.3.0 || ^5.0.0", 31 | "twig/twig": "^3.0.0" 32 | }, 33 | "require-dev": { 34 | "craftcms/ecs": "dev-main", 35 | "craftcms/phpstan": "dev-main", 36 | "craftcms/rector": "dev-main" 37 | }, 38 | "scripts": { 39 | "phpstan": "phpstan --ansi --memory-limit=1G", 40 | "check-cs": "ecs check --ansi", 41 | "fix-cs": "ecs check --fix --ansi" 42 | }, 43 | "config": { 44 | "allow-plugins": { 45 | "craftcms/plugin-installer": true, 46 | "yiisoft/yii2-composer": true 47 | }, 48 | "optimize-autoloader": true, 49 | "sort-packages": true 50 | }, 51 | "autoload": { 52 | "psr-4": { 53 | "nystudio107\\closure\\": "src/" 54 | } 55 | }, 56 | "extra": { 57 | "bootstrap": "nystudio107\\closure\\Closure" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/helpers/Reflection.php: -------------------------------------------------------------------------------- 1 | hasProperty($propertyName)) { 40 | $reflectionProperty = $reflectionObject->getProperty($propertyName); 41 | } else { 42 | 43 | // This is needed for private parent properties only. 44 | $parent = $reflectionObject->getParentClass(); 45 | while ($reflectionProperty === null && $parent !== false) { 46 | if ($parent->hasProperty($propertyName)) { 47 | $reflectionProperty = $parent->getProperty($propertyName); 48 | } 49 | 50 | $parent = $parent->getParentClass(); 51 | } 52 | } 53 | 54 | if (!$reflectionProperty) { 55 | throw new ReflectionException("Property not found: " . $propertyName); 56 | } 57 | 58 | return $reflectionProperty; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/nystudio107/craft-closure/badges/quality-score.png?b=v1)](https://scrutinizer-ci.com/g/nystudio107/craft-closure/?branch=develop) [![Code Coverage](https://scrutinizer-ci.com/g/nystudio107/craft-closure/badges/coverage.png?b=v1)](https://scrutinizer-ci.com/g/nystudio107/craft-closure/?branch=develop) [![Build Status](https://scrutinizer-ci.com/g/nystudio107/craft-closure/badges/build.png?b=v1)](https://scrutinizer-ci.com/g/nystudio107/craft-closure/build-status/develop) [![Code Intelligence Status](https://scrutinizer-ci.com/g/nystudio107/craft-closure/badges/code-intelligence.svg?b=v1)](https://scrutinizer-ci.com/code-intelligence) 2 | 3 | [![No Maintenance Intended](http://unmaintained.tech/badge.svg)](http://unmaintained.tech/) 4 | 5 | # DEPRECATED 6 | 7 | This Craft CMS module is no longer supported or maintained, but it is fully functional, and you may continue to use it as you see fit. The license also allows you to fork it and make changes as needed for legacy support reasons. 8 | 9 | Instead, use [Twig 3.1.5's native functionality](https://github.com/twigphp/Twig/issues/3192) for arrow functions everywhere. 10 | 11 | # Closure for Craft CMS 12 | 13 | Allows you to use arrow function closures in Twig 14 | 15 | While Closure is a bit of a monkey patch, it's a pretty clean/simple one that relies on functionality that is already built into Twig 16 | 17 | ## Requirements 18 | 19 | Closure requires Craft CMS 4.x or 5.x 20 | 21 | ## Installation 22 | 23 | To install Closure, follow these steps: 24 | 25 | 1. Open your terminal and go to your Craft project: 26 | 27 | cd /path/to/project 28 | 29 | 2. Then tell Composer to require the package: 30 | 31 | composer require nystudio107/craft-closure 32 | 33 | ## About Closure 34 | 35 | Twig supports arrow function closures, but only in the [filter](https://twig.symfony.com/doc/3.x/filters/filter.html), [map](https://twig.symfony.com/doc/3.x/filters/map.html), and [reduce](https://twig.symfony.com/doc/3.x/filters/reduce.html) filters. 36 | 37 | Twig unfortunately [has no plans](https://github.com/twigphp/Twig/issues/3402) to allow for more widespread usage of arrow function closures. 38 | 39 | Craft Closure allows you to use arrow function closures anywhere, which is especially useful with [Laravel Collection methods](https://laravel.com/docs/9.x/collections#available-methods), many of which take a closure as a parameter. 40 | 41 | ## Using Closure 42 | 43 | Once you've added the `nystudio107/craft-closure` package to your project, no further setup is needed. This is because it operates as an auto-bootstrapping Yii2 Module. 44 | 45 | You can then pass an [arrow function](https://timkelty.github.io/twig-tips/10-arrow-fn.html) closure as a parameter to anything that accepts them, such as many [Laravel Collection methods](https://laravel.com/docs/9.x/collections#available-methods): 46 | 47 | ```twig 48 | {% set collection = collect(['a', 'b', 'c']) %} 49 | {% set contains = collection.contains((value, key) => value == 'z') %} 50 | ``` 51 | 52 | Or you can assign an arrow function closure to a Twig variable for re-use: 53 | 54 | ```twig 55 | {% set collection = collect(['a', 'b', 'c']) %} 56 | {% set closure = (value, key) => value == 'a' %} 57 | {% set contains = collection.contains(closure) %} 58 | ``` 59 | 60 | Using arrow function closures especially useful now that Craft element queries can all return a Collection via the [.collect()](https://docs.craftcms.com/api/v4/craft-db-query.html#method-collect) method. 61 | 62 | ## More on Arrow Functions in Twig 63 | 64 | More here: [Twig Arrow Functions](https://craftquest.io/courses/arrow-functions-in-twig) 65 | 66 | ## Closure Roadmap 67 | 68 | Some things to do, and ideas for potential features: 69 | 70 | * Initial release 71 | 72 | Brought to you by [nystudio107](https://nystudio107.com/) 73 | -------------------------------------------------------------------------------- /src/Closure.php: -------------------------------------------------------------------------------- 1 | =')) { 73 | Craft::warning('Craft Closure not loaded because this version of Twig already supports arrow functions everywhere. You can safely uninstall Craft Closure by removing it from your composer.json', __METHOD__); 74 | return; 75 | } 76 | // Set the instance of this module class, so we can later access it with `Closure::getInstance()` 77 | static::setInstance($this); 78 | // Configure our module 79 | $this->configureModule(); 80 | // Register our event handlers 81 | $this->registerEventHandlers(); 82 | Craft::info('Closure module bootstrapped', __METHOD__); 83 | } 84 | 85 | // Protected Methods 86 | // ========================================================================= 87 | 88 | /** 89 | * Add our ClosureExpressionParser to default $allowArrow = true to let 90 | * arrow function closures work outside of Twig filter contexts 91 | * 92 | * @param Environment|null $twig 93 | * @return void 94 | */ 95 | public function addClosure(?Environment $twig = null): void 96 | { 97 | // Custom environment if specified, otherwise Craft default 98 | $twig = $twig ?? Craft::$app->getView()->getTwig(); 99 | // Get the parser object used by Twig 100 | try { 101 | $parserReflection = ReflectionHelper::getReflectionProperty($twig, 'parser'); 102 | } catch (ReflectionException $e) { 103 | Craft::error($e->getMessage(), __METHOD__); 104 | return; 105 | } 106 | $parserReflection->setAccessible(true); 107 | $parser = $parserReflection->getValue($twig); 108 | if ($parser === null) { 109 | $parser = new Parser($twig); 110 | $parserReflection->setValue($twig, $parser); 111 | } 112 | // Create the ClosureExpressionParser object and set the parser to use it 113 | try { 114 | $expressionParserReflection = ReflectionHelper::getReflectionProperty($parser, 'expressionParser'); 115 | } catch (ReflectionException $e) { 116 | Craft::error($e->getMessage(), __METHOD__); 117 | return; 118 | } 119 | $expressionParserReflection->setAccessible(true); 120 | $expressionParser = new ClosureExpressionParser($parser, $twig); 121 | $expressionParserReflection->setValue($parser, $expressionParser); 122 | } 123 | 124 | /** 125 | * Configure our module 126 | * 127 | * @return void 128 | */ 129 | protected function configureModule(): void 130 | { 131 | // Register our module 132 | Craft::$app->setModule($this->id, $this); 133 | } 134 | 135 | /** 136 | * Registers our event handlers 137 | * 138 | * @return void 139 | */ 140 | protected function registerEventHandlers(): void 141 | { 142 | // Handler: View::EVENT_AFTER_CREATE_TWIG 143 | Event::on( 144 | View::class, 145 | View::EVENT_AFTER_CREATE_TWIG, 146 | fn(CreateTwigEvent $event) => $this->addClosure($event->twig) 147 | ); 148 | } 149 | } 150 | --------------------------------------------------------------------------------