├── phpstan.neon ├── src ├── stubs │ ├── AutocompleteVariable.php.stub │ └── AutocompleteTwigExtension.php.stub ├── events │ └── DefineGeneratorValuesEvent.php ├── console │ └── controllers │ │ └── AutocompleteController.php ├── base │ ├── GeneratorInterface.php │ └── Generator.php ├── generators │ ├── AutocompleteTwigExtensionGenerator.php │ └── AutocompleteVariableGenerator.php └── Autocomplete.php ├── ecs.php ├── Makefile ├── LICENSE.md ├── composer.json ├── CHANGELOG.md └── README.md /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - %currentWorkingDirectory%/vendor/craftcms/phpstan/phpstan.neon 3 | 4 | parameters: 5 | level: 5 6 | paths: 7 | - src 8 | -------------------------------------------------------------------------------- /src/stubs/AutocompleteVariable.php.stub: -------------------------------------------------------------------------------- 1 | paths([ 8 | __DIR__ . '/src', 9 | __FILE__, 10 | ]); 11 | $ecsConfig->parallel(); 12 | $ecsConfig->sets([SetList::CRAFT_CMS_4]); 13 | }; 14 | -------------------------------------------------------------------------------- /src/stubs/AutocompleteTwigExtension.php.stub: -------------------------------------------------------------------------------- 1 | stdout('Generating autocomplete classes ... ', BaseConsole::FG_YELLOW); 31 | /* @noinspection NullPointerExceptionInspection */ 32 | Autocomplete::getInstance()->generateAutocompleteClasses(); 33 | $this->stdout('done' . PHP_EOL, BaseConsole::FG_GREEN); 34 | 35 | return ExitCode::OK; 36 | } 37 | 38 | /** 39 | * Regenerates all autocomplete classes. 40 | */ 41 | public function actionRegenerate(): int 42 | { 43 | $this->stdout('Regenerating autocomplete classes ... ', BaseConsole::FG_YELLOW); 44 | /* @noinspection NullPointerExceptionInspection */ 45 | Autocomplete::getInstance()->regenerateAutocompleteClasses(); 46 | $this->stdout('done' . PHP_EOL, BaseConsole::FG_GREEN); 47 | 48 | return ExitCode::OK; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/base/GeneratorInterface.php: -------------------------------------------------------------------------------- 1 | values['myVariable'] = 'value'; 40 | * } 41 | * ); 42 | * ``` 43 | */ 44 | public const EVENT_BEFORE_GENERATE = 'beforeGenerate'; 45 | 46 | // Public Static Methods 47 | // ========================================================================= 48 | 49 | /** 50 | * @inheritDoc 51 | */ 52 | public static function getGeneratorName(): string 53 | { 54 | return ''; 55 | } 56 | 57 | /** 58 | * @inheritDoc 59 | */ 60 | public static function getGeneratorStubsPath(): string 61 | { 62 | return Autocomplete::getInstance()->basePath . self::STUBS_DIR; 63 | } 64 | 65 | /** 66 | * @inheritDoc 67 | */ 68 | public static function generate() 69 | { 70 | } 71 | 72 | /** 73 | * @inheritDoc 74 | */ 75 | public static function regenerate() 76 | { 77 | } 78 | 79 | /** 80 | * @inheritDoc 81 | */ 82 | public static function delete() 83 | { 84 | $path = static::getGeneratedFilePath(); 85 | if (is_file($path)) { 86 | @unlink($path); 87 | } 88 | } 89 | 90 | // Protected Static Methods 91 | // ========================================================================= 92 | 93 | /** 94 | * Save the template to disk, with variable substitution 95 | * 96 | * @param array $vars key/value variables to be replaced in the stub 97 | * @return bool Whether the template was successfully saved 98 | */ 99 | protected static function saveTemplate(array $vars): bool 100 | { 101 | $stub = file_get_contents(static::getStubFilePath()); 102 | if ($stub) { 103 | $template = str_replace(array_keys($vars), array_values($vars), $stub); 104 | 105 | return !(file_put_contents(static::getGeneratedFilePath(), $template) === false); 106 | } 107 | 108 | return false; 109 | } 110 | 111 | /** 112 | * Don't regenerate the file if it already exists 113 | * 114 | * @return bool 115 | */ 116 | protected static function shouldRegenerateFile(): bool 117 | { 118 | return !is_file(static::getGeneratedFilePath()); 119 | } 120 | 121 | /** 122 | * Return a path to the generated autocomplete template file 123 | * 124 | * @return string 125 | */ 126 | protected static function getGeneratedFilePath(): string 127 | { 128 | return Craft::$app->getPath()->getCompiledClassesPath() . DIRECTORY_SEPARATOR . static::getGeneratorName() . self::TEMPLATE_EXTENSION; 129 | } 130 | 131 | /** 132 | * Return a path to the autocomplete template stub 133 | * 134 | * @return string 135 | */ 136 | protected static function getStubFilePath(): string 137 | { 138 | return static::getGeneratorStubsPath() . DIRECTORY_SEPARATOR . static::getGeneratorName() . self::STUBS_EXTENSION; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/generators/AutocompleteTwigExtensionGenerator.php: -------------------------------------------------------------------------------- 1 | view->getTwig()->getGlobals(); 78 | foreach ($globals as $key => $value) { 79 | $type = gettype($value); 80 | switch ($type) { 81 | case 'object': 82 | $values[$key] = 'new \\' . get_class($value) . '()'; 83 | break; 84 | 85 | case 'boolean': 86 | $values[$key] = $value ? 'true' : 'false'; 87 | break; 88 | 89 | case 'integer': 90 | case 'double': 91 | $values[$key] = $value; 92 | break; 93 | 94 | case 'string': 95 | $values[$key] = "'" . addslashes($value) . "'"; 96 | break; 97 | 98 | case 'array': 99 | $values[$key] = '[]'; 100 | break; 101 | 102 | case 'NULL': 103 | $values[$key] = 'null'; 104 | break; 105 | } 106 | } 107 | 108 | // Mix in element route variables, and override values that should be used for autocompletion 109 | $values = array_merge( 110 | $values, 111 | static::elementRouteVariables(), 112 | static::globalVariables(), 113 | static::overrideValues() 114 | ); 115 | 116 | // Allow plugins to modify the values 117 | $event = new DefineGeneratorValuesEvent([ 118 | 'values' => $values, 119 | ]); 120 | Event::trigger(self::class, self::EVENT_BEFORE_GENERATE, $event); 121 | $values = $event->values; 122 | 123 | // Format the line output for each value 124 | foreach ($values as $key => $value) { 125 | $values[$key] = " '" . $key . "' => " . $value . ","; 126 | } 127 | 128 | // Save the template with variable substitution 129 | self::saveTemplate([ 130 | '{{ globals }}' => implode(PHP_EOL, $values), 131 | ]); 132 | } 133 | 134 | /** 135 | * Add in the element types that could be injected as route variables 136 | * 137 | * @return array 138 | */ 139 | protected static function elementRouteVariables(): array 140 | { 141 | $routeVariables = []; 142 | $elementTypes = Craft::$app->elements->getAllElementTypes(); 143 | foreach ($elementTypes as $elementType) { 144 | /* @var Element $elementType */ 145 | $key = $elementType::refHandle(); 146 | if (!empty($key) && !in_array($key, static::ELEMENT_ROUTE_EXCLUDES, true)) { 147 | $routeVariables[$key] = 'new \\' . $elementType . '()'; 148 | } 149 | } 150 | 151 | return $routeVariables; 152 | } 153 | 154 | /** 155 | * Add in the global variables manually, because Craft conditionally loads the GlobalsExtension as of 156 | * Craft CMS 3.7.8 only for frontend routes 157 | * 158 | * @return array 159 | */ 160 | protected static function globalVariables(): array 161 | { 162 | $globalVariables = []; 163 | // See if the GlobalsExtension class is available (Craft CMS 3.7.8 or later) and use it 164 | if (class_exists(GlobalsExtension::class)) { 165 | $globalsExtension = new GlobalsExtension(); 166 | foreach ($globalsExtension->getGlobals() as $key => $value) { 167 | $globalVariables[$key] = 'new \\' . get_class($value) . '()'; 168 | } 169 | 170 | return $globalVariables; 171 | } 172 | // Fall back and get the globals ourselves 173 | foreach (Craft::$app->getGlobals()->getAllSets() as $globalSet) { 174 | $globalVariables[$globalSet->handle] = 'new \\' . get_class($globalSet) . '()'; 175 | } 176 | 177 | return $globalVariables; 178 | } 179 | 180 | /** 181 | * Override certain values that we always want hard-coded 182 | * 183 | * @return array 184 | */ 185 | protected static function overrideValues(): array 186 | { 187 | return [ 188 | // Swap in our variable in place of the `craft` variable 189 | 'craft' => 'new \nystudio107\autocomplete\variables\AutocompleteVariable()', 190 | // Set the current user to a new user, so it is never `null` 191 | 'currentUser' => 'new \craft\elements\User()', 192 | // Set the nonce to a blank string, as it changes on every request 193 | 'nonce' => "''", 194 | ]; 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/generators/AutocompleteVariableGenerator.php: -------------------------------------------------------------------------------- 1 | view->getTwig()->getGlobals(); 81 | /* @var CraftVariable $craftVariable */ 82 | if (isset($globals['craft'])) { 83 | $craftVariable = $globals['craft']; 84 | // Handle the components 85 | foreach ($craftVariable->getComponents() as $key => $value) { 86 | try { 87 | $properties[$key] = get_class($craftVariable->get($key)); 88 | } catch (Throwable $e) { 89 | // That's okay 90 | } 91 | } 92 | // Handle the behaviors 93 | foreach ($craftVariable->getBehaviors() as $behavior) { 94 | try { 95 | $reflect = new ReflectionClass($behavior); 96 | // Properties 97 | foreach ($reflect->getProperties(ReflectionProperty::IS_PUBLIC) as $reflectProp) { 98 | // Property name 99 | $reflectPropName = $reflectProp->getName(); 100 | // Ensure the property exists only for this class and not any parent class 101 | if (property_exists(get_parent_class($behavior), $reflectPropName)) { 102 | continue; 103 | } 104 | // Do it this way because getType() reflection method is >= PHP 7.4 105 | $reflectPropType = gettype($behavior->$reflectPropName); 106 | switch ($reflectPropType) { 107 | case 'object': 108 | $properties[$reflectPropName] = get_class($behavior->$reflectPropName); 109 | break; 110 | default: 111 | $properties[$reflectPropName] = $reflectPropType; 112 | break; 113 | } 114 | } 115 | // Methods 116 | foreach ($reflect->getMethods(ReflectionMethod::IS_PUBLIC) as $reflectMethod) { 117 | // Method name 118 | $reflectMethodName = $reflectMethod->getName(); 119 | // Ensure the method exists only for this class and not any parent class 120 | if (method_exists(get_parent_class($behavior), $reflectMethodName)) { 121 | continue; 122 | } 123 | // Method return type 124 | $methodReturn = ''; 125 | $reflectMethodReturnType = $reflectMethod->getReturnType(); 126 | if ($reflectMethodReturnType instanceof ReflectionNamedType) { 127 | $methodReturn = ': ' . $reflectMethodReturnType->getName(); 128 | } 129 | // Method parameters 130 | $methodParams = []; 131 | foreach ($reflectMethod->getParameters() as $methodParam) { 132 | $paramType = ''; 133 | $methodParamType = $methodParam->getType(); 134 | if ($methodParamType) { 135 | $paramType = $methodParamType . ' '; 136 | } 137 | $methodParams[] = $paramType . '$' . $methodParam->getName(); 138 | } 139 | $methods[$reflectMethodName] = '(' . implode(', ', $methodParams) . ')' . $methodReturn; 140 | } 141 | } catch (\ReflectionException $e) { 142 | } 143 | } 144 | } 145 | 146 | // Allow plugins to modify the values 147 | $event = new DefineGeneratorValuesEvent([ 148 | 'values' => $properties, 149 | ]); 150 | Event::trigger(self::class, self::EVENT_BEFORE_GENERATE, $event); 151 | $properties = $event->values; 152 | 153 | // Format the line output for each property 154 | foreach ($properties as $key => $value) { 155 | $properties[$key] = ' * @property \\' . $value . ' $' . $key; 156 | } 157 | // Format the line output for each method 158 | foreach ($methods as $key => $value) { 159 | $methods[$key] = ' * @method ' . $key . $value; 160 | } 161 | 162 | // Save the template with variable substitution 163 | self::saveTemplate([ 164 | '{{ properties }}' => implode(PHP_EOL, array_merge($properties, $methods)), 165 | ]); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/Autocomplete.php: -------------------------------------------------------------------------------- 1 | types[] = MyAutocompleteGenerator::class; 62 | * } 63 | * ); 64 | * ``` 65 | */ 66 | public const EVENT_REGISTER_AUTOCOMPLETE_GENERATORS = 'registerAutocompleteGenerators'; 67 | 68 | public const DEFAULT_AUTOCOMPLETE_GENERATORS = [ 69 | AutocompleteVariableGenerator::class, 70 | AutocompleteTwigExtensionGenerator::class, 71 | ]; 72 | 73 | // Private Properties 74 | // ========================================================================= 75 | 76 | private $allAutocompleteGenerators; 77 | 78 | // Public Methods 79 | // ========================================================================= 80 | 81 | /** 82 | * @inerhitdoc 83 | */ 84 | public function __construct($id = self::ID, $parent = null, $config = []) 85 | { 86 | /** 87 | * Explicitly set the $id parameter, as earlier versions of Yii2 look for a 88 | * default parameter, and depend on $id being explicitly set: 89 | * https://github.com/yiisoft/yii2/blob/f3d1534125c9c3dfe8fa65c28a4be5baa822e721/framework/di/Container.php#L436-L448 90 | */ 91 | parent::__construct($id, $parent, $config); 92 | } 93 | 94 | /** 95 | * Bootstraps the extension 96 | * 97 | * @param YiiApp $app 98 | */ 99 | public function bootstrap($app) 100 | { 101 | // Set the currently requested instance of this module class, 102 | // so we can later access it with `Autocomplete::getInstance()` 103 | static::setInstance($this); 104 | 105 | // Make sure it's Craft 106 | if (!($app instanceof CraftWebApp || $app instanceof CraftConsoleApp)) { 107 | return; 108 | } 109 | // Make sure we're in devMode 110 | if (!Craft::$app->config->general->devMode) { 111 | return; 112 | } 113 | 114 | // Register our event handlers 115 | $this->registerEventHandlers(); 116 | 117 | // Add our console controller 118 | if (Craft::$app->request->isConsoleRequest) { 119 | Craft::$app->controllerMap['autocomplete'] = AutocompleteController::class; 120 | } 121 | } 122 | 123 | /** 124 | * Registers our event handlers 125 | */ 126 | public function registerEventHandlers() 127 | { 128 | Event::on(Plugins::class, Plugins::EVENT_AFTER_INSTALL_PLUGIN, [$this, 'regenerateAutocompleteClasses']); 129 | Event::on(Plugins::class, Plugins::EVENT_AFTER_UNINSTALL_PLUGIN, [$this, 'deleteAutocompleteClasses']); 130 | Event::on(Globals::class, Globals::EVENT_AFTER_SAVE_GLOBAL_SET, [$this, 'deleteAutocompleteClasses']); 131 | Event::on(CraftWebApp::class, CraftWebApp::EVENT_INIT, [$this, 'generateAutocompleteClasses']); 132 | Craft::info('Event Handlers installed', __METHOD__); 133 | } 134 | 135 | /** 136 | * Call each of the autocomplete generator classes to tell them to generate their classes if they don't exist already 137 | */ 138 | public function generateAutocompleteClasses() 139 | { 140 | if (Craft::$app->getIsInstalled()) { 141 | $autocompleteGenerators = $this->getAllAutocompleteGenerators(); 142 | foreach ($autocompleteGenerators as $generatorClass) { 143 | /* @var Generator $generatorClass */ 144 | $generatorClass::generate(); 145 | } 146 | Craft::info('Autocomplete classes generated', __METHOD__); 147 | } 148 | } 149 | 150 | /** 151 | * Call each of the autocomplete generator classes to tell them to regenerate their classes from scratch 152 | */ 153 | public function regenerateAutocompleteClasses() 154 | { 155 | $autocompleteGenerators = $this->getAllAutocompleteGenerators(); 156 | foreach ($autocompleteGenerators as $generatorClass) { 157 | /* @var Generator $generatorClass */ 158 | $generatorClass::regenerate(); 159 | } 160 | Craft::info('Autocomplete classes regenerated', __METHOD__); 161 | } 162 | 163 | /** 164 | * Call each of the autocomplete generator classes to tell them to delete their classes 165 | */ 166 | public function deleteAutocompleteClasses() 167 | { 168 | $autocompleteGenerators = $this->getAllAutocompleteGenerators(); 169 | foreach ($autocompleteGenerators as $generatorClass) { 170 | /* @var Generator $generatorClass */ 171 | $generatorClass::delete(); 172 | } 173 | Craft::info('Autocomplete classes deleted', __METHOD__); 174 | } 175 | 176 | // Protected Methods 177 | // ========================================================================= 178 | 179 | /** 180 | * Returns all available autocomplete generator classes. 181 | * 182 | * @return string[] The available autocomplete generator classes 183 | */ 184 | public function getAllAutocompleteGenerators(): array 185 | { 186 | if ($this->allAutocompleteGenerators) { 187 | return $this->allAutocompleteGenerators; 188 | } 189 | 190 | $event = new RegisterComponentTypesEvent([ 191 | 'types' => self::DEFAULT_AUTOCOMPLETE_GENERATORS, 192 | ]); 193 | $this->trigger(self::EVENT_REGISTER_AUTOCOMPLETE_GENERATORS, $event); 194 | $this->allAutocompleteGenerators = $event->types; 195 | 196 | return $this->allAutocompleteGenerators; 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/nystudio107/craft-autocomplete/badges/quality-score.png?b=v1)](https://scrutinizer-ci.com/g/nystudio107/craft-autocomplete/?branch=v1) [![Code Coverage](https://scrutinizer-ci.com/g/nystudio107/craft-autocomplete/badges/coverage.png?b=v1)](https://scrutinizer-ci.com/g/nystudio107/craft-autocomplete/?branch=v1) [![Build Status](https://scrutinizer-ci.com/g/nystudio107/craft-autocomplete/badges/build.png?b=v1)](https://scrutinizer-ci.com/g/nystudio107/craft-autocomplete/build-status/v1) [![Code Intelligence Status](https://scrutinizer-ci.com/g/nystudio107/craft-autocomplete/badges/code-intelligence.svg?b=v1)](https://scrutinizer-ci.com/code-intelligence) 2 | 3 | # Autocomplete for Craft CMS 3.x, 4.x & 5.x 4 | 5 | Provides Twig template IDE autocompletion for Craft CMS and plugin/module variables and element types. 6 | 7 | Works with PhpStorm provided the [Symfony Support plugin](https://plugins.jetbrains.com/plugin/7219-symfony-plugin 8 | ) is installed. VSCode currently does not support intellisense for Twig extensions. 9 | 10 | > While Craft [3.7.8](https://github.com/craftcms/cms/blob/v3/CHANGELOG.md#L1636) added autocompletion for Craft’s global Twig variables, this does not include autocompletion for plugins and modules that provide their own variables or element types. 11 | 12 | ![demo](https://user-images.githubusercontent.com/57572400/126911028-7d7d06dd-c60f-42b9-ae42-95d5f078a229.gif) 13 | 14 | ## Requirements 15 | 16 | This package requires Craft CMS 3.x, 4.x, or 5.x 17 | 18 | ## Usage 19 | 20 | 1. Install the package using composer, adding it to `require-dev`: 21 | 22 | ```shell 23 | composer require nystudio107/craft-autocomplete --dev 24 | ``` 25 | 26 | 2. Ensure that the [Symfony Support plugin](https://plugins.jetbrains.com/plugin/7219-symfony-plugin) for PhpStorm is installed and enabled by checking the **Enabled for Project** checkbox in the Symfony plugin settings. 27 | 28 | 3. Ensure that `devMode` is enabled. 29 | 30 | 4. Visit the Craft site on which the package is installed to generate the autocomplete classes in `storage/runtime/compiled_classes/` or run the following console command. 31 | 32 | ```shell 33 | php craft autocomplete/generate 34 | ``` 35 | 36 | Once your IDE indexes the autocomplete classes, autocompletion for Craft and all plugins and modules will immediately become available in your Twig templates. 37 | 38 | ![screenshot](https://user-images.githubusercontent.com/57572400/125784167-618830ae-e475-4faf-81d3-194ad7ce3a08.png) 39 | 40 | Additionally, autocompletion for element types provided by both Craft and plugins/modules is available, for example: `asset`, `entry`, `category`, `tag`, `user`, `product` (if Craft Commerce is installed), etc. 41 | 42 | **N.B.:** If you are using a Docker-ized setup, ensure that `storage/runtime/compiled_classes/` is bind mounted on your client machine, so your IDE can find the classes to index them 43 | 44 | ## Regenerating Autocomplete Classes 45 | 46 | The autocomplete classes are all generated any time Craft executes (whether via frontend request or via CLI), if they do not yet exist. 47 | 48 | The autocomplete classes are all regenerated every time you install or uninstall a plugin. 49 | 50 | If you manually add a plugin or module that registers variables on the Craft global variable, you can force the regeneratation of the autocomplete classes by running the following console command. 51 | 52 | ```shell 53 | php craft autocomplete/regenerate 54 | ``` 55 | 56 | ...or since the autocomplete classes are automatically regenerated if they don’t exist, you can clear the Runtime caches with: 57 | 58 | ```shell 59 | php craft clear-caches/temp-files 60 | ``` 61 | 62 | ## Extending 63 | 64 | You can extend the values that a `Generator` class adds using the `EVENT_BEFORE_GENERATE` event. 65 | 66 | ```php 67 | use nystudio107\autocomplete\events\DefineGeneratorValuesEvent; 68 | use nystudio107\autocomplete\generators\AutocompleteTwigExtensionGenerator; 69 | use yii\base\Event; 70 | 71 | Event::on(AutocompleteTwigExtensionGenerator::class, 72 | AutocompleteTwigExtensionGenerator::EVENT_BEFORE_GENERATE, 73 | function(DefineGeneratorValuesEvent $event) { 74 | $event->values['myVariable'] = 'value'; 75 | } 76 | ); 77 | ``` 78 | 79 | In addition to the provided autocomplete generator types, you can write your own by implementing the `GeneratorInterface` class or extending the abstract `Generator` class (recommended). 80 | 81 | ```php 82 | types[] = MyAutocompleteGenerator::class; 104 | } 105 | ); 106 | ``` 107 | 108 | See the included generators for guidance on how to create your own. 109 | 110 | ## How It Works 111 | 112 | On the quest for autocomplete in the PhpStorm IDE, Andrew wrote an article years ago entitled [Auto-Complete Craft CMS 3 APIs in Twig with PhpStorm](https://nystudio107.com/blog/auto-complete-craft-cms-3-apis-in-twig-with-phpstorm) 113 | 114 | This worked on principles similar to how Craft Autocomplete works, but it was a manual process. 115 | Ben and Andrew thought they could do better. 116 | 117 | ### Bootstrapping Yii2 Extension 118 | 119 | This package is a Yii2 extension (and a module) that [bootstraps itself](https://www.yiiframework.com/doc/guide/2.0/en/structure-extensions#bootstrapping-classes). 120 | 121 | This means that it’s automatically loaded with Craft, without you having to install it or configure it in any way. 122 | 123 | It only ever does anything provided that `devMode` is enabled, so it’s fine to keep it installed on production. 124 | 125 | ### The Generated Autocomplete Classes 126 | 127 | All Craft Autocomplete does is generate source code files, very similar to how Craft itself generates a [CustomFieldBehavior](https://github.com/craftcms/cms/blob/96fc9a3f2fc7caabc44d12d786dea2a39ffa4b62/src/Craft.php#L232) class in `storage/runtime/compiled_classes` 128 | 129 | The code that is generated by Craft Autocomplete is never run, however. It exists just to allow your IDE to index it for autocomplete purposes. 130 | 131 | During the bootstrapping process, the package generates two classes, `AutocompleteTwigExtension` and `AutocompleteVariable`, if they do not already exist or if a Craft plugin was just installed or uninstalled. 132 | 133 | The `AutocompleteTwigExtension` class is generated by evaluating all the Twig globals that have been registered. The `AutocompleteVariable` class is generated by dynamically evaluating the global Craft variable, including any variables that have been registered on it (by plugins and modules). 134 | 135 | Here’s an example of what the files it generates might look like, stored in `storage/runtime/compiled_classes`: 136 | 137 | `AutocompleteVariable.php`: 138 | 139 | ```php 140 | new \nystudio107\autocomplete\variables\AutocompleteVariable(), 176 | 'currentSite' => new \craft\models\Site(), 177 | 'currentUser' => new \craft\elements\User(), 178 | // ... 179 | 'seomatic' => new \nystudio107\seomatic\variables\SeomaticVariable(), 180 | 'sprig' => new \putyourlightson\sprig\variables\SprigVariable(), 181 | ]; 182 | } 183 | } 184 | ``` 185 | 186 | ### The Symfony Support Plugin for PhpStorm 187 | 188 | The other half of the equation is on the PhpStorm IDE end of things, provided by the [Symfony Support plugin](https://plugins.jetbrains.com/plugin/7219-symfony-plugin 189 | ). 190 | 191 | One of the things this PhpStorm plugin (written in Java) does is parse your code for Twig extensions that add global variables. 192 | 193 | It’s important to note that it does not actually evaluate any PHP code. Instead, it parses all Twig extension PHP classes looking for a `getGlobals()` method that returns a key/value array via a `return []` statement and makes their values available as global variables in Twig for autocompletion. 194 | 195 | The reason this has never "just worked" in the history of Craft CMS [up until version 3.7.8](https://github.com/craftcms/cms/commit/1718c95271d62d3966f2131d4b7620cc0a6191fe) is that Craft returned an array as a variable, rather than as a static key/value pair array, so the Symfony plugin could not parse it. 196 | 197 | If a plugin or module (or even Craft pre 3.7.8) does not return a key/value array directly then autocompletion simply will not work (Andrew had to discover this by [source-diving the Symfony Support plugin](https://github.com/Haehnchen/idea-php-symfony2-plugin/blob/master/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/variable/collector/GlobalExtensionVariableCollector.java#L19)): 198 | 199 | ```java 200 | /** 201 | * @author Daniel Espendiller 202 | */ 203 | public class GlobalExtensionVariableCollector implements TwigFileVariableCollector { 204 | @Override 205 | public void collectPsiVariables(@NotNull TwigFileVariableCollectorParameter parameter, @NotNull Map variables) { 206 | for(PhpClass phpClass : TwigUtil.getTwigExtensionClasses(parameter.getProject())) { 207 | if(!PhpUnitUtil.isPhpUnitTestFile(phpClass.getContainingFile())) { 208 | Method method = phpClass.findMethodByName("getGlobals"); 209 | if(method != null) { 210 | Collection phpReturns = PsiTreeUtil.findChildrenOfType(method, PhpReturn.class); 211 | for(PhpReturn phpReturn: phpReturns) { 212 | PhpPsiElement returnPsiElement = phpReturn.getFirstPsiChild(); 213 | if(returnPsiElement instanceof ArrayCreationExpression) { 214 | variables.putAll(PhpMethodVariableResolveUtil.getTypesOnArrayHash((ArrayCreationExpression) returnPsiElement)); 215 | } 216 | } 217 | } 218 | } 219 | } 220 | } 221 | } 222 | ``` 223 | 224 | 225 | Once PhpStorm has indexed these two classes, autocompletion for Craft and all plugins and modules immediately becomes available in your Twig templates, just like magic! 226 | 227 | ### Hat tip 228 | 229 | Hat tip to Oliver Stark for his work on [ostark/craft-prompter](https://mobile.twitter.com/o_stark/status/1415743590005944328). 230 | 231 | --- 232 | 233 | Brought to you by [nystudio107](https://nystudio107.com) and [PutYourLightsOn](https://putyourlightson.com/). 234 | --------------------------------------------------------------------------------