Расширяем возможности кастомной entity

как расширить возможности кастомной сущности

В прошлой статье Создание кастомной entity рассмотрели самый простой способ создания кастомной сущности, лишенной практически всех "фишек". Сегодня продолжим развивать тему enitty. Расширим ее возможности, сделаем ее fieldable, exportable, добавим ревизионность, создадим UI для удобного добавления/редактирования/удаления. Помимо всего этого у сущности будет создана другая сущность - тип, которая позволит создать разные типы сущностей со своим набором полей. Стоит отметить, что данная статья не претендует на эталонный образец создания сущностей, скорее послужит хорошим примером быстрого разворачивания собственных.

Содержание

Структура модуля

Создавать энтити будем в рамках кастомного модуля MYMODULE. Во избежание путаницы, не будем ссылаться на какие-либо части кода из прошлой статьи, а начнем все с чистого листа.
Структура модуля будет выглядеть следующим образом:
структура модуля
Инфо файл содержит минимум информации: название модуля, описание и т.д. В нем подключаются два файла: MYMODULE.entity.inc и MYMODULE.entity_type.inc. Данные файлы содержат описание классов контроллеров, необходимых для создания энтити. Кроме того, поскольку используем Entity API, то необходимо указать данный модуль в качестве зависимости. Укажем также ctools, т.к. был задействован при построении компактных ссылок (в принципе это не обязательно, можно обойтись обычным списком).

1
2
3
4
5
6
7
8
9
10
name = MYMODULE
description = This an example of custom entity.
package = Others
core = 7.x
 
dependencies[] = entity
dependencies[] = ctools
 
files[] = includes/MYMODULE.entity.inc
files[] = includes/MYMODULE.entity_type.inc

Обращаю внимание на то, что данным способом в инфо файле подключаются только файлы, содержащие определения классов. Файлы содержащие набор функций и подключаемые таким образом, в итоге подключены не будут.
Далее разберем подробнее содержимое каждого файла.

Структура таблиц

В файле MYMODULE.install находится описание основной таблицы, ревизионной и таблицы типов сущностей.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
/**
 * Implements hook_schema().
 */
function MYMODULE_schema() {
  // Описание основной таблицы энтити.
  $schema['MYMODULE_entity'] = array(
    'description' => 'The base table for custom entity.',
    'fields' => array(
      'eid' => array(
        'description' => 'The primary identifier for MYMODULE entity.',
        'type' => 'serial',
        'unsigned' => TRUE,
        'not null' => TRUE,
      ),
      'vid' => array(
        'description' => 'The current version of MYMODULE entity.',
        'type' => 'int',
        'unsigned' => TRUE,
        'not null' => TRUE,
        'default' => 0,
      ),
      'type' => array(
        'description' => 'The type of MYMODULE entity.',
        'type' => 'varchar',
        'length' => 32,
        'not null' => TRUE,
        'default' => '',
      ),
      'title' => array(
        'description' => 'The title of MYMODULE entity.',
        'type' => 'varchar',
        'length' => 255,
        'not null' => TRUE,
        'default' => '',
      ),
      'uid' => array(
        'description' => 'The user ID that created this MYMODULE entity.',
        'type' => 'int',
        'not null' => TRUE,
        'default' => 0,
      ),
      'created' => array(
        'description' => 'The Unix timestamp when the MYMODULE entity was created.',
        'type' => 'int',
        'not null' => TRUE,
        'default' => 0,
      ),
      'changed' => array(
        'description' => 'The Unix timestamp when the MYMODULE entity was recently saved.',
        'type' => 'int',
        'not null' => TRUE,
        'default' => 0,
      ),
    ),
    'indexes' => array(
      'uid' => array('uid'),
    ),
    'unique keys' => array(
      'vid' => array('vid'),
    ),
    'primary key' => array('eid'),
  );
  // Таблица ревизий.
  $schema['MYMODULE_entity_revision'] = array(
    'description' => 'The revision table for MYMODULE entity.',
    'fields' => array(
      'eid' => array(
        'description' => 'The identifier of MYMODULE entity.',
        'type' => 'int',
        'unsigned' => TRUE,
        'not null' => TRUE,
        'default' => 0,
      ),
      'vid' => array(
        'description' => 'The primary identifier for revision of MYMODULE entity.',
        'type' => 'serial',
        'unsigned' => TRUE,
        'not null' => TRUE,
      ),
      'type' => array(
        'description' => 'The type of MYMODULE entity.',
        'type' => 'varchar',
        'length' => 32,
        'not null' => TRUE,
        'default' => '',
      ),
      'title' => array(
        'description' => 'The title of MYMODULE entity.',
        'type' => 'varchar',
        'length' => 255,
        'not null' => TRUE,
        'default' => '',
      ),
      'uid' => array(
        'description' => 'The user ID that created this version.',
        'type' => 'int',
        'not null' => TRUE,
        'default' => 0,
      ),
      'created' => array(
        'description' => 'The Unix timestamp when the MYMODULE entity was created.',
        'type' => 'int',
        'not null' => TRUE,
        'default' => 0,
      ),
    ),
    'indexes' => array(
      'eid' => array('eid'),
      'uid' => array('uid'),
    ),
    'primary key' => array('vid'),
  );
  // Таблица типов сущности.
  $schema['MYMODULE_entity_type'] = array(
    'description' => 'Stores information about all defined MYMODULE types.',
    'fields' => array(
      'type' => array(
        'description' => 'The machine-readable name of this MYMODULE type.',
        'type' => 'varchar',
        'length' => 32,
        'not null' => TRUE,
      ),
      'label' => array(
        'description' => 'The human-readable name of this MYMODULE type.',
        'type' => 'varchar',
        'length' => 255,
        'not null' => TRUE,
        'default' => '',
      ),
      'description' => array(
        'description' => 'A brief description for this type.',
        'type' => 'text',
        'not null' => TRUE,
        'size' => 'medium',
      ),
      'data' => array(
        'type' => 'text',
        'not null' => FALSE,
        'default' => NULL,
        'size' => 'big',
        'serialize' => TRUE,
        'description' => 'A serialized array of additional data related to this entity_test type.',
        'merge' => TRUE,
      ),
      'status' => array(
        'type' => 'int',
        'not null' => TRUE,
        'default' => 0x01,
        'size' => 'tiny',
        'description' => 'The exportable status of the MYMODULE entity type.',
      ),
      'module' => array(
        'description' => 'The name of the providing module if the entity has been defined in code.',
        'type' => 'varchar',
        'length' => 255,
        'not null' => FALSE,
      ),
    ),
    'primary key' => array('type'),
  );
 
  return $schema;
}

Файл .module

MYMODULE.module содержит исключительно хуки, в том числе hook_entity_info()[1] для определения сущностей. MYMODULE_entity_type тоже является сущностью и бандлом для основной entity MYMODULE_entity. Определение типа как сущность позволяет удобно управлять им с помощью Entity API, сделать его exportable.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
/**
 * @file
 * This file includes hooks and core functions of module MYMODULE.
 */
 
// Подключение необходимых файлов.
module_load_include('inc', 'MYMODULE', 'MYMODULE.entity.crud');
module_load_include('inc', 'MYMODULE', 'MYMODULE.entity_type.crud');
 
// Определение констант путей для MYMODULE_entity и MYMODULE_entity_type.
define('MYMODULE_ENTITY_PATH', 'MYMODULE-entity');
define('MYMODULE_ENTITY_TYPE_PATH', 'admin/structure/MYMODULE-types');
 
/**
 * Implements hook_entity_info().
 */
function MYMODULE_entity_info() {
  $info['MYMODULE_entity'] = array(
    'label' => t('MYMODULE entity'),
    'plural label' => t('MYMODULE entities'),
    'entity class' => 'Entity',
    // Укажем собственный класс контроллера.
    'controller class' => 'MyModuleEntityController',
    'module' => 'MYMODULE',
    // Базовая таблица сущности.
    'base table' => 'MYMODULE_entity',
    // Ревизионная таблица сущности.
    'revision table' => 'MYMODULE_entity_revision',
    'load hook' => 'MYMODULE_entity_object_load',
    'uri callback' => 'MYMODULE_entity_object_uri',
    'fieldable' => TRUE,
    // Мапинг ключей entity со столбцами в таблице.
    'entity keys' => array(
      'id' => 'eid',
      'revision' => 'vid',
      'bundle'   => 'type',
      'label'    => 'title',
    ),
    'bundle keys' => array(
      'bundle' => 'type',
    ),
    'bundles' => array(),
    // Определим свой собственный вью мод.
    'view modes' => array(
      'full' => array(
        'label' => t('Full view'),
        'custom settings' => TRUE,
      ),
    ),
    'access callback' => 'MYMODULE_entity_object_access',
    // Включение административного UI интерфейса для управления сущностью.
    'admin ui' => array(
      'path' => MYMODULE_ENTITY_PATH,
      'file' => 'MYMODULE.entity.pages.inc',
      'file path' => drupal_get_path('module', 'MYMODULE'),
      // Укажем собственный UI контроллер.
      'controller class' => 'MyModuleEntityUIController',
      'menu wildcard' => '%MYMODULE_entity_object',
    ),
  );
 
  // Определение сущности MYMODULE_entity_type.
  $info['MYMODULE_entity_type'] = array(
    'label' => t('MYMODULE entity Type'),
    'entity class' => 'Entity',
    // Укажем собственный класс контроллера.
    'controller class' => 'MyModuleEntityTypeController',
    'base table' => 'MYMODULE_entity_type',
    'load hook' => 'MYMODULE_entity_type_object_load',
    'fieldable' => FALSE,
    // Параметр указывает на то, что данная сущность
    // является бандлом для MYMODULE_entity.
    'bundle of' => 'MYMODULE_entity',
    // Включим поддержку экспорта.
    'exportable' => TRUE,
    // Мапинг ключей entity со столбцами в таблице.
    'entity keys' => array(
      'id' => 'type',
      'label' => 'label',
    ),
    'module' => 'MYMODULE',
    // Включение административного UI интерфейса для управления сущностью.
    'admin ui' => array(
      'path' => MYMODULE_ENTITY_TYPE_PATH,
      'file' => 'MYMODULE.entity_type.pages.inc',
      'controller class' => 'EntityDefaultUIController',
      'menu wildcard' => '%MYMODULE_entity_type_object',
    ),
    'access callback' => 'MYMODULE_entity_type_access',
  );
 
  return $info;
}
 
/**
 * Implements hook_entity_info_alter().
 */
function MYMODULE_entity_info_alter(&$entity_info) {
  foreach (MYMODULE_entity_type_get_types() as $type => $data) {
    $real_type = str_replace('_', '-', $type);
    $entity_info['MYMODULE_entity']['bundles'][$type] = array(
      'label' => $data->label,
      'admin' => array(
        'path' => MYMODULE_ENTITY_TYPE_PATH . '/manage/%MYMODULE_entity_type_object',
        'real path' => MYMODULE_ENTITY_TYPE_PATH . '/manage/' . $real_type,
        'bundle argument' => 4,
        'access arguments' => array('administer MYMODULE entity'),
      ),
    );
  }
}
 
/**
 * Implements hook_permission().
 */
function MYMODULE_permission() {
  $permissions['administer MYMODULE entity'] = array(
    'title' => t('Administer MYMODULE entity'),
    'description' => t('Gives permission to administer the MYMODULE entity.'),
  );
 
  return $permissions;
}
 
/**
 * Implements hook_admin_paths().
 */
function MYMODULE_admin_paths() {
  if (variable_get('node_admin_theme')) {
    $paths = array(
      MYMODULE_ENTITY_PATH . '/*/edit' => TRUE,
      MYMODULE_ENTITY_PATH . '/*/delete' => TRUE,
      MYMODULE_ENTITY_PATH . '/add' => TRUE,
      MYMODULE_ENTITY_PATH . '/list' => TRUE,
      MYMODULE_ENTITY_PATH . '/add/*' => TRUE,
    );
    return $paths;
  }
}
 
/**
 * Implements hook_entity_delete().
 */
function MYMODULE_entity_delete($entity, $type) {
  if ($type == 'MYMODULE_entity_type') {
    // Получение всех айдишек сущностей по удаляемому типу.
    $ids = MYMODULE_entity_object_get_ids($entity->type);
    // Удаление соответствующих типу entity.
    MYMODULE_entity_object_delete_multiple($ids);
  }
}

В хуке MYMODULE_entity_info_alter()[2] заполняем свойство bundles для сущности MYMODULE_entity. Функция MYMODULE_entity_type_get_types(), возвращающая массив созданных типов будет приведена позже. В модуле создан только один пермишен для управления сущнсотью. Акцентирование внимания на пермишенах в данной статье не будет, access колбеки сущностей будут возвращать по умолчанию TRUE. Создать логику для управления доступом не должно составить труда.
В хуке MYMODULE_admin_paths()[3] определим пути, по которым будет использоваться административная тема.

Описание CRUD операций для MYMODULE_entity_type

Далее разберем все, что касается сущности MYMODULE_entity_type. В файле MYMODULE.entity_type.crud.inc расположены функции для работы с энтити (создание/сохранение/удаление), а также вспомогательные функции.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/**
 * @file
 * This file includes CRUD functions for entity "MYMODULE entity type".
 */
 
/**
 * Creates a new entity MYMODULE_entity_type.
 *
 * @param array $data
 *   The input array of data for creating new type.
 *
 * @return mixed
 *   Object of entity.
 */
function MYMODULE_entity_type_create($data) {
  return entity_get_controller('MYMODULE_entity_type')->create($data);
}
 
/**
 * Saves the entity type.
 */
function MYMODULE_entity_type_save($type) {
  entity_get_controller('MYMODULE_entity_type')->save($type);
}
 
/**
 * Access callback for MYMODULE entity type object.
 *
 * @param string $op
 *   The operation being performed ('view', 'update', 'create', 'delete').
 * @param Entity $entity
 *   Optionally an entity to check access for.
 * @param object $account
 *   The user to check for.
 * @param string $entity_type
 *   The entity type of the entity to check for.
 *
 * @return bool
 *   Whether access is allowed or not.
 */
function MYMODULE_entity_type_access($op, $entity = NULL, $account = NULL, $entity_type = NULL) {
  return TRUE;
}
 
/**
 * Returns all entity types.
 */
function MYMODULE_entity_type_get_types() {
  return entity_get_controller('MYMODULE_entity_type')->getTypes();
}
 
/**
 * Loads the type by name.
 */
function MYMODULE_entity_type_object_load($name) {
  $types = MYMODULE_entity_type_object_load_multiple(array($name));
  return reset($types);
}
 
/**
 * Loads the types multiple by IDs.
 */
function MYMODULE_entity_type_object_load_multiple($ids, $conditions = array(), $reset = FALSE) {
  return entity_load('MYMODULE_entity_type', $ids, $conditions, $reset);
}

Класс контроллера для MYMODULE_entity_type

Определение класса для сущности MYMODULE_entity_type расположено в файле MYMODULE.entity_type.inc. Данный класс наследуется от класса EntityAPIController, немного расширяя его новыми методами, а именно:

  • getTypes() - позволяет получить массив всех созданных типов.
  • checkTypeName() - проверяет машинное имя типа на наличие совпадений.

Содержимое указанного файла приведено ниже

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/**
 * @file
 * This file includes classes for entity "MYMODULE entity type".
 */
 
/**
 * Class MyModuleEntityTypeController
 */
class MyModuleEntityTypeController extends EntityAPIController {
 
  /**
   * {@inheritdoc}
   */
  public function create(array $values = array()) {
    // Init the array with predefined values.
    $keys = $this->entityInfo['schema_fields_sql']['base table'];
    $vals = array('', '', '', array(), 1, NULL);
    $init = array_combine($keys, $vals);
 
    // Merge values with input.
    $values = array_merge($init, $values);
 
    return parent::create($values);
  }
 
  /**
   * Returns the all available entity types.
   *
   * @return mixed
   *   Array of type objects.
   */
  public function getTypes() {
    $query = parent::buildQuery(array());
    return $query->execute()->fetchAllAssoc($this->bundleKey);
  }
 
  /**
   * Checks the machine name of type.
   *
   * @param string $name
   *   The type machine name.
   *
   * @return bool
   *   Returns TRUE if this name is already exist, otherwise - FALSE.
   */
  public function checkTypeName($name) {
    $query = parent::buildQuery(array(), array('type' => $name));
    return (bool) $query->execute()->fetchField();
  }
}

Файл MYMODULE.entity_type.pages.inc

Данный файл содержит функции для построения форм создания/редактирования/удаления типа и вспомогательные колбеки.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
/**
 * @file
 * This file includes the page functions for entity "MYMODULE entity type".
 */
 
/**
 * MYMODULE entity type form.
 *
 * Used for create/edit MYMODULE entity types.
 *
 * @see MYMODULE_entity_type_form_submit()
 * @see _MYMODULE_entity_type_validate_machine_name()
 */
function MYMODULE_entity_type_form($form, $form_state, $entity) {
  $form['label'] = array(
    '#type'  => 'textfield',
    '#title' => t('Label of new type'),
    '#description' => t('Human-readable name.'),
    '#default_value' => $entity->label,
    '#required' => TRUE,
    '#maxlength' => 64,
  );
 
  $form['type'] = array(
    '#type' => 'machine_name',
    '#title' => t('Machine name'),
    '#title_display' => 'invisible',
    '#default_value' => $entity->type,
    '#size' => 15,
    '#description' => t('A unique machine-readable name containing letters, numbers, and underscores.'),
    '#maxlength' => 26,
    '#required' => TRUE,
    '#machine_name' => array(
      'source' => array('label'),
      'exists' => '_MYMODULE_entity_type_validate_machine_name',
      'standalone' => FALSE,
    ),
  );
 
  $form['description'] = array(
    '#type'  => 'textarea',
    '#title' => t('Description'),
    '#description' => t('Please provide a short description for this new custom entity type.'),
    '#default_value' => $entity->description,
  );
 
  $form['save'] = array(
    '#type'  => 'submit',
    '#value' => !empty($entity->is_new) ? t('Save a new type') : t('Update the type'),
  );
 
  $form['cancel'] = array(
    '#type'  => 'link',
    '#title' => t('cancel'),
    '#href'  => MYMODULE_ENTITY_TYPE_PATH,
  );
 
  return $form;
}
 
/**
 * Submit callback for form MYMODULE_entity_type_form().
 */
function MYMODULE_entity_type_form_submit($form, &$form_state) {
  // Получаем entity.
  $entity = $form_state['MYMODULE_entity_type'];
  // Обновляем сущность сабмитанными данными.
  entity_form_submit_build_entity($entity->entityType(), $entity, $form, $form_state);
 
  $message = t('New type "@label" has been added successfully.', array('@label' => $entity->label));
  if (empty($entity->is_new)) {
    $message = t('The type "@label" has been updated successfully.', array(
      '@label' => isset($entity->label) ? $entity->label : '',
    ));
  }
 
  // Сохранение энтити.
  MYMODULE_entity_type_save($entity);
  // Отправка сообщения и редирект на общий список типов.
  drupal_set_message($message);
  $form_state['redirect'] = MYMODULE_ENTITY_TYPE_PATH;
}
 
/**
 * Callback for checking the machine name of MYMODULE entity type.
 */
function _MYMODULE_entity_type_validate_machine_name($name) {
  return entity_get_controller('MYMODULE_entity_type')->checkTypeName($name);
}

Описание CRUD операций для MYMODULE_entity

По аналогии с MYMODULE_entity_type файл в основном содержит функции для работы с энтити (create/read/update/delete).
В MYMODULE.entity.crud.inc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
/**
 * @file
 * This file includes CRUD functions for entity "MYMODULE entity type".
 */
 
/**
 * Returns the MYMODULE entity uri.
 */
function MYMODULE_entity_object_uri($entity) {
  return array(
    'path' => MYMODULE_ENTITY_PATH . '/' . $entity->eid,
  );
}
 
/**
 * Loads an entity by entity ID.
 */
function MYMODULE_entity_object_load($id) {
  $entities = MYMODULE_entity_object_load_mulitple(array($id));
  return reset($entities);
}
 
/**
 * Loads entities multiple by arrays of entities IDs.
 */
function MYMODULE_entity_object_load_mulitple($ids, $confitions = array(), $reset = FALSE) {
  return entity_load('MYMODULE_entity', $ids, $confitions, $reset);
}
 
/**
 * Access callback for MYMODULE entity object.
 *
 * @param string $op
 *   The operation being performed ('view', 'update', 'create', 'delete').
 * @param Entity $entity
 *   Optionally an entity to check access for.
 * @param object $account
 *   The user to check for.
 * @param string $entity_type
 *   The entity type of the entity to check for.
 *
 * @return bool
 *   Whether access is allowed or not.
 */
function MYMODULE_entity_object_access($op, $entity = NULL, $account = NULL, $entity_type = NULL) {
  return TRUE;
}
 
/**
 * Creates an entity.
 */
function MYMODULE_entity_object_create($values = array()) {
  return entity_create('MYMODULE_entity', $values);
}
 
/**
 * Saves an entity.
 */
function MYMODULE_entity_object_save($entity) {
  entity_get_controller('MYMODULE_entity')->save($entity);
}
 
/**
 * Deletes entities multiple.
 */
function MYMODULE_entity_object_delete_multiple($ids) {
  entity_delete_multiple('MYMODULE_entity', $ids);
}
 
/**
 * Deletes an entity.
 */
function MYMODULE_entity_object_delete($id) {
  MYMODULE_entity_object_delete_multiple(array($id));
}
 
/**
 * Returns entity IDs by type.
 */
function MYMODULE_entity_object_get_ids($type) {
  return entity_get_controller('MYMODULE_entity')->getIDs($type);
}

Здесь также ничего сложного, в большинстве приведены функции-обертки для вызова методов контроллера.

Классы контроллеров для MYMODULE_type

Следующий этап, описание файла с классами контроллеров.
В MYMODULE.entity.inc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
/**
 * @file
 * This file includes classes for MYMODULE entity.
 */
 
/**
 * Class MyModuleEntityController
 *
 * Extends EntityAPIController class for populate the newly created entity.
 */
class MyModuleEntityController extends EntityAPIController {
  /**
   * {@inheritdoc}
   */
  public function create(array $values = array()) {
    global $user;
    // Создаем массив дефолтных значений для новой entity.
    $keys = $this->entityInfo['schema_fields_sql']['base table'];
    $vals = array(0, 0, '', '', $user->uid, REQUEST_TIME, 0);
    $init = array_combine($keys, $vals);
 
    // Мержим результат со входным массивом.
    $values = array_merge($init, $values);
 
    return parent::create($values);
  }
 
  /**
   * Returns an array of entity identifiers by type.
   *
   * @param string $type
   *   The entity type machine name of MYMODULE_entity_type.
   *
   * @return array
   *   Array of entities data.
   */
  public function getIDs($type) {
    $query = parent::buildQuery(array(), array('type' => $type));
    $entities = $query->execute()->fetchAllAssoc($this->entityInfo['entity keys']['id']);
    return !empty($entities) ? array_keys($entities) : array();
  }
}
 
/**
 * Class MyModuleEntityUIController
 *
 * Extends EntityContentUIController with overriding some methods.
 */
class MyModuleEntityUIController extends EntityContentUIController {
 
  /**
   * Stores the wildcard of entity.
   *
   * @var
   */
  protected $entityWildcard;
 
  /**
   * {@inheritdoc}
   */
  public function __construct($entity_type, $entity_info) {
    parent::__construct($entity_type, $entity_info);
    $this->entityWildcard = $entity_info['admin ui']['menu wildcard'];
  }
 
  /**
   * {@inheritdoc}
   */
  public function hook_forms() {
    $forms = parent::hook_forms();
    // Создаем маппинг ID форм на один колбек.
    $types = entity_get_controller('MYMODULE_entity_type')->getTypes();
    foreach ($types as $type => $info) {
      $forms[$type . '_MYMODULE_entity_form'] = array(
        'callback' => 'MYMODULE_entity_form',
      );
    }
 
    return $forms;
  }
 
  /**
   * {@inheritdoc}
   */
  public function overviewTableRow($conditions, $id, $entity, $additional_cols = array()) {
    $entity_uri = entity_uri($this->entityType, $entity);
 
    $row[] = array(
      'data' => array(
        '#theme' => 'entity_ui_overview_item',
        '#label' => entity_label($this->entityType, $entity),
        '#name'  => !empty($this->entityInfo['exportable']) ? entity_id($this->entityType, $entity) : FALSE,
        '#url'   => $entity_uri ? $entity_uri : FALSE,
        '#entity_type' => $this->entityType,
      ),
    );
 
    $links = array(
      array(
        'title' => t('edit'),
        'href' => $this->path . '/' . $id . '/edit',
      ),
      array(
        'title' => t('delete'),
        'href' => $this->path . '/' . $id . '/delete',
        'query' => drupal_get_destination(),
      ),
    );
    // В качестве темирующей функции используем ctools dropbutton тему.
    $row[] = theme('links__ctools_dropbutton', array(
      'links' => $links,
      'attributes' => array(
        'class' => array('links', 'inline'),
      ),
    ));
 
    return $row;
  }
 
  /**
   * {@inheritdoc}
   */
  public function hook_menu() {
    // Получаем результаты родительского метода.
    $items = parent::hook_menu();
 
    // Удаляем неиспользуемые пути.
    $pattern = $this->path . '/manage/' . $this->entityWildcard;
    foreach ($items as $url => $item) {
      if (strpos($url, $pattern) === 0) {
        unset($items[$url]);
      }
    }
 
    // Получение заголовка сущности во множественном числе.
    $plural_label = $this->entityInfo['plural label'];
 
    // Переопределяем собственную страницу для вывода всех созданных сущностей.
    $items[$this->path . '/list'] = array(
      'title' => $plural_label,
      'page callback' => 'drupal_get_form',
      'page arguments' => array($this->entityType . '_overview_form', $this->entityType),
      'description' => 'Manage ' . $plural_label . '.',
      'access callback' => 'entity_access',
      'access arguments' => array('view', $this->entityType),
      'menu_name' => 'management',
    );
 
    // Установка собственных колбека страницы и аргументов.
    $items[$this->path . '/add']['page callback'] = 'MYMODULE_entity_add_page_callback';
    $items[$this->path . '/add']['page arguments'] = array();
    $items[$this->path . '/add']['type'] = MENU_NORMAL_ITEM;
    $items[$this->path . '/add']['menu_name'] = 'management';
 
    // Добавляем меню айтемы для создания сущностей на основе различных типов.
    foreach (MYMODULE_entity_type_get_types() as $type) {
      $type_url = str_replace('_', '-', $type->type);
      $items[$this->path . '/add/' . $type_url] = array(
        'title' => $type->label,
        'title callback' => 'check_plain',
        'page callback' => 'MYMODULE_entity_add_callback',
        'page arguments' => array(2),
        'access arguments' => array('administer MYMODULE entity'),
        'description' => $type->description,
        'file path' => $this->entityInfo['admin ui']['file path'],
        'file' => $this->entityInfo['admin ui']['file'],
      );
    }
 
    // Изменение контекста и веса для меню айтема "delete".
    $items[$this->path . '/' . $this->entityWildcard . '/delete']['context'] = MENU_CONTEXT_PAGE;
    $items[$this->path . '/' . $this->entityWildcard . '/delete']['weight'] = 1;
 
    return $items;
  }
}

Файл MYMODULE.entity.pages.inc

Код данного файла приведен ниже.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
/**
 * @file
 * This file includes the page functions for entity "MYMODULE entity".
 */
 
/**
 * Delete MYMODULE entity confirmation form.
 *
 * @param array $form
 *   Form array.
 * @param array $form_state
 *   Array of submitted data.
 * @param string $entity_type
 *   The type of entity.
 * @param \Entity $entity
 *   THe entity object that should be removed.
 *
 * @return mixed
 *   The form array.
 */
function MYMODULE_entity_operation_form($form, $form_state, $entity_type, $entity) {
  $form['#MYMODULE_entity'] = $entity;
  return confirm_form($form,
    t('Are you sure you want to delete %title?', array('%title' => $entity->title)),
    'MYMODULE-entity/' . $entity->eid,
    NULL,
    t('Delete'),
    t('Cancel')
  );
}
 
/**
 * Submit callback for MYMODULE_entity_operation_form().
 */
function MYMODULE_entity_operation_form_submit($form, &$form_state) {
  // Get the entity.
  $entity  = $form['#MYMODULE_entity'];
 
  // Status and message by default.
  $status  = 'error';
  $message = t('The remove operation is failed. Entity does not exist.', array('%title' => $entity->title));
 
  if (!empty($entity->eid)) {
    MYMODULE_entity_object_delete($entity->eid);
    $message = t('The %title has been successfully removed', array('%title' => $entity->title));
    $status  = 'status';
  }
 
  // Set the message and redirect url.
  drupal_set_message($message, $status);
  $form_state['redirect'] = 'MYMODULE-entity/list';
}
 
/**
 * Menu callback for adding new entities.
 *
 * @param string $type
 *   The type of entity. Based on this type entity will be created.
 *
 * @return array|int|mixed
 *   The appropriate form.
 */
function MYMODULE_entity_add_callback($type) {
  $types = MYMODULE_entity_type_get_types();
  $type = isset($type) ? str_replace('-', '_', $type) : NULL;
  if (empty($types[$type])) {
    return MENU_NOT_FOUND;
  }
  $entity = MYMODULE_entity_object_create(array('type' => $type));
  drupal_set_title(t('Create @name', array('@name' => $types[$type]->label)), PASS_THROUGH);
  return drupal_get_form($type . '_MYMODULE_entity_form', $entity, $type, 'MYMODULE_entity');
}
 
/**
 * The form for create/edit entity "MYMODULE entity".
 */
function MYMODULE_entity_form($form, &$form_state, $entity) {
  // Set the id to form.
  $form['#id'] = 'MYMODULE-entity-form';
 
  // Save the entity for later, in case we need it.
  $form_state['MYMODULE_entity'] = $entity;
 
  $form['title'] = array(
    '#type' => 'textfield',
    '#title' => t('Title'),
    '#default_value' => $entity->title,
    '#weight' => -5,
    '#required' => TRUE,
  );
 
  $form['revision'] = array(
    '#access' => user_access('administer MYMODULE entity'),
    '#type' => 'checkbox',
    '#title' => t('Create new revision'),
    '#default_value' => 1,
  );
 
  // Add the buttons.
  $form['actions'] = array(
    '#type' => 'actions',
  );
 
  $form['actions']['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Save'),
    '#submit' => array('MYMODULE_entity_form_submit'),
  );
 
  $form['actions']['cancel'] = array(
    '#type' => 'link',
    '#title' => t('Cancel'),
    '#href' => MYMODULE_ENTITY_PATH . '/list',
  );
 
  field_attach_form('MYMODULE_entity', $entity, $form, $form_state);
 
  return $form;
}
 
/**
 * Validation callback for MYMODULE_entity_form().
 */
function MYMODULE_entity_form_validate($form, &$form_state) {
  $entity = $form_state['MYMODULE_entity'];
  // Field validation.
  field_attach_form_validate('MYMODULE_entity', $entity, $form, $form_state);
}
 
/**
 * Submit callabck for MYMODULE_entity_form().
 */
function MYMODULE_entity_form_submit($form, &$form_state) {
  // Get the entity object.
  $entity = $form_state['MYMODULE_entity'];
  // Update the entity with submitted values.
  entity_form_submit_build_entity($entity->entityType(), $entity, $form, $form_state);
 
  // Call field submit.
  field_attach_submit('MYMODULE_entity', $entity, $form, $form_state);
 
  $message = t('The new "@title" has been added successfully.', array('@title' => $entity->title));
  if (empty($entity->is_new)) {
    $message = t('The "@title" has been updated successfully.', array(
      '@title' => $entity->title,
    ));
  }
 
  // Save the entity.
  MYMODULE_entity_object_save($entity);
 
  // Notify the user and set the redirect of url.
  drupal_set_message($message);
  $form_state['redirect'] = MYMODULE_ENTITY_PATH . '/' . $entity->eid;
}
 
/**
 * Callback for displaying the entity types links.
 */
function MYMODULE_entity_add_page_callback() {
  $item  = menu_get_item();
  $links = system_admin_menu_block($item);
  $items = array();
  foreach ($links as $link) {
    // Get the text and description of link.
    $text = l($link['title'], $link['href'], $item['localized_options']);
    $desc = !empty($link['description']) ? ': ' . filter_xss_admin($link['description']) : '';
    $items[] = $text . $desc;
  }
 
  // If there are no links - display a message.
  if (empty($items)) {
    $link = l(t('Add new types'), MYMODULE_ENTITY_TYPE_PATH);
    $items[] = t('There are no any created MYMODULE entity types. !link', array('!link' => $link));
  }
 
  return theme('item_list', array('items' => $items));
}

Результаты

Кода получилось довольно много. От идеи разбить статью на две части пришлось отказаться, т.к. все довольно тесно связано и выделить самостоятельные куски кода довольно проблематично, будет лучше когда все в одном месте.
В итоге получили следующие результаты.
Страница со списком типов сущностей
Таблица типов entity
Форма добавления нового типа.
форма добавления нового типа
Список созданных типов сущностей
список созданных типов сущности
Форма добавления новой сущности на основе конкретного типа
форма создания новой сущности
Список созданных сущностей.
список созданных сущностей

Дополнительная информация по статье

  1. https://api.drupal.org/api/drupal/modules!system!system.api.php/function/hook_entity_info/7 - описание hook_entity_info().
  2. https://api.drupal.org/api/drupal/modules!system!system.api.php/function/hook_entity_info_alter/7 - описание hook_entity_info_alter().
  3. https://api.drupal.org/api/drupal/modules!system!system.api.php/function/hook_admin_paths/7 - описание hook_admin_paths().