ApiDocBuilder   B
last analyzed

Complexity

Total Complexity 39

Size/Duplication

Total Lines 280
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 280
rs 8.2857
c 0
b 0
f 0
wmc 39

11 Methods

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