2. Create Cloudrexx App

Overview

This tutorial will guide you through the creation of a Cloudrexx app in five steps:

  1. Basic setup
    1. Name and type
    2. Create app
    3. MySQL Workbench
    4. Generate model
    5. Add missing relations and behaviors
    6. Business logic
  2. Model interaction and testing
    1. ViewGenerator
    2. BackendController
    3. Events
    4. API
    5. Testing
  3. User experience
    1. Language files
    2. Widgets
    3. Frontend
  4. Settings
    1. Mail templates
    2. Other settings
  5. Cleanup, Export and review
    1. Cleanup
    2. Export
    3. Review
  6. Further reading

1. Basic setup

This tutorial assumes that you have a working setup of Cloudrexx to play with. If you don't, please refer to Environment Setup.

1.1 Name and type

Before any code is written/generated you should think about what your app (further on this will be called it by its technical name "component") should do. Choose a meaningful name based on your considerations which consists of at least two nouns written in camel-case. As an example we will use "TodoManager" here.

As for the type: A component can be of type core, core_module or module:

  • core components are required by Cloudrexx to work properly.
  • core_module components are not technically required by the system, but they are required to make the system do something useful.
  • module components are everything else. Most apps are of this type.

1.2 Create app

After you've chosen your component's name and type you may continue by creating it. In order to save you the work of creating all files manually and register the component in the system by hand Cloudrexx provides a component called "Workbench" which provides some commands to simplify things.
The workbench can be invoked by calling ./cx workbench (or ./cx wb). This command will show you a list of available sub-commands. In this step we will use the "create" sub-command:

./cx wb create <component_type> <component_name>

This will create your component's folder and copy some sample code into it. In addition, it registers your component in the system, adds an entry to the backend navigation and makes sure your component is active/accessable.

If you want to re-distribute Cloudrexx including your component (which is possible under the AGPL), your component has to be licensed under AGPL.

If you use your component for you and your customers only (without re-distribution), you can license your component under any license you wish. You should indicate the license in the file headers and in a license file within your component's directory.

To provide a nice example for this tutorial we will create a simple component of type module named "TodoManager". Create this component by running the following command:

./cx wb create module TodoManager

You should track your code using a VCS. We propose to track the contents of your component directory. For GIT you could do so by issuing the following command from within that directory:

git init && git add * && git commit -m "Initial commit"

A repository with the example component explained in this article can be found on GitHub.

1.3 MySQL Workbench

If your component does not need a database model, you may proceed with Model interaction.

Cloudrexx uses Doctrine to interact with the database. In order to map the database objects with PHP objects Doctrine uses a schema specification which is saved in YAML (*.yml) files.

Since the process of creating the necessary files is quite repetitive, the Workbench component includes a command which can help you create them. For this you need to design your database model in MySQL Workbench first. Place the .mwb file in your component's "Doc" directory.

When designing your model with the MySQL Workbench, you should keep the following rules in mind:

  • Name your tables according to the following scheme: <component_type>_<component_name>_<entity>
    • Tables names are written in lower case
    • <component_type> is one of core,core_module, module.
    • <component_name> is your component's name in one word. Example: todomanager
    • <entity> is the name of your entity which can contain underlines.
    • Do not include the database prefix (DBPREFIX) in the table name. Cloudrexx does this for you.
  • When renaming fields which are part of a relation, MySQL Workbench does not reflect these changes in the relation. Therefore you should consider re-drawing the relations after such an action.
  • Do not include tables from outside of your component. If you have a relation to a framework table (for example to the "user" table), do not add this relation in this step.
  • Always use the database type TIMESTAMP for points in time. Cloudrexx automatically handles timezones for this type.
  • Cloudrexx includes several Doctrine behaviors. Use them where applicable instead of reinventing the wheel in your model:
    • Translatable: You can mark individual columns as " translatable" which allows the translation of them.
    • Tree: Creates tree-like structures using the nested-set technique.
    • Loggable: You can mark individual columns as "loggable" which allows to track their change history.
    These bahaviors are applied in the step Add missing relations and behaviors.

Here's a MySQL Workbench file for our example component:
Download the MySQL Workbench file here and place it in the folder modules/TodoManager/Doc/.

1.4 Generate model

In order to generate the model you've designed in the previous step, use the following command (<component_name> needs to be written in camel-case here):

./cx wb db update <component_type> <component_name>

The command performs the following steps:

YAML generation

The command will show a list of all .mwb files in your component's "Doc" folder prefixed by a number. Choose the correct number for the file you want to use or just press enter to skip this step. In our example we want to use the only file there is so we type "1" and press enter.

The workbench now tries to generate the .yml files for you (under Model/Yaml/ of your component). If you run this more than once, you will be asked if the already existing files should be overwritten. If you did not change anything manually you can answer with yes ("y").

Entity generation

Based on the .yml files Doctrine is now instructed by the workbench to generate the necessary entity classes (under Model/Entity/ of your component). If you run this more than once, you will be asked if the already existing files should be overwritten. If you did not change anything manually you can answer with yes ("y").

If there is a bug in your schema, this step is the most likely to fail. You may want to check the generated .yml files to see where the problem is coming from, in order to fix it in the MySQL Workbench file.

If you cannot solve a problem Cloudrexx offers support. Please file your request here.

Repository generation

Once the entities are generated, Doctrine is instructed to generate a repository class for each entity (under Model/Repository/ of your component). You may or may not need them. If you don't need them you can remove them from your component. In this case you should delete the "repositoryClass" property from the respective .yml file.

Database statements

Now Doctrine shows the database DDL statements necessary to adjust the database to the new schema and you are asked whether those should be executed in the database. Please review the statements carefully before saying yes to this question. If there are statements do not want to execute, you can copy and paste the others end execute them manually.

Validation

As a last step Doctrine is instructed to check if your schema is valid and in sync with the database. If there are any errors here do fix them!

If you cannot solve a problem Cloudrexx offers support. Please file your request here.

TodoManager

In order to generate the model for our example component you should execute the following command:

./cx wb db update module TodoManager

1.5 Add missing relations and behaviors

Relations

Now is the time to add the missing relations to entities which are not part of your component. In our example, there is a relation from the field module_todomanager_todo.user_id to the framework entity User.

If you create your own app you should refer to the Doctrine documentation in order to understand Doctrine mappings.

The generated YAML files contain slightly too much data: Foreign key fields are mapped twice. In our example component the only such case is the category_id field of the Todo entity. Therefore we should drop the following code from the file modules/TodoManager/Model/Yaml/Cx.Modules.TodoManager.Model.Entity.Todo.dcm.yml

    categoryId:
      type: integer
      column: category_id

For the example component, we will add the missing relation to the User entity. For this we drop the old mapping for the user_id field and add a new one. In the file modules/TodoManager/Model/Yaml/Cx.Modules.TodoManager.Model.Entity.Todo.dcm.yml drop the following code:

    userId:
      type: integer
      column: user_id

And add the following code to the end of the file:

    user:
      targetEntity: Cx\Core\User\Model\Entity\User
      joinColumn:
        name: user_id
        referencedColumnName: id

You should then re-run the workbench "db update" command without re-generating the .yml files to apply these changes:

./cx wb db update module TodoManager

Behaviors

As a next step, we add the necessary mappings for the desired behaviors. In our example component, we want the Todo's name and description to be translatable. For this we need to extend the file modules/TodoManager/Model/Yaml/Cx.Modules.TodoManager.Model.Entity.Todo.dcm.yml so it looks as follows:

Cx\Modules\TodoManager\Model\Entity\Todo:
  type: entity
  table: module_todomanager_todo
  repositoryClass: Cx\Modules\TodoManager\Model\Repository\TodoRepository
  gedmo:
    translation:
      locale: locale
      entity: Cx\Core\Locale\Model\Entity\Translation
  indexes:
    fk_module_todomanager_todo_module_todomanager_category_idx:
      columns: [ category_id ]
  id:
    id:
      type: integer
      generator:
        strategy: AUTO
  fields:
    done:
      type: boolean
    name:
      type: string
      length: 50
      gedmo:
        - translatable
    description:
      type: text
      gedmo:
        - translatable
    reminderDate:
      type: datetime
      nullable: true
  manyToOne:
    category:
      targetEntity: Cx\Modules\TodoManager\Model\Entity\Category
      inversedBy: todos
      joinColumn:
        name: category_id
        referencedColumnName: id
    user:
      targetEntity: Cx\Core\User\Model\Entity\User
      joinColumn:
        name: user_id
        referencedColumnName: id

Furthermore our Todo entity needs to implement the Translatable interface. Change the class header of the Todo entity (modules/TodoManager/Model/Entity/Todo.class.php) so it looks as follows:

class Todo extends \Cx\Model\Base\EntityBase implements \Gedmo\Translatable\Translatable {

The class also needs a "locale" property and a method called "setTranslatableLocale($locale)". Add the following code to achieve this:

    /**
     * @var string
     */
    protected $locale;

and

    public function setTranslatableLocale($locale)
    {
        $this->locale = $locale;
    }

You should then re-run the workbench "db update" command without re-generating the .yml files and without overwriting the existing entities to apply these changes:

./cx wb db update module TodoManager

Do not forget to commit your changes to your VCS.

1.6 Business logic

Entity and repository classes are the perfect spot to put your business logic.

In our example we will always set a Todo's user to the current user if none is specified and let the Category identify itself by its name. First add the following constructor to the Todo entity:

    /**
     * Sets $this->user to current user if its not set
     */
    public function __construct() {
        if ($this->user) {
            return;
        }
        $userId = \FWUser::getFWUserObject()->objUser->getId();
        if (!$userId) {
            return;
        }
        $em = $this->cx->getDb()->getEntityManager();
        $userRepo = $em->getRepository('Cx\Core\User\Model\Entity\User');
        $this->user = $userRepo->find($userId);
    }

This sets the user to the current user by default for all new Todo's. To make categories show their name we add the following method to the Category entity:

    /**
     * Makes this entity identify itself by its name
     * @return string
     */
    public function __toString() {
        return $this->getName();
    }

2. Model interaction

This chapter will cover the possibilities to interact with the model.

If your component does not need a database model, you may want to add other types of entities manually. Independent of your component having a database model or any model at all, this chapter might be interesting to see how your component can interact with the world.

2.1 ViewGenerator

If you navigate to the backend of your installed Cloudrexx, you should now be able to find your component in the "Applications" section. If you follow this link you can see that the system automatically created an overview and a settings section. Additionally, there's a section for each entity you created. These sections allow us to add, edit and delete entries for these entities. The piece of code that generates these views is refered to as "ViewGenerator". You can customize these views by using the ViewGenerator's options.

In our example we will hide some columns and show the category name instead of it's ID. Additionally we will add the ability to search through the Todo's. For this we replace the method getViewGeneratorOptions() in the file modules/TodoManager/Controller/BackendController.class.php by the following code:

    protected function getViewGeneratorOptions($entityClassName, $dataSetIdentifier = '') {
        $options = parent::getViewGeneratorOptions($entityClassName, $dataSetIdentifier);

        switch ($entityClassName) {
            case 'Cx\Modules\TodoManager\Model\Entity\Todo':
                $options['fields'] = array(
                    'description' => array(
                        'showOverview' => false,
                    ),
                    'category' => array(
                        'allowFiltering' => true,
                    ),
                );
                $options['functions']['filtering'] = true;
                $options['functions']['searching'] = true;
                break;
            case 'Cx\Modules\TodoManager\Model\Entity\Category':
                $options['fields'] = array(
                    'todos' => array(
                        'showOverview' => false,
                        'showDetail' => false,
                    ),
                );
                break;
        }
        return $options;
    }

2.2 BackendController

By overriding methods of the BackendController's parent class you can easily change the behavior of our backend views. In our example app we do not need the overview section. To achieve this we can simply change the return value of the showOverviewPage() method.

More information on what can be done this way, please refer to the SystemComponentController's DocBlocks.

2.3 Events

Events are a nice tool to simplify interaction between components. If you want your component to provide events you can do so in your ComponentController's registerEvents() method. In order to listen to events you can use your ComponentController's registerEventListeners() method.

For more information please refer to our wiki.

For our example component we will implement an named "TodoManager/Done" that gets triggered whenever a Todo is marked as done. In order to tell the system that we provide such an event we need to change the method registerEvents() in modules/TodoManager/Controller/ComponentController.class.php as follows:

    public function registerEvents() {
        $this->cx->getEvents()->addEvent($this->getName() . '/Done');
    }

Since we want to trigger our event if a change to a Todo happens, we need to listen to model changes of Todo's. Therefore we need an EventListener. Add the following code to the file modules/TodoManager/Model/Event/TodoEventListener.class.php:

<?php declare(strict_types=1);

namespace Cx\Modules\TodoManager\Model\Event;

class TodoEventListener extends \Cx\Core\Event\Model\Entity\DefaultEventListener {


}

Model change events are provided by Doctrine and routed through the Cloudrexx event system. We want to listen to "postUpdate" events in order to trigger our event after the change is made persistent. We don't trigger the event for Todo's that are created in done state. Since we cannot check which fields changed in postUpdate we need to also register to "preUpdate" to get the previous state of the Todo. In order to do so we need to change the method registerEventListeners() in modules/TodoManager/Controller/ComponentController.class.php as follows:

    public function registerEventListeners() {
        $todoListener = new \Cx\Modules\TodoManager\Model\Event\TodoEventListener(
            $this->cx
        );
        $this->cx->getEvents()->addModelListener(
            'preUpdate',
            $this->getNamespace() . '\Model\Entity\Todo',
            $todoListener
        );
        $this->cx->getEvents()->addModelListener(
            'postUpdate',
            $this->getNamespace() . '\Model\Entity\Todo',
            $todoListener
        );
    }

In our EventListener we listen to those events and check if the done property has changed to "true":

    protected $doneChangedToTrue = false;

    protected function preUpdate($lea) {
        $this->doneChangedToTrue = (
            $lea->hasChangedField('done') &&
            $lea->getEntity()->getDone() &&
            !$lea->getOldValue('done')
        );
    }

    protected function postUpdate($lea) {
        if (!$this->doneChangedToTrue) {
            return;
        }
        $this->cx->getEvents()->triggerEvent(
            'TodoManager/Done',
            array(
                $lea->getEntity(),
            )
        );
    }

And that's it. We now have an event named "TodoManager/Done" which is triggered whenever an existing Todo is marked as done.

2.4 API

Cloudrexx automatically provides RESTful API access to all Doctrine entities. In order for this to work, you need to register your entities on the RESTful API and set access permissions on them.

In our example component we will provide full read-only access to both entities using an API key. To do so you need to execute the following statements on your environments database:

INSERT INTO `contrexx_core_module_data_access_apikey` (`id`, `api_key`) VALUES(1, 'dev');
INSERT INTO `contrexx_core_data_source` (`identifier`, `options`, `type`) VALUES('Cx\\Modules\\TodoManager\\Model\\Entity\\Todo', 'a:0:{}', 'doctrineRepository');
INSERT INTO `contrexx_core_module_data_access` (`read_permission`, `write_permission`, `data_source_id`, `name`, `field_list`, `access_condition`, `allowed_output_methods`) VALUES(NULL, NULL, LAST_INSERT_ID(), 'todomanager-todo', 'a:0:{}', 'a:0:{}', 'a:0:{}');
INSERT INTO `contrexx_core_module_data_access_data_access_apikey` (`api_key_id`, `data_access_id`, `read_only`) VALUES(1, LAST_INSERT_ID(), 1);
INSERT INTO `contrexx_core_data_source` (`identifier`, `options`, `type`) VALUES('Cx\\Modules\\TodoManager\\Model\\Entity\\Category', 'a:0:{}', 'doctrineRepository');
INSERT INTO `contrexx_core_module_data_access` (`read_permission`, `write_permission`, `data_source_id`, `name`, `field_list`, `access_condition`, `allowed_output_methods`) VALUES(NULL, NULL, LAST_INSERT_ID(), 'todomanager-category', 'a:0:{}', 'a:0:{}', 'a:0:{}');
INSERT INTO `contrexx_core_module_data_access_data_access_apikey` (`api_key_id`, `data_access_id`, `read_only`) VALUES(1, LAST_INSERT_ID(), 1);

This adds an API key "dev", a DataSource for each of our entities, a DataAccess object for these DataSources and maps our API key to both of these DataAccesses. After those statements are executed you should be able to get a list of your Todo's by opening /api/v1/json/todomanager-todo/?apikey=dev in your browser.

2.5 Testing

In order to simplify maintainability you should use UnitTests. There's an example UnitTest in your component's "Testing" folder. You can run the following command to execute the tests:

./cx workbench test module TodoManager

3 User experience

3.1 Language files

In order to translate frontend and backend views, Cloudrexx uses language files. Each component has its own set of language files located in the component's "lang" directory. English language files are a requirement for all language variables of all components as English is used a fallback if a language variable is otherwise not present.
For each language there is a file for frontend and backend. The components name and description should be set in both of these.

For our example component we overwrite the exising language variables in modules/TodoManager/lang/en/backend.php with the following content:

// Let's start with module info:
$_ARRAYLANG['TXT_MODULE_TODOMANAGER'] = 'Todo Manager';
$_ARRAYLANG['TXT_MODULE_TODOMANAGER_DESCRIPTION'] = 'This is a new module with some sample content to show how to start.';

// Here come the ACTs:
$_ARRAYLANG['TXT_MODULE_TODOMANAGER_ACT_DEFAULT'] = 'Overview';
$_ARRAYLANG['TXT_MODULE_TODOMANAGER_ACT_TODO'] = 'Todo\'s';
$_ARRAYLANG['TXT_MODULE_TODOMANAGER_ACT_CATEGORY'] = 'Categories';
$_ARRAYLANG['TXT_MODULE_TODOMANAGER_ACT_SETTINGS'] = 'Settings';
$_ARRAYLANG['TXT_MODULE_TODOMANAGER_ACT_SETTINGS_DEFAULT'] = 'General';
$_ARRAYLANG['TXT_MODULE_TODOMANAGER_ACT_SETTINGS_HELP'] = 'Mailing';

// Now our content specific values:
$_ARRAYLANG['id'] = 'ID';
$_ARRAYLANG['user'] = 'User';
$_ARRAYLANG['done'] = 'Done';
$_ARRAYLANG['name'] = 'Name';
$_ARRAYLANG['description'] = 'Description';
$_ARRAYLANG['reminderDate'] = 'Reminder date';
$_ARRAYLANG['category'] = 'Category';

You can test if this worked by opening/refreshing the backend view. It should now contain these pretty-looking language strings. You may add the same variables in any other backend language.

3.2 Widgets

Widgets are placeholders with system-wide availability. You can find more info about Widgets here.

For the example component we will create a widget which shows the current user's Todo's. For this we need to register the widget, add code to parse it and drop its cache whenever a change happens. To register the widget add the following code to the postInit() method of modules/TodoManager/Controller/ComponentController.class.php:

        $widgetController = $this->getComponent('Widget');
        $widget = new \Cx\Core_Modules\Widget\Model\Entity\EsiWidget(
            $this,
            'TODO_LIST'
        );
        $widget->setEsiVariable(
            \Cx\Core_Modules\Widget\Model\Entity\EsiWidget::ESI_VAR_ID_USER
        );
        $widgetController->registerWidget(
            $widget
        );

To parse the widget, create a new file modules/TodoManager/Controller/EsiWidgetController.class.php with the following content:

<?php declare(strict_types=1);

namespace Cx\Modules\TodoManager\Controller;

class EsiWidgetController extends \Cx\Core_Modules\Widget\Controller\EsiWidgetController {

    /**
     * Parses a widget
     * @param string $name Widget name
     * @param \Cx\Core\Html\Sigma Widget template
     * @param \Cx\Core\Routing\Model\Entity\Response $response Current response
     * @param array $params Array of params
     */
    public function parseWidget($name, $template, $response, $params) {
        if ($name == 'TODO_LIST') {
            // If no user is logged in we do nothing
            \Cx\Core\Session\Model\Entity\Session::getInstance();
            if (!\FWUser::getFWUserObject()->objUser->login()) {
                return;
            }
            $em = $this->cx->getDb()->getEntityManager();
            $todoRepo = $em->getRepository(
                'Cx\Modules\TodoManager\Model\Entity\Todo'
            );
            // get all todos of current user
            $todos = $todoRepo->findBy(
                array(
                    'user' => \FWUser::getFWUserObject()->objUser->getId(),
                    'done' => false,
                )
            );
            // if user has no todos show nice error message
            if (!count($todos)) {
                global $_ARRAYLANG;
                $template->setVariable(
                    $name,
                    $_ARRAYLANG['TXT_MODULE_TODOMANAGER_NO_TODOS']
                );
                return;
            }
            // show as viewgenerator template
            $vg = new \Cx\Core\Html\Controller\ViewGenerator($todos, array(
                'Cx\Modules\TodoManager\Model\Entity\Todo' => array(
                    'fields' => array(
                        'id' => array(
                            'showOverview' => false,
                        ),
                        'done' => array(
                            'showOverview' => false,
                        ),
                        'user' => array(
                            'showOverview' => false,
                        ),
                    ),
                ),
            ));
            $template->setVariable($name, $vg);
            return;
        }
    }
}

Since we us a language variable here, we need to add it to modules/TodoManager/lang/en/frontend.php:

$_ARRAYLANG['TXT_MODULE_TODOMANAGER_NO_TODOS'] = 'Congratulations, you have no open tasks!';

In order for the system to know about this new EsiWidgetController we need to register it. For this, change the code of getControllerClasses() and getControllersAccessableByJson() methods in modules/TodoManager/Controller/ComponentController.class.php to:

    public function getControllerClasses() {
        return array('Frontend', 'Backend', 'EsiWidget');
    }

    public function getControllersAccessableByJson() {
        return array('EsiWidgetController');
    }

Now the widget should be working. Test this by placing [[TODO_LIST]] somewhere on your website (in a webdesign template file, in a content page or pane, ...). This should show a small table with all of the user's Todo's or a message that he has none. You may want to use a different template for ViewGenerator than the default backend table.

3.3 Frontend

In order to display content on the components application pages we need to create templates in modules/TodoManager/View/Template/Frontend/ and parse them in modules/TodoManager/Controller/FrontendController.class.php.

The example component will show a filterable list of all Todo's on its main page. On a detail page a single Todo is shown. To make this work we will first create two templates "Default.html" and "Detail.html" with the following contents:

<!-- Default.html -->
<!-- BEGIN todos -->
    <!-- BEGIN todo -->
        {ID}, {NAME}, {DESCRIPTION}, {CATEGORY_ID}, {CATEGORY_NAME}, {CATEGORY_DESCRIPTION}, {USER_ID}, {USER_NAME}, {REMINDER_DATE}, {DETAIL_URL},
        <!-- BEGIN todo_done -->
        1
        <!-- END todo_done -->
        <!-- BEGIN todo_open -->
        0
        <!-- END todo_open --><br />
    <!-- END todo -->
<!-- END todos -->
<!-- BEGIN no_todos -->
<!-- END no_todos -->

<!-- Detail.html -->
ID: {ID}<br />
Name: {NAME}<br />
Description: {DESCRIPTION}<br />
Cat ID: {CATEGORY_ID}<br />
Cat Name: {CATEGORY_NAME}<br />
Cat Desc: {CATEGORY_DESCRIPTION}<br />
User ID: {USER_ID}<br />
User Name: {USER_NAME}<br />
Reminder Date: {REMINDER_DATE}<br />
Done: <!-- BEGIN todo_done -->Yes<!-- END todo_done --><!-- BEGIN todo_open -->No<!-- END todo_open -->

Next, add the following method to modules/TodoManager/Controller/ComponentController.class.php as we will need it in different places:

    public function getSubstitutionArrayForTodo($todo) {
        $substitution = array(
            'ID' => $todo->getId(),
            'NAME' => contrexx_raw2xhtml($todo->getName()),
            'DESCRIPTION' => contrexx_raw2xhtml($todo->getDescription()),
            'REMINDER_DATE' => $todo->getReminderDate()->format(
                ASCMS_DATE_FORMAT
            ),
        );
        if ($todo->getCategory()) {
            $substitution += array(
                'CATEGORY_ID' => $todo->getCategory()->getId(),
                'CATEGORY_NAME' => contrexx_raw2xhtml(
                    $todo->getCategory()->getName()
                ),
                'CATEGORY_DESCRIPTION' => contrexx_raw2xhtml(
                    $todo->getCategory()->getDescription()
                ),
            );
        }
        if ($todo->getUser()) {
            $substitution += array(
                'USER_ID' => $todo->getUser()->getId(),
                'USER_NAME' => contrexx_raw2xhtml(
                    \FWUser::getParsedUserTitle($todo->getUser())
                ),
            );
        }
        return $substitution;
    }

Then, adjust the code of modules/TodoManager/Controller/FrontendController.class.php as follows:

    public function parsePage(\Cx\Core\Html\Sigma $template, $cmd) {
        $em = $this->cx->getDb()->getEntityManager();
        $todoRepo = $em->getRepository(
            $this->getNamespace() . '\Model\Entity\Todo'
        );
        switch ($cmd) {
            case 'Detail':
                $params = $this->cx->getRequest()->getUrl()->getParamArray();
                if (!isset($params['id'])) {
                    \Cx\Core\Csrf\Controller\Csrf::redirect(
                        \Cx\Core\Routing\Url::fromModuleAndCmd(
                            $this->getName()
                        )
                    );
                }
                $todo = $todoRepo->find(
                    $params['id']
                );
                $this->parseTodo($template, $todo);
                break;
            default:
                // TODO: We should add paging for performance
                $todos = $todoRepo->findAll();
                if (!count($todos)) {
                    $template->hideBlock('todos');
                    $template->touchBlock('no_todos');
                    return;
                }
                foreach ($todos as $todo) {
                    $this->parseTodo($template, $todo);
                    $template->setVariable(
                        'DETAIL_URL',
                        \Cx\Core\Routing\Url::fromModuleAndCmd(
                            $this->getName(),
                            'Detail',
                            '',
                            array('id' => $todo->getId())
                        )
                    );
                    $template->parse('todo');
                }
                break;
        }
    }

    /**
     * Parses a Todo entity into a template
     * @param \Cx\Core\View\Model\Entity\Sigma $template Template to parse into
     * @param \Cx\Modules\TodoManager\Model\Entity\Todo $todo Todo to parse
     */
    protected function parseTodo($template, $todo) {
        $template->setVariable(
            $this->getSystemComponentController()->getSubstitutionArrayForTodo($todo)
        );
        if ($todo->getDone()) {
            $template->touchBlock('todo_done');
            $template->hideBlock('todo_open');
        } else {
            $template->touchBlock('todo_open');
            $template->hideBlock('todo_done');
        }
    }

This allows you use the default page as an overview over all Todo's and to create a second page with section "Detail" which parses a single Todo.

4 Settings

4.1 Mail templates

By default the BackendController adds a section (under /cadmin/<component_name>/Settings/Mail) to manage e-mail templates that can be used by your component for sending e-mail notifications. The component responsible for managing e-mail templates is called MailTemplate on which you can find more information here.

For the example component we'd like to automatically send out an e-mail whenever a Todo is marked as done. So we can trigger the mail by listening to our own event we created earlier. Add the following code to the file modules/TodoManager/Model/Event/TodoMailEventListener.class.php:

<?php declare(strict_types=1);

namespace Cx\Modules\TodoManager\Model\Event;

class TodoMailEventListener extends \Cx\Core\Event\Model\Entity\DefaultEventListener {

    protected function todoManagerDone($todo) {
        $substitution = $this->cx->getComponent(
            'TodoManager'
        )->getSubstitutionArrayForTodo($todo);
        \Cx\Core\MailTemplate\Controller\MailTemplate::send(array(
            'key' => 'done',
            'section' => 'TodoManager',
            'substitution' => array(
                'open' => array(
                    $substitution,
                ),
                'done' => array(
                    $substitution,
                ),
            ),
        ));
    }
}

In order to trigger this event code we need to register this new event listener as we did before. Add the following code to the method registerEventListeners() in modules/TodoManager/Controller/ComponentController.class.php:

        $this->cx->getEvents()->addEventListener(
            $this->getName() . '/Done',
            new \Cx\Modules\TodoManager\Model\Event\TodoMailEventListener(
                $this->cx
            )
        );

4.2 Other settings

You can easily add settings by using the "Setting" component as these settings are displayed in your component automatically. Just make sure that "section" equals your component's name and "group" is "config". You can then access such settings by using the following code:

\Cx\Core\Setting\Controller\Setting::init(<component_name>, 'config', <engine>);
$value = \Cx\Core\Setting\Controller\Setting::getValue(<setting_name>);

For our example component we will add a setting to hide done Todo's. Execute the following CLI command to initially add the setting:

./cx Setting add TodoManager -group=config -engine=FileSystem hide_done 0 1 checkbox 1

In order to really hide done Todo's if this setting is active we need to add the following code to modules/TodoManager/Model/Repository/TodoRepository.class.php:

    /**
     * @inheritdoc
     */
    public function find($id, $lockMode = \Doctrine\DBAL\LockMode::NONE, $lockVersion = null)
    {
        \Cx\Core\Setting\Controller\Setting::init('TodoManager', 'config', 'FileSystem');
        $hideDone = \Cx\Core\Setting\Controller\Setting::getValue('hide_done');
        $entity = parent::find($id, $lockMode, $lockVersion);
        if ($entity && $hideDone && $entity->getDone()) {
            return null;
        }
        return $entity;
    }

    /**
     * @inheritdoc
     */
    public function findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
    {
        \Cx\Core\Setting\Controller\Setting::init('TodoManager', 'config', 'FileSystem');
        $hideDone = \Cx\Core\Setting\Controller\Setting::getValue('hide_done');
        if ($hideDone) {
            $criteria['done'] = false;
        }
        return parent::findBy($criteria, $orderBy, $limit, $offset);
    }

    /**
     * @inheritdoc
     */
    public function findOneBy(array $criteria, array $orderBy = null)
    {
        \Cx\Core\Setting\Controller\Setting::init('TodoManager', 'config', 'FileSystem');
        $hideDone = \Cx\Core\Setting\Controller\Setting::getValue('hide_done');
        if ($hideDone) {
            $criteria['done'] = false;
        }
        return parent::findBy($criteria, $orderBy);
    }

We need to tell the system how our settings is named for end-users. To do so we add the following entry to modules/TodoManager/lang/en/backend.php:

$_ARRAYLANG['TXT_MODULE_TODOMANAGER_HIDE_DONE'] = 'Hide done todo\'s';

5 Cleanup, Export and review

5.1 Cleanup

Before exporting you should have a look at your component and remove any unused code (new Components contain lots of non-necessary example methods).

Additionally, you should add any missing DocBlocks to ensure maintainablility of your component.

5.2 Export

If you want to use your component on a site hosted in the Cloudrexx cloud or simply need a way to move a component to another installation you need to export your component. You can do this using the following command:

./cx wb export <component_type> <component_name> <zip_file_name>

You can specify an absolute or relative file name for the ZIP package. Please note that when using a dockerized setup you may want to specify a relative path within the Cloudrexx working directory otherwise the ZIP package might end up within the container.

To generate a package for the sample component you can use the following command. You can download the TodoManager component ZIP package here.

./cx wb export module TodoManager TodoManager.zip

5.3 Review

Before using your component on a production environment you should review it. If you want to use your component on a site hosted in the Cloudrexx cloud this needs to be done by us. Cloudrexx also offer reviews for components not hosted on our systems.

In order to let us do the review of your component, please fill out the review request form.

6 Further reading

Here are some links you might find interesting for your Cloudrexx app projects: