Chris@0
|
1 Usage of basic components
|
Chris@0
|
2 =========================
|
Chris@0
|
3
|
Chris@0
|
4 This document explains how to use the parser, the pretty printer and the node traverser.
|
Chris@0
|
5
|
Chris@0
|
6 Bootstrapping
|
Chris@0
|
7 -------------
|
Chris@0
|
8
|
Chris@0
|
9 To bootstrap the library, include the autoloader generated by composer:
|
Chris@0
|
10
|
Chris@0
|
11 ```php
|
Chris@0
|
12 require 'path/to/vendor/autoload.php';
|
Chris@0
|
13 ```
|
Chris@0
|
14
|
Chris@0
|
15 Additionally you may want to set the `xdebug.max_nesting_level` ini option to a higher value:
|
Chris@0
|
16
|
Chris@0
|
17 ```php
|
Chris@0
|
18 ini_set('xdebug.max_nesting_level', 3000);
|
Chris@0
|
19 ```
|
Chris@0
|
20
|
Chris@0
|
21 This ensures that there will be no errors when traversing highly nested node trees. However, it is
|
Chris@0
|
22 preferable to disable XDebug completely, as it can easily make this library more than five times
|
Chris@0
|
23 slower.
|
Chris@0
|
24
|
Chris@0
|
25 Parsing
|
Chris@0
|
26 -------
|
Chris@0
|
27
|
Chris@0
|
28 In order to parse code, you first have to create a parser instance:
|
Chris@0
|
29
|
Chris@0
|
30 ```php
|
Chris@0
|
31 use PhpParser\ParserFactory;
|
Chris@0
|
32 $parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
|
Chris@0
|
33 ```
|
Chris@0
|
34
|
Chris@0
|
35 The factory accepts a kind argument, that determines how different PHP versions are treated:
|
Chris@0
|
36
|
Chris@0
|
37 Kind | Behavior
|
Chris@0
|
38 -----|---------
|
Chris@0
|
39 `ParserFactory::PREFER_PHP7` | Try to parse code as PHP 7. If this fails, try to parse it as PHP 5.
|
Chris@0
|
40 `ParserFactory::PREFER_PHP5` | Try to parse code as PHP 5. If this fails, try to parse it as PHP 7.
|
Chris@0
|
41 `ParserFactory::ONLY_PHP7` | Parse code as PHP 7.
|
Chris@0
|
42 `ParserFactory::ONLY_PHP5` | Parse code as PHP 5.
|
Chris@0
|
43
|
Chris@13
|
44 Unless you have a strong reason to use something else, `PREFER_PHP7` is a reasonable default.
|
Chris@0
|
45
|
Chris@0
|
46 The `create()` method optionally accepts a `Lexer` instance as the second argument. Some use cases
|
Chris@0
|
47 that require customized lexers are discussed in the [lexer documentation](component/Lexer.markdown).
|
Chris@0
|
48
|
Chris@0
|
49 Subsequently you can pass PHP code (including the opening `<?php` tag) to the `parse` method in order to
|
Chris@0
|
50 create a syntax tree. If a syntax error is encountered, an `PhpParser\Error` exception will be thrown:
|
Chris@0
|
51
|
Chris@0
|
52 ```php
|
Chris@13
|
53 <?php
|
Chris@0
|
54 use PhpParser\Error;
|
Chris@0
|
55 use PhpParser\ParserFactory;
|
Chris@0
|
56
|
Chris@13
|
57 $code = <<<'CODE'
|
Chris@13
|
58 <?php
|
Chris@13
|
59 function printLine($msg) {
|
Chris@13
|
60 echo $msg, "\n";
|
Chris@13
|
61 }
|
Chris@13
|
62 printLine('Hello World!!!');
|
Chris@13
|
63 CODE;
|
Chris@13
|
64
|
Chris@0
|
65 $parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
|
Chris@0
|
66
|
Chris@0
|
67 try {
|
Chris@0
|
68 $stmts = $parser->parse($code);
|
Chris@0
|
69 // $stmts is an array of statement nodes
|
Chris@0
|
70 } catch (Error $e) {
|
Chris@0
|
71 echo 'Parse Error: ', $e->getMessage();
|
Chris@0
|
72 }
|
Chris@0
|
73 ```
|
Chris@0
|
74
|
Chris@0
|
75 A parser instance can be reused to parse multiple files.
|
Chris@0
|
76
|
Chris@13
|
77 Node dumping
|
Chris@13
|
78 ------------
|
Chris@0
|
79
|
Chris@13
|
80 To dump the abstact syntax tree in human readable form, a `NodeDumper` can be used:
|
Chris@13
|
81
|
Chris@13
|
82 ```php
|
Chris@13
|
83 <?php
|
Chris@13
|
84 use PhpParser\NodeDumper;
|
Chris@13
|
85
|
Chris@13
|
86 $nodeDumper = new NodeDumper;
|
Chris@13
|
87 echo $nodeDumper->dump($stmts), "\n";
|
Chris@13
|
88 ```
|
Chris@13
|
89
|
Chris@13
|
90 For the sample code from the previous section, this will produce the following output:
|
Chris@0
|
91
|
Chris@0
|
92 ```
|
Chris@0
|
93 array(
|
Chris@13
|
94 0: Stmt_Function(
|
Chris@13
|
95 byRef: false
|
Chris@13
|
96 name: Identifier(
|
Chris@13
|
97 name: printLine
|
Chris@13
|
98 )
|
Chris@13
|
99 params: array(
|
Chris@13
|
100 0: Param(
|
Chris@13
|
101 type: null
|
Chris@13
|
102 byRef: false
|
Chris@13
|
103 variadic: false
|
Chris@13
|
104 var: Expr_Variable(
|
Chris@13
|
105 name: msg
|
Chris@13
|
106 )
|
Chris@13
|
107 default: null
|
Chris@0
|
108 )
|
Chris@13
|
109 )
|
Chris@13
|
110 returnType: null
|
Chris@13
|
111 stmts: array(
|
Chris@13
|
112 0: Stmt_Echo(
|
Chris@13
|
113 exprs: array(
|
Chris@13
|
114 0: Expr_Variable(
|
Chris@13
|
115 name: msg
|
Chris@13
|
116 )
|
Chris@13
|
117 1: Scalar_String(
|
Chris@13
|
118 value:
|
Chris@13
|
119
|
Chris@0
|
120 )
|
Chris@0
|
121 )
|
Chris@13
|
122 )
|
Chris@13
|
123 )
|
Chris@13
|
124 )
|
Chris@13
|
125 1: Stmt_Expression(
|
Chris@13
|
126 expr: Expr_FuncCall(
|
Chris@13
|
127 name: Name(
|
Chris@13
|
128 parts: array(
|
Chris@13
|
129 0: printLine
|
Chris@13
|
130 )
|
Chris@13
|
131 )
|
Chris@13
|
132 args: array(
|
Chris@13
|
133 0: Arg(
|
Chris@13
|
134 value: Scalar_String(
|
Chris@13
|
135 value: Hello World!!!
|
Chris@13
|
136 )
|
Chris@13
|
137 byRef: false
|
Chris@13
|
138 unpack: false
|
Chris@0
|
139 )
|
Chris@0
|
140 )
|
Chris@0
|
141 )
|
Chris@0
|
142 )
|
Chris@0
|
143 )
|
Chris@0
|
144 ```
|
Chris@0
|
145
|
Chris@13
|
146 You can also use the `php-parse` script to obtain such a node dump by calling it either with a file
|
Chris@13
|
147 name or code string:
|
Chris@0
|
148
|
Chris@13
|
149 ```sh
|
Chris@13
|
150 vendor/bin/php-parse file.php
|
Chris@13
|
151 vendor/bin/php-parse "<?php foo();"
|
Chris@13
|
152 ```
|
Chris@13
|
153
|
Chris@13
|
154 This can be very helpful if you want to quickly check how certain syntax is represented in the AST.
|
Chris@13
|
155
|
Chris@13
|
156 Node tree structure
|
Chris@13
|
157 -------------------
|
Chris@13
|
158
|
Chris@13
|
159 Looking at the node dump above, you can see that `$stmts` for this example code is an array of two
|
Chris@13
|
160 nodes, a `Stmt_Function` and a `Stmt_Expression`. The corresponding class names are:
|
Chris@13
|
161
|
Chris@13
|
162 * `Stmt_Function -> PhpParser\Node\Stmt\Function_`
|
Chris@13
|
163 * `Stmt_Expression -> PhpParser\Node\Stmt\Expression`
|
Chris@13
|
164
|
Chris@13
|
165 The additional `_` at the end of the first class name is necessary, because `Function` is a
|
Chris@13
|
166 reserved keyword. Many node class names in this library have a trailing `_` to avoid clashing with
|
Chris@13
|
167 a keyword.
|
Chris@13
|
168
|
Chris@13
|
169 As PHP is a large language there are approximately 140 different nodes. In order to make working
|
Chris@0
|
170 with them easier they are grouped into three categories:
|
Chris@0
|
171
|
Chris@0
|
172 * `PhpParser\Node\Stmt`s are statement nodes, i.e. language constructs that do not return
|
Chris@0
|
173 a value and can not occur in an expression. For example a class definition is a statement.
|
Chris@0
|
174 It doesn't return a value and you can't write something like `func(class A {});`.
|
Chris@0
|
175 * `PhpParser\Node\Expr`s are expression nodes, i.e. language constructs that return a value
|
Chris@0
|
176 and thus can occur in other expressions. Examples of expressions are `$var`
|
Chris@0
|
177 (`PhpParser\Node\Expr\Variable`) and `func()` (`PhpParser\Node\Expr\FuncCall`).
|
Chris@0
|
178 * `PhpParser\Node\Scalar`s are nodes representing scalar values, like `'string'`
|
Chris@0
|
179 (`PhpParser\Node\Scalar\String_`), `0` (`PhpParser\Node\Scalar\LNumber`) or magic constants
|
Chris@0
|
180 like `__FILE__` (`PhpParser\Node\Scalar\MagicConst\File`). All `PhpParser\Node\Scalar`s extend
|
Chris@0
|
181 `PhpParser\Node\Expr`, as scalars are expressions, too.
|
Chris@0
|
182 * There are some nodes not in either of these groups, for example names (`PhpParser\Node\Name`)
|
Chris@0
|
183 and call arguments (`PhpParser\Node\Arg`).
|
Chris@0
|
184
|
Chris@13
|
185 The `Node\Stmt\Expression` node is somewhat confusing in that it contains both the terms "statement"
|
Chris@13
|
186 and "expression". This node distinguishes `expr`, which is a `Node\Expr`, from `expr;`, which is
|
Chris@13
|
187 an "expression statement" represented by `Node\Stmt\Expression` and containing `expr` as a sub-node.
|
Chris@0
|
188
|
Chris@0
|
189 Every node has a (possibly zero) number of subnodes. You can access subnodes by writing
|
Chris@0
|
190 `$node->subNodeName`. The `Stmt\Echo_` node has only one subnode `exprs`. So in order to access it
|
Chris@0
|
191 in the above example you would write `$stmts[0]->exprs`. If you wanted to access the name of the function
|
Chris@0
|
192 call, you would write `$stmts[0]->exprs[1]->name`.
|
Chris@0
|
193
|
Chris@0
|
194 All nodes also define a `getType()` method that returns the node type. The type is the class name
|
Chris@0
|
195 without the `PhpParser\Node\` prefix and `\` replaced with `_`. It also does not contain a trailing
|
Chris@0
|
196 `_` for reserved-keyword class names.
|
Chris@0
|
197
|
Chris@0
|
198 It is possible to associate custom metadata with a node using the `setAttribute()` method. This data
|
Chris@0
|
199 can then be retrieved using `hasAttribute()`, `getAttribute()` and `getAttributes()`.
|
Chris@0
|
200
|
Chris@0
|
201 By default the lexer adds the `startLine`, `endLine` and `comments` attributes. `comments` is an array
|
Chris@0
|
202 of `PhpParser\Comment[\Doc]` instances.
|
Chris@0
|
203
|
Chris@0
|
204 The start line can also be accessed using `getLine()`/`setLine()` (instead of `getAttribute('startLine')`).
|
Chris@0
|
205 The last doc comment from the `comments` attribute can be obtained using `getDocComment()`.
|
Chris@0
|
206
|
Chris@0
|
207 Pretty printer
|
Chris@0
|
208 --------------
|
Chris@0
|
209
|
Chris@0
|
210 The pretty printer component compiles the AST back to PHP code. As the parser does not retain formatting
|
Chris@0
|
211 information the formatting is done using a specified scheme. Currently there is only one scheme available,
|
Chris@0
|
212 namely `PhpParser\PrettyPrinter\Standard`.
|
Chris@0
|
213
|
Chris@0
|
214 ```php
|
Chris@0
|
215 use PhpParser\Error;
|
Chris@0
|
216 use PhpParser\ParserFactory;
|
Chris@0
|
217 use PhpParser\PrettyPrinter;
|
Chris@0
|
218
|
Chris@0
|
219 $code = "<?php echo 'Hi ', hi\\getTarget();";
|
Chris@0
|
220
|
Chris@0
|
221 $parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
|
Chris@0
|
222 $prettyPrinter = new PrettyPrinter\Standard;
|
Chris@0
|
223
|
Chris@0
|
224 try {
|
Chris@0
|
225 // parse
|
Chris@0
|
226 $stmts = $parser->parse($code);
|
Chris@0
|
227
|
Chris@0
|
228 // change
|
Chris@0
|
229 $stmts[0] // the echo statement
|
Chris@0
|
230 ->exprs // sub expressions
|
Chris@0
|
231 [0] // the first of them (the string node)
|
Chris@0
|
232 ->value // it's value, i.e. 'Hi '
|
Chris@0
|
233 = 'Hello '; // change to 'Hello '
|
Chris@0
|
234
|
Chris@0
|
235 // pretty print
|
Chris@0
|
236 $code = $prettyPrinter->prettyPrint($stmts);
|
Chris@0
|
237
|
Chris@0
|
238 echo $code;
|
Chris@0
|
239 } catch (Error $e) {
|
Chris@0
|
240 echo 'Parse Error: ', $e->getMessage();
|
Chris@0
|
241 }
|
Chris@0
|
242 ```
|
Chris@0
|
243
|
Chris@0
|
244 The above code will output:
|
Chris@0
|
245
|
Chris@13
|
246 echo 'Hello ', hi\getTarget();
|
Chris@0
|
247
|
Chris@0
|
248 As you can see the source code was first parsed using `PhpParser\Parser->parse()`, then changed and then
|
Chris@0
|
249 again converted to code using `PhpParser\PrettyPrinter\Standard->prettyPrint()`.
|
Chris@0
|
250
|
Chris@0
|
251 The `prettyPrint()` method pretty prints a statements array. It is also possible to pretty print only a
|
Chris@0
|
252 single expression using `prettyPrintExpr()`.
|
Chris@0
|
253
|
Chris@0
|
254 The `prettyPrintFile()` method can be used to print an entire file. This will include the opening `<?php` tag
|
Chris@0
|
255 and handle inline HTML as the first/last statement more gracefully.
|
Chris@0
|
256
|
Chris@13
|
257 > Read more: [Pretty printing documentation](component/Pretty_printing.markdown)
|
Chris@13
|
258
|
Chris@0
|
259 Node traversation
|
Chris@0
|
260 -----------------
|
Chris@0
|
261
|
Chris@0
|
262 The above pretty printing example used the fact that the source code was known and thus it was easy to
|
Chris@0
|
263 write code that accesses a certain part of a node tree and changes it. Normally this is not the case.
|
Chris@0
|
264 Usually you want to change / analyze code in a generic way, where you don't know how the node tree is
|
Chris@0
|
265 going to look like.
|
Chris@0
|
266
|
Chris@0
|
267 For this purpose the parser provides a component for traversing and visiting the node tree. The basic
|
Chris@0
|
268 structure of a program using this `PhpParser\NodeTraverser` looks like this:
|
Chris@0
|
269
|
Chris@0
|
270 ```php
|
Chris@0
|
271 use PhpParser\NodeTraverser;
|
Chris@0
|
272 use PhpParser\ParserFactory;
|
Chris@0
|
273 use PhpParser\PrettyPrinter;
|
Chris@0
|
274
|
Chris@0
|
275 $parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
|
Chris@0
|
276 $traverser = new NodeTraverser;
|
Chris@0
|
277 $prettyPrinter = new PrettyPrinter\Standard;
|
Chris@0
|
278
|
Chris@0
|
279 // add your visitor
|
Chris@0
|
280 $traverser->addVisitor(new MyNodeVisitor);
|
Chris@0
|
281
|
Chris@0
|
282 try {
|
Chris@0
|
283 $code = file_get_contents($fileName);
|
Chris@0
|
284
|
Chris@0
|
285 // parse
|
Chris@0
|
286 $stmts = $parser->parse($code);
|
Chris@0
|
287
|
Chris@0
|
288 // traverse
|
Chris@0
|
289 $stmts = $traverser->traverse($stmts);
|
Chris@0
|
290
|
Chris@0
|
291 // pretty print
|
Chris@0
|
292 $code = $prettyPrinter->prettyPrintFile($stmts);
|
Chris@0
|
293
|
Chris@0
|
294 echo $code;
|
Chris@0
|
295 } catch (PhpParser\Error $e) {
|
Chris@0
|
296 echo 'Parse Error: ', $e->getMessage();
|
Chris@0
|
297 }
|
Chris@0
|
298 ```
|
Chris@0
|
299
|
Chris@0
|
300 The corresponding node visitor might look like this:
|
Chris@0
|
301
|
Chris@0
|
302 ```php
|
Chris@0
|
303 use PhpParser\Node;
|
Chris@0
|
304 use PhpParser\NodeVisitorAbstract;
|
Chris@0
|
305
|
Chris@0
|
306 class MyNodeVisitor extends NodeVisitorAbstract
|
Chris@0
|
307 {
|
Chris@0
|
308 public function leaveNode(Node $node) {
|
Chris@0
|
309 if ($node instanceof Node\Scalar\String_) {
|
Chris@0
|
310 $node->value = 'foo';
|
Chris@0
|
311 }
|
Chris@0
|
312 }
|
Chris@0
|
313 }
|
Chris@0
|
314 ```
|
Chris@0
|
315
|
Chris@0
|
316 The above node visitor would change all string literals in the program to `'foo'`.
|
Chris@0
|
317
|
Chris@0
|
318 All visitors must implement the `PhpParser\NodeVisitor` interface, which defines the following four
|
Chris@0
|
319 methods:
|
Chris@0
|
320
|
Chris@0
|
321 ```php
|
Chris@0
|
322 public function beforeTraverse(array $nodes);
|
Chris@0
|
323 public function enterNode(\PhpParser\Node $node);
|
Chris@0
|
324 public function leaveNode(\PhpParser\Node $node);
|
Chris@0
|
325 public function afterTraverse(array $nodes);
|
Chris@0
|
326 ```
|
Chris@0
|
327
|
Chris@0
|
328 The `beforeTraverse()` method is called once before the traversal begins and is passed the nodes the
|
Chris@0
|
329 traverser was called with. This method can be used for resetting values before traversation or
|
Chris@0
|
330 preparing the tree for traversal.
|
Chris@0
|
331
|
Chris@0
|
332 The `afterTraverse()` method is similar to the `beforeTraverse()` method, with the only difference that
|
Chris@0
|
333 it is called once after the traversal.
|
Chris@0
|
334
|
Chris@0
|
335 The `enterNode()` and `leaveNode()` methods are called on every node, the former when it is entered,
|
Chris@0
|
336 i.e. before its subnodes are traversed, the latter when it is left.
|
Chris@0
|
337
|
Chris@0
|
338 All four methods can either return the changed node or not return at all (i.e. `null`) in which
|
Chris@0
|
339 case the current node is not changed.
|
Chris@0
|
340
|
Chris@0
|
341 The `enterNode()` method can additionally return the value `NodeTraverser::DONT_TRAVERSE_CHILDREN`,
|
Chris@17
|
342 which instructs the traverser to skip all children of the current node. To furthermore prevent subsequent
|
Chris@17
|
343 visitors from visiting the current node, `NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN` can be used instead.
|
Chris@0
|
344
|
Chris@0
|
345 The `leaveNode()` method can additionally return the value `NodeTraverser::REMOVE_NODE`, in which
|
Chris@0
|
346 case the current node will be removed from the parent array. Furthermore it is possible to return
|
Chris@0
|
347 an array of nodes, which will be merged into the parent array at the offset of the current node.
|
Chris@0
|
348 I.e. if in `array(A, B, C)` the node `B` should be replaced with `array(X, Y, Z)` the result will
|
Chris@0
|
349 be `array(A, X, Y, Z, C)`.
|
Chris@0
|
350
|
Chris@0
|
351 Instead of manually implementing the `NodeVisitor` interface you can also extend the `NodeVisitorAbstract`
|
Chris@0
|
352 class, which will define empty default implementations for all the above methods.
|
Chris@0
|
353
|
Chris@13
|
354 > Read more: [Walking the AST](component/Walking_the_AST.markdown)
|
Chris@13
|
355
|
Chris@0
|
356 The NameResolver node visitor
|
Chris@0
|
357 -----------------------------
|
Chris@0
|
358
|
Chris@13
|
359 One visitor that is already bundled with the package is `PhpParser\NodeVisitor\NameResolver`. This visitor
|
Chris@0
|
360 helps you work with namespaced code by trying to resolve most names to fully qualified ones.
|
Chris@0
|
361
|
Chris@0
|
362 For example, consider the following code:
|
Chris@0
|
363
|
Chris@0
|
364 use A as B;
|
Chris@0
|
365 new B\C();
|
Chris@0
|
366
|
Chris@0
|
367 In order to know that `B\C` really is `A\C` you would need to track aliases and namespaces yourself.
|
Chris@0
|
368 The `NameResolver` takes care of that and resolves names as far as possible.
|
Chris@0
|
369
|
Chris@13
|
370 After running it, most names will be fully qualified. The only names that will stay unqualified are
|
Chris@0
|
371 unqualified function and constant names. These are resolved at runtime and thus the visitor can't
|
Chris@0
|
372 know which function they are referring to. In most cases this is a non-issue as the global functions
|
Chris@0
|
373 are meant.
|
Chris@0
|
374
|
Chris@0
|
375 Also the `NameResolver` adds a `namespacedName` subnode to class, function and constant declarations
|
Chris@0
|
376 that contains the namespaced name instead of only the shortname that is available via `name`.
|
Chris@0
|
377
|
Chris@13
|
378 > Read more: [Name resolution documentation](component/Name_resolution.markdown)
|
Chris@13
|
379
|
Chris@0
|
380 Example: Converting namespaced code to pseudo namespaces
|
Chris@0
|
381 --------------------------------------------------------
|
Chris@0
|
382
|
Chris@0
|
383 A small example to understand the concept: We want to convert namespaced code to pseudo namespaces
|
Chris@0
|
384 so it works on 5.2, i.e. names like `A\\B` should be converted to `A_B`. Note that such conversions
|
Chris@0
|
385 are fairly complicated if you take PHP's dynamic features into account, so our conversion will
|
Chris@0
|
386 assume that no dynamic features are used.
|
Chris@0
|
387
|
Chris@0
|
388 We start off with the following base code:
|
Chris@0
|
389
|
Chris@0
|
390 ```php
|
Chris@0
|
391 use PhpParser\ParserFactory;
|
Chris@0
|
392 use PhpParser\PrettyPrinter;
|
Chris@0
|
393 use PhpParser\NodeTraverser;
|
Chris@0
|
394 use PhpParser\NodeVisitor\NameResolver;
|
Chris@0
|
395
|
Chris@0
|
396 $inDir = '/some/path';
|
Chris@0
|
397 $outDir = '/some/other/path';
|
Chris@0
|
398
|
Chris@0
|
399 $parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
|
Chris@0
|
400 $traverser = new NodeTraverser;
|
Chris@0
|
401 $prettyPrinter = new PrettyPrinter\Standard;
|
Chris@0
|
402
|
Chris@0
|
403 $traverser->addVisitor(new NameResolver); // we will need resolved names
|
Chris@0
|
404 $traverser->addVisitor(new NamespaceConverter); // our own node visitor
|
Chris@0
|
405
|
Chris@0
|
406 // iterate over all .php files in the directory
|
Chris@0
|
407 $files = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($inDir));
|
Chris@0
|
408 $files = new \RegexIterator($files, '/\.php$/');
|
Chris@0
|
409
|
Chris@0
|
410 foreach ($files as $file) {
|
Chris@0
|
411 try {
|
Chris@0
|
412 // read the file that should be converted
|
Chris@16
|
413 $code = file_get_contents($file->getPathName());
|
Chris@0
|
414
|
Chris@0
|
415 // parse
|
Chris@0
|
416 $stmts = $parser->parse($code);
|
Chris@0
|
417
|
Chris@0
|
418 // traverse
|
Chris@0
|
419 $stmts = $traverser->traverse($stmts);
|
Chris@0
|
420
|
Chris@0
|
421 // pretty print
|
Chris@0
|
422 $code = $prettyPrinter->prettyPrintFile($stmts);
|
Chris@0
|
423
|
Chris@0
|
424 // write the converted file to the target directory
|
Chris@0
|
425 file_put_contents(
|
Chris@0
|
426 substr_replace($file->getPathname(), $outDir, 0, strlen($inDir)),
|
Chris@0
|
427 $code
|
Chris@0
|
428 );
|
Chris@0
|
429 } catch (PhpParser\Error $e) {
|
Chris@0
|
430 echo 'Parse Error: ', $e->getMessage();
|
Chris@0
|
431 }
|
Chris@0
|
432 }
|
Chris@0
|
433 ```
|
Chris@0
|
434
|
Chris@0
|
435 Now lets start with the main code, the `NodeVisitor\NamespaceConverter`. One thing it needs to do
|
Chris@0
|
436 is convert `A\\B` style names to `A_B` style ones.
|
Chris@0
|
437
|
Chris@0
|
438 ```php
|
Chris@0
|
439 use PhpParser\Node;
|
Chris@0
|
440
|
Chris@0
|
441 class NamespaceConverter extends \PhpParser\NodeVisitorAbstract
|
Chris@0
|
442 {
|
Chris@0
|
443 public function leaveNode(Node $node) {
|
Chris@0
|
444 if ($node instanceof Node\Name) {
|
Chris@13
|
445 return new Node\Name(str_replace('\\', '_', $node->toString()));
|
Chris@0
|
446 }
|
Chris@0
|
447 }
|
Chris@0
|
448 }
|
Chris@0
|
449 ```
|
Chris@0
|
450
|
Chris@0
|
451 The above code profits from the fact that the `NameResolver` already resolved all names as far as
|
Chris@0
|
452 possible, so we don't need to do that. We only need to create a string with the name parts separated
|
Chris@13
|
453 by underscores instead of backslashes. This is what `str_replace('\\', '_', $node->toString())` does. (If you want to
|
Chris@0
|
454 create a name with backslashes either write `$node->toString()` or `(string) $node`.) Then we create
|
Chris@0
|
455 a new name from the string and return it. Returning a new node replaces the old node.
|
Chris@0
|
456
|
Chris@0
|
457 Another thing we need to do is change the class/function/const declarations. Currently they contain
|
Chris@0
|
458 only the shortname (i.e. the last part of the name), but they need to contain the complete name including
|
Chris@0
|
459 the namespace prefix:
|
Chris@0
|
460
|
Chris@0
|
461 ```php
|
Chris@0
|
462 use PhpParser\Node;
|
Chris@0
|
463 use PhpParser\Node\Stmt;
|
Chris@0
|
464
|
Chris@0
|
465 class NodeVisitor_NamespaceConverter extends \PhpParser\NodeVisitorAbstract
|
Chris@0
|
466 {
|
Chris@0
|
467 public function leaveNode(Node $node) {
|
Chris@0
|
468 if ($node instanceof Node\Name) {
|
Chris@13
|
469 return new Node\Name(str_replace('\\', '_', $node->toString()));
|
Chris@0
|
470 } elseif ($node instanceof Stmt\Class_
|
Chris@0
|
471 || $node instanceof Stmt\Interface_
|
Chris@0
|
472 || $node instanceof Stmt\Function_) {
|
Chris@13
|
473 $node->name = str_replace('\\', '_', $node->namespacedName->toString());
|
Chris@0
|
474 } elseif ($node instanceof Stmt\Const_) {
|
Chris@0
|
475 foreach ($node->consts as $const) {
|
Chris@13
|
476 $const->name = str_replace('\\', '_', $const->namespacedName->toString());
|
Chris@0
|
477 }
|
Chris@0
|
478 }
|
Chris@0
|
479 }
|
Chris@0
|
480 }
|
Chris@0
|
481 ```
|
Chris@0
|
482
|
Chris@0
|
483 There is not much more to it than converting the namespaced name to string with `_` as separator.
|
Chris@0
|
484
|
Chris@0
|
485 The last thing we need to do is remove the `namespace` and `use` statements:
|
Chris@0
|
486
|
Chris@0
|
487 ```php
|
Chris@0
|
488 use PhpParser\Node;
|
Chris@0
|
489 use PhpParser\Node\Stmt;
|
Chris@13
|
490 use PhpParser\NodeTraverser;
|
Chris@0
|
491
|
Chris@0
|
492 class NodeVisitor_NamespaceConverter extends \PhpParser\NodeVisitorAbstract
|
Chris@0
|
493 {
|
Chris@0
|
494 public function leaveNode(Node $node) {
|
Chris@0
|
495 if ($node instanceof Node\Name) {
|
Chris@13
|
496 return new Node\Name(str_replace('\\', '_', $node->toString()));
|
Chris@0
|
497 } elseif ($node instanceof Stmt\Class_
|
Chris@0
|
498 || $node instanceof Stmt\Interface_
|
Chris@0
|
499 || $node instanceof Stmt\Function_) {
|
Chris@13
|
500 $node->name = str_replace('\\', '_', $node->namespacedName->toString();
|
Chris@0
|
501 } elseif ($node instanceof Stmt\Const_) {
|
Chris@0
|
502 foreach ($node->consts as $const) {
|
Chris@13
|
503 $const->name = str_replace('\\', '_', $const->namespacedName->toString());
|
Chris@0
|
504 }
|
Chris@0
|
505 } elseif ($node instanceof Stmt\Namespace_) {
|
Chris@0
|
506 // returning an array merges is into the parent array
|
Chris@0
|
507 return $node->stmts;
|
Chris@0
|
508 } elseif ($node instanceof Stmt\Use_) {
|
Chris@13
|
509 // remove use nodes altogether
|
Chris@13
|
510 return NodeTraverser::REMOVE_NODE;
|
Chris@0
|
511 }
|
Chris@0
|
512 }
|
Chris@0
|
513 }
|
Chris@0
|
514 ```
|
Chris@0
|
515
|
Chris@0
|
516 That's all.
|