Passed
Push — master ( 4b6191...118b18 )
by Julius
01:48
created

ApiDocBuilder::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
c 0
b 0
f 0
nc 1
nop 2
dl 0
loc 3
rs 10
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 string[] */
72
    private $srcDir;
73
74
    /** @var string */
75
    private $dstDir;
76
77
    /** @var bool */
78
    private $verboseOutput = false;
79
80
    /** @var bool */
81
    private $debugOutput = false;
82
83
    /**
84
     * ApiDocBuilder constructor.
85
     *
86
     * @param string[] $srcDir array of paths that should be analysed
87
     * @param string $dstDir path where the output documentation should be stored
88
     */
89
    public function __construct($srcDir, $dstDir) {
90
        $this->dstDir = $dstDir;
91
        $this->srcDir = (array)$srcDir;
92
    }
93
94
    /**
95
     * Run this to build the documentation
96
     */
97
    public function build() {
98
        $this->setupReflection();
99
        $this->createDirectoryStructure();
100
        $this->parseFiles();
101
        $this->buildIndexes();
102
    }
103
104
    /* hacky logging for cli */
105
106
    /**
107
     * Enable verbose logging output
108
     *
109
     * @param bool $v Set to true to enable
110
     */
111
    public function setVerboseOutput($v) {
112
        $this->verboseOutput = $v;
113
    }
114
115
    /**
116
     * Enable debug logging output
117
     *
118
     * @param bool $v Set to true to enable
119
     */
120
    public function setDebugOutput($v) {
121
        $this->debugOutput = $v;
122
    }
123
124
    /**
125
     * Log a message
126
     *
127
     * @param string $message Message to be logged
128
     */
129
    public function log($message) {
130
        if ($this->verboseOutput) {
131
            echo $message . PHP_EOL;
132
        }
133
    }
134
135
    /**
136
     * Log a debug message
137
     *
138
     * @param string $message Message to be logged
139
     */
140
    public function debug($message) {
141
        if ($this->debugOutput) {
142
            echo $message . PHP_EOL;
143
        }
144
    }
145
146
    /**
147
     * @throws \Exception
148
     */
149
    private function setupReflection() {
150
151
        $interfaceList = [];
152
        $this->log('Start parsing files.');
153
        foreach ($this->srcDir as $srcDir) {
154
            $dir = new \RecursiveDirectoryIterator($srcDir);
155
            $files = new \RecursiveIteratorIterator($dir);
156
157
            foreach ($files as $file) {
158
                if ($file->isDir()) {
159
                    continue;
160
                }
161
                try {
162
                    $interfaceList[] = new LocalFile($file->getPathname());
163
                } catch (\Exception $e) {
164
                    $this->log('Failed to load ' . $file->getPathname() . PHP_EOL);
165
                }
166
            }
167
        }
168
169
        $projectFactory = new ProjectFactory([
170
            new Factory\Argument(new PrettyPrinter()),
171
            new Factory\Class_(),
172
            new Factory\Constant(new PrettyPrinter()),
173
            new Factory\DocBlock(DocBlockFactory::createInstance()),
174
            new Factory\File(NodesFactory::createInstance(),
175
                [
176
                    new ErrorHandlingMiddleware($this)
177
                ]),
178
            new Factory\Function_(),
179
            new Factory\Interface_(),
180
            new Factory\Method(),
181
            new Factory\Property(new PrettyPrinter()),
182
            new Factory\Trait_(),
183
        ]);
184
        $this->project = $projectFactory->create('MyProject', $interfaceList);
185
        $this->log('Successfully parsed files.');
186
187
        // load extensions
188
        foreach ($this->extensionNames as $extensionName) {
189
            $extension = new $extensionName($this->project);
190
            if (!is_subclass_of($extension, Extension::class)) {
191
                $this->log('Failed to load extension ' . $extensionName . '.');
192
            }
193
            $this->extensions[] = $extension;
194
            $this->log('Extension ' . $extensionName . ' loaded.');
195
        }
196
    }
197
198
    /**
199
     * @param string $class name of the extension class
200
     * @throws \Exception
201
     */
202
    public function addExtension($class) {
203
        $this->extensionNames[] = $class;
204
    }
205
206
    /**
207
     * Create directory structure for the rst output
208
     * @throws \Exception
209
     */
210
    private function createDirectoryStructure() {
211
        foreach ($this->project->getNamespaces() as $namespace) {
212
            $namespaceDir = $this->dstDir . str_replace('\\', '/', $namespace->getFqsen());
213
            if (is_dir($namespaceDir)) {
214
                continue;
215
            }
216
            if (!mkdir($namespaceDir, 0755, true)) {
217
                throw new WriteException('Could not create directory ' . $namespaceDir);
218
            }
219
        }
220
    }
221
222
    private function parseFiles() {
223
        /** @var Extension $extension */
224
        foreach ($this->extensions as $extension) {
225
            $extension->prepare();
226
        }
227
        $this->log('Start building files');
228
        foreach ($this->project->getFiles() as $file) {
229
            /**
230
             * Go though interfaces/classes/functions of files and build documentation
231
             */
232 View Code Duplication
            foreach ($file->getInterfaces() as $interface) {
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...
233
                $fqsen = $interface->getFqsen();
234
                $builder = new InterfaceFileBuilder($file, $interface, $this->extensions);
235
                $filename = $this->dstDir . str_replace('\\', '/', $fqsen) . '.rst';
236
                file_put_contents($filename, $builder->getContent());
237
                $this->docFiles[(string)$interface->getFqsen()] = str_replace('\\', '/', $fqsen);
238
239
                // also build root namespace in indexes
240
                if (strpos((string)substr($fqsen, 1), '\\') === false) {
241
                    $this->project->getRootNamespace()->addInterface($fqsen);
242
                }
243
                $this->debug('Written interface documentation to ' . $filename);
244
            }
245
246 View Code Duplication
            foreach ($file->getClasses() as $class) {
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...
247
                $fqsen = $class->getFqsen();
248
                $builder = new ClassFileBuilder($file, $class, $this->extensions);
249
                $filename = $this->dstDir . str_replace('\\', '/', $fqsen) . '.rst';
250
                file_put_contents($filename, $builder->getContent());
251
                $this->docFiles[(string)$class->getFqsen()] = str_replace('\\', '/', $fqsen);
252
253
                // also build root namespace in indexes
254
                if (strpos((string)substr($class->getFqsen(), 1), '\\') === false) {
255
                    $this->project->getRootNamespace()->addClass($fqsen);
256
                }
257
                $this->debug('Written class documentation to ' . $filename);
258
            }
259
260 View Code Duplication
            foreach ($file->getTraits() as $trait) {
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...
261
                $fqsen = $trait->getFqsen();
262
                $builder = new TraitFileBuilder($file, $trait, $this->extensions);
263
                $filename = $this->dstDir . str_replace('\\', '/', $fqsen) . '.rst';
264
                file_put_contents($filename, $builder->getContent());
265
                $this->docFiles[(string)$trait->getFqsen()] = str_replace('\\', '/', $fqsen);
266
267
                // also build root namespace in indexes
268
                if (strpos((string)substr($fqsen, 1), '\\') === false) {
269
                    $this->project->getRootNamespace()->addTrait($fqsen);
270
                }
271
                $this->debug('Written trait documentation to ' . $filename);
272
            }
273
274
            // build array of functions per namespace
275 View Code Duplication
            foreach ($file->getFunctions() as $function) {
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...
276
                $namespace = substr(PhpDomainBuilder::getNamespace($function), 0, -2);
277
                $namespace = $namespace === '' ? '\\' : $namespace;
278
                if (!array_key_exists($namespace, $this->functions)) {
279
                    $this->functions[$namespace] = [];
280
                }
281
                $this->functions[$namespace][] = $function;
282
            }
283
            // build array of constants per namespace
284 View Code Duplication
            foreach ($file->getConstants() as $constant) {
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...
285
                $namespace = PhpDomainBuilder::getNamespace($constant);
286
                $namespace = $namespace === '' ? '\\' : $namespace;
287
                if (!array_key_exists($namespace, $this->constants)) {
288
                    $this->constants[$namespace] = [];
289
                }
290
                $this->constants[$namespace][] = $constant;
291
            }
292
293
        }
294
    }
295
296
    private function buildIndexes() {
297
        $this->log('Build indexes.');
298
        $namespaces = $this->project->getNamespaces();
299
        $namespaces['\\'] = $this->project->getRootNamespace();
300
        usort($namespaces, function (Namespace_ $a, Namespace_ $b) {
301
            return strcmp($a->getFqsen(), $b->getFqsen());
302
        });
303
        /** @var Namespace_ $namespace */
304
        foreach ($namespaces as $namespace) {
305
            $fqsen = (string)$namespace->getFqsen();
306
            $this->debug('Build namespace index for ' . $fqsen);
307
            $functions = [];
308
            $constants = [];
309
            if (array_key_exists($fqsen, $this->functions)) {
310
                $functions = $this->functions[$fqsen];
311
            }
312
            if (array_key_exists($fqsen, $this->constants)) {
313
                $constants = $this->constants[$fqsen];
314
            }
315
            $builder = new NamespaceIndexBuilder($this->extensions, $namespaces, $namespace, $functions, $constants);
316
            $builder->render();
317
            $path = $this->dstDir . str_replace('\\', '/', $fqsen) . '/index.rst';
318
            file_put_contents($path, $builder->getContent());
319
        }
320
321
        $this->log('Build main index files.');
322
        $builder = new MainIndexBuilder($namespaces);
323
        $builder->render();
324
        $path = $this->dstDir . '/index-namespaces-all.rst';
325
        file_put_contents($path, $builder->getContent());
326
    }
327
}
328