Even more fluid code generation
I looked at https://github.com/nikic/PHP-Parser/blob/master/doc/4_Code_generation.markdown
I think it could be even more fluid :)
This is a snippet from here, https://github.com/Ocramius/ProxyManager/issues/36#issuecomment-16843161 (further down) I wrote this without knowledge of PHP-Parser syntax, so I don't insist on the exact identifiers.
$engine = new GeneratorEngine(..);
$blueprint = $engine->newClassBlueprint();
$blueprint->extend(..);
// Start with an empty public non-abstract method, then enhance it.
$blueprint->addMethod('foo')
->setProtected()
->setArguments(..)
->setBody(..)
;
// This will automatically scan the interface for methods
$blueprint->implement(..);
// Take the signature directly from the interface
// The interface needs to be added *before* this.
$blueprint->overrideMethod('bar')
->setBody(..)
;
// Class name is not decided before the end.
// Decisions about file generation vs eval() are made in GeneratorEngine,
// we don't have to worry about it.
$reflectionClass = $blueprint->createAtName('MyClass');
// We now have a reflection class to work with.
$object = $reflectionClass->newInstance(..);
// Just for fun, we can create a copy of this class with a different name,
// and a different implementation of foo().
$blueprint->addMethodIfNotExists('foo')
// Set the body to something else.
->setBody(..)
;
$blueprint->createAtName('AlternativeClass');
And for templates..
$blueprint = $engine->classFromTemplate('MyClassTemplate');
$blueprint->addMethod(..);
Arguments are tricky, because they could break the fluid chain. So let's try to squeeze it all in one method "addArgument" (or "addParam" if you like).
$classBlueprint->addMethod('foo')
->makeProtected()
// Argument with type hint and doc block.
->addArgument('arg0', 'Arg0Interface', 'This argument is required.')
// Argument with default value, type hint and doc block.
->addOptionalArgument('arg1', NULL, 'Arg1Interface', 'This argument is optional.')
->setBody(..)
;
The following code would trigger an exception, because optional arguments cannot be followed by required arguments:
$classBlueprint->addMethod('foo')
->addOptionalArgument('arg0', NULL)
->addArgument('arg1')
;
Did some code. https://github.com/donquixote/PHP-Parser/tree/fluid FluidBuilderFactory: https://github.com/donquixote/PHP-Parser/blob/fluid/lib/PHPParser/FluidBuilderFactory.php FluidBuilder_Class, _Method and _Function: https://github.com/donquixote/PHP-Parser/tree/fluid/lib/PHPParser/FluidBuilder
How to use: (equivalent to example in https://github.com/nikic/PHP-Parser/blob/master/doc/4_Code_generation.markdown)
require_once 'PHP-Parser/vendor/autoload.php';
$prettyPrinter = new PHPParser_PrettyPrinter_Default();
$factory = new PHPParser_FluidBuilderFactory();
$class = $factory->class('SomeClass')
->extend('SomeOtherClass')
->implement('A\Few', 'Interfaces')
->makeAbstract()
;
$class->addMethod('someMethod')
->makeAbstract()
->addRequiredParam('someParam', 'SomeClass')
;
$class->addMethod('anotherMethod')
->makeProtected() // ->makePublic() [default], ->makePrivate()
->addOptionalParam('someParam', 'test')
// it is possible to add manually created nodes
->addStmt(new PHPParser_Node_Expr_Print(new PHPParser_Node_Expr_Variable('someParam')))
;
// properties will be correctly reordered above the methods
$class->addProperty('someProperty')
->makeProtected()
;
$class->addProperty('anotherProperty')
->makePrivate()
->setDefault(array(1, 2, 3))
;
$node = $class->getNode();
$stmts = array($node);
echo $prettyPrinter->prettyPrint($stmts);
Hm, to make it more fluid
$factory->class('SomeClass')
->extend('SomeOtherClass')
->addMethod('someMethod')
->makeAbstract()
->addRequiredParam('someParam')
->setTypeHint('SomeClass')
->makeByRef()
->END_PARAM()
->END_METHOD()
->addMethod(..)
[..]
Ideally, it would also wrap the actual "saving to PHP file" in the fluid syntax, so you don't have to juggle around with different services for that.
E.g.
// Specify PSR-0 root dir where classes are to be saved.
$engine = new PSR0GeneratorEngine('/var/projects/mylib/generated');
$instance = $engine->namespace('My\Library')->class('MyClass')
->method(..)
->save()
->newInstance(..)
Quite a lot here, I'll start with this:
<?php
$class->addMethod('someMethod')
->makeAbstract()
->addRequiredParam('someParam', 'SomeClass')
;
If I got it right, there are two things here:
a) This uses a $class->addMethod('foo')->... pattern, rather than the existing $class->addStmt($factor->method('foo')->...) one. Why do you want to make this change? Some of the thoughts behind the current design are:
- You can add the statements from any source, you are not forced to create them inline using the fluid interface. In particular you also have the possibility to create a method once using $factory->method() and then use the same method in multiple classes.
- If you want to stay in the fluid interface after the method you have something like the
END_METHOD()calls you already introduces above. Imho that's not particularly nice. - You do not need to explicitly add a method for every kind of node one can add. In a class body you can have methods, you can have properties, you can have constants and you can have trait uses.
addStmtscovers all these cases, without requiring individual methods for the different types. It also allows the reuse of generation methods e.g. a class constant and methods can be used both by classes and interfaces (note: there are currently builders neither for interfaces nor for class constants ^^).
If the main concern here is keeping things short, then I have two suggestions:
- To make it a bit shorter one could rename
addStmttoadd. So the code would be->add($factory->method()), which I guess reads a bit nicer. - Apart from
->addStmt()the builders also have->addStmts()methods. If you are adding multiple "things", you can avoid the boilerplate:
<?php
$class = $factory->class('SomeClass')->addStmts([
$factory->method('method1')->...,
$factory->method('method2')->...,
$factory->method('method3')->...,
$factory->property('prop1')->...,
$factory->property('prop2')->...,
])->getNode();
b) The code uses ->addRequiredParam('someParam', 'SomeClass') which creates a parameter in a single method call, rather than the multiple calls on $factory->param(). Apart from the comments from a) [regarding reusability and stuff] the issue here is order of optional bits of information (as you already point out in your second comment). You basically have to decide on some arbitrary order, which I personally don't like much (because it usually leads to the , null, null, null, 'foo' pattern and because the meaning of a certain parameter often becomes unclear.)
Regarding this:
$classBlueprint->addMethod('foo')
->addOptionalArgument('arg0', NULL)
->addArgument('arg1')
;
I don't think that this need to throw an exception as PHP itself accepts optional arguments before required arguments. This can be used to create (required) parameters with nullable object type hints.
Thoughts? I'm leaving the rest for later. In any case, thanks for bringing up the topic again. Long time since I looked at the code generation APIs.
Hi, sorry, I also was busy with other things..
If it is all about brevity, then I think your points makes my suggestion obsolete.
But I have another motivation: Passing objects around. E.g. you have a main component that starts with the code generation, but it passes some of the code generation work on to helper components.
$classBlueprint = $factory->class('MyClass');
$helper = ...
$helper->modifyClass($classBlueprint, $factory);
So, the helper needs the factory. Either we pass the factory as an argument, or the helper needs to create one.
In the suggested version, the helper does not need to know the factory or how to create it. It only gets the class. This would allow the helper to work with different implementations of the factory.
b) The code uses ->addRequiredParam('someParam', 'SomeClass') which creates a parameter in a single method call, rather than the multiple calls on $factory->param(). Apart from the comments from a) [regarding reusability and stuff] the issue here is order of optional bits of information (as you already point out in your second comment). You basically have to decide on some arbitrary order, which I personally don't like much (because it usually leads to the , null, null, null, 'foo' pattern and because the meaning of a certain parameter often becomes unclear.)
Yes. This is the ugliest part of the proposal, I think.
Although you will rarely get the null, null, null, 'foo'. The suggested order is the best we could get if we squeeze it in one method.
Is this issue still valid?
I currently do not have an active interest in this issue. I suggest to treat it as an idea, which can be considered for future development, or not. No problem if you want to close it.
Thanks for your clear and fast response.
In that case I suggest closing it, so there is more space for active issues.