Completed
Pull Request — master (#965)
by
unknown
03:51
created

MetaCommand::getClassIdentifier()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 12
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 8
nc 2
nop 3
1
<?php
2
3
namespace N98\Magento\Command\Developer\Ide\PhpStorm;
4
5
use Exception;
6
use N98\Magento\Command\AbstractMagentoCommand;
7
use Symfony\Component\Console\Input\InputInterface;
8
use Symfony\Component\Console\Input\InputOption;
9
use Symfony\Component\Console\Output\OutputInterface;
10
use Symfony\Component\Finder\Finder;
11
use Symfony\Component\Finder\SplFileInfo;
12
use UnexpectedValueException;
13
use Varien_Simplexml_Element;
14
15
class MetaCommand extends AbstractMagentoCommand
16
{
17
    /**
18
     * @var array
19
     */
20
    protected $groups = array(
21
        'blocks',
22
        'helpers',
23
        'models',
24
        'resource models',
25
        'resource helpers',
26
    );
27
28
    /**
29
     * List of supported static factory methods
30
     *
31
     * @var array
32
     */
33
    protected $groupFactories = array(
34
        'blocks' => array(
35
            '\Mage::getBlockSingleton',
36
        ),
37
        'helpers' => array(
38
            '\Mage::helper',
39
        ),
40
        'models' => array(
41
            '\Mage::getModel',
42
            '\Mage::getSingleton',
43
        ),
44
        'resource helpers' => array(
45
            '\Mage::getResourceHelper',
46
        ),
47
        'resource models' => array(
48
            '\Mage::getResourceModel',
49
            '\Mage::getResourceSingleton',
50
        ),
51
    );
52
53
    /**
54
     * @var array
55
     */
56
    protected $missingHelperDefinitionModules = array(
57
        'Backup',
58
        'Bundle',
59
        'Captcha',
60
        'Catalog',
61
        'Centinel',
62
        'Checkout',
63
        'Cms',
64
        'Core',
65
        'Customer',
66
        'Dataflow',
67
        'Directory',
68
        'Downloadable',
69
        'Eav',
70
        'Index',
71
        'Install',
72
        'Log',
73
        'Media',
74
        'Newsletter',
75
        'Page',
76
        'Payment',
77
        'Paypal',
78
        'Persistent',
79
        'Poll',
80
        'Rating',
81
        'Reports',
82
        'Review',
83
        'Rss',
84
        'Rule',
85
        'Sales',
86
        'Shipping',
87
        'Sitemap',
88
        'Tag',
89
        'Tax',
90
        'Usa',
91
        'Weee',
92
        'Widget',
93
        'Wishlist',
94
    );
95
96
    const VERSION_OLD = 'old';
97
    const VERSION_2017 = '2016.2+';
98
99
    protected function configure()
100
    {
101
        $this
102
            ->setName('dev:ide:phpstorm:meta')
103
            ->addOption(
104
                'meta-version',
105
                null,
106
                InputOption::VALUE_REQUIRED,
107
                'PhpStorm Meta version ('.self::VERSION_OLD.', '.self::VERSION_2017.')',
108
                self::VERSION_2017
109
            )
110
            ->addOption('stdout', null, InputOption::VALUE_NONE, 'Print to stdout instead of file .phpstorm.meta.php')
111
            ->setDescription('Generates meta data file for PhpStorm auto completion (default version : '.self::VERSION_2017.')');
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 129 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
112
    }
113
114
    /**
115
     * @param InputInterface  $input
116
     * @param OutputInterface $output
117
     *
118
     * @internal param string $package
119
     * @return void
120
     */
121
    protected function execute(InputInterface $input, OutputInterface $output)
122
    {
123
        $this->detectMagento($output);
124
        if (!$this->initMagento()) {
125
            return;
126
        }
127
128
        if ($this->_magentoMajorVersion == self::MAGENTO_MAJOR_VERSION_1) {
129
            $classMaps = array();
130
131
            foreach ($this->groups as $group) {
132
                $classMaps[$group] = $this->getClassMapForGroup($group, $output);
133
134
                if (!$input->getOption('stdout') && count($classMaps[$group]) > 0) {
135
                    $output->writeln(
136
                        '<info>Generated definitions for <comment>' . $group . '</comment> group</info>'
137
                    );
138
                }
139
            }
140
141
            $this->writeToOutput($input, $output, $classMaps);
0 ignored issues
show
Bug introduced by
The method writeToOutput() does not exist on N98\Magento\Command\Deve...de\PhpStorm\MetaCommand. Did you maybe mean writeToOutputOld()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
142
        } else {
143
            $output->write('Magento 2 is currently not supported');
144
        }
145
    }
146
147
    /**
148
     * @param SplFileInfo $file
149
     * @param string $classPrefix
150
     * @return string
151
     */
152
    protected function getRealClassname(SplFileInfo $file, $classPrefix)
153
    {
154
        $path = $file->getRelativePathname();
155
        if (substr($path, -4) !== '.php') {
156
            throw new UnexpectedValueException(
157
                sprintf('Expected that relative file %s ends with ".php"', var_export($path, true))
158
            );
159
        }
160
        $path = substr($path, 0, -4);
161
        $path = strtr($path, '\\', '/');
162
163
        return trim($classPrefix . '_' . strtr($path, '/', '_'), '_');
164
    }
165
166
    /**
167
     * @param SplFileInfo   $file
168
     * @param string        $classPrefix
169
     * @param string        $group
170
     * @return string
171
     */
172
    protected function getClassIdentifier(SplFileInfo $file, $classPrefix, $group = '')
173
    {
174
        $path = str_replace('.php', '', $file->getRelativePathname());
175
        $path = str_replace('\\', '/', $path);
176
        $parts = explode('/', $path);
177
        $parts = array_map('lcfirst', $parts);
178
        if ($path == 'Data' && ($group == 'helpers')) {
179
            array_pop($parts);
180
        }
181
182
        return rtrim($classPrefix . '/' . implode('_', $parts), '/');
183
    }
184
185
    /**
186
     * Verify whether given class is defined in given file because there is no sense in adding class with incorrect
187
     * file or path. Examples:
188
     * app/code/core/Mage/Core/Model/Mysql4/Design/Theme/Collection.php -> Mage_Core_Model_Mysql4_Design_Theme
189
     * app/code/core/Mage/Payment/Model/Paygate/Request.php             -> Mage_Paygate_Model_Authorizenet_Request
190
     * app/code/core/Mage/Dataflow/Model/Convert/Iterator.php           -> Mage_Dataflow_Model_Session_Adapter_Iterator
191
     *
192
     * @param SplFileInfo     $file
193
     * @param string          $className
194
     * @param OutputInterface $output
195
     * @return bool
196
     */
197
    protected function isClassDefinedInFile(SplFileInfo $file, $className, OutputInterface $output)
198
    {
199
        try {
200
            return preg_match("/class\s+{$className}/m", $file->getContents());
201
        } catch (Exception $e) {
202
            $output->writeln('<error>File: ' . $file->__toString() . ' | ' . $e->getMessage() . '</error>');
203
            return false;
204
        }
205
    }
206
207
    /**
208
     * Resource helper is always one per module for each db type and uses model alias
209
     *
210
     * @return array
211
     */
212
    protected function getResourceHelperMap()
213
    {
214
        $classes = array();
215
216
        if (($this->_magentoEnterprise && version_compare(\Mage::getVersion(), '1.11.2.0', '<='))
217
            || (!$this->_magentoEnterprise && version_compare(\Mage::getVersion(), '1.6.2.0', '<'))
218
        ) {
219
            return $classes;
220
        }
221
222
        $modelAliases = array_keys((array) \Mage::getConfig()->getNode('global/models'));
223
        foreach ($modelAliases as $modelAlias) {
224
            $resourceHelper = @\Mage::getResourceHelper($modelAlias);
225
            if (is_object($resourceHelper)) {
226
                $classes[$modelAlias] = get_class($resourceHelper);
227
            }
228
        }
229
230
        return $classes;
231
    }
232
233
    /**
234
     * @param string $group
235
     * @param OutputInterface $output
236
     *
237
*@return array
238
     */
239
    protected function getClassMapForGroup($group, OutputInterface $output)
240
    {
241
        /**
242
         * Generate resource helper only for Magento >= EE 1.11 or CE 1.6
243
         */
244
        if ($group == 'resource helpers') {
245
            return $this->getResourceHelperMap();
246
        }
247
248
        $classes = array();
249
        foreach ($this->getGroupXmlDefinition($group) as $prefix => $modelDefinition) {
250
            if ($group == 'resource models') {
251
                if (empty($modelDefinition->resourceModel)) {
252
                    continue;
253
                }
254
                $resourceModelNodePath = 'global/models/' . strval($modelDefinition->resourceModel);
255
                $resourceModelConfig = \Mage::getConfig()->getNode($resourceModelNodePath);
256
                if ($resourceModelConfig) {
257
                    $classPrefix = strval($resourceModelConfig->class);
258
                }
259
            } else {
260
                $classPrefix = strval($modelDefinition->class);
261
            }
262
263
            if (empty($classPrefix)) {
264
                continue;
265
            }
266
267
            $classBaseFolder = str_replace('_', '/', $classPrefix);
268
            $searchFolders = array(
269
                \Mage::getBaseDir('code') . DIRECTORY_SEPARATOR . 'core' . DIRECTORY_SEPARATOR . $classBaseFolder,
270
                \Mage::getBaseDir('code') . DIRECTORY_SEPARATOR . 'community' . DIRECTORY_SEPARATOR . $classBaseFolder,
271
                \Mage::getBaseDir('code') . DIRECTORY_SEPARATOR . 'local' . DIRECTORY_SEPARATOR . $classBaseFolder,
272
            );
273
            foreach ($searchFolders as $key => $folder) {
274
                if (!is_dir($folder)) {
275
                    unset($searchFolders[$key]);
276
                }
277
            }
278
279
            if (empty($searchFolders)) {
280
                continue;
281
            }
282
283
            $finder = Finder::create();
284
            $finder
285
                ->files()
286
                ->in($searchFolders)
287
                ->followLinks()
288
                ->ignoreUnreadableDirs(true)
289
                ->name('*.php')
290
                ->notName('install-*')
291
                ->notName('upgrade-*')
292
                ->notName('mysql4-*')
293
                ->notName('mssql-*')
294
                ->notName('oracle-*');
295
296
            foreach ($finder as $file) {
297
                $classIdentifier = $this->getClassIdentifier($file, $prefix, $group);
298
                $classNameByPath = $this->getRealClassname($file, $classPrefix);
299
300
                switch ($group) {
301
                    case 'blocks':
302
                        $classNameAfterRewrites = \Mage::getConfig()->getBlockClassName($classIdentifier);
303
                        break;
304
305
                    case 'helpers':
306
                        $classNameAfterRewrites = \Mage::getConfig()->getHelperClassName($classIdentifier);
307
                        break;
308
309
                    case 'models':
310
                        $classNameAfterRewrites = \Mage::getConfig()->getModelClassName($classIdentifier);
311
                        break;
312
313
                    case 'resource models':
314
                    default:
315
                        $classNameAfterRewrites = \Mage::getConfig()->getResourceModelClassName($classIdentifier);
316
                        break;
317
                }
318
319
                if ($classNameAfterRewrites) {
320
                    $addToList = true;
321
                    if ($classNameAfterRewrites === $classNameByPath
322
                        && !$this->isClassDefinedInFile($file, $classNameByPath, $output)
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->isClassDefinedInF...assNameByPath, $output) of type integer|false is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
323
                    ) {
324
                        $addToList = false;
325
                    }
326
327
                    if ($addToList) {
328
                        $classes[$classIdentifier] = $classNameAfterRewrites;
329
330
                        if ($group == 'helpers' && strpos($classIdentifier, '/') === false) {
331
                            $classes[$classIdentifier . '/data'] = $classNameAfterRewrites;
332
                        }
333
                    }
334
                }
335
            }
336
        }
337
338
        return $classes;
339
    }
340
341
    /**
342
     * @param InputInterface $input
343
     * @param OutputInterface $output
344
     * @param $classMaps
345
     */
346
    protected function writeToOutputOld(InputInterface $input, OutputInterface $output, $classMaps)
347
    {
348
        $map = <<<PHP
349
<?php
350
namespace PHPSTORM_META {
351
    /** @noinspection PhpUnusedLocalVariableInspection */
352
    /** @noinspection PhpIllegalArrayKeyTypeInspection */
353
    /** @noinspection PhpLanguageLevelInspection */
354
    \$STATIC_METHOD_TYPES = [
355
PHP;
356
        $map .= "\n";
357 View Code Duplication
        foreach ($this->groupFactories as $group => $methods) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
358
            foreach ($methods as $method) {
359
                $map .= "        " . $method . "('') => [\n";
360
                foreach ($classMaps[$group] as $classPrefix => $class) {
361
                    if (preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $class)) {
362
                        $map .= "            '$classPrefix' instanceof \\$class,\n";
363
                    } else {
364
                        $output->writeln('<warning>Invalid class name <comment>'.$class.'</comment> ignored</warning>');
365
                    }
366
                }
367
                $map .= "        ], \n";
368
            }
369
        }
370
        $map .= <<<PHP
371
    ];
372
}
373
PHP;
374
        if ($input->getOption('stdout')) {
375
            $output->writeln($map);
376
        } else {
377
            if (\file_put_contents($this->_magentoRootFolder . '/.phpstorm.meta.php', $map)) {
378
                $output->writeln('<info>File <comment>.phpstorm.meta.php</comment> generated</info>');
379
            }
380
        }
381
    }
382
383
    /**
384
     * @param InputInterface $input
385
     * @param OutputInterface $output
386
     * @param $classMaps
387
     */
388
    protected function writeToOutputV2017(InputInterface $input, OutputInterface $output, $classMaps)
389
    {
390
        $baseMap = <<<PHP
391
<?php
392
namespace PHPSTORM_META {
393
    /** @noinspection PhpUnusedLocalVariableInspection */
394
    /** @noinspection PhpIllegalArrayKeyTypeInspection */
395
    /** @noinspection PhpLanguageLevelInspection */
396
    \$STATIC_METHOD_TYPES = [
397
PHP;
398
        $baseMap .= "\n";
399
        foreach ($this->groupFactories as $group => $methods) {
400
            $map = $baseMap;
401 View Code Duplication
            foreach ($methods as $method) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
402
                $map .= "        " . $method . "('') => [\n";
403
                foreach ($classMaps[$group] as $classPrefix => $class) {
404
                    if (preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $class)) {
405
                        $map .= "            '$classPrefix' instanceof \\$class,\n";
406
                    } else {
407
                        $output->writeln('<warning>Invalid class name <comment>'.$class.'</comment> ignored</warning>');
408
                    }
409
                }
410
                $map .= "        ], \n";
411
            }
412
            $map .= <<<PHP
413
    ];
414
}
415
PHP;
416
            if ($input->getOption('stdout')) {
417
                $output->writeln($map);
418
            } else {
419
                $metaPath = $this->_magentoRootFolder . '/.phpstorm.meta.php';
420
                if (is_file($metaPath)) {
421
                    if (\unlink($metaPath)) {
422
                        $output->writeln('<info>Deprecated file <comment>.phpstorm.meta.php</comment> removed</info>');
423
                    }
424
                }
425
                if (!is_dir($metaPath)) {
426
                    if (\mkdir($metaPath)) {
427
                        $output->writeln('<info>Directory <comment>.phpstorm.meta.php</comment> created</info>');
428
                    }
429
                }
430
                $group = str_replace(array(' ', '/'), '_', $group);
431
                if (\file_put_contents($this->_magentoRootFolder . '/.phpstorm.meta.php/magento_'.$group.'.meta.php', $map)) {
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 126 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
432
                    $output->writeln('<info>File <comment>.phpstorm.meta.php/magento_'.$group.'.meta.php</comment> generated</info>');
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 134 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
433
                }
434
            }
435
        }
436
    }
437
438
    /**
439
     * @param string $group
440
     * @return \Mage_Core_Model_Config_Element
441
     */
442
    protected function getGroupXmlDefinition($group)
443
    {
444
        if ($group == 'resource models') {
445
            $group = 'models';
446
        }
447
448
        $definitions = \Mage::getConfig()->getNode('global/' . $group);
449
450
        switch ($group) {
451
            case 'blocks':
452
                $groupClassType = 'Block';
453
                break;
454
455
            case 'helpers':
456
                $groupClassType = 'Helper';
457
                break;
458
459
            case 'models':
460
                $groupClassType = 'Model';
461
                break;
462
463
            default:
464
                return $definitions->children();
465
        }
466
467
        foreach ($this->missingHelperDefinitionModules as $moduleName) {
468
            $children = new Varien_Simplexml_Element(sprintf("<%s/>", strtolower($moduleName)));
469
            $children->class = sprintf('Mage_%s_%s', $moduleName, $groupClassType);
470
            $definitions->appendChild($children);
471
        }
472
473
        return $definitions->children();
474
    }
475
}
476