Completed
Push — develop ( 8fda15...dbbcf5 )
by Jaap
14s
created

Parser/Command/Project/ParseCommand.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * phpDocumentor
4
 *
5
 * PHP Version 5.3
6
 *
7
 * @copyright 2010-2018 Mike van Riel / Naenius (http://www.naenius.com)
8
 * @license   http://www.opensource.org/licenses/mit-license.php MIT
9
 * @link      http://phpdoc.org
10
 */
11
12
namespace phpDocumentor\Parser\Command\Project;
13
14
use phpDocumentor\Command\Command;
15
use phpDocumentor\Command\Helper\ConfigurationHelper;
16
use phpDocumentor\Descriptor\Cache\ProjectDescriptorMapper;
17
use phpDocumentor\Descriptor\ProjectDescriptor;
18
use phpDocumentor\Descriptor\ProjectDescriptorBuilder;
19
use phpDocumentor\DomainModel\Dsn;
20
use phpDocumentor\DomainModel\Parser\FileCollector;
21
use phpDocumentor\Fileset\Collection;
22
use phpDocumentor\Parser\Event\PreFileEvent;
23
use phpDocumentor\Parser\Exception\FilesNotFoundException;
24
use phpDocumentor\Parser\Middleware\CacheMiddleware;
25
use phpDocumentor\Parser\Parser;
26
use phpDocumentor\Parser\Util\ParserPopulator;
27
use phpDocumentor\Partials\Collection as PartialsCollection;
28
use phpDocumentor\Reflection\DocBlock\ExampleFinder;
29
use phpDocumentor\Reflection\File;
30
use Symfony\Component\Console\Helper\ProgressHelper;
31
use Symfony\Component\Console\Input\InputInterface;
32
use Symfony\Component\Console\Input\InputOption;
33
use Symfony\Component\Console\Output\OutputInterface;
34
use Symfony\Component\Filesystem\Filesystem;
35
use Zend\Cache\Storage\StorageInterface;
36
use Zend\I18n\Translator\Translator;
37
38
/**
39
 * Parses the given source code and creates a structure file.
40
 *
41
 * The parse task uses the source files defined either by -f or -d options and
42
 * generates a structure file (structure.xml) at the target location (which is
43
 * the folder 'output' unless the option -t is provided).
44
 */
45
class ParseCommand extends Command
46
{
47
    /** @var ProjectDescriptorBuilder $builder */
48
    protected $builder;
49
50
    /** @var Parser $parser */
51
    protected $parser;
52
53
    /** @var Translator */
54
    protected $translator;
55
56
    /** @var StorageInterface */
57
    private $cache;
58
59
    /**
60
     * @var ExampleFinder
61
     */
62
    private $exampleFinder;
63
64
    /**
65
     * @var PartialsCollection
66
     */
67
    private $partials;
68
69
    /**
70
     * @var FileCollector
71
     */
72
    private $fileCollector;
73
74
    /**
75
     * ParseCommand constructor.
76
     */
77
    public function __construct(
78
        ProjectDescriptorBuilder $builder,
79
        Parser $parser,
80
        FileCollector $fileCollector,
81
        Translator $translator,
82
        StorageInterface $cache,
83
        ExampleFinder $exampleFinder,
84
        PartialsCollection $partials
85
    ) {
86
        $this->builder = $builder;
87
        $this->parser = $parser;
88
        $this->translator = $translator;
89
        $this->cache = $cache;
90
        $this->exampleFinder = $exampleFinder;
91
        $this->partials = $partials;
92
        $this->fileCollector = $fileCollector;
93
94
        parent::__construct('project:parse');
95
    }
96
97
    public function getBuilder(): ProjectDescriptorBuilder
98
    {
99
        return $this->builder;
100
    }
101
102
    public function getParser(): Parser
103
    {
104
        return $this->parser;
105
    }
106
107
    /**
108
     * Returns the Cache.
109
     *
110
     * @throws InvalidArgumentException
111
     */
112 1
    protected function getCache(): StorageInterface
113
    {
114 1
        return $this->cache;
115
    }
116
117
    /**
118
     * Initializes this command and sets the name, description, options and
119
     * arguments.
120
     */
121 1
    protected function configure()
122
    {
123
        // minimization of the following expression
124 1
        $VALUE_OPTIONAL_ARRAY = InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY;
125
126 1
        $this->setAliases(['parse'])
127 1
            ->setDescription($this->__('PPCPP-DESCRIPTION'))
128 1
            ->setHelp($this->__('PPCPP-HELPTEXT'))
129 1
            ->addOption('filename', 'f', $VALUE_OPTIONAL_ARRAY, $this->__('PPCPP:OPT-FILENAME'))
130 1
            ->addOption('directory', 'd', $VALUE_OPTIONAL_ARRAY, $this->__('PPCPP:OPT-DIRECTORY'))
131 1
            ->addOption('target', 't', InputOption::VALUE_OPTIONAL, $this->__('PPCPP:OPT-TARGET'))
132 1
            ->addOption('encoding', null, InputOption::VALUE_OPTIONAL, $this->__('PPCPP:OPT-ENCODING'))
133 1
            ->addOption('extensions', 'e', $VALUE_OPTIONAL_ARRAY, $this->__('PPCPP:OPT-EXTENSIONS'))
134 1
            ->addOption('ignore', 'i', $VALUE_OPTIONAL_ARRAY, $this->__('PPCPP:OPT-IGNORE'))
135 1
            ->addOption('ignore-tags', null, $VALUE_OPTIONAL_ARRAY, $this->__('PPCPP:OPT-IGNORETAGS'))
136 1
            ->addOption('hidden', null, InputOption::VALUE_NONE, $this->__('PPCPP:OPT-HIDDEN'))
137 1
            ->addOption('ignore-symlinks', null, InputOption::VALUE_NONE, $this->__('PPCPP:OPT-IGNORESYMLINKS'))
138 1
            ->addOption('markers', 'm', $VALUE_OPTIONAL_ARRAY, $this->__('PPCPP:OPT-MARKERS'), ['TODO', 'FIXME'])
139 1
            ->addOption('title', null, InputOption::VALUE_OPTIONAL, $this->__('PPCPP:OPT-TITLE'))
140 1
            ->addOption('force', null, InputOption::VALUE_NONE, $this->__('PPCPP:OPT-FORCE'))
141 1
            ->addOption('validate', null, InputOption::VALUE_NONE, $this->__('PPCPP:OPT-VALIDATE'))
142 1
            ->addOption('visibility', null, $VALUE_OPTIONAL_ARRAY, $this->__('PPCPP:OPT-VISIBILITY'))
143 1
            ->addOption('sourcecode', null, InputOption::VALUE_NONE, $this->__('PPCPP:OPT-SOURCECODE'))
144 1
            ->addOption('progressbar', 'p', InputOption::VALUE_NONE, $this->__('PPCPP:OPT-PROGRESSBAR'))
145 1
            ->addOption('parseprivate', null, InputOption::VALUE_NONE, 'PPCPP:OPT-PARSEPRIVATE')
146 1
            ->addOption(
147 1
                'defaultpackagename',
148 1
                null,
149 1
                InputOption::VALUE_OPTIONAL,
150 1
                $this->__('PPCPP:OPT-DEFAULTPACKAGENAME'),
151 1
                'Default'
152
            );
153
154 1
        parent::configure();
155 1
    }
156
157
    /**
158
     * Executes the business logic involved with this command.
159
     *
160
     * @throws Exception if the target location is not a folder.
161
     */
162 1
    protected function execute(InputInterface $input, OutputInterface $output): int
163
    {
164
        /** @var ConfigurationHelper $configurationHelper */
165 1
        $configurationHelper = $this->getHelper('phpdocumentor_configuration');
166 1
        $target = $configurationHelper->getOption($input, 'target', 'parser/target');
167 1
        if (strpos($target, '/tmp/') === 0) {
168
            $target = str_replace('/tmp/', sys_get_temp_dir() . DIRECTORY_SEPARATOR, $target);
169
        }
170
171 1
        $fileSystem = new Filesystem();
172 1
        if (!$fileSystem->isAbsolutePath($target)) {
173 1
            $target = getcwd() . DIRECTORY_SEPARATOR . $target;
174
        }
175
176 1
        if (!file_exists($target)) {
177
            mkdir($target, 0777, true);
178
        }
179
180 1
        if (!is_dir($target)) {
181
            throw new \Exception($this->__('PPCPP:EXC-BADTARGET'));
182
        }
183
184 1
        $this->getCache()->getOptions()->setCacheDir($target);
185
186 1
        if ($input->getOption('force')) {
187
            $this->getCacheMiddleware()->disable();
188
        }
189
190 1
        $builder = $this->getBuilder();
191 1
        $builder->createProjectDescriptor();
192 1
        $projectDescriptor = $builder->getProjectDescriptor();
193
194 1
        $output->write($this->__('PPCPP:LOG-COLLECTING'));
195 1
        $files = $this->getFileCollection($input);
196
197
        //TODO: Should determine root based on filesystems. Could be an issue for multiple.
198
        // Need some config update here.
199 1
        $this->exampleFinder->setSourceDirectory(getcwd());
200 1
        $this->exampleFinder->setExampleDirectories($configurationHelper->getConfigValueFromPath('files/examples'));
201 1
        $output->writeln($this->__('PPCPP:LOG-OK'));
202
203
        /** @var ProgressHelper $progress */
204 1
        $progress = $this->getProgressBar($input);
205 1
        if (!$progress) {
206 1
            $this->getHelper('phpdocumentor_logger')->connectOutputToLogging($output, $this);
0 ignored issues
show
It seems like you code against a concrete implementation and not the interface Symfony\Component\Console\Helper\HelperInterface as the method connectOutputToLogging() does only exist in the following implementations of said interface: phpDocumentor\Command\Helper\LoggerHelper.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
207
        }
208
209 1
        $output->write($this->__('PPCPP:LOG-INITIALIZING'));
210 1
        $this->populateParser($input);
211
212 1
        if ($progress) {
213
            $progress->start($output, \count($files));
214
        }
215
216
        try {
217 1
            $output->writeln($this->__('PPCPP:LOG-OK'));
218 1
            $output->writeln($this->__('PPCPP:LOG-PARSING'));
219
220 1
            $mapper = new ProjectDescriptorMapper($this->getCache());
221
            //TODO: Re-enable garbage collection here.
222
            //$mapper->garbageCollect($files);
223 1
            $mapper->populate($projectDescriptor);
224
225 1
            $visibility = (array) $configurationHelper->getOption($input, 'visibility', 'parser/visibility');
226 1
            $visibilities = [];
227 1
            foreach ($visibility as $item) {
228 1
                $visibilities = $visibilities + explode(',', $item);
229
            }
230
231 1
            $visibility = null;
232 1
            foreach ($visibilities as $item) {
233
                switch ($item) {
234 1
                    case 'public':
235
                        $visibility |= ProjectDescriptor\Settings::VISIBILITY_PUBLIC;
236
                        break;
237 1
                    case 'protected':
238
                        $visibility |= ProjectDescriptor\Settings::VISIBILITY_PROTECTED;
239
                        break;
240 1
                    case 'private':
241
                        $visibility |= ProjectDescriptor\Settings::VISIBILITY_PRIVATE;
242 1
                        break;
243
                }
244
            }
245
246 1
            if ($visibility === null) {
247 1
                $visibility = ProjectDescriptor\Settings::VISIBILITY_DEFAULT;
248
            }
249
250 1
            if ($input->getOption('parseprivate')) {
251
                $visibility |= ProjectDescriptor\Settings::VISIBILITY_INTERNAL;
252
            }
253
254 1
            $projectDescriptor->getSettings()->setVisibility($visibility);
255 1
            $input->getOption('sourcecode')
256
                ? $projectDescriptor->getSettings()->includeSource()
257 1
                : $projectDescriptor->getSettings()->excludeSource();
258
259 1
            $this->getParser()->parse($builder, $files);
260
        } catch (FilesNotFoundException $e) {
261
            throw new \Exception($this->__('PPCPP:EXC-NOFILES'));
262
        } catch (\Exception $e) {
263
            throw new \Exception($e->getMessage(), 0, $e);
264
        }
265
266 1
        if ($progress) {
267
            $progress->finish();
268
        }
269
270 1
        $projectDescriptor->setPartials($this->partials);
271
272 1
        $output->write($this->__('PPCPP:LOG-STORECACHE', (array) $this->getCache()->getOptions()->getCacheDir()));
273 1
        $projectDescriptor->getSettings()->clearModifiedFlag();
274 1
        $mapper->save($projectDescriptor);
275
276 1
        $output->writeln($this->__('PPCPP:LOG-OK'));
277
278 1
        return 0;
279
    }
280
281
    /**
282
     * Returns the collection of files based on the input and configuration.
283
     *
284
     * @param InputInterface $input
285
     *
286
     * @return File[]
287
     * @throws \InvalidArgumentException
288
     */
289 1
    protected function getFileCollection($input): array
290
    {
291
        /** @var ConfigurationHelper $configurationHelper */
292 1
        $configurationHelper = $this->getHelper('phpdocumentor_configuration');
293
294 1
        $ignoreHidden = $configurationHelper->getOption($input, 'hidden', 'files/ignore-hidden', 'off');
295 1
        $file_options = (array) $configurationHelper->getOption($input, 'filename', 'files/files', [], true);
296 1
        $directory_options = $configurationHelper->getOption($input, 'directory', 'files/directories', [], true);
297
298 1
        $ignorePaths = array_map(
299 1
            function ($value) {
300
                if (substr($value, -1) === '*') {
301
                    return substr($value, 0, -1);
302
                }
303
304
                return $value;
305 1
            },
306 1
            $configurationHelper->getOption($input, 'ignore', 'files/ignore', [], true)
307
        );
308
309
        //TODO: Fix this, should we support symlinks? Or just print an error here.
310 1
        if ($configurationHelper->getOption($input, 'ignore-symlinks', 'files/ignore-symlinks', 'off') === 'on') {
311
            echo 'Symlinks are not supported';
312
        }
313
314 1
        $files = [];
315
316 1
        foreach ($file_options as $file) {
317
            $files[] = new File\LocalFile($file);
318
        }
319
320 1
        foreach ($directory_options as $option) {
321
            $files = array_merge(
322
                $files,
323
                $this->fileCollector->getFiles(
324
                    new Dsn('file://' . realpath($option)),
325
                    $directory_options,
326
                    [
327
                        'paths' => $ignorePaths,
328
                        'hidden' => $ignoreHidden !== 'off' && $ignoreHidden === false,
329
                    ],
330
                    $configurationHelper->getOption(
331
                        $input,
332
                        'extensions',
333
                        'parser/extensions',
334
                        ['php', 'php3', 'phtml'],
335
                        true
336
                    )
337
                )
338
            );
339
        }
340
341 1
        return $files;
342
    }
343
344
    /**
345
     * Adds the parser.file.pre event to the advance the progressbar.
346
     *
347
     * @return Symfony\Component\Console\Helper\HelperInterface|null
348
     */
349 1
    protected function getProgressBar(InputInterface $input)
350
    {
351 1
        $progress = parent::getProgressBar($input);
352 1
        if (!$progress) {
353 1
            return null;
354
        }
355
356
        $this->getService('event_dispatcher')->addListener(
357
            'parser.file.pre',
358
            function (PreFileEvent $event) use ($progress) {
0 ignored issues
show
The parameter $event is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
359
                $progress->advance();
360
            }
361
        );
362
363
        return $progress;
364
    }
365
366
    /**
367
     * Translates the provided text and replaces any contained parameters using printf notation.
368
     *
369
     * @param string $text
370
     * @param string[] $parameters
371
     *
372
     * @return string
373
     */
374
    // @codingStandardsIgnoreStart
375 1
    protected function __($text, $parameters = [])
376
    {
377
        // @codingStandardsIgnoreEnd
378 1
        return vsprintf($this->translator->translate($text), $parameters);
379
    }
380
381 1
    protected function populateParser(InputInterface $input)
382
    {
383
        /** @var ConfigurationHelper $configurationHelper */
384 1
        $configurationHelper = $this->getHelper('phpdocumentor_configuration');
385
386 1
        $title = (string) $configurationHelper->getOption($input, 'title', 'title');
387 1
        $this->getBuilder()->getProjectDescriptor()->setName($title ?: 'API Documentation');
388 1
        $parserPopulator = new ParserPopulator();
389 1
        $parserPopulator->populate(
390 1
            $this->getParser(),
391 1
            $input,
392 1
            $configurationHelper
393
        );
394 1
    }
395
396
    private function getCacheMiddleware(): CacheMiddleware
397
    {
398
        return $this->getContainer()->offsetGet('parser.middleware.cache');
399
    }
400
}
401