PHP Code Generator Redux
PHP is an imperative programming language and can also be used as a template engine. This allows programming language constructs and static text to be combined via placeholders or marked areas. In addition, several variants for string concatenation are available. It is also possible to write the generated code to different files. With PHP version 7.0 an Abstract Syntax Tree (AST) is supported. An AST represents a syntactically correct sentence of a programming language as a tree structure. Therefore, PHP is very well suited as a programming language for a code generator.
Model Driven Software Development (MDSD) deals with the automatic creation of software systems based on models. A model-to-text transformation generates code for a specific platform. In a model-to-model transformation, an existing model is enriched with information or an entirely new model can be created.
In order to be able to generate executable software, a domain-specific abstraction with formal modeling is required. Code is automatically generated from models, for instance based on the Unified Modeling Language (UML), a JSON schema, XML etc. Models thus not only serve to document the application software, but also represent the actual code. As many artifacts of a software system as possible should be derived from the formal models. Modeling provides a simplified representation of complex interrelationships. The model is an abstract representation of a software system to be developed. This can be represented both graphically and textually.
The following sections describes which Open-Source PHP libraries can be used to generate PHP code.
PHP Filter
The library open-code-modeling/php-filter provides common filter for PHP code generation. There are preconfigured filters to filter a name / label for class names, constants, properties, methods and namespaces. This library uses laminas/laminas-filter as a great foundation.
PHP Code AST
It is a challenge to combine generated and handwritten code. The library open-code-modeling/php-code-ast ships with an easy to use high level object-oriented API and supports also reverse engineering of your PHP code. During code generation, previously generated code is then analyzed using an AST-based approach. This makes it possible to distinguish between already generated and handwritten code. An approach such as protected areas can thus be dispensed with. Furthermore, the AST-based approach allows parts of the code to be specifically modified. This library uses nikic/php-parser as a great foundation.
Take a look at a straightforward example of generating a class using the ClassBuilder
high level API.
use OpenCodeModeling\CodeAst\Builder;
$parser = (new PhpParser\ParserFactory())->create(PhpParser\ParserFactory::ONLY_PHP7);
$printer = new PhpParser\PrettyPrinter\Standard(['shortArraySyntax' => true]);
$parser = (new PhpParser\ParserFactory())->create(PhpParser\ParserFactory::ONLY_PHP7);
$printer = new PhpParser\PrettyPrinter\Standard(['shortArraySyntax' => true]);
$code = ''; // or file_get_contents() of file
$ast = $parser->parse($code);
$classBuilder = Builder\ClassBuilder::fromScratch('TestClass', 'My\\Awesome\\Service');
$classBuilder
->setFinal(true)
->setExtends('BaseClass')
->setNamespaceImports('Foo\\Bar')
->setImplements('\\Iterator', 'Bar')
->addConstant(
Builder\ClassConstBuilder::fromScratch('AWESOME', true)
)
->addMethod(
Builder\ClassMethodBuilder::fromScratch('sayHello')
->setBody("echo 'Hello World!';")
->setReturnType('void')
);
$nodeTraverser = new PhpParser\NodeTraverser();
$classBuilder->injectVisitors($nodeTraverser, $parser);
print_r($printer->prettyPrintFile($nodeTraverser->traverse($ast)));
The code above will generate the following PHP code.
<?php
declare (strict_types=1);
namespace My\Awesome\Service;
use Foo\Bar;
final class TestClass extends BaseClass implements \Iterator, Bar
{
public const AWESOME = true;
public function sayHello() : void
{
echo 'Hello World!';
}
}
JSON Schema to PHP
A JSON schema can be used as a model for code generation. For instance it can be used to create value objects. The library open-code-modeling/json-schema-to-php parses JSON schema files and provides an API to easily generate code from a JSON schema.
Consider you have this JSON schema.
{
"type": "object",
"required": ["buildingId", "name"],
"additionalProperties": false,
"definitions": {
"name": {
"type": ["string", "null"]
}
},
"properties": {
"buildingId": {
"type": "string",
"pattern": "^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}__aSyNcId_<_lMvVxdUo__quot;
},
"name": {
"$ref": "#/definitions/name"
}
}
}
You can create a TypeSet
definition from the JSON schema above with the following code.
<?php
use OpenCodeModeling\JsonSchemaToPhp\Type;
use OpenCodeModeling\JsonSchemaToPhp\Type\TypeSet;
use OpenCodeModeling\JsonSchemaToPhp\Type\ObjectType;
use OpenCodeModeling\JsonSchemaToPhp\Type\StringType;
$decodedJson = \json_decode($jsonSchema, true);
$typeSet = Type::fromDefinition($decodedJson);
/** @var ObjectType $type */
$type = $typeSet->first();
$type->additionalProperties(); // false
$properties = $type->properties();
/** @var TypeSet $buildingIdTypeSet */
$buildingIdTypeSet = $properties['buildingId'];
/** @var StringType $buildingId */
$buildingId = $buildingIdTypeSet->first();
$buildingId->name(); // buildingId
$buildingId->type(); // string
$buildingId->pattern(); // ^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$
$buildingId->isRequired(); // true
$buildingId->isNullable(); // false
// ...
JSON Schema to PHP AST
The library open-code-modeling/json-schema-to-php-ast compiles a JSON schema to PHP classes / value objects via PHP AST. It provides factories to create nikic/php-parser node visitors or open-code-modeling/php-code-ast class builder objects from JSON schema. It supports the the JSON schema types string, enum, integer, boolean, number and array. The type string supports also the formats date-time, ISO 8601, uuid and BCP 47.
PHP Code Generator
For sophisticated PHP code generation workflows there is the library open-code-modeling/php-code-generator. It provides the runtime environment for various components. These can be interconnected via a configuration. Thus, individual operational sequences can be provided and combined. By this modular structure the code generator can be individually extended and configured by developers.
The code beneath shows a simple Hello World workflow.
use OpenCodeModeling\CodeGenerator;
$workflowContext = new CodeGenerator\Workflow\WorkflowContextMap();
// initialize workflow with some data
$workflowContext->put('hello', 'Hello ');
$config = new CodeGenerator\Config\Workflow(
new CodeGenerator\Workflow\ComponentDescriptionWithSlot(
function (string $input) {
return $input . 'World';
},
'greetings', // output slot
'hello' // input slot
),
new CodeGenerator\Workflow\ComponentDescriptionWithInputSlotOnly(
function (string $input) {
echo $input; // prints Hello World
},
'greetings',
)
);
$workflowEngine = new CodeGenerator\Workflow\WorkflowEngine();
$workflowEngine->run($workflowContext, ...$config->componentDescriptions());
Conclusion
PHP Code generation using an AST-based approach allows already generated code to be matched with new code. Another advantage of this approach is that manually added code is not overwritten. Various detection mechanisms have been implemented for this purpose.
This article shows that PHP code can be generated very well using an AST-based approach and a model in PHP. If you find it useful please spread the word and star the libraries on GitHub.