ApiDocBuilder   B
last analyzed

Complexity

Total Complexity 44

Size/Duplication

Total Lines 331
Duplicated Lines 0 %

Importance

Changes 6
Bugs 1 Features 0
Metric Value
eloc 137
c 6
b 1
f 0
dl 0
loc 331
rs 8.8798
wmc 44

16 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A build() 0 6 1
A parseFunctions() 0 10 4
A log() 0 4 2
B setupReflection() 0 48 7
A addExtension() 0 4 1
A parseTraits() 0 14 3
A parseConstants() 0 10 4
A createDirectoryStructure() 0 9 4
A buildIndexes() 0 31 4
A parseInterfaces() 0 14 3
A parseFiles() 0 19 3
A setDebugOutput() 0 3 1
A debug() 0 4 2
A parseClasses() 0 14 3
A setVerboseOutput() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like ApiDocBuilder often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ApiDocBuilder, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @copyright Copyright (c) 2017 Julius Härtl <[email protected]>
4
 * @author    Julius Härtl <[email protected]>
5
 * @license   GNU AGPL version 3 or any later version
6
 *
7
 *  This program is free software: you can redistribute it and/or modify
8
 *  it under the terms of the GNU Affero General Public License as
9
 *  published by the Free Software Foundation, either version 3 of the
10
 *  License, or (at your option) any later version.
11
 *
12
 *  This program is distributed in the hope that it will be useful,
13
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
 *  GNU Affero General Public License for more details.
16
 *
17
 *  You should have received a copy of the GNU Affero General Public License
18
 *  along with this program. If not, see <http://www.gnu.org/licenses/>.
19
 */
20
21
namespace JuliusHaertl\PHPDocToRst;
22
23
use Exception;
24
use JuliusHaertl\PHPDocToRst\Builder\ClassFileBuilder;
25
use JuliusHaertl\PHPDocToRst\Builder\InterfaceFileBuilder;
26
use JuliusHaertl\PHPDocToRst\Builder\MainIndexBuilder;
27
use JuliusHaertl\PHPDocToRst\Builder\NamespaceIndexBuilder;
28
use JuliusHaertl\PHPDocToRst\Builder\PhpDomainBuilder;
29
use JuliusHaertl\PHPDocToRst\Builder\TraitFileBuilder;
30
use JuliusHaertl\PHPDocToRst\Extension\Extension;
31
use JuliusHaertl\PHPDocToRst\Middleware\ErrorHandlingMiddleware;
32
use phpDocumentor\Reflection\DocBlockFactory;
33
use phpDocumentor\Reflection\File\LocalFile;
34
use phpDocumentor\Reflection\Php\Factory;
35
use phpDocumentor\Reflection\Php\Namespace_;
36
use phpDocumentor\Reflection\Php\NodesFactory;
37
use phpDocumentor\Reflection\Php\Project;
38
use phpDocumentor\Reflection\Php\ProjectFactory;
39
use PhpParser\PrettyPrinter\Standard as PrettyPrinter;
40
use RecursiveDirectoryIterator;
41
use RecursiveIteratorIterator;
42
43
/**
44
 * This class is used to parse a project tree and generate rst files
45
 * for all of the containing PHP structures.
46
 *
47
 * Example usage is documented in examples/example.php
48
 */
49
final class ApiDocBuilder
50
{
51
    /** @var Project */
52
    private $project;
53
54
    /** @var array */
55
    private $docFiles = [];
56
57
    /** @var array */
58
    private $constants = [];
59
60
    /** @var array */
61
    private $functions = [];
62
63
    /** @var Extension[] */
64
    private $extensions = [];
65
66
    /** @var string[] */
67
    private $extensionNames = [];
68
69
    /** @var array[] */
70
    private $extensionArguments = [];
71
72
    /** @var string[] */
73
    private $srcDir = [];
74
75
    /** @var string */
76
    private $dstDir;
77
78
    /** @var bool */
79
    private $verboseOutput = false;
80
81
    /** @var bool */
82
    private $debugOutput = false;
83
84
    /**
85
     * ApiDocBuilder constructor.
86
     *
87
     * @param string[] $srcDir array of paths that should be analysed
88
     * @param string   $dstDir path where the output documentation should be stored
89
     */
90
    public function __construct($srcDir, $dstDir)
91
    {
92
        $this->dstDir = $dstDir;
93
        $this->srcDir = (array) $srcDir;
94
    }
95
96
    /**
97
     * Run this to build the documentation.
98
     */
99
    public function build()
100
    {
101
        $this->setupReflection();
102
        $this->createDirectoryStructure();
103
        $this->parseFiles();
104
        $this->buildIndexes();
105
    }
106
107
    /* hacky logging for cli */
108
109
    /**
110
     * @throws Exception
111
     */
112
    private function setupReflection()
113
    {
114
        $interfaceList = [];
115
        $this->log('Start parsing files.');
116
        foreach ($this->srcDir as $srcDir) {
117
            $dir = new RecursiveDirectoryIterator($srcDir);
118
            $files = new RecursiveIteratorIterator($dir);
119
120
            foreach ($files as $file) {
121
                if ($file->isDir()) {
122
                    continue;
123
                }
124
125
                try {
126
                    $interfaceList[] = new LocalFile($file->getPathname());
127
                } catch (Exception $e) {
128
                    $this->log('Failed to load '.$file->getPathname().PHP_EOL);
129
                }
130
            }
131
        }
132
133
        $projectFactory = new ProjectFactory([
134
            new Factory\Argument(new PrettyPrinter()),
135
            new Factory\Class_(),
136
            new Factory\Define(new PrettyPrinter()),
137
            new Factory\GlobalConstant(new PrettyPrinter()),
138
            new Factory\ClassConstant(new PrettyPrinter()),
139
            new Factory\DocBlock(DocBlockFactory::createInstance()),
140
            new Factory\File(NodesFactory::createInstance(), [
141
                new ErrorHandlingMiddleware($this),
142
            ]),
143
            new Factory\Function_(),
144
            new Factory\Interface_(),
145
            new Factory\Method(),
146
            new Factory\Property(new PrettyPrinter()),
147
            new Factory\Trait_(),
148
        ]);
149
        $this->project = $projectFactory->create('MyProject', $interfaceList);
150
        $this->log('Successfully parsed files.');
151
152
        // load extensions
153
        foreach ($this->extensionNames as $extensionName) {
154
            $extension = new $extensionName($this->project, $this->extensionArguments[$extensionName]);
155
            if (!is_subclass_of($extension, Extension::class)) {
156
                $this->log('Failed to load extension '.$extensionName.'.');
157
            }
158
            $this->extensions[] = $extension;
159
            $this->log('Extension '.$extensionName.' loaded.');
160
        }
161
    }
162
163
    /**
164
     * Log a message.
165
     *
166
     * @param string $message Message to be logged
167
     */
168
    public function log($message)
169
    {
170
        if ($this->verboseOutput) {
171
            echo $message.PHP_EOL;
172
        }
173
    }
174
175
    /**
176
     * Create directory structure for the rst output.
177
     *
178
     * @throws WriteException
179
     */
180
    private function createDirectoryStructure()
181
    {
182
        foreach ($this->project->getNamespaces() as $namespace) {
183
            $namespaceDir = $this->dstDir.str_replace('\\', '/', $namespace->getFqsen());
184
            if (is_dir($namespaceDir)) {
185
                continue;
186
            }
187
            if (!mkdir($namespaceDir, 0755, true)) {
188
                throw new WriteException('Could not create directory '.$namespaceDir);
189
            }
190
        }
191
    }
192
193
    private function parseFiles()
194
    {
195
        /** @var Extension $extension */
196
        foreach ($this->extensions as $extension) {
197
            $extension->prepare();
198
        }
199
200
        $this->log('Start building files');
201
202
        foreach ($this->project->getFiles() as $file) {
203
            /*
204
             * Go though interfaces/classes/functions of files and build documentation
205
             */
206
207
            $this->parseInterfaces($file);
208
            $this->parseClasses($file);
209
            $this->parseTraits($file);
210
            $this->parseFunctions($file);
211
            $this->parseConstants($file);
212
        }
213
    }
214
215
    /**
216
     * Log a debug message.
217
     *
218
     * @param string $message Message to be logged
219
     */
220
    public function debug($message)
221
    {
222
        if ($this->debugOutput) {
223
            echo $message.PHP_EOL;
224
        }
225
    }
226
227
    private function buildIndexes()
228
    {
229
        $this->log('Build indexes.');
230
        $namespaces = $this->project->getNamespaces();
231
        $namespaces['\\'] = $this->project->getRootNamespace();
232
        usort($namespaces, function (Namespace_ $a, Namespace_ $b) {
233
            return strcmp($a->getFqsen(), $b->getFqsen());
234
        });
235
        /** @var Namespace_ $namespace */
236
        foreach ($namespaces as $namespace) {
237
            $fqsen = (string) $namespace->getFqsen();
238
            $this->debug('Build namespace index for '.$fqsen);
239
            $functions = [];
240
            $constants = [];
241
            if (array_key_exists($fqsen, $this->functions)) {
242
                $functions = $this->functions[$fqsen];
243
            }
244
            if (array_key_exists($fqsen, $this->constants)) {
245
                $constants = $this->constants[$fqsen];
246
            }
247
            $builder = new NamespaceIndexBuilder($this->extensions, $namespaces, $namespace, $functions, $constants);
248
            $builder->render();
249
            $path = $this->dstDir.str_replace('\\', '/', $fqsen).'/index.rst';
250
            file_put_contents($path, $builder->getContent());
251
        }
252
253
        $this->log('Build main index files.');
254
        $builder = new MainIndexBuilder($namespaces);
255
        $builder->render();
256
        $path = $this->dstDir.'/index-namespaces-all.rst';
257
        file_put_contents($path, $builder->getContent());
258
    }
259
260
    /**
261
     * Enable verbose logging output.
262
     *
263
     * @param bool $v Set to true to enable
264
     */
265
    public function setVerboseOutput($v)
266
    {
267
        $this->verboseOutput = $v;
268
    }
269
270
    /**
271
     * Enable debug logging output.
272
     *
273
     * @param bool $v Set to true to enable
274
     */
275
    public function setDebugOutput($v)
276
    {
277
        $this->debugOutput = $v;
278
    }
279
280
    /**
281
     * @param string $class name of the extension class
282
     *
283
     * @throws Exception
284
     */
285
    public function addExtension($class, $arguments = [])
286
    {
287
        $this->extensionNames[] = $class;
288
        $this->extensionArguments[$class] = $arguments;
289
    }
290
291
    /**
292
     * @param \phpDocumentor\Reflection\Php\File $file
293
     */
294
    private function parseInterfaces(\phpDocumentor\Reflection\Php\File $file): void
295
    {
296
        foreach ($file->getInterfaces() as $interface) {
297
            $fqsen = $interface->getFqsen();
298
            $builder = new InterfaceFileBuilder($file, $interface, $this->extensions);
299
            $filename = $this->dstDir.str_replace('\\', '/', $fqsen).'.rst';
300
            file_put_contents($filename, $builder->getContent());
301
            $this->docFiles[(string) $interface->getFqsen()] = str_replace('\\', '/', $fqsen);
302
303
            // also build root namespace in indexes
304
            if (strpos((string) substr($fqsen, 1), '\\') === false) {
305
                $this->project->getRootNamespace()->addInterface($fqsen);
306
            }
307
            $this->debug('Written interface documentation to '.$filename);
308
        }
309
    }
310
311
    /**
312
     * @param \phpDocumentor\Reflection\Php\File $file
313
     */
314
    private function parseClasses(\phpDocumentor\Reflection\Php\File $file): void
315
    {
316
        foreach ($file->getClasses() as $class) {
317
            $fqsen = $class->getFqsen();
318
            $builder = new ClassFileBuilder($file, $class, $this->extensions);
319
            $filename = $this->dstDir.str_replace('\\', '/', $fqsen).'.rst';
320
            file_put_contents($filename, $builder->getContent());
321
            $this->docFiles[(string) $class->getFqsen()] = str_replace('\\', '/', $fqsen);
322
323
            // also build root namespace in indexes
324
            if (strpos((string) substr($class->getFqsen(), 1), '\\') === false) {
325
                $this->project->getRootNamespace()->addClass($fqsen);
326
            }
327
            $this->debug('Written class documentation to '.$filename);
328
        }
329
    }
330
331
    /**
332
     * @param \phpDocumentor\Reflection\Php\File $file
333
     */
334
    private function parseTraits(\phpDocumentor\Reflection\Php\File $file): void
335
    {
336
        foreach ($file->getTraits() as $trait) {
337
            $fqsen = $trait->getFqsen();
338
            $builder = new TraitFileBuilder($file, $trait, $this->extensions);
339
            $filename = $this->dstDir.str_replace('\\', '/', $fqsen).'.rst';
340
            file_put_contents($filename, $builder->getContent());
341
            $this->docFiles[(string) $trait->getFqsen()] = str_replace('\\', '/', $fqsen);
342
343
            // also build root namespace in indexes
344
            if (strpos((string) substr($fqsen, 1), '\\') === false) {
345
                $this->project->getRootNamespace()->addTrait($fqsen);
346
            }
347
            $this->debug('Written trait documentation to '.$filename);
348
        }
349
    }
350
351
    /**
352
     * @param \phpDocumentor\Reflection\Php\File $file
353
     */
354
    private function parseFunctions(\phpDocumentor\Reflection\Php\File $file): void
355
    {
356
        // build array of functions per namespace
357
        foreach ($file->getFunctions() as $function) {
358
            $namespace = substr(PhpDomainBuilder::getNamespace($function), 0, -2);
359
            $namespace = $namespace === '' ? '\\' : $namespace;
360
            if (!array_key_exists($namespace, $this->functions)) {
361
                $this->functions[$namespace] = [];
362
            }
363
            $this->functions[$namespace][] = $function;
364
        }
365
    }
366
367
    /**
368
     * @param \phpDocumentor\Reflection\Php\File $file
369
     */
370
    private function parseConstants(\phpDocumentor\Reflection\Php\File $file): void
371
    {
372
        // build array of constants per namespace
373
        foreach ($file->getConstants() as $constant) {
374
            $namespace = PhpDomainBuilder::getNamespace($constant);
375
            $namespace = $namespace === '' ? '\\' : $namespace;
376
            if (!array_key_exists($namespace, $this->constants)) {
377
                $this->constants[$namespace] = [];
378
            }
379
            $this->constants[$namespace][] = $constant;
380
        }
381
    }
382
}
383