1<?php declare(strict_types=1);
2
3namespace PhpParser;
4
5use PhpParser\Node\Arg;
6use PhpParser\Node\Attribute;
7use PhpParser\Node\Expr;
8use PhpParser\Node\Expr\BinaryOp\Concat;
9use PhpParser\Node\Identifier;
10use PhpParser\Node\Name;
11use PhpParser\Node\Scalar\Int_;
12use PhpParser\Node\Scalar\String_;
13
14class BuilderFactoryTest extends \PHPUnit\Framework\TestCase {
15    /**
16     * @dataProvider provideTestFactory
17     */
18    public function testFactory($methodName, $className): void {
19        $factory = new BuilderFactory();
20        $this->assertInstanceOf($className, $factory->$methodName('test'));
21    }
22
23    public function provideTestFactory() {
24        return [
25            ['namespace',   Builder\Namespace_::class],
26            ['class',       Builder\Class_::class],
27            ['interface',   Builder\Interface_::class],
28            ['trait',       Builder\Trait_::class],
29            ['enum',        Builder\Enum_::class],
30            ['method',      Builder\Method::class],
31            ['function',    Builder\Function_::class],
32            ['property',    Builder\Property::class],
33            ['param',       Builder\Param::class],
34            ['use',         Builder\Use_::class],
35            ['useFunction', Builder\Use_::class],
36            ['useConst',    Builder\Use_::class],
37            ['enumCase',    Builder\EnumCase::class],
38        ];
39    }
40
41    public function testFactoryClassConst(): void {
42        $factory = new BuilderFactory();
43        $this->assertInstanceOf(Builder\ClassConst::class, $factory->classConst('TEST', 1));
44    }
45
46    public function testAttribute(): void {
47        $factory = new BuilderFactory();
48        $this->assertEquals(
49            new Attribute(new Name('AttributeName'), [new Arg(
50                new String_('bar'), false, false, [], new Identifier('foo')
51            )]),
52            $factory->attribute('AttributeName', ['foo' => 'bar'])
53        );
54    }
55
56    public function testVal(): void {
57        // This method is a wrapper around BuilderHelpers::normalizeValue(),
58        // which is already tested elsewhere
59        $factory = new BuilderFactory();
60        $this->assertEquals(
61            new String_("foo"),
62            $factory->val("foo")
63        );
64    }
65
66    public function testConcat(): void {
67        $factory = new BuilderFactory();
68        $varA = new Expr\Variable('a');
69        $varB = new Expr\Variable('b');
70        $varC = new Expr\Variable('c');
71
72        $this->assertEquals(
73            new Concat($varA, $varB),
74            $factory->concat($varA, $varB)
75        );
76        $this->assertEquals(
77            new Concat(new Concat($varA, $varB), $varC),
78            $factory->concat($varA, $varB, $varC)
79        );
80        $this->assertEquals(
81            new Concat(new Concat(new String_("a"), $varB), new String_("c")),
82            $factory->concat("a", $varB, "c")
83        );
84    }
85
86    public function testConcatOneError(): void {
87        $this->expectException(\LogicException::class);
88        $this->expectExceptionMessage('Expected at least two expressions');
89        (new BuilderFactory())->concat("a");
90    }
91
92    public function testConcatInvalidExpr(): void {
93        $this->expectException(\LogicException::class);
94        $this->expectExceptionMessage('Expected string or Expr');
95        (new BuilderFactory())->concat("a", 42);
96    }
97
98    public function testArgs(): void {
99        $factory = new BuilderFactory();
100        $unpack = new Arg(new Expr\Variable('c'), false, true);
101        $this->assertEquals(
102            [
103                new Arg(new Expr\Variable('a')),
104                new Arg(new String_('b')),
105                $unpack
106            ],
107            $factory->args([new Expr\Variable('a'), 'b', $unpack])
108        );
109    }
110
111    public function testNamedArgs(): void {
112        $factory = new BuilderFactory();
113        $this->assertEquals(
114            [
115                new Arg(new String_('foo')),
116                new Arg(new String_('baz'), false, false, [], new Identifier('bar')),
117            ],
118            $factory->args(['foo', 'bar' => 'baz'])
119        );
120    }
121
122    public function testCalls(): void {
123        $factory = new BuilderFactory();
124
125        // Simple function call
126        $this->assertEquals(
127            new Expr\FuncCall(
128                new Name('var_dump'),
129                [new Arg(new String_('str'))]
130            ),
131            $factory->funcCall('var_dump', ['str'])
132        );
133        // Dynamic function call
134        $this->assertEquals(
135            new Expr\FuncCall(new Expr\Variable('fn')),
136            $factory->funcCall(new Expr\Variable('fn'))
137        );
138
139        // Simple method call
140        $this->assertEquals(
141            new Expr\MethodCall(
142                new Expr\Variable('obj'),
143                new Identifier('method'),
144                [new Arg(new Int_(42))]
145            ),
146            $factory->methodCall(new Expr\Variable('obj'), 'method', [42])
147        );
148        // Explicitly pass Identifier node
149        $this->assertEquals(
150            new Expr\MethodCall(
151                new Expr\Variable('obj'),
152                new Identifier('method')
153            ),
154            $factory->methodCall(new Expr\Variable('obj'), new Identifier('method'))
155        );
156        // Dynamic method call
157        $this->assertEquals(
158            new Expr\MethodCall(
159                new Expr\Variable('obj'),
160                new Expr\Variable('method')
161            ),
162            $factory->methodCall(new Expr\Variable('obj'), new Expr\Variable('method'))
163        );
164
165        // Simple static method call
166        $this->assertEquals(
167            new Expr\StaticCall(
168                new Name\FullyQualified('Foo'),
169                new Identifier('bar'),
170                [new Arg(new Expr\Variable('baz'))]
171            ),
172            $factory->staticCall('\Foo', 'bar', [new Expr\Variable('baz')])
173        );
174        // Dynamic static method call
175        $this->assertEquals(
176            new Expr\StaticCall(
177                new Expr\Variable('foo'),
178                new Expr\Variable('bar')
179            ),
180            $factory->staticCall(new Expr\Variable('foo'), new Expr\Variable('bar'))
181        );
182
183        // Simple new call
184        $this->assertEquals(
185            new Expr\New_(new Name\FullyQualified('stdClass')),
186            $factory->new('\stdClass')
187        );
188        // Dynamic new call
189        $this->assertEquals(
190            new Expr\New_(
191                new Expr\Variable('foo'),
192                [new Arg(new String_('bar'))]
193            ),
194            $factory->new(new Expr\Variable('foo'), ['bar'])
195        );
196    }
197
198    public function testConstFetches(): void {
199        $factory = new BuilderFactory();
200        $this->assertEquals(
201            new Expr\ConstFetch(new Name('FOO')),
202            $factory->constFetch('FOO')
203        );
204        $this->assertEquals(
205            new Expr\ClassConstFetch(new Name('Foo'), new Identifier('BAR')),
206            $factory->classConstFetch('Foo', 'BAR')
207        );
208        $this->assertEquals(
209            new Expr\ClassConstFetch(new Expr\Variable('foo'), new Identifier('BAR')),
210            $factory->classConstFetch(new Expr\Variable('foo'), 'BAR')
211        );
212        $this->assertEquals(
213            new Expr\ClassConstFetch(new Name('Foo'), new Expr\Variable('foo')),
214            $factory->classConstFetch('Foo', $factory->var('foo'))
215        );
216    }
217
218    public function testVar(): void {
219        $factory = new BuilderFactory();
220        $this->assertEquals(
221            new Expr\Variable("foo"),
222            $factory->var("foo")
223        );
224        $this->assertEquals(
225            new Expr\Variable(new Expr\Variable("foo")),
226            $factory->var($factory->var("foo"))
227        );
228    }
229
230    public function testPropertyFetch(): void {
231        $f = new BuilderFactory();
232        $this->assertEquals(
233            new Expr\PropertyFetch(new Expr\Variable('foo'), 'bar'),
234            $f->propertyFetch($f->var('foo'), 'bar')
235        );
236        $this->assertEquals(
237            new Expr\PropertyFetch(new Expr\Variable('foo'), 'bar'),
238            $f->propertyFetch($f->var('foo'), new Identifier('bar'))
239        );
240        $this->assertEquals(
241            new Expr\PropertyFetch(new Expr\Variable('foo'), new Expr\Variable('bar')),
242            $f->propertyFetch($f->var('foo'), $f->var('bar'))
243        );
244    }
245
246    public function testInvalidIdentifier(): void {
247        $this->expectException(\LogicException::class);
248        $this->expectExceptionMessage('Expected string or instance of Node\Identifier');
249        (new BuilderFactory())->classConstFetch('Foo', new Name('foo'));
250    }
251
252    public function testInvalidIdentifierOrExpr(): void {
253        $this->expectException(\LogicException::class);
254        $this->expectExceptionMessage('Expected string or instance of Node\Identifier or Node\Expr');
255        (new BuilderFactory())->staticCall('Foo', new Name('bar'));
256    }
257
258    public function testInvalidNameOrExpr(): void {
259        $this->expectException(\LogicException::class);
260        $this->expectExceptionMessage('Name must be a string or an instance of Node\Name or Node\Expr');
261        (new BuilderFactory())->funcCall(new Node\Stmt\Return_());
262    }
263
264    public function testInvalidVar(): void {
265        $this->expectException(\LogicException::class);
266        $this->expectExceptionMessage('Variable name must be string or Expr');
267        (new BuilderFactory())->var(new Node\Stmt\Return_());
268    }
269
270    public function testIntegration(): void {
271        $factory = new BuilderFactory();
272        $node = $factory->namespace('Name\Space')
273            ->addStmt($factory->use('Foo\Bar\SomeOtherClass'))
274            ->addStmt($factory->use('Foo\Bar')->as('A'))
275            ->addStmt($factory->useFunction('strlen'))
276            ->addStmt($factory->useConst('PHP_VERSION'))
277            ->addStmt($factory
278                ->class('SomeClass')
279                ->extend('SomeOtherClass')
280                ->implement('A\Few', '\Interfaces')
281                ->addAttribute($factory->attribute('ClassAttribute', ['repository' => 'fqcn']))
282                ->makeAbstract()
283
284                ->addStmt($factory->useTrait('FirstTrait'))
285
286                ->addStmt($factory->useTrait('SecondTrait', 'ThirdTrait')
287                    ->and('AnotherTrait')
288                    ->with($factory->traitUseAdaptation('foo')->as('bar'))
289                    ->with($factory->traitUseAdaptation('AnotherTrait', 'baz')->as('test'))
290                    ->with($factory->traitUseAdaptation('AnotherTrait', 'func')->insteadof('SecondTrait')))
291
292                ->addStmt($factory->method('firstMethod')
293                    ->addAttribute($factory->attribute('Route', ['/index', 'name' => 'homepage']))
294                )
295
296                ->addStmt($factory->method('someMethod')
297                    ->makePublic()
298                    ->makeAbstract()
299                    ->addParam($factory->param('someParam')->setType('SomeClass'))
300                    ->setDocComment('/**
301                                      * This method does something.
302                                      *
303                                      * @param SomeClass And takes a parameter
304                                      */'))
305
306                ->addStmt($factory->method('anotherMethod')
307                    ->makeProtected()
308                    ->addParam($factory->param('someParam')
309                        ->setDefault('test')
310                        ->addAttribute($factory->attribute('TaggedIterator', ['app.handlers']))
311                    )
312                    ->addStmt(new Expr\Print_(new Expr\Variable('someParam'))))
313
314                ->addStmt($factory->property('someProperty')->makeProtected())
315                ->addStmt($factory->property('anotherProperty')
316                    ->makePrivate()
317                    ->setDefault([1, 2, 3]))
318                ->addStmt($factory->property('integerProperty')
319                    ->setType('int')
320                    ->addAttribute($factory->attribute('Column', ['options' => ['unsigned' => true]]))
321                    ->setDefault(1))
322                ->addStmt($factory->classConst('CONST_WITH_ATTRIBUTE', 1)
323                    ->makePublic()
324                    ->addAttribute($factory->attribute('ConstAttribute'))
325                )
326
327                ->addStmt($factory->classConst("FIRST_CLASS_CONST", 1)
328                    ->addConst("SECOND_CLASS_CONST", 2)
329                    ->makePrivate()))
330            ->getNode()
331        ;
332
333        $expected = <<<'EOC'
334<?php
335
336namespace Name\Space;
337
338use Foo\Bar\SomeOtherClass;
339use Foo\Bar as A;
340use function strlen;
341use const PHP_VERSION;
342#[ClassAttribute(repository: 'fqcn')]
343abstract class SomeClass extends SomeOtherClass implements A\Few, \Interfaces
344{
345    use FirstTrait;
346    use SecondTrait, ThirdTrait, AnotherTrait {
347        foo as bar;
348        AnotherTrait::baz as test;
349        AnotherTrait::func insteadof SecondTrait;
350    }
351    #[ConstAttribute]
352    public const CONST_WITH_ATTRIBUTE = 1;
353    private const FIRST_CLASS_CONST = 1, SECOND_CLASS_CONST = 2;
354    protected $someProperty;
355    private $anotherProperty = [1, 2, 3];
356    #[Column(options: ['unsigned' => true])]
357    public int $integerProperty = 1;
358    #[Route('/index', name: 'homepage')]
359    function firstMethod()
360    {
361    }
362    /**
363     * This method does something.
364     *
365     * @param SomeClass And takes a parameter
366     */
367    abstract public function someMethod(SomeClass $someParam);
368    protected function anotherMethod(#[TaggedIterator('app.handlers')] $someParam = 'test')
369    {
370        print $someParam;
371    }
372}
373EOC;
374
375        $stmts = [$node];
376        $prettyPrinter = new PrettyPrinter\Standard();
377        $generated = $prettyPrinter->prettyPrintFile($stmts);
378
379        $this->assertEquals(
380            str_replace("\r\n", "\n", $expected),
381            str_replace("\r\n", "\n", $generated)
382        );
383    }
384}
385