2. Create Cloudrexx App
Overview
This tutorial will guide you through the creation of a Cloudrexx app in five steps:
- Basic setup
- Model interaction and testing
- User experience
- Settings
- Cleanup, Export and review
- 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.
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: