├── modman ├── composer.json ├── shell ├── helper │ └── instantiableTester.php └── generate-phpstorm-map.php └── README.md /modman: -------------------------------------------------------------------------------- 1 | shell/generate-phpstorm-map.php shell/generate-phpstorm-map.php 2 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"netzarbeiter/phpstorm-magento-mapper", 3 | "type":"magento-module", 4 | "license":"OSL-3.0", 5 | "homepage":"https://github.com/Vinai/phpstorm-magento-mapper", 6 | "description":"Shell script to create a phpstorm factory method class map", 7 | "authors":[ 8 | { 9 | "name":"Vinai Kopp", 10 | "email":"vinai@netzarbeiter.com" 11 | }, 12 | { 13 | "name":"Erik Wohllebe", 14 | "email":"Erik.Wohllebe@googlemail.com" 15 | } 16 | ], 17 | "require":{ 18 | "magento-hackathon/magento-composer-installer":"*" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /shell/helper/instantiableTester.php: -------------------------------------------------------------------------------- 1 | _getRootPath() . 'app' . DIRECTORY_SEPARATOR . 'Mage.php'; 27 | parent::__construct(); 28 | 29 | //Display errors to show warnings and strict notices if the error level strict or notice is set 30 | ini_set('display_errors', 1); 31 | } 32 | 33 | /** 34 | * Check if all classes given by the cli are instantiable. 35 | */ 36 | public function run() 37 | { 38 | $classNames = array_keys($this->_args); 39 | 40 | //No classes given 41 | if (!$classNames) { 42 | exit(1); 43 | } 44 | 45 | //Perform single checks for the classes 46 | foreach ($classNames as $className) { 47 | $reflectionClass = new ReflectionClass($className); 48 | 49 | //Is an interface? 50 | if ($reflectionClass->isInterface()) { 51 | echo "Interface"; 52 | exit(1); 53 | } 54 | 55 | //Is an abstract class? 56 | if ($reflectionClass->isAbstract()) { 57 | echo "Abstract"; 58 | exit(1); 59 | } 60 | 61 | //Is a trait? 62 | if ($reflectionClass->isTrait()) { 63 | echo "Trait"; 64 | exit(1); 65 | } 66 | 67 | //Can create the class with new? 68 | if (!$reflectionClass->isInstantiable()) { 69 | echo "Not instantiable"; 70 | exit(1); 71 | } 72 | } 73 | 74 | echo 'Done'; 75 | } 76 | } 77 | 78 | $classCheck = new PhpStorm_Map_Generator_instantiableTester(); 79 | $classCheck->run(); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PHPStorm Magento Mapper 2 | ======================== 3 | This extension is for Magento developers using PhpStorm. It generates a class map for autocompletion. 4 | 5 | Facts 6 | ----- 7 | - version: 0.2 8 | - extension key: - none - 9 | - Magento Connect 1.0 extension key: - none - 10 | - Magento Connect 2.0 extension key: - none - 11 | - [extension on GitHub](https://github.com/Vinai/phpstorm-magento-mapper) 12 | - [direct download link](https://github.com/Vinai/phpstorm-magento-mapper/zipball/master) 13 | 14 | Description 15 | ----------- 16 | Build a class map for the PhpStorm facorty support introduced with the blog posts. 17 | 18 | - [Support of static factories](http://blog.jetbrains.com/webide/2013/04/phpstorm-6-0-1-eap-build-129-177/) 19 | - [Support of non static factories](https://youtrack.jetbrains.com/issue/WI-27712) 20 | 21 | You need to rerun the script every time you add a class or configure a rewrite. 22 | 23 | - Supported Factory Methods 24 | - Mage::getModel() 25 | - Mage::getSingleton() 26 | - Mage::getResourceModel() 27 | - Mage::getResourceSingleton() 28 | - Mage::getBlockSingleton() 29 | - Mage::helper() 30 | - Mage_Core_Model_Factory::getModel() 31 | - Mage_Core_Model_Factory::getSingleton() 32 | - Mage_Core_Model_Factory::getResourceModel() 33 | - Mage_Core_Model_Factory::getHelper() 34 | - Mage_Core_Block_Abstract::helper() 35 | - Mage_Core_Model_Layout::createBlock() 36 | - Mage_Core_Model_Layout::getBlockSingleton() 37 | - Mage_Core_Block_Abstract::getHelper() 38 | - Respects class rewrites 39 | 40 | Usage 41 | ----- 42 | ```php shell/generate-phpstorm-map.php --file .phpstorm.meta.php``` 43 | 44 | If no file is specified the class map will be output to STDOUT 45 | 46 | Parameters 47 | ----- 48 | 49 | | Option | Default | Description | 50 | |---------------------------|:------------:|---------------------------------------------------------------------------------------------------------------| 51 | | ```--file``` | ```stdout``` | File location to save the output. | 52 | | ```--instantiableCheck``` | ```Off``` | Perform an additional instantiable check for each class. If it's enabled the generate process will slow down. | 53 | | ```--phpExecutable``` | ```php``` | Path to the php executable to start the instantiable check. | 54 | | ```--debug``` | ```Off``` | Print debug output on ```stderr``` why classes gets excluded. | 55 | 56 | Support 57 | ------- 58 | If you have any issues with this extension, open an issue on GitHub (see URL above). 59 | 60 | Contribution 61 | ------------ 62 | Any contributions are highly appreciated. The best way to contribute code is to open a 63 | [pull request on GitHub](https://help.github.com/articles/using-pull-requests). 64 | 65 | Developer 66 | --------- 67 | * Vinai Kopp 68 | [http://www.netzarbeiter.com](http://www.netzarbeiter.com) 69 | [@VinaiKopp](https://twitter.com/VinaiKopp) 70 | * Erik Wohllebe 71 | 72 | Licence 73 | ------- 74 | [OSL - Open Software Licence 3.0](http://opensource.org/licenses/osl-3.0.php) 75 | 76 | Copyright 77 | --------- 78 | (c) 2013 Vinai Kopp -------------------------------------------------------------------------------- /shell/generate-phpstorm-map.php: -------------------------------------------------------------------------------- 1 | _config)) { 24 | $this->_config = Mage::getConfig(); 25 | } 26 | return $this->_config; 27 | } 28 | 29 | public function setConfig($config) 30 | { 31 | $this->_config = $config; 32 | return $this; 33 | } 34 | 35 | public function run() 36 | { 37 | $models = $blocks = $helpers = $resourceModels = array(); 38 | 39 | foreach ($this->getActiveModules() as $module) { 40 | $moduleConfig = $this->_getModuleConfig($module); 41 | if ($moduleConfig && $moduleConfig->getNode()) { 42 | $models += $this->_getMap('model', $moduleConfig, $module); 43 | $blocks += $this->_getMap('block', $moduleConfig, $module); 44 | $helpers += $this->_getMap('helper', $moduleConfig, $module); 45 | $resourceModels += $this->_getResourceMap($moduleConfig, $module); 46 | } 47 | } 48 | 49 | //Sort the results from a to z 50 | ksort($models); 51 | ksort($blocks); 52 | ksort($helpers); 53 | ksort($resourceModels); 54 | 55 | $map = array( 56 | //Default static factories 57 | "\\Mage::getModel('')" => $models, 58 | "\\Mage::getSingleton('')" => $models, 59 | "\\Mage::getResourceModel('')" => $resourceModels, 60 | "\\Mage::getResourceSingleton('')" => $resourceModels, 61 | "\\Mage::getBlockSingleton('')" => $blocks, 62 | "\\Mage::helper('')" => $helpers, 63 | //Default non static factories 64 | "\\Mage_Core_Model_Factory::getModel('')" => $models, 65 | "\\Mage_Core_Model_Factory::getSingleton('')" => $models, 66 | "\\Mage_Core_Model_Factory::getResourceModel('')" => $resourceModels, 67 | "\\Mage_Core_Model_Factory::getHelper('')" => $helpers, 68 | //Other helper factories 69 | "\\Mage_Core_Block_Abstract::helper('')" => $helpers, 70 | //Other block factories 71 | "\\Mage_Core_Model_Layout::createBlock('')" => $blocks, 72 | "\\Mage_Core_Model_Layout::getBlockSingleton('')" => $blocks, 73 | "\\Mage_Core_Block_Abstract::getHelper('')" => $blocks, 74 | ); 75 | 76 | //Create an extension point to extend the map without override the file 77 | $eventTransport = new stdClass(); 78 | $eventTransport->map = $map; 79 | Mage::dispatchEvent('phpstorm_map_generator_extend_map', array('transport' => $eventTransport)); 80 | $map = $eventTransport->map; 81 | 82 | if ($this->isInstantiableCheckActive()) { 83 | $map = $this->_cleanMap($map); 84 | } 85 | $this->_writeMap($map); 86 | } 87 | 88 | /** 89 | * @return array 90 | */ 91 | public function getActiveModules() 92 | { 93 | /* @var $config Mage_Core_Model_Config_Element */ 94 | $modules = array(); 95 | $config = $this->getConfig()->getNode('modules'); 96 | foreach ($config->asArray() as $module => $info) { 97 | if ('true' === $info['active']) { 98 | $modules[] = $module; 99 | } 100 | } 101 | return $modules; 102 | } 103 | 104 | /** 105 | * @param $module 106 | * @return Mage_Core_Model_Config_Base 107 | */ 108 | protected function _getModuleConfig($module) 109 | { 110 | /** @var $moduleConfig Mage_Core_Model_Config_Base */ 111 | $moduleConfig = Mage::getModel('core/config_base'); 112 | 113 | $moduleConfig->loadFile( 114 | Mage::getModuleDir('etc', $module) . DS . 'config.xml' 115 | ); 116 | return $moduleConfig; 117 | } 118 | 119 | /** 120 | * @param string $type 121 | * @param string $module 122 | * @return array|bool 123 | */ 124 | protected function _getMageDefaults($type, $module) 125 | { 126 | if (preg_match('/^Mage_([^_]+)$/', $module, $m)) { 127 | $classGroup = strtolower($m[1]); 128 | $classPrefix = 'Mage_' . ucfirst($classGroup) . '_' . ucfirst($type); 129 | return array( 130 | 'classGroup' => $classGroup, 131 | 'classPrefix' => $classPrefix, 132 | ); 133 | } 134 | return false; 135 | } 136 | 137 | /** 138 | * @param string $type 139 | * @param Mage_Core_Model_Config_Base $moduleConfig 140 | * @param string $module 141 | * @return array 142 | */ 143 | protected function _getMap($type, Mage_Core_Model_Config_Base $moduleConfig, $module) 144 | { 145 | $map = array(); 146 | $classGroup = $this->_getClassGroup($type, $moduleConfig); 147 | $classPrefix = $this->_getClassPrefix($type, $moduleConfig); 148 | 149 | // Defaults for Mage namespace 150 | if (!$classGroup && ($defaults = $this->_getMageDefaults($type, $module))) { 151 | $classGroup = $defaults['classGroup']; 152 | $classPrefix = $defaults['classPrefix']; 153 | } 154 | 155 | if ($classGroup && $classPrefix) { 156 | foreach ($this->_collectClassSuffixes($classPrefix) as $suffix) { 157 | $factoryName = $classGroup . '/' . $suffix; 158 | $map[$factoryName] = $this->getConfig()->getGroupedClassName($type, $factoryName); 159 | // Add default for data helpers 160 | if ('helper' === $type && 'data' === $suffix) { 161 | $map[$classGroup] = $map[$factoryName]; 162 | } 163 | } 164 | } 165 | return $map; 166 | } 167 | 168 | /** 169 | * @param Mage_Core_Model_Config_Base $moduleConfig 170 | * @param $module 171 | * @return array 172 | */ 173 | protected function _getResourceMap(Mage_Core_Model_Config_Base $moduleConfig, $module) 174 | { 175 | $map = array(); 176 | $resourceClassPrefix = false; 177 | $classGroup = $this->_getClassGroup('model', $moduleConfig); 178 | if (!$classGroup && ($defaults = $this->_getMageDefaults('model', $module))) { 179 | $classGroup = $defaults['classGroup']; 180 | } 181 | if ($classGroup) { 182 | $xpath = "global/models/{$classGroup}/resourceModel"; 183 | $resourceClassGroupConfig = $moduleConfig->getNode()->xpath($xpath); 184 | if ($resourceClassGroupConfig) { 185 | $xpath = "global/models/{$resourceClassGroupConfig[0]}/class"; 186 | $resourceClassPrefixConfig = $moduleConfig->getNode()->xpath($xpath); 187 | if ($resourceClassPrefixConfig) { 188 | $resourceClassPrefix = (string) $resourceClassPrefixConfig[0]; 189 | } 190 | } 191 | 192 | if (! $resourceClassPrefix && 'Mage_Core' == $module) { 193 | // Apply defaults from app/etc/config.xml 194 | $resourceClassPrefix = 'Mage_Core_Model_Resource'; 195 | } 196 | 197 | if ($resourceClassPrefix) { 198 | foreach ($this->_collectClassSuffixes($resourceClassPrefix) as $suffix) { 199 | $factoryName = $classGroup . '/' . $suffix; 200 | $map[$factoryName] = $this->getConfig()->getResourceModelClassName($factoryName); 201 | } 202 | } 203 | } 204 | return $map; 205 | } 206 | 207 | protected function _getClassGroup($type, Mage_Core_Model_Config_Base $moduleConfig) 208 | { 209 | if ($classConfig = $this->_getClassConfig($type, $moduleConfig)) { 210 | return $classConfig->getName(); 211 | } 212 | return false; 213 | } 214 | 215 | protected function _getClassPrefix($type, Mage_Core_Model_Config_Base $moduleConfig) 216 | { 217 | if ($classConfig = $this->_getClassConfig($type, $moduleConfig)) { 218 | return $classConfig->class; 219 | } 220 | return false; 221 | } 222 | 223 | /** 224 | * @param $type 225 | * @param Mage_Core_Model_Config_Base $moduleConfig 226 | * @return Mage_Core_Model_Config_Element 227 | */ 228 | protected function _getClassConfig($type, Mage_Core_Model_Config_Base $moduleConfig) 229 | { 230 | $xpath = "global/{$type}s/*[class]"; 231 | $classConfigs = $moduleConfig->getNode()->xpath($xpath); 232 | if ($classConfigs) { 233 | return $classConfigs[0]; 234 | } 235 | 236 | return false; 237 | } 238 | 239 | /** 240 | * Scan files in directory mapped by class prefix 241 | * Build class names from files 242 | * 243 | * @param string $prefix 244 | * @return array 245 | */ 246 | protected function _collectClassSuffixes($prefix) 247 | { 248 | $classes = array(); 249 | $path = str_replace('_', DS, $prefix); 250 | 251 | foreach (explode(PS, get_include_path()) as $includePath) { 252 | $dir = $includePath . DS . $path; 253 | if (file_exists($dir) && is_dir($dir)) { 254 | foreach ($this->_collectClassSuffixesForPrefixInDir($dir) as $suffix) { 255 | // Still to many people without PHP 5.3 to 256 | // be able to use a anonymous function here *sigh* 257 | // Lowercase the first character and every first character after an underscore 258 | $toLowerCase = create_function( 259 | '$matches', 'return strtolower($matches[0]);' 260 | ); 261 | $suffix = lcfirst(preg_replace_callback('/_([A-Z])/', $toLowerCase, $suffix)); 262 | $classes[] = $suffix; 263 | } 264 | } 265 | } 266 | return $classes; 267 | } 268 | 269 | /** 270 | * @param $dir 271 | * @return array 272 | */ 273 | protected function _collectClassSuffixesForPrefixInDir($dir) 274 | { 275 | $classes = array(); 276 | $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($dir), 277 | \RecursiveIteratorIterator::CHILD_FIRST); 278 | /** @var $item SplFileInfo */ 279 | foreach ($iterator as $item) { 280 | if ($item->isFile() && 281 | preg_match('/^[A-Za-z0-9\_]+\.php$/', $item->getBasename()) 282 | ) { 283 | $file = substr($item->getPathname(), strlen($dir) + 1); // Remove leading path 284 | $file = substr($file, 0, -4); // Remove .php 285 | $className = str_replace(DS, '_', $file); 286 | 287 | if (!$this->isInstantiableCheckActive() 288 | && ($item->getBasename() == 'Abstract.php' || $item->getBasename() == 'Interface.php') 289 | ) { 290 | $this->debug("Not instantiable: {$className}"); 291 | continue; 292 | } 293 | 294 | $classes[] = $className; 295 | } 296 | } 297 | return $classes; 298 | } 299 | 300 | /** 301 | * Remove all entries from the given class map that are not instantiable with a new operator. 302 | * 303 | * @param array $factoryMap 304 | * @return array 305 | */ 306 | protected function _cleanMap(array $factoryMap) 307 | { 308 | foreach ($factoryMap as $factory => $classMap) { 309 | $classMapChunks = array_chunk($classMap, 10, true); 310 | foreach ($classMapChunks as $classMapChunk) { 311 | $invalidClasses = $this->_getNotInstantiableClasses($classMapChunk); 312 | if ($invalidClasses) { 313 | foreach ($invalidClasses as $factoryName => $className) { 314 | unset($factoryMap[$factory][$factoryName]); 315 | } 316 | } 317 | } 318 | } 319 | 320 | return $factoryMap; 321 | } 322 | 323 | /** 324 | * @param array $classMap 325 | * @return array 326 | */ 327 | protected function _getNotInstantiableClasses(array $classMap) 328 | { 329 | $invalidClasses = array(); 330 | 331 | //Remove classes that was already checked 332 | foreach ($classMap as $factoryName => $className) { 333 | if (isset($this->_instantiableClassCache[$className])) { 334 | if (!$this->_instantiableClassCache[$className]) { 335 | $invalidClasses[$factoryName] = $className; 336 | } 337 | unset($classMap[$factoryName]); 338 | } 339 | } 340 | 341 | //Check if all items was resolved form the cache 342 | if (!$classMap) { 343 | return $invalidClasses; 344 | } 345 | 346 | //Check if all classes are valid 347 | if ($this->_isClassInstantiable($classMap, true)) { 348 | foreach ($classMap as $className) { 349 | $this->_instantiableClassCache[$className] = true; 350 | } 351 | return $invalidClasses; 352 | } 353 | 354 | //It not, we need to check all classes once by once 355 | foreach ($classMap as $factoryName => $className) { 356 | if ($this->_isClassInstantiable($className)) { 357 | $this->_instantiableClassCache[$className] = true; 358 | } else { 359 | $invalidClasses[$factoryName] = $className; 360 | $this->_instantiableClassCache[$className] = false; 361 | } 362 | } 363 | 364 | return $invalidClasses; 365 | } 366 | 367 | /** 368 | * Perform an instantiable check for the given classes. 369 | * 370 | * The calculation the result for an single class creates a big overhead. Its better to call this method 371 | * with an array of class names. If it fails for an chunk of class names you need to iterate over all 372 | * elements of the chunk to detect all invalid class. Don't break on the first invalid class because the 373 | * chunk may contains multiple invalid classes. 374 | * 375 | * @param string|array $classNames 376 | * @param bool $skipLog 377 | * @return bool 378 | */ 379 | protected function _isClassInstantiable($classNames, $skipLog = false) 380 | { 381 | $file = __DIR__ . DS . 'helper' . DS . 'instantiableTester.php'; 382 | if (!is_array($classNames)) { 383 | $classNames = array($classNames); 384 | } 385 | 386 | $cmdClassNames = implode(' ', array_map('escapeshellarg', $classNames)); 387 | exec("{$this->getPhpExecutable()} -f={$file} {$cmdClassNames}", $execOutputLines); 388 | if (isset($execOutputLines[0]) && 'Done' === $execOutputLines[0]) { 389 | return true; 390 | } 391 | 392 | //Search the first non empty message 393 | if (!$skipLog) { 394 | foreach ($execOutputLines as $line) { 395 | if (($line = trim($line))) { 396 | $this->debug("{$line}: " . implode(', ', $classNames)); 397 | break; 398 | } 399 | } 400 | } 401 | 402 | return false; 403 | } 404 | 405 | protected function _writeMap(array $map) 406 | { 407 | $f = fopen($this->_getOutputFile(), 'w'); 408 | $str = ' $classes) { 418 | fwrite($f, " $factory => [\n"); 419 | foreach ($classes as $factoryName => $className) { 420 | fwrite($f, " '$factoryName' instanceof \\$className,\n"); 421 | } 422 | fwrite($f, " ],\n"); 423 | } 424 | 425 | $str = ' ]; 426 | }'; 427 | fwrite($f, $str); 428 | fclose($f); 429 | } 430 | 431 | protected function _getOutputFile() 432 | { 433 | if ($file = $this->getArg('file')) { 434 | return $file; 435 | } 436 | return 'php://stdout'; 437 | } 438 | 439 | public function usageHelp() 440 | { 441 | $fileName = pathinfo(__FILE__, PATHINFO_BASENAME); 442 | return << Defaults to 446 | --instantiableCheck Activate instantiable check for every class 447 | --phpExecutable Define path to the php executable for the 448 | instantiable check, "php" by default 449 | --debug Enable debug output on 450 | help This help 451 | 452 | USAGE; 453 | } 454 | 455 | /** 456 | * Log the given message to stderr if the debug flag is active. 457 | * 458 | * @param $message 459 | * @return $this 460 | */ 461 | public function debug($message) 462 | { 463 | if ($this->isDebugActive()) { 464 | fwrite(STDERR, $message . PHP_EOL); 465 | } 466 | 467 | return $this; 468 | } 469 | 470 | /** 471 | * Return true if the instantiable check is active. 472 | * 473 | * @return bool 474 | */ 475 | public function isInstantiableCheckActive() 476 | { 477 | return array_key_exists('instantiableCheck', $this->_args); 478 | } 479 | 480 | /** 481 | * Return the path to the php executable. 482 | * 483 | * @return string 484 | */ 485 | public function getPhpExecutable() 486 | { 487 | if (isset($this->_args['phpExecutable'])) { 488 | return $this->_args['phpExecutable']; 489 | } 490 | 491 | return 'php'; 492 | } 493 | 494 | /** 495 | * Return true if the debug feature is enabled. 496 | * 497 | * @return bool 498 | */ 499 | public function isDebugActive() 500 | { 501 | return array_key_exists('debug', $this->_args); 502 | } 503 | } 504 | 505 | $shell = new PhpStorm_Map_Generator(); 506 | $shell->setConfig(Mage::getConfig())->run(); 507 | --------------------------------------------------------------------------------