В этой статье я хотел бы познакомить читателей с архитектурой весьма интересного open-source (GPL3) проекта EspoCRM на примере создания нового модуля для этой системы.Что такое CRM-система (Customer relationship management), думаю, многие уже давно знают. Особенность данной CRM-системы в том, что она написана как Single Page Application и поэтому довольно «шустрая».
Простой дизайн и современные технологии программирования многим придутся по вкусу, а быстродействие данной CRM-системы приятно удивит. На сайте доступна демо-версия.
Добиться высокой скорости работы помогло кэширование скриптов и шаблонов в Local Storage. Все view вместе с дочерними собираются в один большой HTML, который отображается на экране у пользователя.
Система имеет мощный API, использующий JSON, а веб-интерфейс по сути является API-клиентом.
Система не перегружена функционалом, но имеет все необходимое, а также неплохо настраивается.
Создание нового модуля
Для начала нужно создать рабочую директорию нового модуля (это будет пакет модуля) и поместить её в
application/Espo/Modules:application/Espo/Modules/PM
Структура каталогов нашего модуля должна быть следующая:
application/Espo/Modules/PM/Controllers/ application/Espo/Modules/PM/Entities/ application/Espo/Modules/PM/Resources/
Описание метаданных
При разработке PM мы должны описать две сущности — Project и ProjectTask. Для этого необходимо создать два JSON-файла со следующим содержанием:
application/Espo/Modules/PM/Resources/metadata/scopes/Project.json
{ "entity": true, "layouts": true, "tab": true, "acl": true, "module": "PM", "customizable": true, "stream": true, "importable": true }
application/Espo/Modules/PM/Resources/metadata/scopes/ProjectTask.json
{ "entity": true, "layouts": true, "tab": false, "acl": true, "module": "PM", "customizable": true, "stream": true, "importable": true }
После этого мы должны описать поля и связи для наших сущностей в метаданных
entityDefs.application/Espo/Modules/PM/Resources/metadata/entityDefs/Project.json
{ "fields": { "name": { "type": "varchar", "required": true }, "status": { "type": "enum", "options": [ "Draft", "Active", "Completed", "On Hold", "Dropped" ], "default": "Active" }, "description": { "type": "text" }, "account": { "type": "link" }, "createdAt": { "type": "datetime", "readOnly": true }, "modifiedAt": { "type": "datetime", "readOnly": true }, "createdBy": { "type": "link", "readOnly": true }, "modifiedBy": { "type": "link", "readOnly": true }, "assignedUser": { "type": "link", "required": true }, "teams": { "type": "linkMultiple" } }, "links": { "createdBy": { "type": "belongsTo", "entity": "User" }, "modifiedBy": { "type": "belongsTo", "entity": "User" }, "assignedUser": { "type": "belongsTo", "entity": "User" }, "teams": { "type": "hasMany", "entity": "Team", "relationName": "EntityTeam" }, "account": { "type": "belongsTo", "entity": "Account", "foreign": "projects" }, "projectTasks": { "type": "hasMany", "entity": "ProjectTask", "foreign": "project" } }, "collection": { "sortBy": "createdAt", "asc": false, "boolFilters": [ "onlyMy" ] } }
application/Espo/Modules/PM/Resources/metadata/entityDefs/ProjectTask.json
{ "fields": { "name": { "type": "varchar", "required": true }, "status": { "type": "enum", "options": [ "Not Started", "Started", "Completed", "Canceled" ], "default": "Not Started" }, "dateStart": { "type": "date" }, "dateEnd": { "type": "date" }, "estimatedEffort": { "type": "float" }, "actualDuration": { "type": "float" }, "description": { "type": "text" }, "project": { "type": "link" }, "createdAt": { "type": "datetime", "readOnly": true }, "modifiedAt": { "type": "datetime", "readOnly": true }, "createdBy": { "type": "link", "readOnly": true }, "modifiedBy": { "type": "link", "readOnly": true }, "assignedUser": { "type": "link", "required": true }, "teams": { "type": "linkMultiple" } }, "links": { "createdBy": { "type": "belongsTo", "entity": "User" }, "modifiedBy": { "type": "belongsTo", "entity": "User" }, "assignedUser": { "type": "belongsTo", "entity": "User" }, "teams": { "type": "hasMany", "entity": "Team", "relationName": "EntityTeam" }, "project": { "type": "belongsTo", "entity": "Project", "foreign": "projectTasks" } }, "collection": { "sortBy": "createdAt", "asc": false, "boolFilters": [ "onlyMy" ] } }
Указываем, что будем использовать контроллер для CRUD. Файл помещаем в директорию
clientDefs.application/Espo/Modules/PM/Resources/metadata/clientDefs/Project.json
{ "controller": "Controllers.Record" }
application/Espo/Modules/PM/Resources/metadata/clientDefs/ProjectTask.json
{ "controller": "Controllers.Record" }
Контроллеры классов
Для работы сущностей необходимо описать их контроллеры.
application/Espo/Modules/PM/Controllers/Project.php
<?php namespace Espo\Modules\PM\Controllers; class Project extends \Espo\Core\Controllers\Record { }
application/Espo/Modules/PM/Controllers/ProjectTask.php
<?php namespace Espo\Modules\PM\Controllers; class ProjectTask extends \Espo\Core\Controllers\Record { }
Классы сущностей
Помимо определения контроллеров, также необходимо определить и классы для сущностей.
application/Espo/Modules/PM/Entities/Project.php
<?php namespace Espo\Modules\PM\Entities; class Project extends \Espo\Core\ORM\Entity { }
application/Espo/Modules/PM/Entities/ProjectTask.php
<?php namespace Espo\Modules\PM\Entities; class ProjectTask extends \Espo\Core\ORM\Entity { }
Локализация (I18n)
Добавляем имена наших сущностей в глобальный файл локализации
Global.json:application/Espo/Modules/PM/Resources/i18n/en_US/Global.json
{ "scopeNames": { "Project": "Project", "ProjectTask": "Project Task" }, "scopeNamesPlural": { "Project": "Projects", "ProjectTask": "Project Tasks" } }
Для перевода полей, выпадающих списков, связей и т.п. в наших сущностях нужно создать отдельные файлы локализации:
application/Espo/Modules/PM/Resources/i18n/en_US/Project.json
{ "labels": { "Create Project": "Create Project" }, "fields": { "name": "Name", "status": "Status", "account": "Account" }, "links": { "projectTasks": "Project Tasks" } }
application/Espo/Modules/PM/Resources/i18n/en_US/ProjectTask.json
{ "labels": { "Create ProjectTask": "Create Project Task" }, "fields": { "name": "Name", "status": "Status", "project": "Project", "dateStart": "Date Start", "dateEnd": "Date End", "estimatedEffort": "Estimated Effort (hrs)", "actualDuration": "Actual Duration (hrs)" } }
Последний штрих
После осуществления всех вышеописанных действий переходим в панель Администратора и производим восстановление системы (Menu -> Administration -> Rebuild), а затем обновляем страницу.
Теперь наш модуль Projects по умолчанию является скрытым; для отображения переходим в панель Администратора (Administration -> User Interface) и делаем его видимым. При помощи Field Manager и Layout Manager можно легко добавить новые поля и настроить внешний вид отображения данных.
Я по сути не имею отношения к проекту, я лишь довольный пользователь. Процесс создания модуля описан разработчиками и публикуется с их согласия. И разработчики, и я готовы ответить на все интересующие вопросы.
