Completed
Push — master ( 7133c6...a8b998 )
by Christian
08:08 queued 04:58
created

MetaCommand::isClassDefinedInFile()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 9
rs 9.6666
c 0
b 0
f 0
cc 2
eloc 6
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 133 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
            $version = $input->getOption('meta-version');
142
            if ($version == self::VERSION_OLD) {
143
                $this->writeToOutputOld($input, $output, $classMaps);
144
            } elseif ($version == self::VERSION_2017) {
145
                $this->writeToOutputV2017($input, $output, $classMaps);
146
            }
147
        } else {
148
            $output->write('Magento 2 is currently not supported');
149
        }
150
    }
151
152
    /**
153
     * @param SplFileInfo $file
154
     * @param string $classPrefix
155
     * @return string
156
     */
157
    protected function getRealClassname(SplFileInfo $file, $classPrefix)
158
    {
159
        $path = $file->getRelativePathname();
160
        if (substr($path, -4) !== '.php') {
161
            throw new UnexpectedValueException(
162
                sprintf('Expected that relative file %s ends with ".php"', var_export($path, true))
163
            );
164
        }
165
        $path = substr($path, 0, -4);
166
        $path = strtr($path, '\\', '/');
167
168
        return trim($classPrefix . '_' . strtr($path, '/', '_'), '_');
169
    }
170
171
    /**
172
     * @param SplFileInfo   $file
173
     * @param string        $classPrefix
174
     * @param string        $group
175
     * @return string
176
     */
177
    protected function getClassIdentifier(SplFileInfo $file, $classPrefix, $group = '')
178
    {
179
        $path = str_replace('.php', '', $file->getRelativePathname());
180
        $path = str_replace('\\', '/', $path);
181
        $parts = explode('/', $path);
182
        $parts = array_map('lcfirst', $parts);
183
        if ($path == 'Data' && ($group == 'helpers')) {
184
            array_pop($parts);
185
        }
186
187
        return rtrim($classPrefix . '/' . implode('_', $parts), '/');
188
    }
189
190
    /**
191
     * Verify whether given class is defined in given file because there is no sense in adding class with incorrect
192
     * file or path. Examples:
193
     * app/code/core/Mage/Core/Model/Mysql4/Design/Theme/Collection.php -> Mage_Core_Model_Mysql4_Design_Theme
194
     * app/code/core/Mage/Payment/Model/Paygate/Request.php             -> Mage_Paygate_Model_Authorizenet_Request
195
     * app/code/core/Mage/Dataflow/Model/Convert/Iterator.php           -> Mage_Dataflow_Model_Session_Adapter_Iterator
196
     *
197
     * @param SplFileInfo     $file
198
     * @param string          $className
199
     * @param OutputInterface $output
200
     * @return bool
201
     */
202
    protected function isClassDefinedInFile(SplFileInfo $file, $className, OutputInterface $output)
203
    {
204
        try {
205
            return preg_match("/class\s+{$className}/m", $file->getContents());
206
        } catch (Exception $e) {
207
            $output->writeln('<error>File: ' . $file->__toString() . ' | ' . $e->getMessage() . '</error>');
208
            return false;
209
        }
210
    }
211
212
    /**
213
     * Resource helper is always one per module for each db type and uses model alias
214
     *
215
     * @return array
216
     */
217
    protected function getResourceHelperMap()
218
    {
219
        $classes = array();
220
221
        if (($this->_magentoEnterprise && version_compare(\Mage::getVersion(), '1.11.2.0', '<='))
222
            || (!$this->_magentoEnterprise && version_compare(\Mage::getVersion(), '1.6.2.0', '<'))
223
        ) {
224
            return $classes;
225
        }
226
227
        $modelAliases = array_keys((array) \Mage::getConfig()->getNode('global/models'));
228
        foreach ($modelAliases as $modelAlias) {
229
            $resourceHelper = @\Mage::getResourceHelper($modelAlias);
230
            if (is_object($resourceHelper)) {
231
                $classes[$modelAlias] = get_class($resourceHelper);
232
            }
233
        }
234
235
        return $classes;
236
    }
237
238
    /**
239
     * @param string $group
240
     * @param OutputInterface $output
241
     *
242
*@return array
243
     */
244
    protected function getClassMapForGroup($group, OutputInterface $output)
245
    {
246
        /**
247
         * Generate resource helper only for Magento >= EE 1.11 or CE 1.6
248
         */
249
        if ($group == 'resource helpers') {
250
            return $this->getResourceHelperMap();
251
        }
252
253
        $classes = array();
254
        foreach ($this->getGroupXmlDefinition($group) as $prefix => $modelDefinition) {
255
            if ($group == 'resource models') {
256
                if (empty($modelDefinition->resourceModel)) {
257
                    continue;
258
                }
259
                $resourceModelNodePath = 'global/models/' . strval($modelDefinition->resourceModel);
260
                $resourceModelConfig = \Mage::getConfig()->getNode($resourceModelNodePath);
261
                if ($resourceModelConfig) {
262
                    $classPrefix = strval($resourceModelConfig->class);
263
                }
264
            } else {
265
                $classPrefix = strval($modelDefinition->class);
266
            }
267
268
            if (empty($classPrefix)) {
269
                continue;
270
            }
271
272
            $classBaseFolder = str_replace('_', '/', $classPrefix);
273
            $searchFolders = array(
274
                \Mage::getBaseDir('code') . DIRECTORY_SEPARATOR . 'core' . DIRECTORY_SEPARATOR . $classBaseFolder,
275
                \Mage::getBaseDir('code') . DIRECTORY_SEPARATOR . 'community' . DIRECTORY_SEPARATOR . $classBaseFolder,
276
                \Mage::getBaseDir('code') . DIRECTORY_SEPARATOR . 'local' . DIRECTORY_SEPARATOR . $classBaseFolder,
277
            );
278
            foreach ($searchFolders as $key => $folder) {
279
                if (!is_dir($folder)) {
280
                    unset($searchFolders[$key]);
281
                }
282
            }
283
284
            if (empty($searchFolders)) {
285
                continue;
286
            }
287
288
            $finder = Finder::create();
289
            $finder
290
                ->files()
291
                ->in($searchFolders)
292
                ->followLinks()
293
                ->ignoreUnreadableDirs(true)
294
                ->name('*.php')
295
                ->notName('install-*')
296
                ->notName('upgrade-*')
297
                ->notName('mysql4-*')
298
                ->notName('mssql-*')
299
                ->notName('oracle-*');
300
301
            foreach ($finder as $file) {
302
                $classIdentifier = $this->getClassIdentifier($file, $prefix, $group);
303
                $classNameByPath = $this->getRealClassname($file, $classPrefix);
304
305
                switch ($group) {
306
                    case 'blocks':
307
                        $classNameAfterRewrites = \Mage::getConfig()->getBlockClassName($classIdentifier);
308
                        break;
309
310
                    case 'helpers':
311
                        $classNameAfterRewrites = \Mage::getConfig()->getHelperClassName($classIdentifier);
312
                        break;
313
314
                    case 'models':
315
                        $classNameAfterRewrites = \Mage::getConfig()->getModelClassName($classIdentifier);
316
                        break;
317
318
                    case 'resource models':
319
                    default:
320
                        $classNameAfterRewrites = \Mage::getConfig()->getResourceModelClassName($classIdentifier);
321
                        break;
322
                }
323
324
                if ($classNameAfterRewrites) {
325
                    $addToList = true;
326
                    if ($classNameAfterRewrites === $classNameByPath
327
                        && !$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...
328
                    ) {
329
                        $addToList = false;
330
                    }
331
332
                    if ($addToList) {
333
                        $classes[$classIdentifier] = $classNameAfterRewrites;
334
335
                        if ($group == 'helpers' && strpos($classIdentifier, '/') === false) {
336
                            $classes[$classIdentifier . '/data'] = $classNameAfterRewrites;
337
                        }
338
                    }
339
                }
340
            }
341
        }
342
343
        return $classes;
344
    }
345
346
    /**
347
     * @param InputInterface $input
348
     * @param OutputInterface $output
349
     * @param $classMaps
350
     */
351
    protected function writeToOutputOld(InputInterface $input, OutputInterface $output, $classMaps)
352
    {
353
        $map = <<<PHP
354
<?php
355
namespace PHPSTORM_META {
356
    /** @noinspection PhpUnusedLocalVariableInspection */
357
    /** @noinspection PhpIllegalArrayKeyTypeInspection */
358
    /** @noinspection PhpLanguageLevelInspection */
359
    \$STATIC_METHOD_TYPES = [
360
PHP;
361
        $map .= "\n";
362 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...
363
            foreach ($methods as $method) {
364
                $map .= "        " . $method . "('') => [\n";
365
                foreach ($classMaps[$group] as $classPrefix => $class) {
366
                    if (preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $class)) {
367
                        $map .= "            '$classPrefix' instanceof \\$class,\n";
368
                    } else {
369
                        $output->writeln('<warning>Invalid class name <comment>' . $class . '</comment> ignored</warning>');
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 124 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...
370
                    }
371
                }
372
                $map .= "        ], \n";
373
            }
374
        }
375
        $map .= <<<PHP
376
    ];
377
}
378
PHP;
379
        if ($input->getOption('stdout')) {
380
            $output->writeln($map);
381
        } else {
382
            if (\file_put_contents($this->_magentoRootFolder . '/.phpstorm.meta.php', $map)) {
383
                $output->writeln('<info>File <comment>.phpstorm.meta.php</comment> generated</info>');
384
            }
385
        }
386
    }
387
388
    /**
389
     * @param InputInterface $input
390
     * @param OutputInterface $output
391
     * @param $classMaps
392
     */
393
    protected function writeToOutputV2017(InputInterface $input, OutputInterface $output, $classMaps)
394
    {
395
        $baseMap = <<<PHP
396
<?php
397
namespace PHPSTORM_META {
398
    /** @noinspection PhpUnusedLocalVariableInspection */
399
    /** @noinspection PhpIllegalArrayKeyTypeInspection */
400
    /** @noinspection PhpLanguageLevelInspection */
401
    \$STATIC_METHOD_TYPES = [
402
PHP;
403
        $baseMap .= "\n";
404
        foreach ($this->groupFactories as $group => $methods) {
405
            $map = $baseMap;
406 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...
407
                $map .= "        " . $method . "('') => [\n";
408
                foreach ($classMaps[$group] as $classPrefix => $class) {
409
                    if (preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $class)) {
410
                        $map .= "            '$classPrefix' instanceof \\$class,\n";
411
                    } else {
412
                        $output->writeln('<warning>Invalid class name <comment>' . $class . '</comment> ignored</warning>');
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 124 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...
413
                    }
414
                }
415
                $map .= "        ], \n";
416
            }
417
            $map .= <<<PHP
418
    ];
419
}
420
PHP;
421
            if ($input->getOption('stdout')) {
422
                $output->writeln($map);
423
            } else {
424
                $metaPath = $this->_magentoRootFolder . '/.phpstorm.meta.php';
425
                if (is_file($metaPath)) {
426
                    if (\unlink($metaPath)) {
427
                        $output->writeln('<info>Deprecated file <comment>.phpstorm.meta.php</comment> removed</info>');
428
                    }
429
                }
430
                if (!is_dir($metaPath)) {
431
                    if (\mkdir($metaPath)) {
432
                        $output->writeln('<info>Directory <comment>.phpstorm.meta.php</comment> created</info>');
433
                    }
434
                }
435
                $group = str_replace(array(' ', '/'), '_', $group);
436
                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 130 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...
437
                    $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 138 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...
438
                }
439
            }
440
        }
441
    }
442
443
    /**
444
     * @param string $group
445
     * @return \Mage_Core_Model_Config_Element
446
     */
447
    protected function getGroupXmlDefinition($group)
448
    {
449
        if ($group == 'resource models') {
450
            $group = 'models';
451
        }
452
453
        $definitions = \Mage::getConfig()->getNode('global/' . $group);
454
455
        switch ($group) {
456
            case 'blocks':
457
                $groupClassType = 'Block';
458
                break;
459
460
            case 'helpers':
461
                $groupClassType = 'Helper';
462
                break;
463
464
            case 'models':
465
                $groupClassType = 'Model';
466
                break;
467
468
            default:
469
                return $definitions->children();
470
        }
471
472
        foreach ($this->missingHelperDefinitionModules as $moduleName) {
473
            $children = new Varien_Simplexml_Element(sprintf("<%s/>", strtolower($moduleName)));
474
            $children->class = sprintf('Mage_%s_%s', $moduleName, $groupClassType);
475
            $definitions->appendChild($children);
476
        }
477
478
        return $definitions->children();
479
    }
480
}
481