Extending Siesta with custom generator


Siesta as code generation framework


Siesta is actually a code generating framework for entity relationship based scenarios. ORM is a pretty strong use case for that approach. Siesta is organized in Generator and Plugins. A generator is used to generate a class and a plugin is used to add member, methods etc. inside a class. Siesta ships with two Generators: one for the entity and one for the service class. But you could define more generator to generate data transfer objects or even Javascript classes that represent the entity.

Generator


A Siesta generator is simply a class that implements the Generator Interface. It only has 2 methods. addPlugin allows to initialize it with the plugins that are configured for the specific generator and a method to perform the magic (generate). The entity object passed has all the information about the entity that needs to be generated.

    namespace Siesta\Contract;

    interface Generator
    {
        public function addPlugin(Plugin $plugin);

        public function generate(Entity $entity, string $baseDir);
    }

Siesta generator are defined in a json file. This defines the implementing class and the plugins to be used. Furthermore validator can be defined that must pass when reading the xml schema. (Standard Config) You can also change the behaviour of Siesta itself. If you do not want the toArray()/fromArray() methods generated just copy the standard configuration modify it and point to it in the siesta configuration. You could also build your own plugins and replace existing ones.

    {
      "generatorList": [
        {
          "name": "Entity Generator",
          "className": "Siesta\\Generator\\EntityGenerator",
          "pluginList": [
            "Siesta\\GeneratorPlugin\\Entity\\CollectionManyAccess",
            "Siesta\\GeneratorPlugin\\Entity\\ArePKIdenticalPlugin"
          ],
          "config": {
          },
          "validatorList": [
            "Siesta\\Validator\\DefaultDataModelValidator",
          ]
        }
      ]
    }

Plugin


A plugin is the a class that add functionality to a generated class. For example the MemberPlugin (Code) will add the members to the generated class. The method getUseClassList is supposed to return an array of strings with all the classes that are used from the generated code. The getDependantPluginList is not used at the moment. The getInterfaceList allows to return additional interfaces that the class implements as a result of the plugin. Inside the generate method the code is placed to add functionality to the class. The code generator has methods to add methods to the generated class.
Have a look at the MemberPlugin.

    /**
     * @author Gregor Müller
     */
    interface Plugin
    {

        /**
         * @param Entity $entity
         *
         * @return array
         */
        public function getUseClassNameList(Entity $entity) : array;

        /**
         * @return string[]
         */
        public function getDependantPluginList() : array;

        /**
         * @return array
         */
        public function getInterfaceList() : array;

        /**
         * @param Entity $entity
         * @param CodeGenerator $codeGenerator
         */
        public function generate(Entity $entity, CodeGenerator $codeGenerator);

    }

The following example shows a plugin that transforms the entity data to xml. You can use the BasePlugin Class, it has already implementation of getUseClassNameList, getDependantPluginList and getInterfaceList. You can simply overwrite them if you need custom behaviour. The given example is supposed to show the idea of a plugin. In reality you would want references, collections etc. also serialized into XML.

First command is to create a method. This is done by invoking $codeGenerator->newPublicMethod("toXML") with the method name (toXML). The code generator will return a method object, to which you can add Parameters (\DOMElement) and their names in the method (element).

The entity allows you to iterate over all attributes. (see datamodel below for details). First some base information is retrieved (type, name and memberName of the attribute).

Depending on the attributes type different coding is needed in the toXML method. If the php type is bool, int, float or string, we add an if statement to the generated method ($method->addIfStart) You just pass the if condition. In this case we check if the member attribute is null. $memberName . ' !== null' This is important in strict_types environment, because \DOMElement->setAttribute will not accept a null value. The if statement is closed with $method->addIfEnd();

If the php type is SiestaDateTime (extends from \DateTime) then we need the null check as well. If the datetime attribute is not null, we can invoke the method getSQLDateTime(). This is done in this line: $method->addLine('$element->setAttribute("' . $name . '", ' . $memberName . '->getSQLDateTime());'); The addLine method allows you to write php code. The element variable is defined as a method parameter and we invoke the setAttribute method on it. We pass as key the name of the attribute and as value the result of getSQLDateTime.

    <?php

    declare(strict_types = 1);

    namespace Siesta\GeneratorPlugin\Entity;

    use Siesta\CodeGenerator\CodeGenerator;
    use Siesta\GeneratorPlugin\BasePlugin;
    use Siesta\Model\Entity;
    use Siesta\Model\PHPType;

    class ToXMLPlugin extends BasePlugin
    {

        public function generate(Entity $entity, CodeGenerator $codeGenerator)
        {

            // create a new method and add a parameter
            $method = $codeGenerator->newPublicMethod("toXML");
            $method->addParameter('\DOMElement', 'element');

            // iterate all attributes
            foreach ($entity->getAttributeList() as $attribute) {
                $phpType = $attribute->getPhpType();
                $name =  $attribute->getPhpName();
                $memberName = '$this->' . $name;

                // primitive types can be set directly
                if ($phpType === PHPType::BOOL || $phpType === PHPType::INT || $phpType === PHPType::FLOAT || $phpType === PHPType::STRING) {

                    $method->addIfStart($memberName . ' !== null');
                    $method->addLine('$element->setAttribute("' . $name . '", ' . $memberName . ');');
                    $method->addIfEnd();
                    continue;
                }

                // datetime need method call
                if ($phpType === PHPType::SIESTA_DATE_TIME) {
                    $method->addIfStart($memberName . ' !== null');
                    $method->addLine('$element->setAttribute("' . $name . '", ' . $memberName . '->getSQLDateTime());');
                    $method->addIfEnd();
                }
            }

            // really important !!!
            $method->end();
        }

    }

To activate your personal plugin you just have to follow the instructions in the next paragraph (Changing default behaviour). You have to add the name of the plugin to the generator in which you want to have the toXML() Method.

    {
          "name": "Entity Generator",
          "className": "Siesta\\Generator\\EntityGenerator",
          "pluginList": [
            ... ,
            "Siesta\\GeneratorPlugin\\Entity\\ToXMLPlugin"
          ],
          "config": {
          },
          "validatorList": [
            ...,
          ]
    }

Changing default behaviour


To change the default behaviour, for example to remove plugins or add plugins, just copy the siesta.generator.config.json from (vendor/gm314/siesta/src/Siesta/Config) make the modifications you want and point in the siesta.config.json (the one generated with the init command) to your generic configuration. A value of null will use the default file in vendor/gm314/siesta/src/Siesta/Config

   {
        "connection": [
            { ... }
        ],
        "generator": {
            "dropUnusedTables": true,
            "entityFileSuffix": ".entity.xml",
            "migrationTargetPath": "migration",
            "tableNamingStrategy": "Siesta\\NamingStrategy\\ToUnderScoreStrategy",
            "columnNamingStrategy": "Siesta\\NamingStrategy\\ToUnderScoreStrategy",
            "migrationMethod": "direct",
            "baseDir": "src",
            "connectionName": null,
            "genericGeneratorConfiguration": "config/myGenerator.json"
        },
        "reverse": { ... }
    }

    }

The datamodel


Both Generator and Plugin interface will get the Entity object passed, that is subject to generation. The Entity class represents a generated class respectively a table in the database. It has 1..n Attributes which in term represent a member in the generated entity as well as a column in the corresponding table. An Index consists of 1..n IndexPart which in term reference an attribute. An attribute/column can be part of serveral indexes. A reference (foreign key) always refers a foreign entity and has 1..n ReferenceMapping. A referenceMapping maps a local attribute (in the entity in which it is defined) and a foreign Attribute in the foreign entity.

The Entity has methods like

  • Entity->getAttributeList()
  • Entity->getReferenceList()
  • Entity->getIndexList()
to access the corresponding objects. The source code has all the phpdoc annotations to give you code completion in a development environment like PhpStorm, Zend Studio etc.

A little bit more complex are Collections and Many 2 Many Collections. An entity can have 0..n collections. A collection always refers to a foreign entity and a foreign reference. So for example if you have the CartItem object which references a Cart. (CartItem has a foreign key/ references to the cart object) A collection allows you to collect all CartItems which refer the the given Cart. Therefore a collection needs to know the foreign entity and the foreign reference.

An entity can also 0..n Collection Many. As an example you find in the tests the relationship between students and exams. A student can participate in 0..n exams. and every exam can have 0..n students participating. This is typically realized with a mapping table. Therefore a collection Many has a foreignEntity and a mapping entity. Furthermore to foreignReference is the reference between the mapping table and the foreign entity. The mappingReference is the reference between the entity and the mapping entity.

Also here you will find all needed methods to access these objects.

  • Entity->getCollectionList()
  • Entity->getCollectionManyList()
  • Collection->getForeignEntity()
  • Collection->getForeignReference()
  • CollectionMany->getMappingEntity()

Adding validators for your plugins


If your plugin needs validation logic you can add custom validators. A validator must implement one of the following interfaces.

    Siesta\Contract\EntityValidator
    Siesta\Contract\AttributeValidator
    Siesta\Contract\ReferenceValidator
    Siesta\Contract\IndexValidator
    Siesta\Contract\CollectionValidator
    Siesta\Contract\CollectionManyValidator