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) { |
|
|
|
|
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) { |
|
|
|
|
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) { |
|
|
|
|
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) { |
|
|
|
|
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) { |
|
|
|
|
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
|
|
|
|
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.