Test Failed
Push — master ( 105c52...1d45cb )
by Francesco
05:40 queued 04:31
created

ApiDocBuilder::setupReflection()   B

Complexity

Conditions 7
Paths 15

Size

Total Lines 46
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
eloc 32
c 1
b 0
f 0
nc 15
nop 0
dl 0
loc 46
rs 8.4746
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 phpDocumentor\Reflection\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\Constant(new PrettyPrinter()),
137
            new Factory\DocBlock(DocBlockFactory::createInstance()),
138
            new Factory\File(NodesFactory::createInstance(), [
139
                    new ErrorHandlingMiddleware($this),
140
                ]),
141
            new Factory\Function_(),
142
            new Factory\Interface_(),
143
            new Factory\Method(),
144
            new Factory\Property(new PrettyPrinter()),
145
            new Factory\Trait_(),
146
        ]);
147
        $this->project = $projectFactory->create('MyProject', $interfaceList);
148
        $this->log('Successfully parsed files.');
149
150
        // load extensions
151
        foreach ($this->extensionNames as $extensionName) {
152
            $extension = new $extensionName($this->project, $this->extensionArguments[$extensionName]);
153
            if (!is_subclass_of($extension, Extension::class)) {
154
                $this->log('Failed to load extension '.$extensionName.'.');
155
            }
156
            $this->extensions[] = $extension;
157
            $this->log('Extension '.$extensionName.' loaded.');
158
        }
159
    }
160
161
    /**
162
     * Log a message.
163
     *
164
     * @param string $message Message to be logged
165
     */
166
    public function log($message)
167
    {
168
        if ($this->verboseOutput) {
169
            echo $message.PHP_EOL;
170
        }
171
    }
172
173
    /**
174
     * Create directory structure for the rst output.
175
     *
176
     * @throws WriteException
177
     */
178
    private function createDirectoryStructure()
179
    {
180
        foreach ($this->project->getNamespaces() as $namespace) {
181
            $namespaceDir = $this->dstDir.str_replace('\\', '/', $namespace->getFqsen());
182
            if (is_dir($namespaceDir)) {
183
                continue;
184
            }
185
            if (!mkdir($namespaceDir, 0755, true)) {
186
                throw new WriteException('Could not create directory '.$namespaceDir);
187
            }
188
        }
189
    }
190
191
    private function parseFiles()
192
    {
193
        /** @var Extension $extension */
194
        foreach ($this->extensions as $extension) {
195
            $extension->prepare();
196
        }
197
198
        $this->log('Start building files');
199
200
        foreach ($this->project->getFiles() as $file) {
201
            /*
202
             * Go though interfaces/classes/functions of files and build documentation
203
             */
204
205
            $this->parseInterfaces($file);
206
            $this->parseClasses($file);
207
            $this->parseTraits($file);
208
            $this->parseFunctions($file);
209
            $this->parseConstants($file);
210
211
        }
212
    }
213
214
    /**
215
     * Log a debug message.
216
     *
217
     * @param string $message Message to be logged
218
     */
219
    public function debug($message)
220
    {
221
        if ($this->debugOutput) {
222
            echo $message.PHP_EOL;
223
        }
224
    }
225
226
    private function buildIndexes()
227
    {
228
        $this->log('Build indexes.');
229
        $namespaces = $this->project->getNamespaces();
230
        $namespaces['\\'] = $this->project->getRootNamespace();
231
        usort($namespaces, function (Namespace_ $a, Namespace_ $b) {
232
            return strcmp($a->getFqsen(), $b->getFqsen());
233
        });
234
        /** @var Namespace_ $namespace */
235
        foreach ($namespaces as $namespace) {
236
            $fqsen = (string) $namespace->getFqsen();
237
            $this->debug('Build namespace index for '.$fqsen);
238
            $functions = [];
239
            $constants = [];
240
            if (array_key_exists($fqsen, $this->functions)) {
241
                $functions = $this->functions[$fqsen];
242
            }
243
            if (array_key_exists($fqsen, $this->constants)) {
244
                $constants = $this->constants[$fqsen];
245
            }
246
            $builder = new NamespaceIndexBuilder($this->extensions, $namespaces, $namespace, $functions, $constants);
247
            $builder->render();
248
            $path = $this->dstDir.str_replace('\\', '/', $fqsen).'/index.rst';
249
            file_put_contents($path, $builder->getContent());
250
        }
251
252
        $this->log('Build main index files.');
253
        $builder = new MainIndexBuilder($namespaces);
254
        $builder->render();
255
        $path = $this->dstDir.'/index-namespaces-all.rst';
256
        file_put_contents($path, $builder->getContent());
257
    }
258
259
    /**
260
     * Enable verbose logging output.
261
     *
262
     * @param bool $v Set to true to enable
263
     */
264
    public function setVerboseOutput($v)
265
    {
266
        $this->verboseOutput = $v;
267
    }
268
269
    /**
270
     * Enable debug logging output.
271
     *
272
     * @param bool $v Set to true to enable
273
     */
274
    public function setDebugOutput($v)
275
    {
276
        $this->debugOutput = $v;
277
    }
278
279
    /**
280
     * @param string $class name of the extension class
281
     *
282
     * @throws Exception
283
     */
284
    public function addExtension($class, $arguments = [])
285
    {
286
        $this->extensionNames[] = $class;
287
        $this->extensionArguments[$class] = $arguments;
288
    }
289
290
    /**
291
     * @param \phpDocumentor\Reflection\Php\File $file
292
     */
293
    private function parseInterfaces(\phpDocumentor\Reflection\Php\File $file): void
294
    {
295
        foreach ($file->getInterfaces() as $interface) {
296
            $fqsen = $interface->getFqsen();
297
            $builder = new InterfaceFileBuilder($file, $interface, $this->extensions);
298
            $filename = $this->dstDir.str_replace('\\', '/', $fqsen).'.rst';
299
            file_put_contents($filename, $builder->getContent());
300
            $this->docFiles[(string) $interface->getFqsen()] = str_replace('\\', '/', $fqsen);
301
302
            // also build root namespace in indexes
303
            if (strpos((string) substr($fqsen, 1), '\\') === false) {
304
                $this->project->getRootNamespace()->addInterface($fqsen);
305
            }
306
            $this->debug('Written interface documentation to '.$filename);
307
        }
308
    }
309
310
    /**
311
     * @param \phpDocumentor\Reflection\Php\File $file
312
     */
313
    private function parseClasses(\phpDocumentor\Reflection\Php\File $file): void
314
    {
315
        foreach ($file->getClasses() as $class) {
316
            $fqsen = $class->getFqsen();
317
            $builder = new ClassFileBuilder($file, $class, $this->extensions);
318
            $filename = $this->dstDir.str_replace('\\', '/', $fqsen).'.rst';
319
            file_put_contents($filename, $builder->getContent());
320
            $this->docFiles[(string) $class->getFqsen()] = str_replace('\\', '/', $fqsen);
321
322
            // also build root namespace in indexes
323
            if (strpos((string) substr($class->getFqsen(), 1), '\\') === false) {
324
                $this->project->getRootNamespace()->addClass($fqsen);
325
            }
326
            $this->debug('Written class documentation to '.$filename);
327
        }
328
    }
329
330
    /**
331
     * @param \phpDocumentor\Reflection\Php\File $file
332
     */
333
    private function parseTraits(\phpDocumentor\Reflection\Php\File $file): void
334
    {
335
        foreach ($file->getTraits() as $trait) {
336
            $fqsen = $trait->getFqsen();
337
            $builder = new TraitFileBuilder($file, $trait, $this->extensions);
338
            $filename = $this->dstDir.str_replace('\\', '/', $fqsen).'.rst';
339
            file_put_contents($filename, $builder->getContent());
340
            $this->docFiles[(string) $trait->getFqsen()] = str_replace('\\', '/', $fqsen);
341
342
            // also build root namespace in indexes
343
            if (strpos((string) substr($fqsen, 1), '\\') === false) {
344
                $this->project->getRootNamespace()->addTrait($fqsen);
345
            }
346
            $this->debug('Written trait documentation to '.$filename);
347
        }
348
    }
349
350
    /**
351
     * @param \phpDocumentor\Reflection\Php\File $file
352
     */
353
    private function parseFunctions(\phpDocumentor\Reflection\Php\File $file) :void
354
    {
355
        // build array of functions per namespace
356
        foreach ($file->getFunctions() as $function) {
357
            $namespace = substr(PhpDomainBuilder::getNamespace($function), 0, -2);
358
            $namespace = $namespace === '' ? '\\' : $namespace;
359
            if (!array_key_exists($namespace, $this->functions)) {
360
                $this->functions[$namespace] = [];
361
            }
362
            $this->functions[$namespace][] = $function;
363
        }
364
    }
365
366
    /**
367
     * @param \phpDocumentor\Reflection\Php\File $file
368
     */
369
    private function parseConstants(\phpDocumentor\Reflection\Php\File $file): void
370
    {
371
        // build array of constants per namespace
372
        foreach ($file->getConstants() as $constant) {
373
            $namespace = PhpDomainBuilder::getNamespace($constant);
374
            $namespace = $namespace === '' ? '\\' : $namespace;
375
            if (!array_key_exists($namespace, $this->constants)) {
376
                $this->constants[$namespace] = [];
377
            }
378
            $this->constants[$namespace][] = $constant;
379
        }
380
    }
381
}
382